Back to Blog

Iceberg's Puffin Sidecars: Portable Stats for the Open Lakehouse

Prateek SinghMarch 18, 202610 min read104 views
Iceberg's Puffin Sidecars: Portable Stats for the Open Lakehouse

Apache Iceberg's Puffin file format is the most strategically important subsystem nobody is talking about. It is the mechanism by which an open lakehouse can carry warehouse-grade statistics across vendors — write the sketch once in Trino, read it tomorrow in Snowflake, plan a join correctly on the first cold query.

The cardinality problem nobody quotes in the lakehouse pitch decks

Every open-lakehouse pitch deck of the last four years walks a customer through the same arithmetic. Object storage at $0.023/GB-month. Compute decoupled from storage. Bring your own engine. Multi-engine reads. No vendor lock-in. The math is compelling, which is why Iceberg adoption has been one of the fastest-moving things in data infrastructure since the Databricks–Tabular acquisition closed in 2024.

What the pitch deck never mentions is the moment a Trino engineer points the new lakehouse at a 2 TB fact table and runs the first analytical join. The planner has no statistics. It doesn't know the NDV of the join key. It defaults to broadcasting the wrong side, shuffles four times more data than it needs to, and the query takes eight minutes when the same query on a warehouse would have taken forty seconds. The engineer's first impression of the open lakehouse is that it is slow.

The reason isn't the engine — Trino's planner is excellent. The reason is that nothing wrote any stats into the table. Iceberg's manifest layer carries min/max/null-count per column per data file, which is enough for partition pruning but nowhere near enough for join planning. The optimizer needs NDVs, histograms, sometimes Bloom filters, and those have to live somewhere portable — somewhere the engine that wrote them and the engine that reads them next don't have to agree on a Java class name to interpret. That "somewhere" is Puffin, and it is the part of the open lakehouse architecture almost nobody outside the Iceberg PMC is paying close enough attention to.

What Puffin actually is

Strip away the marketing and Puffin is delightfully simple. It is a single binary sidecar file with the structure Magic Blob₁ Blob₂ ... Blobₙ Footer — a magic-number header, a sequence of opaque blob payloads, and a footer carrying a UTF-8 JSON index describing what each blob is, which snapshot it belongs to, which columns it covers, and how to decompress it (per the Apache Iceberg Puffin spec).

The crucial design choice is that Iceberg does not interpret the blob payloads. The footer says "blob 3 is type apache-datasketches-theta-v1, applies to column l_orderkey on snapshot 4982371, is 16 KB long, starts at byte offset 8192." Iceberg knows where the blob is and what it claims to be. It does not crack the bytes open — that job belongs to whatever library knows how to read that blob type.

That separation is the same trick Parquet's footer uses for column chunk encodings, and it's the reason it scales. New stat types can be added to Puffin without changing the Iceberg core. An engine that doesn't recognize a blob type silently skips it. An engine that does, decodes it and uses it. There is no version-coupling between Iceberg and the things stored inside its sidecars — the only design that survives in a multi-engine world.

The four blob types that matter most

The spec officially defines two blob types today: apache-datasketches-theta-v1 for NDV sketches, and deletion-vector-v1 for Roaring-bitmap deletion vectors (added with Iceberg v3). Two more are under active discussion. Together they cover the four use cases that decide whether a planner produces a good plan:

  1. NDV sketches (Theta). Approximate distinct-value count, stored as a serialized compact Apache DataSketches Theta sketch. A 4096-entry sketch carries ±1.56% relative standard error at 68% confidence — accurate enough to drive every join-order and broadcast-vs-shuffle decision a modern optimizer makes. The blob metadata can also carry a pre-computed ndv property so readers don't have to deserialize the sketch just to read the cardinality.
  2. Bloom filters. Not yet a formal blob type, but the spec discussion on bloom-filter properties has been live since early 2025. Practical use: shuffle the build-side bloom to probe-side workers and skip entire row groups guaranteed not to match.
  3. Histograms. Equi-depth histograms or KLL quantile sketches give the planner distribution information rather than just NDV. The "update supported blob types" thread proposes both a ColumnStatistics blob and a KLL sketch type. Hive 4 already writes histogram-enriched Puffin per snapshot.
  4. Expression statistics / partial-aggregation reuse. The newest, least-settled category. Cache pre-computed sub-aggregates so a later query asking a superset question composes the answer from the sidecar rather than rescanning. Same idea as warehouse materialized views, expressed as a portable blob.

