1. Data Warehouses: Schema-on-Write & Columnar OLAP

Modern Data Warehouses (Snowflake, BigQuery) enforce strict Schema-on-Write validation during ingestion. Data is stored in Columnar OLAP Format:

  • Columnar stripes group values of identical attributes contiguously on disk.
  • Enables vector-quantized dictionary compression and run-length encoding (RLE).
  • Queries skip unreferenced columns entirely, minimizing physical I/O bandwidth.

2. Data Lakes: Schema-on-Read & Object Storage

A Data Lake stores raw structured, semi-structured, and unstructured blobs on object storage (AWS S3, GCS) under Schema-on-Read:

  • Ingestion incurs zero transformation latency.
  • Parsing schemas are applied dynamically at query time.
  • Without catalog governance, immutable Parquet/JSON files degrade into un-indexed Data Swamps.

3. Data Lakehouses: ACID Transactional Metadata Layers

A Data Lakehouse overlays ACID transactional guarantees onto cheap object storage using open table formats (Apache Iceberg, Delta Lake).

   SQL Query Engine (Trino / Spark / DuckDB)
                     │
                     ▼ (Reads Snapshot Manifest List)
   ├── Snapshot Manifest List (snap-1024.avro)
   │    ├── Manifest File A -> references Parquet Data File 1
   │    └── Manifest File B -> references Parquet Data File 2

Optimistic Concurrency Control (OCC)

Transactions commit by atomically swapping the active snapshot pointer. If two concurrent writes modify overlapping Parquet files, OCC detects file-level serialization conflicts and retries the transaction.


4. Python Verification: Columnar vs Row-Oriented I/O Simulation

import numpy as np

def simulate_columnar_vs_row_storage(num_rows: int = 100_000):
    """Demonstrates I/O efficiency of columnar scanning over row-oriented layouts."""
    np.random.seed(42)
    # Row-oriented: list of tuples (id: int, price: float, volume: int)
    ids     = np.arange(num_rows, dtype=np.int32)
    prices  = np.random.normal(100.0, 15.0, size=num_rows).astype(np.float64)
    volumes = np.random.randint(100, 5000, size=num_rows, dtype=np.int32)
    
    # In row storage, scanning 'prices' requires skipping 4B id and 4B volume per row
    row_bytes_read = num_rows * (4 + 8 + 4)  # Reads entire 16 bytes/row
    
    # In columnar storage (Parquet stripe), scanning 'prices' reads only the contiguous float64 column
    col_bytes_read = num_rows * 8
    
    print(f"Total Rows Scanned:                 {num_rows:,}")
    print(f"Row-Oriented Physical I/O Read:     {row_bytes_read / 1024:.2f} KB")
    print(f"Columnar Stripe Physical I/O Read:  {col_bytes_read / 1024:.2f} KB")
    print(f"Exact Bandwidth Saved by Columnar:  {(1.0 - col_bytes_read/row_bytes_read)*100:.1f}%")

if __name__ == "__main__":
    simulate_columnar_vs_row_storage(100_000)