Chapter 18 · Columnar Storage, Compression, Encoding, Vectorized Execution, and Analytical Scan Economics

Row vs Column Storage: Projection, Compression, CPU, Updates, and Analytical Workloads

Compare row-oriented and column-oriented physical layouts without changing AtlasMart grain or metrics, then observe how projection width, compression, update locality, and workload shape change the amount of work required.

Intermediate → Advanced135–165 minutesRow-vs-column mechanism labPython 3.13.5 · SQLite 3.46.1 · local/syntheticLast reviewed: September 2026

Learning outcomes

01

Explain the central warehouse mechanism in “Row vs Column Storage: Projection, Compression, CPU, Updates, and Analytical Workloads” and connect it to AtlasMart’s declared grain and governed metrics.

02

Keep logical correctness, history, and control totals unchanged while evaluating the lesson’s physical or operational choice.

03

Run or interpret the deterministic local evidence and distinguish what it proves from engine-, cache-, scale-, or cloud-dependent behavior.

04

Diagnose the controlled failure, repair it safely, and state the production checks required before adopting the pattern.

Continuity guardrail

The Chapter 15–17 canonical current-state controls remain 9 paid lines, 7 orders, 11 units, 740 USD GMV, 450 USD cost, and 290 USD gross profit. Chapter 18 expands those nine seed lines deterministically to 216,000 benchmark rows only to make storage mechanisms observable. Benchmark rows are never reported as new production facts.

Lab contract

Runtime: mandatory path uses Python 3 standard library; generation evidence below used Python 3.13.5 and SQLite 3.46.1. Storage: local filesystem only. Source: synthetic AtlasMart sales facts. Time: dates are UTC business dates; no time-zone conversion is hidden in the benchmark. Grain: one current paid order line. Security: synthetic identifiers only; no secrets or PII. History: the benchmark is a deterministic physical scale expansion, not a new business history. Cache: OS cache is not flushed; timing probes use two warm-ups and seven measured runs. Cost: no paid service is required.

1. The AtlasMart decision: reduce work without changing meaning

AtlasMart’s finance dashboard usually needs order_date, product, and amount, while the paid order-line fact also carries customer, channel, quantity, cost, profit, status, and operational keys. A row-oriented layout keeps the fields of one logical row physically close; a column-oriented layout keeps values from the same column close, usually in chunks. A projection is the subset of columns a query asks to read. An analytical scan is work that evaluates many fact rows, often while projecting only a few columns.

The business question is unchanged: “What was paid GMV for this date/product slice?” Chapter 18 changes only the physical representation and execution work. That separation is essential: a faster answer with different filters, grain, or restatement semantics is not an optimization—it is a different calculation.

2. Row and column layouts optimize different locality

Property Row-oriented tendency Column-oriented tendency Decision implication
Projection Fetching a row naturally brings many fields together. A narrow query can read only requested column chunks. Wide facts + narrow BI projections favor column locality.
Compression Adjacent values have mixed types/distributions. Same-type adjacent values often expose repetition/ranges. Encoding opportunities improve, but depend on distribution.
Point update One row is localized in a page/record structure. A changed row may touch multiple column segments or require rewrite/merge machinery. High-frequency OLTP mutation can favor row stores.
Large aggregate May decode fields the query never uses. Can often project only measure/filter columns. Scan bytes can fall without changing logical grain.
CPU path Tuple-at-a-time engines may repeatedly interpret heterogeneous rows. Analytical engines often combine columnar storage with batch/vector execution. Storage format and execution model are distinct mechanisms.

3. Projection changes physical work, not the metric

In the end-to-end harness, the compressed row CSV is 1,344,039 bytes. The clustered didactic column store is 808,424 bytes across all column chunks and metadata. Those whole-representation sizes are descriptive, not sufficient to predict a query. For the selective date+P100 query, the row CSV must be parsed end-to-end, while the column harness reads only three needed columns from one qualifying row group: order_date, product_code, and amount_cents.

Mechanism test

The query total is 2,660,000 cents in the row CSV, SQLite table, clustered column store, and shuffled column store. Only after proving equality is it meaningful to compare work. Different totals would invalidate the benchmark.

4. Controlled failure: “columnar is always faster”

