A 48-row × 6-column table (id, category, region, status, score, flag) is laid out two ways. In row-major storage a whole record sits contiguously, so answering a query still means reading every column of every unpruned row. In columnar storage each column is its own contiguous run, so a query touching one column reads only that column.
Dictionary encoding (per column):
bits = ceil(log2(distinct_values))
bytes = distinct_values × 4 + rows × bits / 8
(skipped when distinct_values ≈ rows: id, score)
Zone-map predicate pushdown (per row group):
skip group if [qMin,qMax] ∩ [groupMin(id), groupMax(id)] = ∅
→ only its footer min/max stats are read to decide this,
never the row-group's actual data
- Storage layout — row-major must read every column of a surviving row group; columnar reads only the
score column.
- Dictionary encoding — replaces repeated values with small integer codes into a small dictionary; helps low-cardinality columns (category: 5, region: 4, status: 3, flag: 2) far more than near-unique ones (id, score).
- Row groups — the id column is sorted, so each group covers a contiguous id range; more/smaller groups make the query-range prune more precisely.
- Query id BETWEEN [min,max] — row groups whose id range never overlaps the filter turn gray (pruned) and cost nothing to scan.
Real-world relevance: this is exactly how Parquet/ORC files are read by Spark SQL and Hive on top of HDFS — column pruning plus row-group (stripe) statistics are why a columnar warehouse query can scan megabytes instead of the whole terabyte table.