Lakshya

Databricks & Snowflake · Chapter 11 of 16

Table format internals: the log, the tree, and the timeline

What Delta, Iceberg and Hudi actually write to storage — and why the three designs behave differently under concurrency and at scale.

9 min read2 diagramsAll 16 chapters

Every comparison of these formats you will read is a feature table. The interesting differences are structural, they are visible in the specifications, and they explain the behaviour you will actually meet in production. This chapter reads the specs so you do not have to.

DELTA — REPLAY A LOG ...000.jsonadd, metaData...001.jsonadd, remove...002.jsonadd, txn010.checkpointfull state...011.jsonadd read = newest checkpoint + JSON commits after it _last_checkpoint points at it, so no full LIST V2 checkpoints reference sidecar parquet files holding only add/remove — multi-part checkpoints are deprecated (not atomic). ICEBERG — WALK A TREE, PRUNING AT EVERY LEVEL catalog current metadata pointer one atomic swap metadata.json schemas, specs, snapshots the table's identity manifest list one row per manifest + partition summaries manifest one row per data file + column bounds data files parquet / orc / avro and delete files THE THING MOST TUTORIALS MISS — AND THE REASON ICEBERG PLANNING SCALES The manifest list carries partition field summaries (contains_null, lower_bound, upper_bound) for each manifest. So a planner skips whole manifests before opening any of them. Pruning happens twice: manifest level, then file level. Data-file sequence numbers are written as null and inherited from the manifest at read time — so a retry only rewrites the manifest list, notthe manifests. Retries are cheap by design. Hudi takes a third route: an ordered timeline of instants with monotonic globally-increasing times and bounded clock uncertainty — Spanner'sTrueTime idea. Delta and Iceberg order on version; Hudi orders on time. That is the structural difference, and it is why Hudi's story is streaming upserts.
Delta replays a log; Iceberg walks a tree; Hudi orders a timeline. The two-level pruning in the middle is the single best answer to ‘why does Iceberg planning scale’, and almost no tutorial mentions it.

Delta: an ordered log of actions

  • _delta_log/ holds newline-delimited JSON commits named by version, zero-padded to twenty digits. A commit file is the unit of atomicity. Version v is the set of actions applied to the state at v-1.
  • Actions are typed: metaData, add, remove, protocol, txn, commitInfo, domainMetadata, sidecar, checkpointMetadata.
  • Checkpoints collapse the log into a snapshot so readers do not replay from zero. Modern V2 checkpoints reference sidecar parquet files carrying only add and remove actions, which keeps the checkpoint itself small. Multi-part checkpoints are explicitly deprecated because they cannot be created atomically, and readers are required to ignore one with missing parts.
  • The widely-repeated ‘checkpoint every ten commits’ is a reference-implementation default, not part of the protocol — the spec explicitly tells readers to make no assumption about checkpoint frequency.
  • _last_checkpoint is a pointer file that lets a reader skip a full listing of a log with tens of thousands of files. On object storage, that listing is the expensive part.

Deletion vectors, at the byte level

A deletion vector marks rows as logically removed without rewriting the file. The mechanics are worth knowing precisely because they explain both the performance and the correctness rules:

  • The descriptor carries a storage type — a relative UUID path, an inline value, or an absolute path — plus an offset, a size and a cardinality.
  • The bitmap itself is a 64-bit portable Roaring bitmap, which switches encoding per 16-bit block between an array, a dense bitmap and run-length encoding depending on density. That adaptivity is why marking a million scattered deletions costs kilobytes.
  • There is a file-format wrapper with a magic number, a size and a CRC-32 checksum, and inline encodings use Z85 Base85 because it is JSON-safe.
  • The writer rule that catches implementations: a logical file carrying a deletion vector must record numRecords equal to the rows in the data file, not the valid rows in the logical file. Get that backwards and every statistic downstream is wrong.

Row-level concurrency, and the three conditions nobody tells you

Databricks enables row-level conflict resolution — so two concurrent updates to different rows of the same file do not conflict — only when all three hold: a recent runtime, deletion vectors enabled, and the table is not partitioned.

That third condition is the one that surprises people. Partitioning a Delta table disables row-level concurrency. A team that partitions for performance can find they have quietly traded away concurrent write capability.

The conflict matrix has one more teaching point: update-versus-optimize and optimize-versus-optimize conflicts arise with Z-ORDER only. Liquid clustering removes that class entirely, which is a concrete architectural argument rather than a preference.

Iceberg: a tree you prune twice

Iceberg's hierarchy is catalog → metadata JSON → manifest list → manifests → data files. The reason it scales is in the middle of that chain.

The manifest list carries partition field summaries for each manifest — does it contain nulls, what are the lower and upper bounds. So a query planner can eliminate entire manifests before opening any of them, and only then prune individual files using the column bounds inside the surviving manifests. Two levels of pruning, and the first one costs a single file read.

A second design detail worth carrying: data-file sequence numbers are written as null and inherited from the manifest at read time. The consequence is stated in the spec: to retry a commit with a new sequence number, only the manifest list must be rewritten. Manifests are reusable across attempts, which makes optimistic retries cheap — and optimistic retry is how the format handles concurrency.

