Data lake vs data warehouse (and the lakehouse)

Finance wants one trusted number; data scientists want every raw click, review and photo. A data warehouse serves the first, a data lake the second, and a lakehouse tries to serve both from one copy of data. In this lesson you watch a messy record get refined from bronze to gold, see a lake turn into a swamp, and build a small lake and warehouse on your own computer.

Module 4 · The Ecosystem⏱ 28 minBeginner → AdvancedRuns in your browser (Python)3 real-time scenariosMini lake + SQL on your PC9 practice problems
1 The story

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.

Data warehouse = library cleancatalogued only approved books, fixed shelves Data lake = godown everything, as delivered cheap, huge, any shape Lakehouse catalogue godown storage + library rules Finance needs the library. The data scientist needs the godown. Modern platforms try to give both.
Kabir

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.

Arjun Sir

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.

Riya

Then why not just use the godown for everything? It holds more.

Arjun Sir

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.

Kabir

So I need both?

Arjun Sir

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.

2 Definition

What are a data lake and a data warehouse?

Formal definition (write this in exams)

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.

In simple words

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

SchemaThe structure of data: which columns exist and their types.
Schema-on-writeCheck and enforce the structure before storing. Bad rows are rejected at load time.
Schema-on-readStore anything; apply structure only when reading it for a question.
ETL / ELTExtract-Transform-Load (clean before loading, classic warehouse) vs Extract-Load-Transform (load raw first, transform inside the platform).
OLTP vs OLAPOLTP databases run the app (many small writes); OLAP systems answer analytical questions over huge history (warehouses).
Fact / dimensionA fact table holds events (orders, amounts); dimension tables describe them (kitchen, city, date).
ParquetA compressed, columnar file format: queries read only the columns they need.
Data swampA lake with no catalogue, owners or quality checks, where data can't be found or trusted.

Lake, warehouse and lakehouse at a glance

Data warehouseStructured · schema-on-write · cleaned, trusted · fast SQL · higher cost per TB · Snowflake, BigQuery, Redshift, Synapse, Teradata
Data lakeAny format · schema-on-read · raw · very cheap storage · needs a catalogue to stay useful · HDFS, S3, ADLS, Google Cloud Storage
LakehouseLake 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

Pick your level. New to this? Start at Beginner and climb.

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.

3 Types and ways to use it

Side by side, and the three zones of a lake

Data warehouseData lakeLakehouse
DataStructured tablesAnything: tables, JSON, logs, images, audioAnything, with tables on top
SchemaOn write (enforced at load)On read (applied by the reader)Enforced for tables, flexible for raw zone
QualityCleaned and trustedRaw, mixed qualityRaw → cleaned in zones
Main usersBusiness analysts, finance, BIData engineers, data scientistsBoth
Storage costHigherLowestLow (lake storage)
Updates / deletesEasy (SQL)Hard (rewrite files)Easy (table formats)
Typical toolsSnowflake, BigQuery, RedshiftS3, ADLS, GCS, HDFSDelta 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 everything

Silver (clean)

De-duplicated, typed, validated and joined where needed. One row per real-world thing. Data scientists usually start here.

Trust the rows

Gold (business)

Aggregated, business-ready tables: daily revenue by city, cancellation rate by kitchen. Dashboards and finance read these.

Answer questions

File formats you will meet

FormatLayoutGood for
CSVText rowsSimple exchange, small data; no types, no compression
JSONText, nestedEvents from apps and APIs; flexible but large
AvroBinary rows with schemaStreaming (Kafka) and record-by-record writes
Parquet / ORCBinary columns, compressedAnalytics: 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.

4 Step by step

