The Storage Question: Intermediate
What you will be able to do
Three mechanisms, one file layout
A CSV or JSON file stores one record after another, so reading the country column means reading every byte of every row. Parquet groups rows into row groups and stores each column's values contiguously inside the group, so a query that touches 3 of 40 columns reads about 3/40 of the bytes, and the file's footer records the byte offset of every column chunk so the engine seeks straight to the chunks it needs.
| Property | CSV | JSON | Parquet |
|---|---|---|---|
| Schema and types | none, every field parsed from text on every read | field names repeated in every record | declared once in the footer, values stored as typed binary |
| Reading one column | full row scan | full row scan | one column chunk per row group |
| Compression | whole-file gzip, not splittable | whole-file gzip, not splittable | per column chunk, after encoding, splittable by row group |
| Statistics | none | none | min, max and null count per column per row group |
| Nested fields | flattened or quoted | native | native, as repeated and nested groups |
Types matter as much as bytes. A CSV timestamp is parsed from text on every read, so a WHERE on event_ts costs a parse per row; a Parquet timestamp is a 64-bit integer compared directly.
Worked example: 1 billion rows, 3 columns
| Step | CSV | JSON | Parquet |
|---|---|---|---|
| Bytes on disk | 400 GB | 1.2 TB | 60 GB |
| Bytes read for 3 columns | 400 GB | 1.2 TB | 1.5 GB |
| After row group skipping on event_date | 400 GB | 1.2 TB | 4 MB |
| Ratio against Parquet | 100,000x | 300,000x | 1x |
Encoding before compression
Where Parquet loses
- ▸Point lookups by id: the engine opens a row group, reads a chunk per column and reassembles the row, which costs more than a row store's single seek.
- ▸One event at a time: Parquet files are immutable, so each append writes a new file, and a day of small files is what the advanced tier of this track is about.
- ▸Updates and deletes: changing one value means rewriting the whole file that holds it.
- ▸Debugging by eye: a column chunk of packed codes tells a human nothing without the footer and the dictionary.
What a partition physically is
A partition is a directory. A table partitioned by event_date stores the 20 October rows under events/event_date=2026-10-20/ and the next day's rows under the next directory, with the partition column absent from the files themselves because its value is the directory name. When a query filters on a constant, the engine matches it against directory names and lists only the directories that match. That pruning happens before any file is opened, which places it one level above the row group statistics in the previous section: partitions decide which directories to list, statistics decide which row groups inside those files to read.
Cardinality analysis
| Partition keys | Partitions per year | Average size | Largest to smallest | Verdict |
|---|---|---|---|---|
| event_date | 365 | 50 GB | 1.5x, weekends are lighter | fits any query that filters by day |
| event_date, hour | 8,760 | 2.1 GB | 6x, nights are quiet | fits only if queries filter by hour |
| event_date, event_type (30) | 10,950 | 1.7 GB | 40x | workable if most queries name a type |
| event_date, country (200) | 73,000 | 250 MB | 5,000,000x, 10 GB against 2 KB | most partitions are a single tiny file |
| event_date, user_id (50 M) | up to 18 billion | 1 KB | none, every partition is tiny | never |
The over-partition trap
High-cardinality keys: sort or bucket, never partition
- Queries filter on it with constants most of the time
- Distinct values stay in the hundreds or low thousands
- Each value holds at least 1 GB over the table's life
- Old values stop receiving writes, so a date or a month
- Distinct values run to millions, such as user or order ids
- It is the join key between 2 large tables
- Lookups need one file rather than one directory
- The value is spread across all time, so no partition would ever close
The problem a directory cannot solve
How each format writes the log
Delta Lake keeps a _delta_log/ directory of numbered JSON commits, 00000000000000000041.json for commit 41, each listing the files added and removed and any schema change. Every 10 commits it writes a Parquet checkpoint of the full state so readers replay only the tail. A commit is the atomic creation of the next number; if 2 writers race for 41, the object store accepts 1 and the other rereads the log and retries as 42.
Iceberg keeps a tree. A metadata.json file names the current snapshot and the schema and partition spec history; each snapshot points to a manifest list; each manifest lists data files with per-file column statistics. A commit swaps the pointer to the new metadata file in the catalog, 1 atomic operation, and a loser in a race rebases on the winner's snapshot and retries. Because statistics live in the manifests, planning a query reads a few metadata files rather than 73,000 directory listings.
Three axes, compared
| Property | Delta Lake | Iceberg |
|---|---|---|
| Atomic commit | create the next numbered log file; collisions retry | swap the catalog pointer to a new metadata file; collisions rebase and retry |
| Isolation | readers pin to the log version they started on | readers pin to the snapshot they started on |
| Time travel | by version number or timestamp, back to the oldest retained log entry | by snapshot id or timestamp, back to the oldest retained snapshot |
| Retention | VACUUM removes files older than the retention window, 7 days by default | expire_snapshots removes snapshots and the files only they reference |
| Schema evolution | add, rename and drop; rename and drop require column mapping mode so files are matched by id rather than name | columns carry ids from creation, so add, rename, drop and reorder never touch data files |
| Partition evolution | partition columns are physical; changing them rewrites the table | partition spec is versioned; new data uses the new spec, old files keep theirs, queries plan across both |
| File statistics | per file in the log, first 32 columns by default | per file in manifests, configurable per column |
Making the choice in the room
Three shapes, separated by who owns the files
| Axis | Warehouse | Data lake | Lakehouse |
|---|---|---|---|
| Who can read the files | the warehouse engine | any engine with a Parquet reader | any engine that speaks the table format |
| Transactions | full, row level | none without a table format | snapshot-level ACID from the log |
| Updates and deletes | in place, cheap | rewrite files by hand | rewrite files, tracked by the log |
| Raw and semi-structured data | loaded and typed first | stored as landed | stored as landed, promoted when typed |
| Storage price | the vendor's rate, often near object storage | object storage rate | object storage rate plus log and snapshot overhead |
| Compute price | billed by the vendor's unit on every read | whichever engine you run, including free ones | same as the lake |
| Operational burden | lowest | highest: compaction, statistics and consistency are yours | medium: the format handles consistency, compaction stays yours |
A scenario with 2 workloads
Raw stays open and cheap, the curated layer gains transactions from the table format, and only the 2 TB that 400 people query sub-second pays warehouse prices.
Where the lakehouse pays off
The questions that decide it
- ▸'Lake, because storage is cheaper' with no consumer named: the 400 dashboard users have no index and no transactions.
- ▸'Warehouse for everything': 500 TB of training data paid for twice, storage and per-scan compute, through a connector.
- ▸'Lakehouse' as a brand word: the interviewer asks what the log does, and the mechanism from the previous section is the answer they want.
The lines on the bill
| Tier | Price per GB-month | Retrieval | Minimum duration | Fits |
|---|---|---|---|---|
| Standard | $0.023 | none, milliseconds | none | last 30 days, queried daily |
| Infrequent access | $0.0125 | $0.01 per GB, milliseconds | 30 days | 31 to 365 days, queried weekly |
| Archive | $0.0036 | per GB plus hours to restore | 90 days | retained for compliance, read once a year or never |
Prices are the published list prices of the largest cloud object store for its US regions in 2026, rounded. Your provider and region differ by tens of percent, and the ratios between tiers are what the model turns on.
Worked model: 1 year of the events table
Compute: where the format decision gets multiplied
Two smaller lines that bite
- Convert at landing so the 6.7x size ratio applies to every downstream copy
- Set lifecycle tiers from the longest query window rather than the ingest rate
- Price a query by bytes scanned, and quote it per day at dashboard volumes
- Count retained snapshots as storage you pay for
- Quote storage alone when a per-scan engine is in the design
- Archive data a dashboard still reads
- Mistake request dollars for the small-file cost; the cost is latency
- Assume list prices match across providers and regions
> A food delivery company lands 60 GB a day of order and courier location events as JSON, must keep them 5 years for disputes, trains delivery-time models on 6 months of history, settles late order corrections every morning, and serves a finance dashboard of settled orders to 300 people who compare each month to the same month last year.
Parquet, partitions, table formats, lake or warehouse, and the bill: the storage question with its numbers
- Category
- Pipeline Architecture
- Difficulty
- intermediate
- Duration
- 35 minutes
- Challenges
- 2 hands-on challenges
Topics covered: Why Parquet?, How Would You Partition?, Delta or Iceberg?, Data Lake or Warehouse?, How Much Will This Cost?
Lesson Sections
- Why Parquet? (concepts: Columnar vs Row Formats)
Three mechanisms, one file layout The question arrives in a plain form: the events land as JSON, so why convert them to Parquet before anyone queries them? A complete answer names 3 mechanisms in the file layout and puts a number on each. Columnar storage cuts the bytes a query must read. Column-aware encoding shrinks what remains before compression touches it. Footer statistics let the engine skip whole blocks without reading them. Types matter as much as bytes. A CSV timestamp is parsed from t
- How Would You Partition? (concepts: Partitioning Strategies)
What a partition physically is Cardinality analysis The partition count is the product of the distinct values of every partition key, and each partition should hold enough data to fill files of 128 MB to 1 GB. Those 2 facts turn the question into arithmetic. Before choosing, measure the candidates against the data you have. Take the events table at 50 GB of Parquet per day and score each candidate key set by partitions per year, average partition size, and skew between the largest and smallest p
- Delta or Iceberg? (concepts: Table Formats)
The problem a directory cannot solve A partitioned directory of Parquet files has no definition of the table other than whatever a list call returns at the moment it runs. That produces 4 failures the interviewer will probe. A writer that dies after uploading 3 of 8 files leaves 3 orphans that every reader counts. Two writers that overwrite the same day each believe they finished, and the last upload wins silently. A reader that starts during a rewrite sees old files for one partition and new fi
- Data Lake or Warehouse? (concepts: Data Lake Architecture)
Three shapes, separated by who owns the files A warehouse stores tables in its own internal format that only its engine reads, and in return gives transactions, row-level updates, indexes or clustering, and query performance that nobody has to tune at the file level. A data lake stores open files, Parquet or JSON, on object storage that any engine can read, charges the object store's rate for storage, and gives no transactions, no schema enforcement and no consistent view unless something adds t
- How Much Will This Cost? (concepts: Columnar vs Row Formats, Compression & Encoding)
The lines on the bill A storage estimate has 4 lines, and in analytics the last one usually dominates. Storage is billed per GB-month by tier. Requests are billed per 1,000 calls, with list and put costing about 10 times a get. Retrieval from cold tiers is billed per GB restored, on top of a wait. Compute is billed either by the TB a query scans or by the hours a cluster runs, and the scan model prices the format and partition decisions from the first 2 sections directly. Prices are the publishe