The same bad write, three ways: the lake's query fails, the warehouse rejects the bad rows, and the lakehouse (a real Iceberg table) rejects them and keeps its commit history. Both answer $74.50.
python3 --version.pip install "pyarrow>=18"pip install "duckdb>=1.0"pip install "pyiceberg[pyarrow,sql-sqlite]>=0.12"git clone https://github.com/DayanEbrar0X/data-anatomy.ai.git cd data-anatomy.ai
python3 -m venv .venv source .venv/bin/activate
pip install -r requirements.txt # or just this lesson: pip install "pyarrow>=18" "duckdb>=1.0" "pyiceberg[pyarrow,sql-sqlite]>=0.12"
cd data-engineering/24-lake-vs-warehouse-vs-lakehouse cd src python3 three_ways.py
import os, json, duckdb
from pyiceberg.types import StringType
from store import DAY1, DAY2, BAD, SCHEMA
from store import fresh, catalog, append
SUM = "SELECT sum(usd) FROM "
def lake(): # files in a folder, schema on read
fresh("lake")
for i, rows in enumerate([DAY1, DAY2, BAD]):
with open(f"lake/{i}.json", "w") as f:
json.dump(rows, f) # any shape lands
n = len(os.listdir("lake"))
print(f"lake: {n} files written")
return duckdb.sql(SUM + "'lake/*.json'")
def warehouse(): # typed table, schema on write
db = duckdb.connect("shop.duckdb")
db.sql("""CREATE OR REPLACE TABLE events
(id INT, kind TEXT, usd DOUBLE)""")
for rows in (DAY1, DAY2, BAD):
try:
db.executemany("INSERT INTO events "
"VALUES ($id, $kind, $usd)", rows)
except duckdb.Error:Three names for where the data lives, and they're not the same. Data lake. Data warehouse. Lakehouse.
A lake is a storage unit: cheap, takes anything. A warehouse is a library: checked on the way in, easy to find. A lakehouse keeps the cheap storage, adds the checks, and logs every change. The test: six shop events, then one bad write, an amount typed as text.
Each way answers one question: total revenue. The lake is just a folder. Each batch is dumped as a JSON file. Nothing checks it, so the bad one lands.
The schema is only worked out when DuckDB reads the files. The warehouse is a DuckDB table with types. Schema on write. A value that doesn't fit its type fails the insert.
Then the same sum. The lakehouse is an Apache Iceberg table: Parquet files plus metadata, with a SQLite catalog. Each append is a commit, checked against the schema. Adding a column only changes metadata.
No files rewritten. And every commit is a snapshot you can read back later. Same sum, through DuckDB. One loop asks all three.
Run it. The lake wrote all three files, then the query failed. The files disagree on what usd is. The warehouse rejected the bad rows: 74 dollars 50.
The lakehouse rejected them too. Two commits, same 74.50. So which?
A lake is cheapest and holds anything, but quality is the reader's problem. A warehouse gives fast, governed SQL, but data usually lives in its own storage. A lakehouse puts table rules on open files that Spark, Trino or DuckDB can all read. That's Iceberg, Delta Lake and Hudi.
Many teams use all three. Check on read. Check on write. Or check on write, over open files.
Read the lesson on GitHub →