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.
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: 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_checkpointis 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
numRecordsequal 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
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