Foundations of distributed data · FND-06
Storage formats
- 3 h
- requires FND-01
From your world. Why Athena charges by scanned bytes; row-group pruning is why a WHERE on a sorted column is cheap
Read Parquet footers and predict row-group pruning
Implement the row-group pruning logic that an engine uses to skip data. You read only the Parquet footer (metadata + statistics of each column chunk). You never read the data pages. You decide which row groups a filter must scan.
Functions to implement in solution.py
1. footer_stats(parquet_path, column) -> list[dict]
Open the file with pyarrow.parquet. For the named column, return one dict per row group, in
row-group order:
{"row_group": i, "num_rows": int,
"min": <value or None>, "max": <value or None>, "null_count": <int or None>}
Read this from the metadata only (ParquetFile(path).metadata → row_group(i).column(j) →
.statistics). Do not read the column data.
2. row_groups_to_scan(parquet_path, column, op, value) -> list[int]
Return the sorted list of row-group indices that you cannot prune for the predicate
column <op> value. Here op is one of ">", ">=", "<", "<=", "==".
The pruning rule: skip a row group only when its [min, max] makes a match impossible:
">": skip iffmax <= value">=": skip iffmax < value"<": skip iffmin >= value"<=": skip iffmin > value"==": skip iffvalue < minorvalue > max
Safety rule: suppose the statistics of a group are missing (min/max is None). Then you
must not prune it. Return it as a group to scan. An engine that pruned on absent stats would
drop rows.
What the tests check (DuckDB is the oracle, G6)
- The test writes a Parquet file with a known number of row groups and controlled, sorted values. So the expected pruned and kept groups are known.
footer_statsreports the correctmin/max/num_rowsfor each group. It reads metadata only (the test verifies this againstparquet_metadataof DuckDB).- Correctness/soundness invariant (Hypothesis): for random data, predicates and row-group sizes, your function says to scan every row group that holds a row that satisfies the predicate. Pruning must never drop a matching row. (DuckDB computes which groups actually contain matches.)
- When statistics are absent, no group is pruned.
Constraints: pure Python + pyarrow + duckdb (oracle in tests). Deterministic. lab: none.
Starter code
"""Starter for FND-06/01-parquet-pruning. See README.md."""
from __future__ import annotations
def footer_stats(parquet_path, column: str) -> list[dict]:
# TODO: read ParquetFile(path).metadata and return per-row-group
# {"row_group", "num_rows", "min", "max", "null_count"} for `column`.
raise NotImplementedError
def row_groups_to_scan(parquet_path, column: str, op: str, value) -> list[int]:
# TODO: return row-group indices that cannot be pruned for column <op> value.
# Never prune when stats are missing.
raise NotImplementedError
Hints
Hint 1
Open the file with pyarrow.parquet.ParquetFile(path). Its .metadata gives num_row_groups; for each row group, .row_group(i).column(j) exposes path_in_schema and .statistics with .min, .max, .null_count. Match the column by path_in_schema.
Hint 2
A row group CAN match a predicate when its [min, max] overlaps the predicate's possible values. For '>' value v, skip the group iff max <= v. For '>=' skip iff max < v. For '<' skip iff min >= v. For '==' v, skip iff v < min or v > max. If statistics are missing (None), you must NOT prune — assume the group matches.
Hint 3
footer_stats should return, per row group, the min/max/null_count/num_rows for the chosen column by reading ONLY the metadata (no data pages). row_groups_to_scan returns the sorted list of row-group indices that cannot be pruned for (column, op, value).
uv run de start FND-06/01-parquet-pruning
# edit workspace/FND-06/01-parquet-pruning/
uv run de test