learn-data-engineering-with-phoebe / Builder session 7 of 10
Learn Data Engineering with Phoebe · Builder track · Session 7 of 10

Storage engineering

A pipeline that lands correct data can still be slow and expensive if the data is laid out badly on disk. Storage engineering is the craft of physical layout: how you partition data so queries skip what they do not need, how you tier it by cost, and why the modern stack stores everything on cheap object storage with compute kept separate. You will partition 300,000 Daybreak orders live and watch a query prune - on DuckDB in your browser.

🔴 Builder track Practitioners: DE · analysts · DS · PMs Runs in your browser · DuckDB 45 min
0-3 · Recap 3-20 · Partitioning, tiers, object storage 20-42 · Partition and prune, live 42-45 · Q&A
Part 0

Where the pipeline stands

Daybreak's pipeline now extracts (b2), ingests incrementally (b3), lands in good formats (b4), transforms in the engine (b5), and can even handle a stream (b6). The data is correct and well-shaped. This session asks a different question: how is it physically laid out on storage? Get that wrong and every query pays for it - scanning terabytes it did not need, on storage that costs more than it should. Storage engineering is the physical-layout craft that makes correct data also fast and cheap.

Live - presented in session Self-study - read after class ▶ Live pipeline - editable & runnable Official sources covered
★ What you walk out with today A working feel for partitioning - splitting data by a column so queries skip irrelevant files - proven live by writing 300k Daybreak orders to monthly partitions and watching a query touch one month instead of all of them; the hot/warm/cold tiering model that trades latency for cost; why the modern stack sits on object storage (S3) with compute separate; and the over-partitioning trap that turns the cure into the disease.
Part 1 · covers DLAI Storage & Queries M1 (physical storage)

Physical layout: partition so queries skip 6 min live

The single biggest physical-layout decision is partitioning: splitting a dataset into separate files by the value of a column - most often a date. Then a query filtered to one month only opens that month's files and skips the rest. This is partition pruning, and it is the difference between scanning all your data and scanning a sliver of it. The art is picking a partition column that matches how queries actually filter.

Unpartitioned - one query reads ALL the data one big file - every query scans the whole thing Filter to March? The engine still reads Jan..Dec to find it. Partitioned by month - the query prunes to one Jan Feb Mar Apr May Jun query for March opens only the Mar file Same data, laid out so the engine reads 1 file, not 6. That is partition pruning.
🔍 Click to zoom - one big file reads everything; partitions let the query skip most of it
LivePartitioning, and the two ways to get it wrong3 min

Partitioning writes each group of rows to its own file (or folder of files) keyed by a column. A query that filters on that column reads only the matching partitions - it prunes the rest. Pick the column queries filter on most, and pick a granularity that keeps partitions a sensible size.

  • One big file is bad: every query, however selective, must scan the whole thing. No pruning is possible - the engine cannot skip what is not separated.
  • A million tiny files is worse: over-partition (say, by the second) and you drown in file overhead - each file has a cost to open and read metadata for. The engine spends more time managing files than reading data.
  • The sweet spot: partitions large enough to be worth opening, small enough that a typical query skips most of them. Date partitions (day or month) hit this for most time-series data.
Real world

Partitioning is often the single cheapest speedup. A team's dashboard query scanned a whole year of event data every load - slow and, on a pay-per-scan warehouse, expensive. Partitioning the table by day meant the "last 7 days" dashboard read seven partitions instead of 365. No new hardware, no rewrite of the query - just laying the same bytes out to match how they were queried. Cost and latency both dropped by more than an order of magnitude.

Self-studyFile layout: not too few, not too many2 min read

Beyond which column, file layout matters. Columnar formats like Parquet (b4) store row groups you can skip within a file; partitioning skips whole files between them. The two compound. The failure modes are symmetric: too few files (one giant blob) means no skipping at all; too many files (the "small files problem") means the engine pays per-file overhead that swamps the read. Healthy targets are partitions in the tens-to-hundreds-of-megabytes range - big enough to amortize opening, small enough to prune usefully. Compaction jobs exist precisely to merge small files back up when a streaming or frequent-append pipeline has fragmented a table.

Part 2 · partitioning on real scale

Partition 300k orders, then query one 5 min live

This playground loads big_orders - 300,000 synthetic Daybreak orders, generated in-browser, nothing shipped over a network. You will write them out partitioned by a derived month, then read a single month back and see that only that partition's rows are touched. This is partition pruning on data big enough to feel it.

