skip to content

Why does a table format's metadata layer keep its own per-file min/max column statistics?

level: middleimportance: should knowfreq 55%

answer

  1. deciding what to read before reading anything
  2. opening every footer is thousands of tiny reads
  3. a summary copied upward from each file
  4. bounds prune only when values are clustered

basics

~20 s

So the planner can eliminate whole data files from a query by reading metadata alone. Without those bounds in the table layer, the engine would have to open every file's footer just to learn it contains nothing relevant.

solid answer

~50 s

Each data file already stores statistics about itself, but reading them means opening every file — thousands of small random reads against object storage before the query starts. A table format therefore copies per-file summaries into its own metadata: row count, file size, null counts, and lower/upper bounds per column. Scan planning then filters the file list first, so a predicate like `event_date = '2026-03-01'` may drop 99% of the files without a single data read. What survives planning is then narrowed again *inside* each file by the file format's own skipping. The catch is that bounds only help if values are clustered: if every file's range spans the whole domain — the usual result of unsorted streaming ingest — the min/max test passes everywhere and prunes nothing. That is why compaction usually sorts as well as merges.

code

json · 10 lines
json
{
  "file_path": "data/part-00042.parquet",
  "record_count": 1200000,
  "file_size_bytes": 268435456,
  "column_stats": {
    "event_date": { "lower": "2026-03-01", "upper": "2026-03-01", "null_count": 0 },
    "customer_id": { "lower": 1004, "upper": 1099, "null_count": 0 },
    "amount": { "lower": 0.01, "upper": 8421.55, "null_count": 17 }
  }
}

go deeper

for a junior

Remember the purpose: the table's metadata records the value range of each column in each file, so the engine can decide a file is irrelevant without opening it.

for a middle

Explain both levels of skipping and why the metadata copy exists at all — footer reads are small random I/O against object storage, and there may be a million of them.

for a senior

Diagnose the case where statistics are present and pruning still fails: unclustered writes make every file's range overlap the predicate, so the fix is a sorting compaction, not more statistics.

for a principal

Own the tradeoff curve: which columns get statistics, how aggressively to sort during maintenance, and the point where metadata volume from too many small files costs more than the pruning it enables.

## Where the statistics live Statistics about data exist at two levels in a lakehouse table, and the distinction is exactly the file-layer/table-layer split. Inside a data file, the file format records information about its own contents so a reader can skip parts of it. Above that, the **table layer** keeps a per-file summary in its metadata: how many rows the file has, how large it is, how many nulls each column holds, and the lower and upper bound of each column's values in that file. The second is a deliberate duplication of information already present in the first, and the duplication is the whole point. ## Why duplicate it Query planning has to decide which files to read before it reads anything. If per-file bounds live only inside the files, the planner has two bad options: open every file's footer — thousands of small, high-latency random reads on object storage, often more expensive than the scan they were meant to avoid — or skip the check and read everything. With the bounds in metadata, planning is a filter over a compact list. A query with `WHERE event_date = '2026-03-01'` compares its predicate to each file's bounds and keeps only files whose range could contain matching rows. On a well-clustered table this drops the file list by orders of magnitude, and the cost of that decision is a handful of metadata reads. ## Two levels of skipping The two levels compose: 1. **Planning-time file pruning** in the table layer removes files entirely — they are never opened, never fetched, never scheduled onto a task. 2. **Read-time skipping** inside each surviving file, using the file format's own internal statistics, avoids decoding chunks of the file that cannot match. Level one saves I/O, request count and scheduling; level two saves decode CPU and bytes transferred. A candidate who conflates them usually cannot explain why planning got slow on a table with a million tiny files: the answer is that level one has to consider every entry in the file list, so metadata volume itself becomes the bottleneck. ## Why bounds sometimes prune nothing Min/max pruning is only as good as the correlation between the predicate column and the physical file layout. Consider a table ingested from a stream, where every micro-batch contains events from every customer. Each file's `customer_id` range then runs from nearly the lowest id to nearly the highest, so a filter on any single customer overlaps every file's range and prunes zero files — while the metadata dutifully reports statistics for all of them. The fix is physical: write or rewrite the data so that values of the filtered column are concentrated in few files, by sorting or clustering during compaction. This is why maintenance jobs typically sort as well as bin-pack. Partitioning does the same thing at a coarser grain, by making the partition value itself part of file placement. ## Practical limits A few caveats worth naming: - **String bounds are usually truncated.** Keeping full values for long text columns would bloat metadata, so bounds are stored as truncated prefixes that still bound the range correctly but prune less precisely. - **Bounds are conservative, never exact.** A file whose range overlaps the predicate may still contain no matching row; pruning avoids false negatives, not false positives. - **Statistics must be produced on write.** Files registered into a table by a path that does not compute statistics carry no useful bounds, and the planner must then assume they might match anything. - **Metadata grows with file count.** Millions of tiny files mean millions of statistics entries to evaluate, which is one more reason the small-files problem hurts even when total data volume is modest. - **Not every column is worth tracking.** Some formats let you limit statistics collection to the columns actually used in filters, trading metadata size against pruning power. ## The interview framing "The file knows about itself; the table knows about all its files." Copying a small summary upward turns per-file knowledge into a planning decision that can be made once, cheaply, for the entire table — and that is the mechanism behind most of the scan-reduction a lakehouse table delivers over a bare directory of files.

  • A table has correct per-file bounds on customer_id, yet filtering by one customer still scans every file. Why?
    Because the values are not physically clustered. Streaming ingest puts events from all customers into every file, so each file's min/max range spans nearly the whole id domain and overlaps any single-customer predicate. The statistics are accurate and useless simultaneously. The remedy is to sort or cluster the data by that column during compaction, or to partition on a coarser derived value.
  • Why are string min/max bounds often stored truncated?
    Long text values would make metadata large enough to slow the very planning step it is meant to speed up. Truncating to a prefix keeps a valid bound — the true minimum is at least the stored lower prefix, the true maximum at most the incremented upper prefix — so pruning stays correct, just less precise for predicates that differ only after the truncation point.
  • Does having per-file statistics in metadata make the file format's own internal statistics redundant?
    No, they solve different halves of the problem. Metadata bounds decide which files to open at all; the file's internal statistics decide how much of an opened file must actually be decoded. Dropping either one costs you a distinct level of skipping.

saying these in an interview costs you the question

  • Assuming statistics make a query fast regardless of data layout
  • Confusing pruning whole files with skipping inside a file
  • Thinking the planner opens each file to read its footer stats
  • Believing min/max bounds guarantee matching rows exist in a file
  • Ignoring that millions of files make planning itself the bottleneck

context