A price was wrong for a month
Kabir has joined a fintech in Bengaluru as a data engineer. On his first Monday the canteen app team calls: chai was billed at ₹15 all of September, but the real price was ₹18, and a coffee supplier's orders turned out to be test data. "Fix the table, but finance must still be able to see what the report said last week." On a folder of CSV files that would mean rewriting everything and losing the old version. Arjun Sir shows him a table format that keeps both.
In the Hadoop course we only ever wrote whole files. How do I change a few rows without breaking the September report?
With a table format like Apache Iceberg. The data still lives in HDFS as Parquet files, but Iceberg keeps a list of which files make up the table at each moment: a snapshot. DELETE and UPDATE write new files and a new snapshot; the old snapshot stays readable. That is the lakehouse idea: warehouse features on cheap lake storage.
And that's Kabir's whole job? Fixing tables?
That's one hour of it. Today we follow a data engineer through a real day: night jobs, a failed pipeline, a schema change, a backfill, a migration task. And we run the Iceberg part on our own cluster.
Pipelines, migrations and the lakehouse
A data pipeline is an automated, scheduled sequence of steps that ingests data from sources, validates and transforms it through stages (commonly raw/bronze, cleaned/silver and aggregated/gold) and publishes it for consumers, run by an orchestrator such as Apache Airflow that manages dependencies, retries, backfills and SLAs. A migration moves data, metadata and workloads from one platform to another (for example from an on-premises Hadoop cluster to cloud object storage or to a lakehouse), through inventory, copy, dual-running and reconciliation, cut-over and decommissioning. A lakehouse stores data in open file formats on low-cost storage (HDFS or object stores) and adds warehouse capabilities through an open table format (Apache Iceberg, Delta Lake, Apache Hudi): ACID transactions, row-level updates and deletes, snapshots and time travel, schema and partition evolution, all exposed through a catalog to several engines (Spark, Trino, Flink, Hive).
A data engineer builds and runs the machines that move data every day, moves them to new platforms when the company changes, and today usually stores the results in tables that behave like a database but live as files in the lake.
Words you'll meet in this lesson
Architecture at a glance
A modern lakehouse on Hadoop or cloud storage. The green boxes are what we ran in this lesson; the rest is the usual company setup around it.
From our lab (Spark 4.0.4 + Iceberg 1.10.2 on Hadoop 3.5.0, YARN)
| Load | 500,000 orders from student_orders.csv into lake.canteen.orders, partitioned by days(ts); revenue ₹12,852,002 |
|---|---|
| Fix | DELETE … WHERE item = 'coffee' and UPDATE … SET amount = 18 WHERE item = 'chai' → 459,636 orders, ₹12,493,729 |
| Snapshots | append (added 500,000) → overwrite (added 459,636, deleted 500,000) → overwrite (added 459,636, deleted 459,636): copy-on-write rewrites whole files |
| Time travel | VERSION AS OF the first snapshot still answers 500,000 / ₹12,852,002 |
| In HDFS | data: 31 folders (one per day + root), 90 files, 7,617,041 bytes · metadata: 14 files (v1–v3.metadata.json, snap-*.avro manifest lists, manifests) |
| Incident | first run failed: "Self-suppression not permitted", then "could only be written to 0 of the 1 minReplication nodes": the disk was 97% full |
The job at three levels
Beginner A day in the life
- A data engineer is the plumber of a company's data: makes sure every morning's reports and models get fresh, correct data.
- Mornings start with checking last night's jobs. Days fill with fixing breaks, building new pipelines and answering "why is this number different?".
- The tools change (Hadoop, Spark, cloud, lakehouse), the job stays: move data reliably, prove it is right, keep costs sane.
Intermediate Pipelines that survive Monday
- Layers: bronze keeps raw data exactly as received (so you can always rebuild), silver cleans and types it, gold aggregates for the business.
- Idempotent steps: each run overwrites its own partition (e.g.
dt=2026-09-14) instead of appending, so a retry or backfill never doubles numbers. - Orchestration: Airflow DAGs declare dependencies, retries and SLAs;
airflow dags backfillre-runs a date range. Oozie, the old Hadoop scheduler, was retired to the Apache Attic in February 2025. - Quality gates: row counts, null checks and reconciliations against the source fail the DAG before bad data reaches gold (counters from Lesson 9.4 are the MapReduce version of this).
Advanced Migrations and the lakehouse
- Migration path: inventory (tables, jobs, users, sizes) → copy (DistCp and snapshot-diff, Lesson 12.2) → dual-run old and new → reconcile counts and sums → cut over consumers → decommission.
- Hive tables to Iceberg: migrate in place (Iceberg reads the existing Parquet/ORC files and only writes metadata) or rewrite with
CREATE TABLE AS SELECTto fix partitioning and file sizes (Lesson 7.5). - Jobs: MapReduce, Pig and Hive jobs are usually rewritten as Spark SQL or dbt models; the logic (keys, joins, aggregations from Modules 8–9) carries over.
- Table formats: Iceberg, Delta Lake and Hudi all give ACID commits, snapshots, time travel and schema evolution; Iceberg's hidden partitioning (
days(ts)) means queries filter ontsand never name the partition column.
Exam tip: For "What is a lakehouse?", write: open files (Parquet) on cheap storage + an open table format (Iceberg/Delta/Hudi) that adds ACID transactions, row-level changes, snapshots/time travel and schema evolution, read by several engines through a catalog. Give one example: our DELETE + UPDATE made snapshot 3 while snapshot 1 stayed readable.
Table formats, pipeline styles and migration routes
| Apache Iceberg | Delta Lake | Apache Hudi | |
|---|---|---|---|
| Origin | Netflix, Apache project | Databricks, Linux Foundation | Uber, Apache project |
| Strength | Engine-neutral; hidden partitioning; partition evolution; REST catalog | Deep Spark/Databricks integration | Upserts and incremental pulls for streaming CDC |
| Row changes | Copy-on-write or merge-on-read | Copy-on-write; deletion vectors | Copy-on-write or merge-on-read |
| Shipped in EMR 7.14 | 1.10.1 | 3.3.2 | 1.0.2 |
Pipeline and migration choices
Batch (daily/hourly)
Airflow runs Spark jobs per partition; simplest and cheapest. Most finance and reporting pipelines.
most commonStreaming
Kafka → Flink or Spark Structured Streaming → Iceberg every minute; for fraud alerts and live dashboards.
freshLift and shift
Copy HDFS to object storage, run the same Hive/Spark jobs on a cloud cluster. Fast, keeps old problems.
quickModernise
Rewrite into a lakehouse: Iceberg tables, Spark SQL/dbt, Airflow. Slower, but fixes small files and old engines.
long-termAnimated example: one day at a fintech
- 09:30: at stand-up the overnight settlement DAG is red; gold tables are a day old and banks expect the file by 11:00.
- 10:15: the cause: a partner added a column without warning, and the parser rejects rows with 9 fields.
- 12:00: the silver schema gets the new column and the failed day is re-run; because the step overwrites its own partition, nothing is counted twice.
- 14:30: migration work: the old Hive job and the new Spark + Iceberg job ran on the same day; row counts and rupee totals must match.
- 16:30: cost review finds lakhs of tiny files; compaction and snapshot expiry get scheduled.
- 18:30: hand-over: runbook updated, a data contract drafted and a new quality check added. (Illustrative day.)
Make every step idempotent before you need a backfill: write each run's output to its own partition with overwrite, never blind append. Then "re-run September" is a calm command instead of a cleanup project.
A lakehouse table on our Hadoop cluster
Spark 4.0.4 (Lesson 13.2) with the Iceberg runtime jar from Maven Central, on our YARN, writing to HDFS. The catalog is the simplest kind, a folder in HDFS (type=hadoop).
Create a partitioned Iceberg table and load it
q("CREATE TABLE lake.canteen.orders (order_id BIGINT, student STRING, ts TIMESTAMP, item STRING, amount INT) " "USING iceberg PARTITIONED BY (days(ts))") src.writeTo("lake.canteen.orders").append()-- 1. loaded +------+--------+ |orders|revenue | +------+--------+ |500000|12852002| +------+--------+days(ts)is hidden partitioning: Iceberg derives the day from the timestamp. Queries filter ontsand Iceberg prunes days on its own.Change rows with SQL
12,852,002 − 40,364 × 20 (coffee) + 149,669 × 3 (chai +₹3) = 12,493,729. On a plain folder of CSV or Parquet files this needs a full rewrite job.DELETE FROM lake.canteen.orders WHERE item = 'coffee'; UPDATE lake.canteen.orders SET amount = 18 WHERE item = 'chai';-- 2. after DELETE coffee + UPDATE chai price +------+--------+ |orders|revenue | +------+--------+ |459636|12493729| +------+--------+Every change is a snapshot
Copy-on-write: the DELETE rewrote every data file without the coffee rows (500,000 out, 459,636 in), and the UPDATE rewrote them again. Old files stay until the snapshots that use them are expired.SELECT snapshot_id, operation, summary['added-records'], summary['deleted-records'] FROM lake.canteen.orders.snapshots ORDER BY committed_at|6640816061555354185|append |500000|NULL | |4428609581335996210|overwrite|459636|500000 | |1149537224652247478|overwrite|459636|459636 |Time travel
Finance's request from the story: last week's report can be reproduced exactly, while today's table is corrected.SELECT count(*), sum(amount) FROM lake.canteen.orders VERSION AS OF 6640816061555354185+------+--------+ |orders|revenue | +------+--------+ |500000|12852002| +------+--------+Evolve the schema
A metadata-only change: no data file is rewritten. Iceberg tracks columns by ID, so rename and drop are safe too (compare Avro's rules in Lesson 10.1).ALTER TABLE lake.canteen.orders ADD COLUMN payment STRING|order_id|item |amount|payment| |1 |dosa |45 |NULL | |2 |samosa|18 |NULL |Look inside HDFS
It is still just HDFS: day folders of Parquet files plus JSON and Avro metadata. A commit is one atomic swap of the currenthdfs dfs -count /lakehouse/canteen/orders/data /lakehouse/canteen/orders/metadata31 90 7617041 /lakehouse/canteen/orders/data 1 14 75103 /lakehouse/canteen/orders/metadata v1.metadata.json v2.metadata.json v3.metadata.json snap-….avro (×3)metadata.json; that is where the transaction comes from.
Our first run on YARN failed with "IllegalArgumentException: Self-suppression not permitted", which hides the real cause. The next attempt showed it: "could only be written to 0 of the 1 minReplication nodes". The laptop disk was 97% full and the DataNodes had 1.4–1.8 GB left. Freeing space fixed it. Monitor DataNode free space (Lesson 12.4) and read past the first exception (Lesson 12.6).
SQL on the left, files in HDFS on the right
Six steps of our Iceberg run. Watch which files each statement adds and which snapshot the table points to.
- The table is created with hidden day partitions and 500,000 orders are appended.
- One SQL DELETE removes the 40,364 coffee orders.
- One SQL UPDATE fixes the chai price; revenue becomes ₹12,493,729.
- The snapshots table shows three commits: an append and two overwrites.
- Time travel to the first snapshot still gives the original numbers.
- A new column appears instantly; existing rows read it as NULL.
- The append writes the first set of Parquet files, one folder per day, and v1.metadata.json points to snapshot 1.
- The DELETE writes a second set without coffee and commits v2: snapshot 2 is now current.
- The UPDATE writes a third set with the new chai price and commits v3.
- Manifest lists (snap-*.avro) record which files belong to each snapshot.
- Time travel reads snapshot 1's files, which are still in HDFS.
- Adding a column only writes new metadata; data files are untouched. (File squares are symbolic.)
Data engineering at work
1. Payments: daily settlement
A payments company builds the next-day settlement files for partner banks from the previous day's transactions, with reconciliation against bank statements before anything is sent.
2. Telecom: migrating years of call records
An operator moves petabytes of Hive tables on HDFS to Iceberg: old partitions are migrated in place (metadata only), new data is written by Spark, and both run side by side for a month.
3. E-commerce: live sale dashboards
During a festival sale, clickstream flows through Kafka and Flink into Iceberg every minute, and Trino dashboards show orders per city while the batch layer keeps the full history.
4. Insurance: audit-ready reports
An insurer tags the snapshot used for each regulatory filing, so auditors can query exactly the data behind a report months later, even after corrections.
5. Ed-tech: deleting a learner's data
When a learner asks for their data to be erased, a row-level DELETE runs on every table that holds it, and old snapshots are expired so the deleted rows really leave storage.
Simplified; organisations differ in tools and policies.
Sources (checked 22 Oct 2026): Apache Attic: retired projects (Oozie Feb 2025, Sqoop Jun 2021) · Amazon EMR 7.x application versions (Iceberg 1.10.1, Delta 3.3.2, Hudi 1.0.2) · Cloudera Base 7.3.2 summary (Iceberg REST catalog sharing) · endoflife.date: Apache Spark
A table that keeps its history
A copy-on-write table in a few lines of Python: every change writes a new list of rows (new "files") and records a snapshot. The numbers are our lab's real totals, aggregated per item.
# per item: (orders, price) from our 500,000-order table
base = {"chai": (149669, 15), "coffee": (40364, 20), "dosa": (49927, 45), "idli": (44774, 30),
"poha": (45336, 25), "samosa": (69884, 18), "thali": (30292, 80), "vada pav": (69754, 20)}
snapshots = []
def commit(op, rows):
snapshots.append((len(snapshots) + 1, op, dict(rows))) # copy-on-write: a whole new version
def stats(rows):
return sum(n for n, p in rows.values()), sum(n * p for n, p in rows.values())
commit("append", base)
v2 = {k: v for k, v in snapshots[-1][2].items() if k != "coffee"} # DELETE coffee
commit("overwrite", v2)
v3 = {k: (n, 18 if k == "chai" else p) for k, (n, p) in snapshots[-1][2].items()} # UPDATE chai
commit("overwrite", v3)
for sid, op, rows in snapshots:
print(sid, op.ljust(9), "orders %d, revenue %d" % stats(rows))
print("time travel to 1:", "orders %d, revenue %d" % stats(snapshots[0][2]))snapshots.append((len(snapshots) + 1, op, dict(rows))) # copy-on-write: a whole new versionLike Iceberg's copy-on-write: a change writes new data and a new snapshot; the old one is untouched.v3 = {k: (n, 18 if k == "chai" else p) for k, (n, p) in snapshots[-1][2].items()} # UPDATE chaiOur real UPDATE: chai from 15 to 18 rupees.print("time travel to 1:", "orders %d, revenue %d" % stats(snapshots[0][2]))VERSION AS OF the first snapshot gave 500,000 and 12,852,002 in the lab.
Run it on your PC: an Iceberg lakehouse on the Module 8 lab
Use the Module 8 lab with Spark 4.0.4 installed as in Lesson 13.2. Download the Iceberg runtime for Spark 4.0 (iceberg-spark-runtime-4.0_2.13-1.10.2.jar, 47 MB) from Maven Central (repo1.maven.org/maven2/org/apache/iceberg/) and copy it into the namenode container. The table lives in hdfs://namenode/lakehouse. Keep at least a few GB free on your disk: our first attempt failed because the disk was 97% full.
Load, change, time travel, evolve
# lakehouse_iceberg.py: an Apache Iceberg table on HDFS (Spark 4.0 + Iceberg 1.10).
# Things a plain folder of Parquet files cannot do: row-level DELETE/UPDATE, snapshots, time travel,
# schema evolution - all as ordinary SQL, with the data still sitting in HDFS.
from pyspark.sql import SparkSession
spark = (SparkSession.builder.appName("lakehouse demo")
.config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions")
.config("spark.sql.catalog.lake", "org.apache.iceberg.spark.SparkCatalog")
.config("spark.sql.catalog.lake.type", "hadoop") # catalog = a folder in HDFS
.config("spark.sql.catalog.lake.warehouse", "hdfs://namenode/lakehouse")
.getOrCreate())
spark.sparkContext.setLogLevel("ERROR")
q = lambda sql: spark.sql(sql)
def show(title, sql):
print(f"-- {title}"); q(sql).show(truncate=False)
q("DROP TABLE IF EXISTS lake.canteen.orders PURGE")
q("CREATE TABLE lake.canteen.orders (order_id BIGINT, student STRING, ts TIMESTAMP, item STRING, amount INT) "
"USING iceberg PARTITIONED BY (days(ts))")
src = (spark.read.csv("hdfs://namenode/user/hadoop/data/student_orders.csv", inferSchema=True)
.toDF("order_id", "student", "ts", "item", "amount"))
src.writeTo("lake.canteen.orders").append()
show("1. loaded", "SELECT count(*) AS orders, sum(amount) AS revenue FROM lake.canteen.orders")
q("DELETE FROM lake.canteen.orders WHERE item = 'coffee'") # e.g. a supplier's orders were fake
q("UPDATE lake.canteen.orders SET amount = 18 WHERE item = 'chai'") # price correction
show("2. after DELETE coffee + UPDATE chai price", "SELECT count(*) AS orders, sum(amount) AS revenue FROM lake.canteen.orders")
show("3. every change is a snapshot", "SELECT snapshot_id, operation, summary['added-records'] AS added, summary['deleted-records'] AS deleted FROM lake.canteen.orders.snapshots ORDER BY committed_at")
first = q("SELECT snapshot_id FROM lake.canteen.orders.snapshots ORDER BY committed_at LIMIT 1").first()[0]
show("4. time travel: the table as it was at the first snapshot",
f"SELECT count(*) AS orders, sum(amount) AS revenue FROM lake.canteen.orders VERSION AS OF {first}")
q("ALTER TABLE lake.canteen.orders ADD COLUMN payment STRING")
show("5. schema evolution: a new column, old rows read as NULL", "SELECT order_id, item, amount, payment FROM lake.canteen.orders ORDER BY order_id LIMIT 3")
show("6. partitions Iceberg manages for us (first 3 days)", "SELECT partition, record_count, file_count FROM lake.canteen.orders.partitions ORDER BY partition LIMIT 3")
spark.stop()
# Inside lab8-namenode-1 (SPARK_HOME = where you unpacked Spark 4.0.4):
# export HADOOP_CONF_DIR=/opt/hadoop/etc/hadoop
# $SPARK_HOME/bin/spark-submit --master yarn --num-executors 3 --executor-memory 768m \
# --executor-cores 1 --driver-memory 768m \
# --jars iceberg-spark-runtime-4.0_2.13-1.10.2.jar lakehouse_iceberg.py
# hdfs dfs -count /lakehouse/canteen/orders/data /lakehouse/canteen/orders/metadataReal output from our cluster (YARN client lines removed). Snapshot IDs are random and differ on every run; the counts and totals are the same.
A backfill that cannot double count
Three days are loaded, day 2 is reloaded after a fix. Watch the partition dictionary: the reload replaces day 2 instead of adding to it.
Worked examples: real problems, solved and traced in memory
Four everyday data-engineering calculations from different industries.
How long will the backfill take?
Problem statement
A logistics company finds a bug in its delivery-time pipeline for the last 45 days. One day takes 12 minutes on the cluster; the team can run 5 days in parallel without hurting other jobs. How long is the backfill, and when is it done if it starts at 19:00?
45 days · 12 min/day · 5 parallel
9 waves, 108 minutes, done at 20:48
Approach
- Waves = days ÷ parallel, rounded up.
- Time = waves × minutes per day.
- Add to the start time.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
A migration timeline
Problem statement
A telecom operator moves 600 Hive tables and 900 jobs from on-prem Hadoop to an Iceberg lakehouse. A team migrates 40 jobs a week; every batch then dual-runs for 2 weeks before cut-over; the data copy (DistCp + snapshot diffs) starts at week 0 and takes 6 weeks. When can the old cluster be switched off?
900 jobs · 40/week · 2-week dual-run · 6-week copy
jobs rewritten by week 23, last cut-over week 25
Approach
- Weeks to rewrite all jobs = 900 ÷ 40.
- The last batch still needs its 2 weeks of dual-running.
- The copy must finish too; take the later date.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Do the two systems agree?
Problem statement
An insurer dual-runs claims aggregation. Old Hive output: 1,20,450 claims, ₹84.3 crore. New Spark output: 1,20,438 claims, ₹84.29 crore. Is the difference acceptable if the rule is "counts exact, money within 0.01%"?
counts and totals from both systems
counts match: False | missing: 12 money diff: 0.012%
Approach
- Compare counts exactly.
- Compare money as a percentage.
- A mismatch means find the 12 missing claims before cut-over.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
What does keeping snapshots cost?
Problem statement
A retail table is 8 TB and copy-on-write updates rewrite about 5% of its files every day. Snapshots are kept for 30 days for time travel. How much extra storage do the old files use, and what if retention drops to 7 days? (Replication or EC overhead not included.)
8 TB · 5% rewritten/day · 30 vs 7 days
30 days: 12.0 TB of old files (150% of the table) 7 days: 2.8 TB of old files (35% of the table)
Approach
- Old files per day = 5% of 8 TB.
- Kept for the retention period.
- Compare.
Solution: run it live
What happens in memory, step by step
Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.
Check yourself
Quick revision
Lesson 13.4 in one screen
- A data engineer moves data reliably every day, proves it is correct, migrates platforms and controls cost.
- Pipelines: bronze (raw) → silver (clean) → gold (business); Airflow DAGs with retries, SLAs, backfills; Oozie retired Feb 2025.
- Idempotent steps overwrite their own partition, so retries and backfills never double count.
- Migration: inventory → copy (DistCp, snapshot diff) → dual-run → reconcile → cut-over → decommission.
- Lakehouse = open files on cheap storage + table format (Iceberg/Delta/Hudi): ACID, row-level changes, snapshots, time travel, schema evolution.
- Lab: 500,000 → 459,636 orders, ₹12,852,002 → ₹12,493,729; three snapshots; time travel restored the first; 90 data files after copy-on-write.
- Expire old snapshots and compact small files, or storage grows with every change.
Interview and viva questions
What does a data engineer do every day?
Checks overnight pipelines and fixes failures, builds and changes pipelines (ingest, clean, aggregate), handles schema changes from source teams, runs backfills, adds data quality checks, supports analysts with "why is this number different" questions, works on platform migrations and reviews storage and compute costs.
What is a lakehouse?
A data architecture that keeps data in open file formats such as Parquet on low-cost storage (HDFS or object storage) and adds database features through an open table format (Apache Iceberg, Delta Lake or Apache Hudi): ACID transactions, updates and deletes, snapshots and time travel, schema evolution, read by several engines through a shared catalog.
Iceberg vs Delta Lake vs Hudi: which should I learn?
All three solve the same problem. Iceberg is engine-neutral and widely supported by Spark, Trino, Flink, Hive and the cloud platforms; Delta is closest to Spark and Databricks; Hudi is strong for streaming upserts. Learn the shared ideas (snapshots, copy-on-write vs merge-on-read, compaction, schema evolution) and one format in depth; switching later is easy.
How do you migrate a Hadoop cluster to the cloud or a lakehouse?
Take an inventory of data, tables, jobs and users; copy data with DistCp and snapshot-diff syncs; convert or rewrite tables (Hive to Iceberg in place or by rewrite); rewrite jobs (MapReduce, Pig, Hive) in Spark SQL or similar; dual-run old and new and reconcile counts and totals; cut consumers over; keep the old system read-only for a while; then decommission.
What is an idempotent pipeline?
A pipeline in which running a step again for the same input gives the same result, for example by overwriting the output partition for that date instead of appending to it. It makes retries and backfills safe and is one of the most important habits in data engineering.
Practice problems
Data-engineering exercises using our lakehouse run and everyday scenarios.
Check the revenue arithmetic
Recompute the revenue after DELETE coffee and UPDATE chai from the loaded revenue.
12,852,002 · 40,364 coffee at ₹20 · 149,669 chai +₹3
12493729
Show solution and explanation
print(12852002 - 40364 * 20 + 149669 * 3)How it works: 12,493,729, exactly the lab result.
Rows written by copy-on-write
How many rows did Iceberg write in total across the three commits?
500,000 + 459,636 + 459,636
1419272
Show solution and explanation
print(500000 + 459636 + 459636)How it works: 1,419,272 rows written to keep a 459,636-row table with full history: the price of copy-on-write.
Days in the table
The data folder had 31 directories. How many day partitions is that?
31 dirs incl. data/ itself
30 days (September 2026)
Show solution and explanation
print(31 - 1, "days (September 2026)")How it works: 30 day folders plus the data/ folder itself.
A simple quality gate
Fail the load if more than 1% of rows have a missing amount or a negative one.
a list of amounts
2 bad rows (20%) FAIL
Show solution and explanation
amounts = [15, 20, None, 45, -18, 30, 25, 18, 20, 80]
bad = sum(1 for a in amounts if a is None or a < 0)
share = bad / len(amounts)
print(f"{bad} bad rows ({share:.0%})", "FAIL" if share > 0.01 else "OK")How it works: 20% bad rows: the gate fails the task and gold is not updated.
Schema change detector
Compare yesterday's and today's column lists from a partner and report what changed.
two schemas
added: ['bank_ref'] removed: []
Show solution and explanation
old = ["txn_id", "payer", "payee", "amount", "ts"]
new = ["txn_id", "payer", "payee", "amount", "ts", "bank_ref"]
print("added:", [c for c in new if c not in old], "removed:", [c for c in old if c not in new])How it works: A new column: add it to silver (metadata-only in Iceberg) and update the data contract.
Small files after streaming
A stream commits every minute into 12 partitions. How many files a day, and how many after an hourly compaction into one file per partition?
1 commit/min · 12 files/commit
17280 files/day -> 288 after hourly compaction
Show solution and explanation
per_day = 60 * 24 * 12
print(per_day, "files/day ->", 24 * 12, "after hourly compaction")How it works: 17,280 tiny files a day become 288 (Lesson 7.5).
Snapshot retention vs cost
Old snapshot files use 12 TB at 30-day retention. Storage costs ₹1,800 per TB per month. What does cutting retention to 7 days save per year?
12 TB · ₹1,800/TB/month
Rs 198,720 per year
Show solution and explanation
saved_tb = 12 * (1 - 7 / 30)
print(f"Rs {saved_tb * 1800 * 12:,.0f} per year")How it works: About ₹1.98 lakh a year (illustrative price), at the cost of a shorter time-travel window for audits.
Merge-on-read vs copy-on-write
A 1,000-file table gets 50 small updates a day, each touching 20 files. Copy-on-write rewrites the touched files; merge-on-read writes one small delete file per update and compacts nightly. Compare files written per day.
50 updates × 20 files
copy-on-write: 1000 files/day | merge-on-read: 70 files/day
Show solution and explanation
cow = 50 * 20
mor = 50 + 20 # 50 small delete files + nightly compaction rewriting about 20 files
print("copy-on-write:", cow, "files/day | merge-on-read:", mor, "files/day")How it works: 1,000 vs 70 files written: merge-on-read suits frequent small updates; readers pay a little to merge the delete files until compaction.
Real-world scenario questions (asked in company interviews)
Data-engineering interviews mix pipeline design, debugging stories and lakehouse questions.
"Your nightly job failed. Walk me through your first hour."
The settlement file is due at 11:00.
The interviewer is checking: calm, systematic debugging and communication.
Show a strong answer
- Tell stakeholders it is late and when the next update comes; check which task failed and its logs (Lessons 11.5, 12.6).
- Find the cause (here: an upstream schema change), fix the smallest thing, re-run only the failed partition: safe because the step is idempotent.
- Afterwards: a data contract or schema check so it fails loudly and early next time.
"Finance wants corrected numbers but also last month's report exactly as published."
Corrections arrive weeks after month-end.
The interviewer is checking: snapshots, tags and retention.
Show a strong answer
- Use a table format: apply corrections with UPDATE/MERGE; tag the snapshot used for each published report.
- Reports query the tag (time travel); dashboards query the current snapshot.
- Set snapshot retention to keep tagged snapshots and expire the rest to control storage.
"Plan the move of a 2 PB Hadoop cluster to a lakehouse"
Hundreds of Hive tables, MapReduce and Pig jobs, an Oozie scheduler.
The interviewer is checking: phasing and risk control.
Show a strong answer
- Inventory and prioritise (usage, owners, dependencies); retire unused tables first.
- Copy with DistCp and snapshot diffs; migrate Hive tables to Iceberg in place or rewrite badly partitioned ones; move Oozie flows to Airflow and jobs to Spark SQL.
- Dual-run per domain with reconciliation reports, cut over consumer by consumer, keep the old cluster read-only, then decommission.
"Why not just use Parquet files in folders?"
A follow-up after you mention Iceberg.
The interviewer is checking: what the table format adds.
Show a strong answer
- Plain folders have no transactions: readers can see half-written data, and there is no safe UPDATE or DELETE.
- A table format adds atomic commits, snapshots and time travel, schema and partition evolution, and file-level statistics for pruning.
- The data is still Parquet, so any engine with a connector can read it.