One messy order, from bronze to the finance report

  1. Bronze: store it exactly as it arrived

    {"order_id": 2, "kitchen": "K12", "item": "biryani", "amount": "250"}
    {"order_id": 2, "kitchen": "K12", "item": "biryani", "amount": "250"}
    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.
  2. Silver: clean while reading (schema-on-read)

    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}]
    The reader decides the structure: one row per order, amount as an integer. The result is written as Parquet in the silver zone.
  3. Gold: load into a warehouse table (schema-on-write)

    CREATE TABLE fact_orders (
      order_id INTEGER PRIMARY KEY,
      kitchen  TEXT REFERENCES dim_kitchen(kitchen),
      amount   INTEGER NOT NULL CHECK (amount > 0)
    );
    The warehouse table has rules. A row with amount −90 is rejected at load time. Finance can trust every row that got in.
  4. Model: facts and dimensions

    -- fact_orders(order_id, kitchen, date_key, amount)
    -- dim_kitchen(kitchen, kitchen_name, city)
    -- dim_date(date_key, day, month, is_weekend)
    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.
  5. Query: the finance number

    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;
    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.
5 See it happen

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.

Lake, warehouse, lakehouse 6 steps
What you see Kabir's inbox New data arriving every day: orderstable rows clicksJSON reviewstext photosimages Four shapes of data. Where should each go? Finance head “Revenue by city for October. It must match the bank settlement exactly.” Repeated question, needs trusted numbers. Data scientist “Do angry reviews and dark food photos predict cancellations? I need raw data.” New question, needs raw, unstructured data. App team “New app version is live! Orders now include a coupon field.” The data's shape just changed overnight. New analyst, six months later “There are 3 folders of rider GPS data. Which one is correct? Who owns it?” Nobody knows. The lake is becoming a swamp. After the redesign ✓ Finance reads gold tables: trusted ✓ Data scientist reads bronze + silver ✓ New fields don't break anything ✓ Every table is in the catalogue myphatshala.in
Behind the screen Data lake (S3) Warehouse (SQL) orders/…jsonclicks/…jsonreviews/…txtphotos/…jpg all stored as-is ✓ fact_orders only orders fit a table The lake accepts every shape; the warehouse only tables. load fact_orders:order 1 ✓order 2 ✓ (dedup)order 3 ✗ amount −90order 4 ✓ schema-on-WRITE raw copy stays here Bad rows are stopped at the door. Finance trusts the rest. reviews/…txt ← readphotos/…jpg ← readorders/…json ← read schema-on-READ structure chosen in the notebook, for this question no reviews or photos in here at all Only the lake has what the data scientist needs. {"order_id": 9,"amount": 120,"coupon": "LUNCH20"} stored fine ✓ LOAD FAILED:unknown column"coupon" needs ALTER TABLE + pipeline change Strict schemas protect quality but resist change. rider_gps/rider_gps_new/rider_gps_FINAL_v2/tmp_kabir_test/old_dump_2025/ no owner, no docs = DATA SWAMP warehouse is tidy, but small and rigid Cheap storage + no rules = nobody can trust anything. Lakehouse on S3 (Delta / Iceberg tables) BRONZEraw, all shapesnever edited SILVERclean tablesschema enforced GOLDbusiness tablesfor finance & BI Catalogue: owner · schema · freshness · lineage ACID updates · schema evolution · time travel One copy of data, library rules on godown storage. myphatshala.in
6 Real-world use

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 1

The 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 → silver → gold 5 steps
Animated example BRONZE SILVER GOLD {"order_id": 2,"kitchen":"K12","amount":"250","ts":"12:31:08"}+ duplicate copy 12:31 · raw event saved exactly as received, twice.Nothing is fixed in bronze. It is the evidence. order_id 2kitchen K12amount 250 (int)ts 2026-10-19 12:31:08 IST1 row ✓ 12:45 · silver job: dedupe, cast types, fix time zone.Written as Parquet in a Delta/Iceberg table. daily_city_salesdate 2026-10-19city Puneorders 9,312revenue ₹14.9 L(incl. our ₹250) 13:30 · gold job: join kitchen → city, aggregate.Thousands of silver rows become one business row. 16:00 · bug found: refunds were counted as sales!fix the silver code → rebuild silver and gold from bronze 16:20 · corrected numbers, nothing lostpossible only because bronze kept the raw truth myphatshala.in
Real-time scenario 2

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.