On versions, be precise, because most secondary material is not. Versions 1, 2 and 3 are complete and adopted; version 4 is under active development and not formally adopted. The format version defaults to 2, so V3 features — nanosecond timestamps, variant and geometry types, row lineage, and binary deletion vectors stored as Puffin blobs — are opt-in. Engine support for V3 is uneven, with deletion vectors generally ahead of row lineage.

Schema evolution rests on immutable, never-reused field IDs. Permitted promotions are narrow — int to long, float to double, date to timestamp, widening a decimal's precision. Forbidden: grouping or ungrouping nested fields, swapping a primitive for a struct, and reusing a field ID. Partition evolution works by giving the new spec a new ID and leaving old manifests on their old spec, since each manifest holds files for exactly one partition spec — which is why reads are planned on data predicates rather than on partition values.

Hudi: ordering on time rather than on version

Hudi's timeline is a sequence of instants, each moving through requested, inflight and completed. What makes it structurally different is that instant times are monotonically increasing globally with bounded clock uncertainty — explicitly modelled on Spanner's TrueTime.

Delta and Iceberg order on a version or sequence number; Hudi orders on time. That single choice is why Hudi's centre of gravity is streaming upserts, and why its concurrency story reads differently: it offers a non-blocking mode where multiple writers touch the same file group and conflicts are resolved by the reader and the compactor, rather than by refusing the commit.

Two operational details worth knowing. Hudi's read-optimized query type reads only compacted base files, so you can explicitly opt out of merge cost per query — neither of the other formats gives you that lever. And under multi-writer, only upsert carries the never-duplicates guarantee: plain insert and bulk-insert may produce duplicates even with deduplication enabled.

Copy-on-write versus merge-on-read, decided properly

DELETE 1,000 ROWS SCATTERED ACROSS 500 FILES OF 256 MB COPY-ON-WRITE rewrite all 500 files without those rows write amplification: ~128 GB written to remove ~100 KB read amplification: zero — readers see clean files right when deletes are rare or whole-partition MERGE-ON-READ write a deletion vector marking those rows write amplification: kilobytes read amplification: every reader applies the vector right for CDC upserts and scattered GDPR deletes WHAT EACH FORMAT'S DEFAULT TELLS YOU ABOUT THE WORKLOAD ITS DESIGNERS ASSUMED Icebergupdate/delete default to COPY-ON-WRITEassumes batch, read-heavy, infrequent DMLDelta on Databricksnew tables default to DELETION VECTORSassumes frequent MERGE and UPDATEHudiMoR plus a read-optimized query typelets you opt out of the merge cost per query The failure mode of MoR is unbounded delete accumulation. Reads degrade monotonically until you compact, and nothing warns you. So MoR is not “the modern choice” — it is a decision to trade a scheduled maintenance job for cheap writes. If nobody owns thecompaction, choose CoW.
The same delete, two strategies. (CDC — change data capture — is the stream of row changes copied from a source database.) Note the bottom row — merge-on-read is not the modern default, it is a decision to trade a scheduled compaction job for cheap writes.

The decision is a straight trade between write amplification and read amplification, and each format's default tells you what its designers assumed. Iceberg defaults deletes, updates and merges to copy-on-write — a batch, read-heavy assumption. Delta on Databricks defaults new tables to deletion vectors — a frequent-DML assumption. Hudi gives you a separate query type so you can choose per query.

The failure mode to name in a design review: merge-on-read accumulates deletes silently, and read performance degrades monotonically until someone compacts. If no one owns that maintenance job, copy-on-write is the safer choice regardless of what the benchmark says.

The convergence, and what it actually means

The format war ended, and it did not end because one format won. Delta gained Iceberg compatibility features; Iceberg V3 adopted the same Roaring-bitmap deletion-vector idea Delta shipped first; Databricks acquired the Iceberg spec authors; and both vendors now speak the Iceberg REST catalog API — a plain web interface over HTTP.

What actually happened is that the contended layer moved from the format to the catalog. Delta's newer catalogManaged feature makes the catalog rather than the filesystem the arbiter of commit success — staged commits are ratified by the catalog, and filesystem-based access to such tables is explicitly unsupported. That solves the old object-store atomicity problem, and it means the question is no longer ‘which format?’ but ‘whose catalog vends the credentials and enforces the policy?’

A caution on the interoperability shims: format-translation features generate the second format's metadata asynchronously after the primary commit, and under a high commit rate a second concurrent generation is skipped — so the translated view lags and is not guaranteed current. Treat it as an interoperability convenience, not as a consistency guarantee, and prefer native support where it exists.

Check yourself

Why does partitioning a Delta table cost you concurrency?Row-level concurrency requires deletion vectors and an unpartitioned table. Partitioning disables it, so concurrent writers conflict at file granularity.
Why does Iceberg query planning scale?Two-level pruning. The manifest list carries partition summaries per manifest, so whole manifests are eliminated before any are opened.
What is structurally different about Hudi?It orders on time with bounded clock uncertainty rather than on a version number, which is why its design centres on streaming upserts and non-blocking concurrency.
When is merge-on-read the wrong choice?When nobody owns the compaction job. Deletes accumulate silently and read performance degrades monotonically with no warning.
← Choosing, migrating, and the interviewQuery execution: Photon, micro-partitions and the profile →