LiveWrite partitioned, then read one partition4 min

Partitioning means splitting a table into separate files by a column, so a query that filters on that column reads only the files it needs. The simplest form is one file per partition: write each month to its own Parquet file, then a month-query reads just that one file. Press ▶ Run - note the row count is one month's slice, not all 300k.

-- hand-partition: write January's slice to its own file
COPY (
  SELECT * FROM big_orders
  WHERE strftime(order_date, '%Y-%m') = '2026-01'
) TO 'orders_2026_01.parquet' (FORMAT PARQUET);

-- read ONE partition file - only January's rows are scanned
SELECT count(*)                           AS rows_scanned,
       ROUND(avg(quantity * unit_price),2) AS avg_line_value
FROM 'orders_2026_01.parquet';
What the big engines automate Snowflake and BigQuery let you declare a partition column and they manage the file-per-partition split for you (and go finer with micro-partitions and clustering). The mechanic is exactly what you just did by hand: filter on the partition column, touch only the matching files.
Self-studyStorage tiers: hot, warm, cold2 min read

Not all data deserves the same storage. Tiering trades latency for cost:

  • Hot: queried constantly, must be instant. Kept on the fastest, most expensive storage (SSD-backed, standard object storage). Daybreak's last-90-days orders.
  • Warm: queried occasionally. Cheaper storage, slightly slower retrieval. Last year's orders you still report on monthly.
  • Cold: rarely touched, kept for compliance or the odd deep dive. Cheapest storage (S3 Glacier and the like), retrieval measured in minutes to hours. Daybreak's five-year-old records.

The base of all three is object storage (Amazon S3, GCS, Azure Blob) - cheap, effectively infinite, and where lifecycle rules automatically move data hot → warm → cold as it ages. You engineer the tiering; the cloud enforces it.

Part 3 · covers DLAI Storage & Queries M1 (distributed, object storage)

Object storage and separated compute 4 min live

The modern stack made one decisive move: store data on cheap object storage and keep compute separate, spinning it up only when you query. This is why a Parquet file on your laptop (b4) and a Parquet object in S3 are the same thing - the engine reads either the same way. Storage scales independently of compute, so you pay for a petabyte of storage without paying for a petabyte of always-on compute.

Compute engines - spun up on demand, paid per query DuckDB Spark Snowflake your laptop, reading a file all read the same files - storage is shared, compute is not Object storage (S3 / GCS / Azure Blob) - cheap, near-infinite, the shared base Parquet objects - the same format you wrote to your laptop in b4, now in the cloud Storage scales independently of compute. Pay for the petabyte; rent the CPUs only when querying.
🔍 Click to zoom - many engines, one cheap shared storage base
Self-studyDistributed storage, in one idea (read-only)3 min read

Behind an S3 bucket is a distributed file system: your "file" is split into pieces, replicated across many machines for durability, and addressed as one logical object. You never see the machines - you just read a path. That abstraction is what lets a single SELECT ... FROM 's3://daybreak/orders/*.parquet' transparently read data spread across a fleet. The read below is exactly what your playground does locally; only the path changes when it points at the cloud:

The same read, local vs cloud - path is the only difference -- local (what the playground runs) SELECT count(*) FROM 'orders_by_month/month=2026-01/*.parquet'; -- cloud object storage (a real deployment) - identical query shape SELECT count(*) FROM 's3://daybreak/orders_by_month/month=2026-01/*.parquet';
Why this matters for you The partitioning you just did on a local folder is byte-for-byte the layout you would push to S3. Learn the physical layout on your laptop; the cloud deployment is the same idea with a longer path - and object storage is the base every modern warehouse and lake sits on.
Demo 1 of 2

Partition, then prove the prune with a timer ★ 11 min · everyone builds

Write the 300k orders partitioned by month, then run two reads back to back: one hitting a single month, one scanning every partition. Compare the "ms in your browser" figures - the pruned query touches a fraction of the data.

Write: partition big_orders into one folder per month.

Prune: read a single month - the engine opens just that partition.

Compare: read every partition and watch the row count (and time) jump - that is the scan you avoided.

-- hand-partition: write one month to its own file
COPY (
  SELECT * FROM big_orders
  WHERE strftime(order_date, '%Y-%m') = '2026-01'
) TO 'orders_2026_01.parquet' (FORMAT PARQUET);