Finding the right data 5 steps
Animated example Company A: no catalogue Company B: catalogue gps/gps_new/rider_gps_FINAL/rider_loc_v2/tmp_test_gps/09:30 · five folders.Which one? 🔍 rider distance09:30 · searches thecatalogue. 11:00 · asks on Slack.“Ask Rohan.” Rohan leftthe company in June. silver.rider_tripsowner: logistics teamkm_travelled (float)fresh: updated 2 h agofrom: bronze.gps_pings 14:00 · opens 3 folders.Different columns, differentdates. Picks one, unsure. SELECT SUM(km_travelled)FROM silver.rider_tripsWHERE week = 42;09:50 · answer: 4,81,200 km 17:30 · answer: 6,12,000 km(included test data andduplicate pings!) 10:00 · moves on to thenext question. 1 day, wrong answer20 minutes, right answer Same storage, same cost.The difference is metadata:owner, schema, freshness,lineage.and the next analyststarts from zero again. myphatshala.in
Real-time scenario 3 · Advanced

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).

Pruning what you don't need 5 steps
Animated example SELECT SUM(amount) FROM orders WHERE city='Pune' AND month=10 data read by the query 1.1 TBCSV, one folder: read every row, every column 93 GB · partitioned by month: skip 11 months ≈ 9 GB · Parquet: read only city + amount columns ≈ 3 GB · compressed + skip files with no Pune rows A CSV dump forces a full scan of 1.1 TB. Folders like month=10/ let the engine skip 11 of 12 months. Columnar files: only 2 of 20 columns are read. Compression and per-file min/max stats skip even more. 1.1 TB → about 3 GB: hundreds of times less to readfaster answers, and much cheaper on pay-per-scan engines myphatshala.in

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.

Transactions→Warehouse (reports) + lake (ML)→Audit + fraud scores

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.

Scans + records→Lake (images) + warehouse (billing)→Research, reports
BSG

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.

Events + orders→Bronze → silver → gold→BI + ML

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.

Call records→Partitioned Parquet in the lake→Warehouse summaries

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.

App database→Cloud warehouse + dbt→Dashboards

Simplified examples; real platforms vary.

7 Run the code

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)
order 1: warehouse -> stored lake -> stored as-is order 2: warehouse -> REJECTED: amount is not int lake -> stored as-is order 3: warehouse -> REJECTED: amount must be > 0 lake -> stored as-is order 4: warehouse -> REJECTED: unknown column ['coupon'] lake -> stored as-is Revenue read from the lake (cleaned while reading): Rs 490
  • SCHEMA = {"order_id": int, "kitchen": str, "amount": int}The warehouse's contract: exactly these columns, with these types.
  • extra = set(row) - set(SCHEMA)A new coupon column 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.py
BRONZE : 5 raw order events + 1 review file saved, nothing rejected SILVER : 4 rows after de-duplication and type fixes GOLD : rejected order 3 -> CHECK constraint failed: amount > 0 QUERY : Pune 3 orders Rs 490

Recorded 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.

8 Line by line

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.

Example 1BeginnerPlatform design · first step

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.

Input
orders (structured, report), clicks (semi), reviews (unstructured), photos (unstructured), payments (structured, report)
Expected output
orders    -> lake (raw) + warehouse (clean)
clicks    -> lake
reviews   -> lake
photos    -> lake
payments  -> lake (raw) + warehouse (clean)

Approach

  1. Describe each dataset with its kind and whether reports need it.
  2. Apply the rules in order with if/elif.
  3. 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.

Example 2IntermediateData engineering · silver zone

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).

Input
O1 placed@10, O1 delivered@40, O2 placed@15, O1 refunded@90, O2 delivered@50
Expected output
O1 refunded at 90
O2 delivered at 50

Approach

  1. Walk through the bronze rows.
  2. Keep a dictionary order_id → latest row.
  3. 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.

Example 3IntermediateBI · star schema

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).