The combination matters. NDVs decide join order. Bloom filters cut probe-side work. Histograms turn predicate-selectivity guesses into accurate row counts. Expression stats short-circuit the scan. A planner with all four matches warehouse plan quality. A planner with none of them is the eight-minute Trino above.

Why vendor lock-in dies here

The concrete walk-through. A team operates an Iceberg fact table on S3 with a REST catalog. Monday night a Spark job lands fresh data and runs the Iceberg compute_table_stats procedure (or a Trino job runs ANALYZE, which writes NDV statistics to the Iceberg Puffin file). The sketch lands as a blob in a sidecar. The snapshot commits.

Tuesday morning, a different team queries the same table from Snowflake's externally-managed-Iceberg path — which became GA in October 2025. Snowflake's planner reads the manifest, sees the Puffin file reference, decodes the NDV blob, uses it. Snowflake didn't have to run its own ANALYZE. The Tuesday query plans correctly on the first execution because the stats Trino wrote on Monday night are still in the sidecar, in a format Snowflake understands.

That single property — stats surviving the engine that produced them — is what makes the open lakehouse architecturally credible. Without it, every engine pays the "first-touch tax": scan-and-rebuild of statistics the moment a new engine encounters the table. With it, the stats are infrastructure, not per-engine state.

Cold-query latency — first analytical join on a 2 TB Iceberg fact table Lower is better. Same query, same data, three planner stat conditions. Vendor warehouse (Snowflake native) ~38 s · baseline Lakehouse + Puffin sidecar (NDV present) ~51 s · 1.3× baseline Lakehouse, no Puffin (planner blind) ~510 s · 13× baseline Indicative shape from operator reports. The Puffin bar lands close to the warehouse bar because planning quality, not engine internals, dominates cold-query latency. The blind lakehouse pays a one-off scan cost to compute the same stats Snowflake had pre-baked and that Trino could have written into Puffin yesterday. Numbers illustrative; the order-of-magnitude gap between "blind planner" and "stats present" is the real story.

The lakehouse-with-Puffin number is not equal to the warehouse number — the warehouse still wins on result caching, micro-partition skipping, and warm compute pools. But the gap is single-digit-multiplier, not order-of-magnitude. The catastrophic bar is the bottom one, and that bar exists in production today on every Iceberg deployment whose operators haven't wired up Puffin writes. The cure is not a new engine. The cure is calling ANALYZE.

The production gotchas

Honest accounting: Puffin in 2026 is not a finished story, and four things bite operators in practice.

  • Writer support is opt-in and patchy. Trino writes Puffin NDVs through ANALYZE. Spark writes them through the compute_table_stats procedure. Most other engines don't write Puffin at all yet — writing is still not widely adopted and is not fully standardized across the ecosystem. If nobody in your stack writes Puffin, nobody benefits from reading it.
  • Reader support is also uneven. Even engines that ship "Iceberg support" may not consult the Puffin sidecar during planning. Snowflake's external-Iceberg path reads Puffin; Spark's NDV consumption is still tracked as an open issue. Check the version matrix before you assume a stats blob is being used.
  • Cardinality estimates rot. A Theta sketch written against snapshot 4982371 describes that snapshot's data. Append 30% more data over a week without re-running ANALYZE and the sketch is stale — still readable, still parsing, just wrong. Iceberg's snapshot model lets readers detect this, but it's a discipline question more than a format question. Schedule the stats refresh the same way you schedule compaction.
  • Sketches are approximate by construction. A Theta sketch carries ±1.56% relative standard error at 68% confidence, ±3.1% at 95% for the default 4K-entry sketch. That is fine for join planning and group-by sizing. It is not fine for a billion-row anti-join where false positives in a Bloom filter inflate the probe side by 1%. Know the error budget of the structure you're trusting.

None of these are fatal. They are the normal cost of a format that is still maturing in public. They are the reason the post is titled "the most strategically important subsystem nobody is talking about" rather than "the finished thing you already use."

What this means for table-format wars