-- PRUNED read (one partition file) vs FULL scan (the whole table)
SELECT (SELECT count(*) FROM 'orders_2026_01.parquet') AS pruned_read,
       count(*)                                        AS full_scan_read
FROM big_orders;
Real world

The number you just saw is the whole point of storage engineering. On a pay-per-scan cloud warehouse, that pruned read is not just faster - it is cheaper by the same ratio, because you are billed for bytes scanned. Teams cut warehouse bills dramatically by nothing more than partitioning tables to match their query filters. The data did not change; its physical layout did.

Demo 2 of 2

Your turn: partition differently, count, and reflect ★ 10 min · build your own

Q1 partitions by a different column. Q2 counts rows per partition to check the layout is balanced. Q3 is the trap every storage engineer must learn to avoid.

LiveQ1 · Partition by channel instead, then read one3 min
COPY (SELECT * FROM big_orders WHERE channel = 'web')
TO 'orders_web.parquet' (FORMAT PARQUET);

SELECT count(*) AS web_orders
FROM 'orders_web.parquet';
LiveQ2 · Count rows per partition4 min
-- how many rows each monthly partition would hold
SELECT strftime(order_date, '%Y-%m') AS month,
       count(*)                       AS rows_in_partition
FROM big_orders
GROUP BY month
ORDER BY month;
Self-studyQ3 · The over-partitioning trap3 min

No SQL - reason about the failure mode. Partitioning helps by letting queries skip files, so it is tempting to partition on everything: month and channel and customer and product. Do that and you shatter 300k rows into thousands of tiny files, each with a handful of rows. Now every query, even a pruned one, must open, read metadata for, and stitch together a huge number of files - and the per-file overhead swamps any saving from skipping. This is the small files problem, and at real scale it is a classic performance killer: a table "optimized" into millions of kilobyte files runs slower than the single blob it started as. The rule: partition on the one or two columns queries actually filter on, at a granularity that keeps each partition a healthy size (tens to hundreds of MB). More partitions is not more speed - past a point it is the opposite. When a streaming or frequent-append job has already fragmented a table, a compaction step merges the shards back up.

Homework

Try it yourself - this week ◐ 20-30 min total

Source material

Official sources covered

This session teaches the physical-storage core of the DeepLearning.AI Data Engineering Professional Certificate (Joe Reis) and Reis & Housley's Fundamentals of Data Engineering, run on a live engine instead of slides. This page covers:

DLAI Data Storage & Queries - M1: physical storage, distributed, tiersParts 1-3 · partitioning, hot/warm/cold, object storage, separated compute
Fundamentals of Data Engineering (Reis & Housley) - storageParts 1-3 · file layout, the small-files problem, storage abstractions
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · What does partitioning a table by date save you?

Partitioning splits data into files by a column so a query filtered on that column prunes to the matching partitions and skips the rest. Less data scanned means faster queries and, on pay-per-scan warehouses, a directly lower bill.

2 · What is the difference between a hot and a cold storage tier?

Tiers trade latency for cost. Hot data is on fast, expensive storage for instant access; cold data (e.g. S3 Glacier) is cheapest but takes minutes to hours to retrieve. You engineer the tiering; cloud lifecycle rules move data hot → warm → cold as it ages.

3 · Why can over-partitioning hurt performance?

Partitioning on too many columns or too fine a grain creates the "small files problem" - the engine spends more time opening and reading metadata for countless tiny files than it saves by pruning. Partition on the one or two columns queries filter on, at a healthy per-partition size.

Builder session 7 cheat sheet · pin this

PartitioningSplit data into files by a column (often date). Queries filtered on it prune - read only matching files.
Partition pruningThe payoff: a one-month query opens one month's files, skips the rest. Less scanned = faster + cheaper.
One big fileBad: every query scans everything, no skipping possible.
A million tiny filesWorse: per-file overhead swamps the read. The small-files problem. Compaction merges them back.
Pick the keyPartition on the column queries filter on most, at a grain that keeps partitions tens-to-hundreds of MB.
TiersHot (fast, pricey, frequent) · warm (occasional) · cold (cheap, slow, rare, e.g. Glacier). Latency vs cost.
Object storageS3 / GCS / Blob - cheap, near-infinite base of the modern stack. Your laptop's Parquet file, in the cloud.
Separated computeStorage scales apart from compute. Pay for the petabyte; rent CPUs only when you query.