Two bosses, two kinds of questions
TiffinGo is growing fast, and Kabir now gets requests from two very different people. The finance head wants one trusted number: last month's revenue by city, matching the bank to the rupee. The data scientist wants everything: raw app clicks, rider GPS trails, customer reviews and kitchen photos, to find out why customers cancel. Kabir tries to keep all of it in one MySQL database. Within a week, it breaks.
Sir, the finance report needs perfect tables. But the data scientist's photos and JSON clicks don't fit into tables at all, and every app update adds new fields that break my loads.
You're trying to put a godown inside a library, Kabir. A data warehouse is the library: only clean, checked data, on fixed shelves, so anyone can find the right answer fast. A data lake is the godown: everything kept exactly as it arrived, cheaply, in any shape.
Then why not just use the godown for everything? It holds more.
Because a godown with no catalogue becomes a swamp. Six months later nobody knows which of three copies of rider data is right, and finance can't trust any number from it.
So I need both?
Most companies use both, or a lakehouse that puts library rules on top of godown storage. Today you'll build a tiny lake and warehouse yourself and see exactly where each one shines.
What are a data lake and a data warehouse?
A data warehouse is a central repository of structured, cleaned and integrated data, organised (often as fact and dimension tables) for SQL analysis and reporting. Data must match a defined schema before it is loaded (schema-on-write). A data lake is a repository that stores large volumes of raw data in its native format (structured, semi-structured and unstructured) on cheap, scalable storage; structure is applied when the data is read (schema-on-read). A lakehouse combines lake storage with warehouse features such as ACID transactions, schema enforcement and fast SQL, using open table formats.
A warehouse keeps only clean, checked tables so business questions get trusted answers fast. A lake keeps everything as it arrives, so nothing is lost and anyone can explore it later. A lakehouse tries to do both in one place.
Words you'll meet in this lesson
Lake, warehouse and lakehouse at a glance
| Data warehouse | Structured · schema-on-write · cleaned, trusted · fast SQL · higher cost per TB · Snowflake, BigQuery, Redshift, Synapse, Teradata |
|---|---|
| Data lake | Any format · schema-on-read · raw · very cheap storage · needs a catalogue to stay useful · HDFS, S3, ADLS, Google Cloud Storage |
| Lakehouse | Lake storage + table formats (ACID, schema checks, time travel) · one copy of data for BI and ML · Delta Lake, Apache Iceberg, Apache Hudi; Databricks, Trino, Spark |
Lake vs warehouse at three levels
Beginner Library vs godown
A library (warehouse) accepts only catalogued books and puts each one on the right shelf. Finding a book is quick and you can trust it. A godown (lake) takes every delivery as it is: boxes, sacks, loose items. It holds far more, costs less per square foot, but you need a list to find anything.
- Warehouse questions: “What was revenue by city last month?” “How many orders were cancelled?” Clean, repeated, business questions.
- Lake questions: “Do photos with good lighting get more orders?” “What do angry reviews have in common?” New, exploratory questions on raw data.
Intermediate Schemas, formats, zones and models
- Schema-on-write vs schema-on-read: the warehouse rejects a row with text in a number column; the lake stores it and the reader decides what to do.
- ETL vs ELT: classic warehouses cleaned data before loading (ETL). Modern platforms load raw data first and transform inside (ELT), often with SQL tools like dbt.
- File formats: CSV and JSON are easy but slow; Parquet and ORC are columnar and compressed; Avro is row-based and common for streaming.
- Partitioning: folders like
date=2026-10-19/let queries skip days they don't need. - Medallion zones: bronze (raw), silver (cleaned, de-duplicated), gold (business-ready aggregates).
- Star schema: one central fact table (orders) joined to dimension tables (kitchen, customer, date). It makes BI queries simple and fast.
Advanced Lakehouse internals, governance and cost
- Open table formats (Iceberg, Delta Lake, Hudi) keep a transaction log next to Parquet files. That gives ACID commits, MERGE/UPDATE/DELETE on lake data, schema evolution and time travel (“query the table as it was yesterday”).
- Catalog and governance: Hive Metastore, AWS Glue Data Catalog or Unity Catalog record tables, owners, schemas and lineage; access policies and masking protect personal data.
- Performance: partition pruning, column pruning, file statistics (min/max per file) and avoiding the small-files problem (millions of tiny files slow every query).
- Cost model: lake storage is cheap and compute is paid per query or per cluster. Modern warehouses also separate storage from compute, so the line between lake and warehouse is blurring.
- Choosing: many companies keep a lake (or lakehouse) as the single source of raw truth and serve curated gold tables to a warehouse or directly to BI.
Exam tip: Tabulate data lake vs data warehouse on: data type, schema (on-read vs on-write), data quality, users, processing (ELT vs ETL), cost, performance and examples. Then add one line on the lakehouse and the data swamp risk.
Side by side, and the three zones of a lake
| Data warehouse | Data lake | Lakehouse | |
|---|---|---|---|
| Data | Structured tables | Anything: tables, JSON, logs, images, audio | Anything, with tables on top |
| Schema | On write (enforced at load) | On read (applied by the reader) | Enforced for tables, flexible for raw zone |
| Quality | Cleaned and trusted | Raw, mixed quality | Raw → cleaned in zones |
| Main users | Business analysts, finance, BI | Data engineers, data scientists | Both |
| Storage cost | Higher | Lowest | Low (lake storage) |
| Updates / deletes | Easy (SQL) | Hard (rewrite files) | Easy (table formats) |
| Typical tools | Snowflake, BigQuery, Redshift | S3, ADLS, GCS, HDFS | Delta Lake, Iceberg, Hudi + Spark/Trino |
The medallion zones
Bronze (raw)
Data exactly as received: duplicates, wrong types, extra fields and all. Never edited, so it can always be reprocessed.
Keep everythingSilver (clean)
De-duplicated, typed, validated and joined where needed. One row per real-world thing. Data scientists usually start here.
Trust the rowsGold (business)
Aggregated, business-ready tables: daily revenue by city, cancellation rate by kitchen. Dashboards and finance read these.
Answer questionsFile formats you will meet
| Format | Layout | Good for |
|---|---|---|
| CSV | Text rows | Simple exchange, small data; no types, no compression |
| JSON | Text, nested | Events from apps and APIs; flexible but large |
| Avro | Binary rows with schema | Streaming (Kafka) and record-by-record writes |
| Parquet / ORC | Binary columns, compressed | Analytics: read only needed columns, much smaller files |
A lake without a catalogue becomes a swamp. Every dataset needs an owner, a description, a schema and a freshness check recorded somewhere people can search. Otherwise teams create copies, nobody trusts any of them, and storage bills grow for data nobody uses.
One messy order, from bronze to the finance report
Bronze: store it exactly as it arrived
The same order arrived twice (an app retry) and the amount is text. The lake stores both lines anyway. Rejecting data at this stage would lose evidence we may need later.{"order_id": 2, "kitchen": "K12", "item": "biryani", "amount": "250"} {"order_id": 2, "kitchen": "K12", "item": "biryani", "amount": "250"}Silver: clean while reading (schema-on-read)
The reader decides the structure: one row per order, amount as an integer. The result is written as Parquet in the silver zone.raw = [{"order_id": 2, "amount": "250"}, {"order_id": 2, "amount": "250"}] seen, silver = set(), [] for r in raw: if r["order_id"] not in seen: seen.add(r["order_id"]) silver.append({"order_id": r["order_id"], "amount": int(r["amount"])}) print(silver)[{'order_id': 2, 'amount': 250}]Gold: load into a warehouse table (schema-on-write)
The warehouse table has rules. A row with amount −90 is rejected at load time. Finance can trust every row that got in.CREATE TABLE fact_orders ( order_id INTEGER PRIMARY KEY, kitchen TEXT REFERENCES dim_kitchen(kitchen), amount INTEGER NOT NULL CHECK (amount > 0) );Model: facts and dimensions
A star schema: the fact table in the middle, small descriptive tables around it. Any “by city” or “by month” question is a simple join.-- fact_orders(order_id, kitchen, date_key, amount) -- dim_kitchen(kitchen, kitchen_name, city) -- dim_date(date_key, day, month, is_weekend)Query: the finance number
Fast and trusted, because the hard work (de-duplicating, fixing types, rejecting bad rows) happened before. Meanwhile, the data scientist can still go back to bronze for the raw reviews and clicks.SELECT k.city, SUM(f.amount) AS revenue FROM fact_orders f JOIN dim_kitchen k ON f.kitchen = k.kitchen GROUP BY k.city;
A week of requests: what each team sees, and where the data lives
Step through six frames. Left: the requests Kabir receives. Right: what happens in the lake and the warehouse.
- Four kinds of data arrive every day: order rows, JSON clicks, review text and kitchen photos.
- Finance wants October revenue by city, matching the bank exactly.
- The data scientist wants raw reviews and photos to study cancellations.
- The app team adds a new
couponfield overnight. - Six months later, a new analyst finds three copies of rider data and no owner.
- After a redesign, every team gets what it needs from one platform.
- The lake stores all four shapes as files. The warehouse only accepts data that fits its tables.
- Loading orders into the warehouse checks every row: duplicates removed, the −90 order rejected. This is schema-on-write.
- The data scientist reads raw reviews, photos and orders from the lake and decides the structure in the notebook: schema-on-read.
- A new
couponfield is stored by the lake without trouble, but breaks the warehouse load until the table and pipeline are changed. - Without owners or documentation, the lake fills with copies and test folders: a data swamp.
- The redesign: a lakehouse with bronze, silver and gold tables on lake storage, open table formats and a catalogue.
Real-time scenarios inside a data platform
Watch a single record being refined, see how a catalogue saves a new analyst's week, and (advanced) see why the right file layout makes a query 100 times cheaper.
Real-time scenario 1The refinery: one order record from bronze to gold
At 12:31 an order event lands in the lake. Watch it move through the zones over the next hour, and see exactly what changes at each step.
- Bronze, 12:31: the event is stored exactly as received, duplicate and text amount included.
- Silver, 12:45: a job removes the duplicate, converts the amount to a number and the time to local time, and writes a clean table.
- Gold, 13:30: another job joins kitchens to cities and totals the day: one business-ready row per city.
- 16:00: someone finds a bug: refunds were counted as sales.
- 16:20: the code is fixed and silver and gold are rebuilt from bronze. Keeping raw data is what made the fix possible.
Swamp vs catalogue: a new analyst's first day
Two companies, same lake size. A new analyst is asked: “How many km did riders travel last week?” Watch both days unfold.
- 09:30: in Company A, the analyst finds five similarly named folders. In Company B, she searches the catalogue.
- 11:00: A's analyst is told to ask someone who has left. B's analyst sees a table with an owner, column description, freshness and lineage.
- 14:00: A's analyst opens folders with different columns and dates and guesses. B's analyst ran one SQL query at 09:50.
- 17:30: A's answer is 27% too high because it included test data and duplicates. B moved on hours ago.
- Same storage, same cost. Metadata and governance make the difference between a lake and a swamp. (Numbers illustrative.)
Same question, 100× less data read: partitions + columns
The table orders holds one year of data: 1.1 TB, 20 columns. The question: “Revenue in Pune during October?” Watch how file layout changes how much data the engine must read (and how much you pay, on pay-per-scan engines).
- CSV in one folder: to answer one question, the engine reads all 1.1 TB, every row and every column.
- Partitioned by month: data stored in folders like
month=10/, so the engine reads only October: about 93 GB. - Parquet (columnar): only the
cityandamountcolumns are read, about a tenth of the bytes. - Compression and file statistics: files whose min/max city values exclude Pune are skipped; compressed columns are smaller still.
- From 1.1 TB to roughly 3 GB. On engines that charge per byte scanned, this is the difference between a costly and a cheap query. (Sizes are illustrative.)
Who uses what
1. Banks: warehouse for reports, lake for fraud models
Regulatory and finance reports come from a tightly controlled warehouse where every number is audited. Fraud teams train models on years of raw transactions, device logs and call-centre notes kept in the lake.
2. Hospitals: images in the lake
X-rays and scans are huge unstructured files that belong in a lake; billing, admissions and pharmacy data live in warehouse tables. Researchers join both, with strict access control over patient data.
3. E-commerce: lakehouse with medallion zones
Clicks, orders and catalogue changes land in bronze, are cleaned into silver and summarised into gold. Dashboards read gold; recommendation models read silver. One copy of data, two kinds of users.
4. Telecom: cheap history, fast summaries
Billions of call and data records per day are kept in the lake as compressed, partitioned files for years. Daily summaries per customer and tower are loaded into the warehouse for billing and network dashboards.
5. Startups: start with a warehouse
A young company with only database tables often starts with a cloud warehouse and adds a lake later, when it needs raw events, images or machine learning. There is no rule that you need both on day one.
Simplified examples; real platforms vary.
Schema-on-write vs schema-on-read
The same four order rows go to two places. The warehouse checks every row against a schema before storing it; the lake stores everything as it is. Later, someone reads the lake and applies their own rules. Compare which rows survive where. Then try the challenges: medallion zones, a star schema, columnar storage, partitions and schema changes.
rows = [
{"order_id": 1, "kitchen": "K11", "amount": 120},
{"order_id": 2, "kitchen": "K12", "amount": "250"}, # text, not a number
{"order_id": 3, "kitchen": "K13", "amount": -90}, # impossible value
{"order_id": 4, "kitchen": "K11", "amount": 120, "coupon": "LUNCH20"}, # new column
]
SCHEMA = {"order_id": int, "kitchen": str, "amount": int}
def warehouse_load(row): # schema-on-WRITE: check before saving
extra = set(row) - set(SCHEMA)
if extra:
return f"REJECTED: unknown column {sorted(extra)}"
for col, typ in SCHEMA.items():
if not isinstance(row[col], typ):
return f"REJECTED: {col} is not {typ.__name__}"
if row["amount"] <= 0:
return "REJECTED: amount must be > 0"
return "stored"
lake = []
def lake_store(row): # schema-on-READ: store anything now
lake.append(row)
return "stored as-is"
for r in rows:
print(f"order {r['order_id']}: warehouse -> {warehouse_load(r):36} lake -> {lake_store(r)}")
# Later, an analyst READS the lake and applies a schema for their own question
revenue = sum(int(r["amount"]) for r in lake if int(r["amount"]) > 0)
print("Revenue read from the lake (cleaned while reading): Rs", revenue)SCHEMA = {"order_id": int, "kitchen": str, "amount": int}The warehouse's contract: exactly these columns, with these types.extra = set(row) - set(SCHEMA)A newcouponcolumn is rejected: strict schemas protect quality but make change slower.lake.append(row)The lake keeps every row, even broken ones. Nothing is lost, but nothing is checked either.int(r["amount"]) for r in lake if int(r["amount"]) > 0Schema-on-read: the reader converts types and filters bad rows at query time, for this question only.
Run it on your PC: build a mini lake and a SQL warehouse
This program creates a real folder-based data lake (lake/bronze, lake/silver), then a real SQL warehouse using SQLite, which comes built into Python. The warehouse table has a CHECK rule, so it rejects the impossible order at load time, while the lake kept it. Save it as mini_lake.py in an empty folder and run it; then open the folders it creates.
Bronze → silver → SQL warehouse
import csv, json, os, sqlite3
# ---------- BRONZE: the data lake keeps raw data exactly as it arrived ----------
raw_dir = os.path.join("lake", "bronze", "orders", "date=2026-10-19")
os.makedirs(raw_dir, exist_ok=True)
raw_events = [
{"order_id": 1, "kitchen": "K11", "item": "thali", "amount": 120},
{"order_id": 2, "kitchen": "K12", "item": "biryani", "amount": "250"}, # amount as text
{"order_id": 2, "kitchen": "K12", "item": "biryani", "amount": "250"}, # duplicate (retry)
{"order_id": 3, "kitchen": "K13", "item": "dosa", "amount": -90}, # impossible amount
{"order_id": 4, "kitchen": "K11", "item": "thali", "amount": 120, "coupon": "LUNCH20"}, # new field!
]
with open(os.path.join(raw_dir, "part-0001.json"), "w") as f:
for e in raw_events:
f.write(json.dumps(e) + "\n")
with open(os.path.join("lake", "bronze", "review_0001.txt"), "w") as f:
f.write("Thali was hot and tasty, delivery 5 min late.")
print("BRONZE :", len(raw_events), "raw order events + 1 review file saved, nothing rejected")
# ---------- SILVER: schema-on-READ - impose structure when we read ----------
clean, seen = [], set()
with open(os.path.join(raw_dir, "part-0001.json")) as f:
for line in f:
r = json.loads(line)
if r["order_id"] in seen:
continue
seen.add(r["order_id"])
clean.append({"order_id": r["order_id"], "kitchen": r["kitchen"], "amount": int(r["amount"])})
os.makedirs(os.path.join("lake", "silver"), exist_ok=True)
with open(os.path.join("lake", "silver", "orders.csv"), "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["order_id", "kitchen", "amount"])
w.writeheader(); w.writerows(clean)
print("SILVER :", len(clean), "rows after de-duplication and type fixes")
# ---------- GOLD: a warehouse with schema-on-WRITE (rules checked on insert) ----------
if os.path.exists("warehouse.db"):
os.remove("warehouse.db")
db = sqlite3.connect("warehouse.db")
db.execute("CREATE TABLE dim_kitchen (kitchen TEXT PRIMARY KEY, city TEXT NOT NULL)")
db.execute("""CREATE TABLE fact_orders (order_id INTEGER PRIMARY KEY,
kitchen TEXT REFERENCES dim_kitchen(kitchen),
amount INTEGER NOT NULL CHECK (amount > 0))""")
db.executemany("INSERT INTO dim_kitchen VALUES (?, ?)", [("K11", "Pune"), ("K12", "Pune"), ("K13", "Mumbai")])
for r in clean:
try:
db.execute("INSERT INTO fact_orders VALUES (?, ?, ?)", (r["order_id"], r["kitchen"], r["amount"]))
except sqlite3.IntegrityError as err:
print("GOLD : rejected order", r["order_id"], "->", err)
db.commit()
query = """SELECT k.city, COUNT(*) AS orders, SUM(f.amount) AS revenue
FROM fact_orders f JOIN dim_kitchen k ON f.kitchen = k.kitchen
GROUP BY k.city ORDER BY revenue DESC"""
for city, orders, revenue in db.execute(query):
print(f"QUERY : {city:7} {orders} orders Rs {revenue}")
db.close()
# In a terminal: python mini_lake.pyRecorded from a real run with Python 3.10 and its built-in SQLite. The wording of the rejection message can differ slightly with older SQLite versions. Notice: the bronze file still contains all 5 raw events including the duplicate and the −90 order, silver fixed types and removed the duplicate but did not judge the −90 amount, and only the warehouse's schema-on-write rule stopped it. Mumbai has no revenue because its only order was rejected.
Bronze to gold, one line at a time
Three raw rows (one duplicate) move through the zones. Watch the seen set block the duplicate, the amount change from text to a number in silver, and the gold dictionary fill up.
Worked examples: real problems, solved and traced in memory
Four tasks a data engineer does with lakes and warehouses, from deciding where data goes to estimating query savings. Each runs live and is traced.
Where should each dataset go?
Problem statement
Route each dataset to the lake, the warehouse or both. Rules: unstructured data → lake only; structured data needed for reports → both (raw in lake, clean in warehouse); everything else → lake.
orders (structured, report), clicks (semi), reviews (unstructured), photos (unstructured), payments (structured, report)
orders -> lake (raw) + warehouse (clean) clicks -> lake reviews -> lake photos -> lake payments -> lake (raw) + warehouse (clean)
Approach
- Describe each dataset with its kind and whether reports need it.
- Apply the rules in order with if/elif.
- Print the destination.
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.
Keep the latest version of each record
Problem statement
The bronze zone receives order updates: the same order can appear several times as its status changes. Silver must keep only the latest version of each order (by update time).
O1 placed@10, O1 delivered@40, O2 placed@15, O1 refunded@90, O2 delivered@50
O1 refunded at 90 O2 delivered at 50
Approach
- Walk through the bronze rows.
- Keep a dictionary order_id → latest row.
- Replace the stored row whenever a newer one appears.
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.
Revenue by city and weekday from a star schema
Problem statement
The fact table stores (kitchen, date_key, amount). Dimension tables give each kitchen's city and each date_key's weekday. Produce revenue per (city, weekday).
5 facts, 3 kitchens, 2 dates
Mumbai Sun 90 Pune Sat 370 Pune Sun 200
Approach
- Look up the city in
dim_kitchenand the weekday indim_datefor each fact. - Use the pair (city, weekday) as a dictionary key.
- Add up the amounts.
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.
How much does layout save on a pay-per-scan engine?
Problem statement
A table holds 365 days × 3 GB of CSV. A query needs one month (31 days) and 2 of 20 columns. Parquet compresses to one third. At Rs 400 per TB scanned (illustrative), what does the query cost with each layout?
365 × 3 GB, 31 days, 2/20 columns, compression 3×, Rs 400/TB
CSV, one folder scans 1095.0 GB costs Rs 427.73 CSV, by date scans 93.0 GB costs Rs 36.33 Parquet, by date scans 3.1 GB costs Rs 1.21
Approach
- CSV, no partitions: everything is scanned.
- Partitioned CSV: only 31 days.
- Partitioned Parquet: 31 days × 2/20 columns ÷ 3 compression.
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 4.3 in one screen
- Warehouse: structured, cleaned, schema-on-write, fast trusted SQL, for business reporting.
- Lake: any format, raw, schema-on-read, cheapest storage, for exploration and ML.
- Lakehouse: lake storage + table formats (Delta, Iceberg, Hudi): ACID, MERGE, schema evolution, time travel.
- ETL (transform before loading) vs ELT (load raw, transform inside the platform).
- Zones: bronze (raw) → silver (clean) → gold (business-ready).
- Star schema: fact table + dimension tables. Formats: CSV/JSON (simple), Avro (streams), Parquet/ORC (analytics).
- No catalogue = data swamp. Partition + columnar formats = far less data scanned.
Interview and viva questions
Differentiate a data lake and a data warehouse.
A data warehouse stores structured, cleaned and integrated data in a predefined schema (schema-on-write), optimised for SQL reporting and BI, at higher storage cost. A data lake stores raw data of any format (structured, semi-structured, unstructured) in native form on cheap storage, applying structure when read (schema-on-read). Warehouses serve business analysts with trusted numbers; lakes serve data engineers and scientists exploring raw data and training models.
What is schema-on-read vs schema-on-write?
Schema-on-write defines and enforces the structure before data is stored, rejecting non-conforming rows; queries are fast and data is trusted, but changes are slower. Schema-on-read stores data as-is and applies a structure at query time; it is flexible and keeps everything, but every reader must handle quality issues.
What is a lakehouse?
An architecture that adds warehouse capabilities (ACID transactions, schema enforcement and evolution, updates and deletes, time travel, performance optimisations and governance) on top of low-cost lake storage, using open table formats such as Delta Lake, Apache Iceberg or Apache Hudi. It lets BI and machine learning use one copy of the data.
What is the medallion architecture?
A way of organising a lake or lakehouse in zones of increasing quality: bronze holds raw data as ingested, silver holds cleaned, de-duplicated and conformed data, and gold holds aggregated, business-ready tables for reporting.
What is a star schema?
A dimensional model with a central fact table containing measurable events and foreign keys, surrounded by dimension tables describing them (date, product, customer, location). It simplifies and speeds up analytical queries and is the standard design for data warehouses.
Practice problems
Storage and modelling exercises, from file counting to schema evolution. The last two are business tasks.
Count files by format
Count the files in a lake listing by extension.
a.json, b.json, c.parquet, d.jpg, e.parquet, f.parquet
{'json': 2, 'parquet': 3, 'jpg': 1}Show solution and explanation
files = ["a.json", "b.json", "c.parquet", "d.jpg", "e.parquet", "f.parquet"]
count = {}
for f in files:
ext = f.rsplit(".", 1)[1]
count[ext] = count.get(ext, 0) + 1
print(count)How it works: rsplit(".", 1) splits once from the right, giving the extension.
Required columns present?
A warehouse needs order_id, kitchen and amount. Print whether each incoming row has all of them.
3 rows
1 OK
2 MISSING {'kitchen'}
3 OKShow solution and explanation
required = {"order_id", "kitchen", "amount"}
rows = [{"order_id": 1, "kitchen": "K1", "amount": 9},
{"order_id": 2, "amount": 5},
{"order_id": 3, "kitchen": "K2", "amount": 7, "note": "x"}]
for r in rows:
print(r["order_id"], "OK" if required <= set(r) else "MISSING " + str(required - set(r)))How it works: required <= set(r) checks that every required column is present (extra columns are allowed here).
Lake vs warehouse storage cost
Storing 50 TB costs Rs 1,800 per TB-month in a lake and Rs 2,300 per TB-month in a warehouse (illustrative). Print the monthly cost of each.
50 TB
Lake : Rs 90000 Warehouse : Rs 115000
Show solution and explanation
tb = 50
print("Lake : Rs", tb * 1800)
print("Warehouse : Rs", tb * 2300)How it works: Raw history usually stays in the cheaper lake; only curated tables go to the warehouse.
Build partition paths
Given order dates, print the folder each order belongs to, using the pattern orders/date=YYYY-MM-DD/.
2026-10-19, 2026-10-20
orders/date=2026-10-19/ orders/date=2026-10-20/ orders/date=2026-10-19/
Show solution and explanation
for d in ["2026-10-19", "2026-10-20", "2026-10-19"]:
print(f"orders/date={d}/")How it works: Hive-style key=value folder names let engines skip partitions they don't need.
Latest status per order
Keep only the latest status of each order (higher version wins).
(O1,v1,placed), (O1,v2,delivered), (O2,v1,placed)
{'O1': 'delivered', 'O2': 'placed'}Show solution and explanation
updates = [("O1", 1, "placed"), ("O1", 2, "delivered"), ("O2", 1, "placed")]
latest = {}
for oid, version, status in updates:
if oid not in latest or version > latest[oid][0]:
latest[oid] = (version, status)
print({oid: s for oid, (v, s) in latest.items()})How it works: This is an upsert: what MERGE INTO does in a lakehouse table.
Merge two schemas
Rows from an old and a new app version have different columns. Print the merged column list and the rows with missing values as None.
old: id, amount · new: id, amount, coupon
['id', 'amount', 'coupon'] [1, 120, None] [2, 90, 'C10']
Show solution and explanation
rows = [{"id": 1, "amount": 120}, {"id": 2, "amount": 90, "coupon": "C10"}]
cols = []
for r in rows:
cols += [c for c in r if c not in cols]
print(cols)
for r in rows:
print([r.get(c) for c in cols])How it works: Schema evolution: add new columns and fill old rows with null instead of failing the load.
The small-files problem
A streaming job wrote 50,000 files of 100 KB each. How many 128 MB files would hold the same data after compaction?
50,000 × 100 KB, target 128 MB
Total: 4.77 GB After compaction: 39 files
Show solution and explanation
import math
total_kb = 50_000 * 100
target_kb = 128 * 1024
print("Total:", round(total_kb / 1024 / 1024, 2), "GB")
print("After compaction:", math.ceil(total_kb / target_kb), "files")How it works: Every file has opening overhead; thousands of tiny files slow queries. Table formats run compaction to fix this.
Revenue by city (star schema)
Using the fact and dimension below, print revenue by city.
facts: K1 300, K2 200, K1 100 · K1 in Pune, K2 in Nashik
{'Pune': 400, 'Nashik': 200}Show solution and explanation
facts = [("K1", 300), ("K2", 200), ("K1", 100)]
dim = {"K1": "Pune", "K2": "Nashik"}
rev = {}
for k, amt in facts:
rev[dim[k]] = rev.get(dim[k], 0) + amt
print(rev)How it works: The same join a BI tool sends to the warehouse when you drag “city” onto a chart.
Retention plan
Bronze grows 200 GB/day and is kept 90 days; gold grows 2 GB/day and is kept 3 years. Print the steady-state size of each in TB.
bronze 200 GB × 90 d · gold 2 GB × 1095 d
Bronze: 17.6 TB Gold : 2.1 TB
Show solution and explanation
bronze_tb = 200 * 90 / 1024
gold_tb = 2 * 1095 / 1024
print("Bronze:", round(bronze_tb, 1), "TB")
print("Gold :", round(gold_tb, 1), "TB")How it works: Raw data is large but often kept for a shorter time; small curated tables can be kept for years.
Real-world scenario questions (asked in company interviews)
Lake and warehouse decisions you will face in real teams. Decide your answer first, then open the model answer.
“Should we put everything in the warehouse?”
A retailer's warehouse holds sales tables. The marketing team now wants to keep website clickstream (JSON), product images and customer chat transcripts “in the warehouse too”.
The interviewer is checking: do you know which data fits which store and why?
Show a strong answer
- Images and chat transcripts are unstructured; clickstream is high-volume semi-structured JSON. They belong in a lake (object storage), which is far cheaper per TB and accepts any format.
- Process clickstream into clean, aggregated tables (sessions, conversions) and load those into the warehouse for marketing reports.
- Catalogue the lake datasets with owners and descriptions from day one.
Avoid: forcing raw JSON and binary files into warehouse tables, or dumping them into the lake with no documentation.
The nightly load fails after every app release
Each time the app team adds a field to the payment event, the ETL job loading the warehouse fails and the morning dashboard is empty.
The interviewer is checking: schema evolution and decoupling ingestion from strict schemas.
Show a strong answer
- Land raw events in a bronze zone that accepts new fields (schema-on-read), so ingestion never fails.
- Build silver and gold with explicit column selection, so new fields are ignored until someone decides to use them.
- Use a table format or warehouse feature that supports adding columns safely (schema evolution), and a schema registry or data contract with the app team.
- Alert on unexpected schema changes instead of failing silently.
Avoid: asking the app team to stop adding fields, or loosening every column to text.
Three numbers for “active customers”
The CEO gets three different “monthly active customers” figures from three teams. Each built its own table from different folders in the lake.
The interviewer is checking: do you recognise a data swamp and know how governance fixes it?
Show a strong answer
- Agree one business definition (what counts as active, which events, which time zone).
- Build one certified gold table from silver, with an owner, documentation and tests.
- Register it in the catalogue with lineage, mark it as the official source, and deprecate the copies.
- Add data-quality checks (row counts, null rates, freshness) so problems are seen early.
Avoid: picking the number that “looks right” without fixing the definition and ownership.
A query bill that doubled in a month
Analysts query a 400 TB table of call records on a pay-per-scan SQL engine. The bill doubled after a new dashboard went live. The data is stored as CSV in one folder per year.
The interviewer is checking: do you understand file layout, pruning and lakehouse optimisations?
Show a strong answer
- Rewrite the table as compressed Parquet (or into an Iceberg/Delta table), partitioned by date (and maybe region), so queries read only the needed days and columns.
- Compact small files and keep file statistics so engines can skip files.
- Build a small gold aggregate for the dashboard so it never scans raw call records.
- Set query cost limits and monitor the most expensive queries.
Avoid: only asking analysts to “query less” without fixing the storage layout.