The 2020-2024 lakehouse pitch was about storage economics — Parquet on object storage versus columnar storage in a warehouse. That war is over; storage costs converged. The 2025-2026 pitch is about planner quality — whether the open ecosystem can carry warehouse-grade statistics across the engines that share a table. That war is being fought right now, in the Puffin spec discussions and the writer/reader matrices.

Delta Lake is making the same move from the other side. Its deletion vectors are now stored using the same Puffin deletion-vector-v1 blob layout Iceberg uses, and there is active work to push more Delta stats into Puffin-shaped sidecars. Hudi has its own indexing story tuned for high-frequency upserts. Each format is climbing the same hill — portable, engine-neutral statistics — by slightly different routes. The portability winner will be whoever ends up with the largest readers-and-writers matrix, not whoever has the prettiest spec.

The bet worth making is that Iceberg + Puffin gets there first, not on technical superiority but on the fact that more engines have agreed to read its sidecars than read anything else's. Snowflake reads Iceberg Puffin. Trino writes it. Spark is closing the gap. The newer entrants — DuckDB-Iceberg, the streaming engines, the GPU-accelerated layer — all default to Iceberg compatibility. The layer doing the load-bearing work in that win is the unflashy one with the puffin on its logo.

If you operate an Iceberg table in production and you are not writing Puffin sidecars, you are running a slow warehouse and calling it a lakehouse. The fix is two lines of SQL and one cron job. The strategic implication is much larger than that suggests.

Subscribe to new posts from theaivibe.org

No spam — just new posts. One-click unsubscribe.
Share this article

Related Posts

sqljev: TypeSafe Jev's jev() for SQL Server, Postgres, Snowflake, BigQuery and DuckDB, on Jev or Open-Weight Laya
Data Engineering10 min read

sqljev: TypeSafe Jev's jev() for SQL Server, Postgres, Snowflake, BigQuery and DuckDB, on Jev or Open-Weight Laya

SQL cannot say 'the customer threatens to cancel'. sqljev adds jev(), jev_prob() and jev_choice() to SQL Server, PostgreSQL, MySQL, Snowflake, Databricks, BigQuery, Redshift and DuckDB, and answers them with Laya, an open-weight decision model that runs on your hardware, returns calibrated probabilities instead of text, and can be fine-tuned on your own tables. 140,000 decisions in 271 seconds on one laptop GPU; re-running all 13 queries, 0.9 seconds. Apache 2.0, version 0.1.0.

65 views
Read
pankhllm: The LLM Gateway That Learns to Skip the LLM, Without Replacing the Stack You Already Run
Data Engineering10 min read

pankhllm: The LLM Gateway That Learns to Skip the LLM, Without Replacing the Stack You Already Run

Most of an agent's LLM calls are not writing anything. They are decisions: which tool, which skill, which parameters, made thousands of times a day by a model paid in seconds and tokens. pankhllm sits where your app already calls an LLM, learns those decisions from its own traffic, and starts making them in 0.2 ms on a CPU with a 262 KB model. What it is not sure about still goes to your LLM. On the same 14 questions: a 12B planner 1,743 ms, Laya 49 ms, pankhllm's own model 4 ms, all 14 correct. Here is what it is, what it is not, and where it stops.

76 views
Read
Your Apple Silicon GPU Loses to One CPU Core Until a Million Rows. I Measured 111 Operations, Then Rebuilt ArrowMetal 0.2.0 Around the Answer
Data Engineering19 min read

Your Apple Silicon GPU Loses to One CPU Core Until a Million Rows. I Measured 111 Operations, Then Rebuilt ArrowMetal 0.2.0 Around the Answer

Every GPU data library benchmarks itself at 50 million rows. Your dataframe has 80,000. On an Apple M4 Max, summing 1,000 integers takes the GPU 112 microseconds and Polars less than one: the GPU is more than 100 times behind. I built one of these libraries, so I measured the row count where the GPU overtakes the fastest CPU code for 111 operations: the median needs 10,000,000 rows against a multi-core library, about a million against one core, and sixteen never get there. So ArrowMetal 0.2.0 refuses the GPU below the line, byte-identical, and around that router it grew GPU readers for CSV, JSON, nested Parquet, Delta Lake and Iceberg, a Polars engine and a DuckDB optimizer extension.

77 views
Read