Lesson 10 Iceberg Tables
Theme:
Lesson 10 · Slide 1

Lesson 10 — Iceberg Tables

Add a table format to your data lake — and get snapshots, time travel, and ACID for free.

Data Engineering Module 4 Modern Lakehouse
Lesson 10 · Slide 2

The problem L09 left open

What L09 gave us

DuckDB queried our Parquet directly — fast, zero-config, no copy step. The same SQL worked in CLI scripts, in tests, and inside Airflow tasks.

What we’re still missing

What’s the schema of events? What if two writers append at once? What did the data look like yesterday? Plain Parquet on disk has no answers to any of these.

Lesson 10 · Slide 3

File format vs table format

File formatTable format
ExamplesParquet, ORC, AvroIceberg, Delta Lake, Hudi
StoresBytes (one file)A set of files + metadata + history
Knows the schema?One file at a timeAcross the whole table
Snapshots / time travelNoYes
ACID writesNoYes
Many engines read it?YesYes (decouples engine from format)

The Iceberg pitch: keep your Parquet files, gain a metadata layer on top.

Lesson 10 · Slide 4

What Iceberg is

Lesson 10 · Slide 5

The Iceberg directory layout

warehouse/default/events/
  data/
    event_date=2026-05-26/
      00000-0-<uuid>.parquet
      00000-1-<uuid>.parquet
    event_date=2026-05-27/
      ...
  metadata/
    v1.metadata.json    ← table-level metadata (schemas, snapshots)
    v2.metadata.json    ← new version after first append
    snap-<id>-1.avro    ← snapshot manifest (lists data files)
    <uuid>.manifest.avro ← per-file stats (min/max per column)

The data/ half is the Parquet you already know. The metadata/ half is what Iceberg adds.

Lesson 10 · Slide 6

Snapshots

Every write to an Iceberg table produces a new immutable snapshot — a pointer to the exact set of data files that constitute the table at that instant.

Append → new snapshot. Overwrite → new snapshot. Delete → new snapshot. The table history is a linked list of snapshots.

Lesson 10 · Slide 7

Time travel in 3 lines

# Current state
arrow_curr = table.scan().to_arrow()

# State as of an earlier snapshot (pyiceberg API)
arrow_prev = table.scan(snapshot_id=12345).to_arrow()

# Or via DuckDB by pointing at an older metadata.json
# SELECT count(*) FROM iceberg_scan('warehouse/.../v3.metadata.json');

Every snapshot points to a metadata.json. Reading an older JSON reads the table as of that point — no backup tape, no restore step.

Lesson 10 · Slide 8

ACID on a data lake

Lesson 10 · Slide 9

Schema evolution

table.update_schema().add_column("commit_count", LongType()).commit()
# Rename without rewriting any data files:
table.update_schema().rename_column("actor", "actor_username").commit()

Backward-compatible by design — no file rewrite needed for any of these operations.

Lesson 10 · Slide 10

Partition evolution

Lesson 10 · Slide 11

The Iceberg + DuckDB local stack

Write path (pyiceberg)

Catalog: SqlCatalog backed by a SQLite file.
Warehouse: a local directory.
Writer: write_from_parquet.py reads L02 Parquet → appends to the Iceberg table. One append = one snapshot.

Read path (DuckDB)

INSTALL iceberg; LOAD iceberg;
SELECT * FROM iceberg_scan('…metadata.json');
No JVM, no Spark, no Hadoop. The whole stack fits on a laptop.

Lesson 10 · Slide 12

Maintenance

Expiring snapshots is irreversible — once a snapshot is expired, its data files are subject to deletion and time travel to that point is gone. Test the retention policy on a dev table first. Default retention: at least as long as your longest backfill window.

Lesson 10 · Slide 13

What Iceberg is not

Lesson 10 · Slide 14

Recap & what's next

Coming up: Lesson 11 — lift the lakehouse into Snowflake (or BigQuery): external tables on Iceberg, cost models, materialise-vs-query. With a DuckDB-only fallback for readers without a paid cloud account.
1 / ?