Which statistics does an Iceberg manifest store, and how do they prune a scan?
answer
- the planner reads numbers, not data files
- two passes: manifests first, then files
- min and max per column, plus null counts
- bounds only prune when data is clustered
basics
~20 sEach Iceberg manifest entry stores per-file record_count, file_size_in_bytes, column_sizes, value_counts, null_value_counts, nan_value_counts and per-column lower_bounds and upper_bounds. Planning compares the predicate against those bounds and drops files that cannot match, before opening any data file.
solid answer
~40 sAn Iceberg manifest row describes one file and carries the numbers a planner needs: `record_count`, `file_size_in_bytes`, and per-column maps keyed by field ID — `column_sizes`, `value_counts`, `null_value_counts`, `nan_value_counts`, `lower_bounds` and `upper_bounds` — plus the file's partition tuple and `split_offsets`. Pruning happens in two passes. First, the manifest list's per-partition-field summaries (`contains_null`, `lower_bound`, `upper_bound`) let the planner skip whole manifests. Then, within surviving manifests, the partition tuple removes non-matching partitions and the per-column min/max bounds remove individual files — a file whose `upper_bounds` for `amount` is 40 cannot satisfy `amount > 100`. All of this happens **before** any data file is opened, which is the difference from footer-based pruning. By default bounds for string columns are truncated (`truncate(16)`), and metrics are collected only for a bounded number of leading columns unless `write.metadata.metrics.column.<col>` says otherwise.
code
sql · 8 lines-- per-file statistics recorded in manifests
SELECT file_path, record_count, null_value_counts, lower_bounds, upper_bounds
FROM db.events.files
WHERE content = 0;
-- per-manifest partition summaries used to skip manifests
SELECT path, added_data_files_count, partition_summaries
FROM db.events.manifests;go deeper
Recall that Iceberg stores per-file counts and per-column minimum and maximum values in its manifests, and that queries use them to skip files.
Name the actual fields — record_count, value_counts, null_value_counts, lower_bounds, upper_bounds — and describe the two pruning passes: manifest summaries, then per-file bounds.
Diagnose with them: check the files metadata table when a filter fails to prune, and know that bounded metrics collection and unclustered data are the two usual causes.
Decide the platform defaults — which columns get full metrics, what sort orders writers use, and how manifest size and statistics width trade off against planning latency.
## What a manifest entry records Every row of an Iceberg manifest is a `manifest_entry`: a `status` (ADDED, EXISTING, DELETED), a `snapshot_id`, sequence numbers, and a nested `data_file` struct. That struct is where the statistics live: - `content` — 0 data file, 1 position deletes, 2 equality deletes (v2+) - `file_path`, `file_format`, `partition` (the tuple of transformed partition values) - `record_count`, `file_size_in_bytes` - `column_sizes` — bytes per column, keyed by field ID - `value_counts` — non-null-plus-null value count per column - `null_value_counts`, `nan_value_counts` - `lower_bounds`, `upper_bounds` — the min and max value per column, stored as binary in the column's type encoding - `split_offsets` — where the file can be split for parallel reads - `sort_order_id`, `key_metadata`, and for equality deletes, `equality_ids` Because these are keyed by **field ID**, not by column name, they survive renames: a column renamed from `amt` to `amount` keeps its statistics. ## Two-stage pruning **Stage one — skip manifests.** The manifest list row for each manifest carries a `partitions` array of field summaries: `contains_null`, `contains_nan`, `lower_bound`, `upper_bound` for each partition field. A query filtering `ts >= '2026-05-01'` on a table partitioned by `days(ts)` discards every manifest whose `ts_day` upper bound is earlier — those manifests are never opened. This is what keeps planning cheap on tables with millions of files. **Stage two — skip files.** Inside a surviving manifest, each entry is tested. The partition tuple is checked against the predicate's partition residual, then the per-column bounds are checked directly: `lower_bounds[amount] > 100` fails `amount < 50`; `null_value_counts[x] == record_count` means the file is all-null for `x`, so `x = 5` cannot match. What survives becomes scan tasks. The crucial property: **no data file is opened to decide this.** Formats like Parquet keep their own footer and row-group statistics, and engines do use them for finer-grained skipping once a file is opened — but that requires a read per file. Iceberg's manifests hoist file-level statistics into the table layer so the plan is built from a handful of metadata reads. ## What limits their usefulness Min/max bounds only prune when data is **clustered** by the filtered column. If every file spans the whole value range of `user_id`, no file's bounds exclude any particular user and every file is read. That is why write sort orders and file rewriting matter: they narrow the bounds. Statistics are collected, not guessed — they describe reality, so scattered data yields useless bounds. Collection is also configurable and bounded on purpose. `write.metadata.metrics.default` controls the mode — `none`, `counts`, `truncate(N)` or `full` — with truncated bounds the default for strings so that manifests do not balloon on wide text columns. Metrics are inferred for a limited number of leading columns; beyond that you must opt in per column with `write.metadata.metrics.column.<column>`. A frequent production surprise is a filter column that is deep in a wide schema and therefore has no bounds recorded at all. Truncated bounds stay **correct but inclusive**: a truncated lower bound is rounded down and an upper bound rounded up, so pruning may keep a file it could have skipped, but never skips a file it should have kept. ## Beyond manifests: Puffin statistics Manifest statistics are per-file and exact for min/max, but they say nothing about cardinality. Iceberg therefore supports **Puffin** files — a binary format for statistics and index blobs — referenced from the metadata file's `statistics` list (each entry naming a `snapshot-id`, `statistics-path` and blob metadata). The common blob is a Theta sketch (`apache-datasketches-theta-v1`) giving approximate distinct counts per column, which cost-based optimizers use for join ordering. These help the optimizer choose a plan; they do not prune files. ## Inspecting them Metadata tables expose everything: `db.tbl.files` shows per-file `record_count`, `lower_bounds` and `upper_bounds`; `db.tbl.manifests` shows `partition_summaries` per manifest. When a query reads more files than expected, querying those two tables tells you immediately whether the bounds were absent, useless, or simply not selective. ## Common mistakes Calling these Parquet row-group statistics — they are file-level and stored in the table layer. Assuming every column has bounds — collection is bounded by default. Assuming bounds guarantee pruning — they only help when data is clustered. And assuming truncation causes wrong results — it causes conservative, not incorrect, pruning.
- A query filters on a column but still reads every file — what do you check first?Query the files metadata table for that column's `lower_bounds` and `upper_bounds`. Either the bounds are missing, because metrics were not collected for that column, or each file spans nearly the whole value range, meaning the data is not clustered by it. The first is a metrics-property fix; the second needs a sort order or a rewrite.
- Why are Iceberg's bounds keyed by field ID rather than column name?Field IDs are assigned once and never reused, so renaming or reordering a column preserves its statistics and its data-file mapping. Name-keyed statistics would silently become unusable after a rename, and could even be matched to the wrong column if a name were reused.
- What do Puffin statistics files add that manifests do not?Manifests carry per-file min/max and counts, which drive pruning. Puffin files hold sketch blobs — commonly Theta sketches for approximate distinct counts — referenced from the metadata file's `statistics` entries. Cost-based optimizers use them to estimate cardinality and order joins; they do not eliminate files from a scan.
saying these in an interview costs you the question
- Calls manifest statistics Parquet row-group statistics
- Assumes every column has min/max bounds recorded
- Says min/max bounds prune regardless of data clustering
- Thinks truncated string bounds can produce wrong results
- Believes the planner opens data files to read their footers