Input
5 facts, 3 kitchens, 2 dates
Expected output
Mumbai Sun 90
Pune Sat 370
Pune Sun 200

Approach

  1. Look up the city in dim_kitchen and the weekday in dim_date for each fact.
  2. Use the pair (city, weekday) as a dictionary key.
  3. 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.

Example 4AdvancedCost optimisation · file layout

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?

Input
365 × 3 GB, 31 days, 2/20 columns, compression 3×, Rs 400/TB
Expected output
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

  1. CSV, no partitions: everything is scanned.
  2. Partitioned CSV: only 31 days.
  3. 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.

9 Quiz and revision

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.

P1Easy

Count files by format

Count the files in a lake listing by extension.

Input
a.json, b.json, c.parquet, d.jpg, e.parquet, f.parquet
Expected output
{'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)
{'json': 2, 'parquet': 3, 'jpg': 1}

How it works: rsplit(".", 1) splits once from the right, giving the extension.

P2Easy

Required columns present?

A warehouse needs order_id, kitchen and amount. Print whether each incoming row has all of them.

Input
3 rows
Expected output
1 OK
2 MISSING {'kitchen'}
3 OK
Show 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)))
1 OK 2 MISSING {'kitchen'} 3 OK

How it works: required <= set(r) checks that every required column is present (extra columns are allowed here).

P3Easy

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.

Input
50 TB
Expected output
Lake      : Rs 90000
Warehouse : Rs 115000
Show solution and explanation
tb = 50
print("Lake      : Rs", tb * 1800)
print("Warehouse : Rs", tb * 2300)
Lake : Rs 90000 Warehouse : Rs 115000

How it works: Raw history usually stays in the cheaper lake; only curated tables go to the warehouse.

P4Medium

Build partition paths

Given order dates, print the folder each order belongs to, using the pattern orders/date=YYYY-MM-DD/.

Input
2026-10-19, 2026-10-20
Expected output
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}/")
orders/date=2026-10-19/ orders/date=2026-10-20/ orders/date=2026-10-19/

How it works: Hive-style key=value folder names let engines skip partitions they don't need.

P5Medium

Latest status per order

Keep only the latest status of each order (higher version wins).

Input
(O1,v1,placed), (O1,v2,delivered), (O2,v1,placed)
Expected output
{'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()})
{'O1': 'delivered', 'O2': 'placed'}

How it works: This is an upsert: what MERGE INTO does in a lakehouse table.

P6Advanced

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.

Input
old: id, amount · new: id, amount, coupon
Expected output
['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])
['id', 'amount', 'coupon'] [1, 120, None] [2, 90, 'C10']

How it works: Schema evolution: add new columns and fill old rows with null instead of failing the load.

P7Advanced

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?

Input
50,000 × 100 KB, target 128 MB
Expected output
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")
Total: 4.77 GB After compaction: 39 files

How it works: Every file has opening overhead; thousands of tiny files slow queries. Table formats run compaction to fix this.

P8Business problem

Revenue by city (star schema)

Using the fact and dimension below, print revenue by city.

Input
facts: K1 300, K2 200, K1 100 · K1 in Pune, K2 in Nashik
Expected output
{'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)
{'Pune': 400, 'Nashik': 200}

How it works: The same join a BI tool sends to the warehouse when you drag “city” onto a chart.

P9Business problem

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.

Input
bronze 200 GB × 90 d · gold 2 GB × 1095 d
Expected output
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")
Bronze: 17.6 TB Gold : 2.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.

Retail · architectureFresher

“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.

Fintech · reliability1–3 years

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
  1. Land raw events in a bronze zone that accepts new fields (schema-on-read), so ingestion never fails.
  2. Build silver and gold with explicit column selection, so new fields are ignored until someone decides to use them.
  3. 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.
  4. Alert on unexpected schema changes instead of failing silently.

Avoid: asking the app team to stop adding fields, or loosening every column to text.

E-commerce · governance1–3 years

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.

Telecom · cost and performance3+ years

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.