That statement collapses workload, cache, update pattern, engine, encoding, and data distribution into one slogan. A point lookup returning a complete row, a high-churn OLTP update, or a tiny hot dataset can erase the advantages demonstrated by a wide analytical scan. Even in this chapter’s own harness, a full SUM(amount) cannot skip logical rows; it only benefits from projecting one physical column.

The repair is to define a representative query corpus first: projection width, filter selectivity, grouping keys, update rate, concurrency, freshness, and history behavior. Then compare correct implementations under the same semantics.

5. Hands-on mechanism probe: projection bytes

lesson1_projection_probe.py
import csv, gzip, tempfilefrom pathlib import Pathrows = [    ("2026-09-18", "P100", 10000, 6000, "web"),    ("2026-09-18", "P200",  2500, 1500, "web"),] * 5000with tempfile.TemporaryDirectory() as td:    root = Path(td)    with gzip.open(root / "rows.csv.gz", "wt", newline="") as f:        w = csv.writer(f); w.writerow(["date","product","amount","cost","channel"]); w.writerows(rows)    with gzip.open(root / "amount.txt.gz", "wt") as f:        f.write("\n".join(str(r[2]) for r in rows))    row_bytes = (root / "rows.csv.gz").stat().st_size    projected_bytes = (root / "amount.txt.gz").stat().st_size    assert sum(r[2] for r in rows) == 62500000    print("same logical sum:", 62500000)    print("row representation bytes:", row_bytes)    print("projected amount-column bytes:", projected_bytes)# Exact byte counts vary by Python/zlib build; the equality must not.

The exact gzip byte counts are implementation-dependent. The invariant is the measure equality; the physical observation is that a representation capable of opening only the amount column can avoid reading unrelated fields for this narrow aggregate.

6. What this evidence does and does not prove

  • Proves: the fixture preserves the same metric across layouts; narrow projections can reduce bytes opened; layout can create compression opportunities.
  • Does not prove: Parquet/ORC behavior, cloud scan billing, SIMD use, production latency, or a universal row-vs-column crossover point.
  • Dataset dependent: compression ratio and row-group elimination.
  • Engine dependent: pushdown, vectorization, late materialization, cache design, and update strategy.
  • Security dependent: column-level access controls may constrain what can be projected or materialized, but the local lab has no real PII.

7. Production judgment and bridge

Choose physical locality from workload evidence, not from the label “warehouse.” Preserve replayability and reconciliation: a new format must be reproducible from governed raw/integration data, and rollback must restore the prior representation without changing facts. Measure both read savings and write/maintenance cost. Lesson 2 opens the column chunks themselves and asks why some value distributions compress or encode well while others do not.

Knowledge check

Check your understanding

  1. What is the logical contract that must not change when storage changes?
  2. Why does narrow projection often favor column locality?
  3. Why is whole-file size insufficient to predict one query?
  4. Give one workload where row orientation may be preferable.
  5. Why must result equality be checked before timing?
Review the answers

1. Grain, keys/history semantics, filters, and metric definitions.

2. The engine can often open/decode only needed columns rather than unrelated fields in every row.

3. Queries touch subsets of rows/columns and may use metadata, pruning, caches, or indexes differently.

4. A high-rate point-update workload that reads/writes complete rows.

5. Faster wrong results are not valid performance improvements.

Authoritative references

  • Apache Parquet — ConceptsOfficial terminology for row groups, column chunks, pages, and the units at which I/O and encoding occur.
  • Apache Parquet — File FormatOfficial layout showing column chunks organized inside row groups and metadata used to locate relevant chunks.
  • Apache Parquet — EncodingsOfficial definitions of plain, dictionary, run-length/bit-packed, and delta encodings. Parquet is a reference, not a prerequisite for the mandatory lab.
  • DuckDB — Execution FormatCurrent official example of a vectorized analytical engine. This course uses the page only as a non-prerequisite reference for the execution concept.
  • SQLite — EXPLAIN QUERY PLANOfficial documentation for the local row-store plan evidence. The plan text is diagnostic output, not a stable application API.
  • Python — gzipStandard-library compression used by the dependency-free local storage harness.
  • Python — structStandard-library binary packing used for fixed-width didactic column chunks.
  • Python — sqlite3Standard-library SQLite interface used for the local row-oriented table and query-plan evidence.
  • Kimball Group — Dimensional Modeling TechniquesBackground for keeping dimensional grain and metric semantics stable while changing physical storage and access paths.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.