Build1 distinct publisher3 min readPublished
A distinct-value sketch is thousands of bytes and a delete bitmap has no fixed size, which is why Iceberg keeps both in a separate file, and why a bad join plan is a statistics problem rather than a rewriting one.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
Take the delete case at its own numbers. Three hundred rows out of four million is 0.0075 percent of the file, about one row in 13,333 [20]. In format version 2 that edit lands as a Parquet position delete file naming the data file and the position of each deleted row [4]. The write is cheap. The read path is where it settles, because every later read of that data file has to open the delete file, decode Parquet, build a set of positions, and filter [5]. Do that across ten thousand data files and, per the article, delete handling dominates query time [5].
For a long time my answer to delete-heavy tables was to compact more often. That works by rewriting the data files, which is a full write issued to avoid a per-read decode. The version 3 deletion vector goes at the decode instead: a bitmap blob fetched by offset, described in the footer rather than parsed out of a second Parquet file [15][16]. Whether that trade favours you depends on your read-to-write ratio, and the source gives no number for either.
The join case is the same mismatch pointed the other way. The planner has row counts, bounds, and null counts for every column in every file [2], which tells it how large the scan is and nothing about how selective the join key is. Two billion rows against forty thousand is a 50,000-to-1 ratio [19]; the small side looks obviously broadcastable right up to the point where the optimizer wants the distinct count on the fact side, does not find it, guesses, and shuffles terabytes a broadcast join would have avoided [1][3].
You cannot fix that by adding a column to the manifest. Per-file distinct counts do not sum, because the same value turns up in many files [9]. What does merge is a sketch, and a mergeable sketch is thousands of bytes [10] against manifest metrics that run a few bytes per column per file [8]. The planner reads those entries on every query [7]. Anything you park there, every query carries.
So the container is deliberately dull: four magic bytes, 0x50 0x46 0x41 0x31, blobs written back to back, and a JSON footer saying what each blob is, what it was computed for, and where it sits [14][15]. The footer is the index, which is what makes reading one statistic without reading the file possible, and random access was one of the goals stated when the format was proposed [13]. The magic spells PFA1, for Fratercula arctica version 1 [17]. Someone enjoyed themselves.
None of the figures here are measurements. The two-billion-row join and the three-hundred-row delete are illustrations chosen to show the shape [1][4], and the shape transfers only if your deletes are scattered through files rather than clustered into whole files you could simply drop, and if your join keys are high-cardinality enough that the broadcast decision actually turns on the distinct count. The author works at Dremio, whose query engine consumes Puffin statistics, and says so in the piece while claiming nothing vendor-specific [18]. Read the ledger accordingly. An extensible container currently holding two blob types is mostly a bet on the third [16].
Ranked by verification strength, evidence, and original report placement.
The article's worked example is a query joining a 2-billion-row fact table to a 40,000-row dimension table, where the optimizer must decide which side to broadcast and which to hash.
Reading the manifests, the optimizer finds row counts, min and max values, and null counts for every column in every file, but does not find how many distinct customer IDs exist in the fact table.
In format version 2, deleting 300 rows from a data file that holds 4 million rows is written as a position delete file: a Parquet file listing the path of the data file and the position of each deleted row.
An Iceberg manifest is an Avro file with one entry per data file or delete file, each entry carrying the file path, format, partition tuple, record count, file size, and per-column metrics: value counts, null counts, NaN counts, and lower and upper bounds.
The planner reads manifest entries for every query; bounds let it skip files whose value ranges cannot match a predicate, and counts let it estimate scan size.
Manifest metrics share three properties: they are small at a few bytes per column per file, cheap to compute during the write because the writer already sees every value, and per file, which is the granularity the planner needs for pruning.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · September 1, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
Six specifications decide whether an agent can move off the harness it was built on1 distinct publisher
build
Object or file storage: the wrong pick stays quiet until inode exhaustion1 distinct publisher
build
Iceberg votes to forbid new equality deletes in V4, and Parquet 1.18.0 lands with two corruption bugs1 distinct publisher
build
Iceberg v3 moves the coordinate reference system out of the column comment and into the schema1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Checkable specifics, one voice
The load of this story sits on facts that are precise enough to be falsified — four magic bytes, signed little-endian integers, two defined blob types, a footer read in two ranges — and precision of that kind rarely survives being wrong. What it does not have is a second reader: no Apache spec text, no other engine's implementation notes, nothing but dev.to's own reconstruction of a public format.
One named consumer
Puffin is in the Iceberg spec and deletion vectors are tied to format version 3, so the format is not speculative — but the only engine our coverage can name as actually reading these blobs is the author's employer. Trino appears once, as the integration work the proposal came alongside in 2022, not as a present-day consumer.
Modest piece, two loud numbers
Slightly overstated, and in a narrow way. The format description asks the reader to believe nothing that cannot be verified, and the author explicitly declines a vendor pitch. The overreach is confined to the two hooks — terabytes of avoidable shuffle, delete handling that dominates query time — which do the persuasive work of a benchmark without being one.
Disclosed and self-limiting
An engine vendor's employee explaining why the metadata layer his employer's optimizer depends on is designed the way it is. He says so in the introduction, which converts a hidden interest into a visible one, and the subject matter offers little room to tilt — you cannot flatter your own product by reciting a magic number. The pull that remains is in emphasis: the problems chosen are the ones sketch-consuming engines solve well.
Firm on structure, thin on consequence
We would stand behind the mechanics: what manifests carry, why a mergeable sketch and an arbitrary-size bitmap cannot live in them, how the container is laid out. We would not stand behind the performance claims, and with one publisher and one author there is nothing in this reporting to correct either of them against.