Turning a 10TB Scan Into 100GB

Concepts covered: Columnar vs Row Formats

Each technique so far cut bytes on its own. Applied to a single table, the cuts multiply. Here is page_views as it first landed: 450 days of uncompressed, row-oriented CSV, 10TB in all. It has 40 columns, and its rows average 290 bytes. The quarterly report counts sessions per country and page type over the last 90 days, excluding bots.

SELECT
  country,
  page_type,
  approx_distinct(session_id) AS sessions
FROM page_views
WHERE view_date >= DATE '2026-07-25'
  AND is_bot = false
GROUP BY
  country,
  page_type
Events land as CSV. Every hour, the rewrite job writes them as zstd-compressed columnar files, sorted by country, into that day's view_date partition.
  • clickstream (source, web and app events)
  • landing_csv (bronze, object storage, CSV)
  • hourly_rewrite (transform, hourly batch job)
  • page_views (silver, columnar, zstd, by view_date)
  • quarterly_report (consumer, BI dashboard)
  • clickstream -> landing_csv
  • landing_csv -> hourly_rewrite
  • hourly_rewrite -> page_views
  • page_views -> quarterly_report

3 factors, multiplied

StepWhat the scan skipsFactorBytes scanned
Start: row CSVNothing1x10TB
Partition by view_date, filter on it360 of 450 days5x2TB
Columnar files36 of 40 columns, 232 of 290 bytes5x400GB
Encodings plus zstd3 of every 4 bytes in those columns4x100GB

Each factor comes from an earlier section. Partitioning removes the days the report does not ask for, and it works only because the WHERE clause compares view_date directly, the pushdown habit. Columnar layout removes 36 of the 40 columns. Encodings and zstd shrink what remains 4x, helped by the country sort that lengthens runs. The order of the steps changes nothing, because each factor divides the bytes that are left.

Remove one piece, keep the rest

Missing pieceBytes scannedWhy
Nothing100GBAll 3 factors apply
Pushdown: filter written as date_format(view_ts, '%Y-%m-%d') >= '2026-07-25'Over 500GBNo partition pruned, and view_ts becomes a fifth column read
PartitioningOver 500GBAll 450 days of the 4 columns, plus a date column to filter
Encodings and compression400GBPartitions and columns still cut 25x
Columnar layout500GBWhole rows for 90 days, even granting row files 4x compression

A table without partitions can still skip row groups by the min and max of a date column if its files were written in date order. To find out which ones to skip, though, the engine first reads the footer of every file across all 450 days.

The cheapest piece to lose is the one that costs nothing to keep. A single function around a timestamp turns the 100GB report back into a scan of more than 500GB, while the files themselves stay exactly as they were. Columnar layout and compression pay off whatever the filter says, so they are defaults; partitioning is a per-table decision that pays only when filters reach it.

On the final page_views layout, a query reads page_url and load_ms, which average 87 of each row's 290 bytes and also compress 4x. It filters view_date directly over the last 9 days. About how many bytes does it scan?

Going below 100GB

The quarterly report reads every country, so the country sort helped only compression. A Germany-only version adds country = 'DE', and row-group min and max on country can now prune, but only a whole row group at a time. Each hourly file of about 230MB holds 2 row groups, so German rows share row groups with other countries and the scan falls at best to about half, near 50GB. A daily job that rewrites each day sorted by country makes about 43 row groups per day; if German views are 6% of traffic, they fill about 3 of them, and the scan approaches 6GB.

A teammate proposes adding country as a second partition level under view_date, keeping the hourly load, so the Germany report prunes better. What happens to the quarterly report?

About this lesson

This section is part of the Storage Layers: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.