The footer pays for column skipping
Measure your machineNeeds Python with PyArrow
Problem
Parquet stores each column's data in separate chunks and writes a footer at the end of the file that says where every chunk is and what its statistics are. A reader opens the footer first and fetches only the chunks it needs, so reading 3 of 200 columns costs roughly 3/200 of the data. The price is that the file is complete only when the footer is written: it cannot be appended to, and a file cut short has no index to its contents.
Write a Parquet file with 200 columns and 1,000,000 rows. Read 3 columns, then read all 200. Time both and compare bytes read. Then print the footer statistics, and try to append to the file.
The question. How much cheaper is reading 3 columns than 200, and what happens when bytes are appended after the footer?
Predict
Reading 3 columns is far faster than 200 (close to 3/200 of the work for data of this kind). You cannot append to the file, because the footer is written at the end.
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
the-footer-pays-for-column-skipping.py # /// script # requires-python = ">=3.10" # dependencies = ["numpy", "pyarrow"] # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """The footer pays for column skipping. A Parquet file of 200 columns and 1,000,000 rows: reading 3 columns against all 200 (time and bytes); the footer's per-row-group statistics; and an attempt to append to the file. """ import argparse, json, os, tempfile, time import numpy as np, pyarrow as pa, pyarrow.parquet as pq p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--rows", type=int, default=1_000_000, help="Rows.") p.add_argument("--cols", type=int, default=200, help="Columns.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() path = os.path.join(tempfile.mkdtemp(), "wide.parquet") table = pa.table({f"c{i}": np.random.default_rng(i).integers(0, 1000, args.rows) for i in range(args.cols)}) pq.write_table(table, path, row_group_size=100_000) md = pq.ParquetFile(path).metadata def chunk_bytes(cols): return sum(md.row_group(g).column(c).total_compressed_size for g in range(md.num_row_groups) for c in cols) pq.read_table(path, columns=["c1"]) # warm the file into the cache for both reads t = time.perf_counter(); pq.read_table(path, columns=["c1", "c2", "c3"]); t3 = time.perf_counter() - t t = time.perf_counter(); pq.read_table(path); tall = time.perf_counter() - t st = md.row_group(0).column(0).statistics try: with open(path, "ab") as f: f.write(b"more rows") pq.ParquetFile(path); appended = "the file still reads" except Exception as e: appended = f"the file no longer reads: {type(e).__name__}" rows = [{"read": "3 columns", "seconds": t3, "mb": chunk_bytes([1, 2, 3]) / 1e6}, {"read": f"all {args.cols} columns", "seconds": tall, "mb": chunk_bytes(range(args.cols)) / 1e6}] for r in rows: print(f"{r['read']:<12} {r['seconds']:6.3f} s {r['mb']:7.1f} MB of column chunks") print(f"footer statistics, row group 0, c0: min {st.min}, max {st.max}, nulls {st.null_count}") print(f"appending bytes after the footer: {appended}") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"reads": rows, "summary": [{"time_ratio": tall / t3, "byte_ratio": rows[1]["mb"] / rows[0]["mb"], "append": appended, "stat_min": st.min, "stat_max": st.max}]}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run the-footer-pays-for-column-skipping.py uv run the-footer-pays-for-column-skipping.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --rows1,000,000 | Rows. |
| --cols200 | Columns. |
Our run
Column reads
| Read | Time | Column chunks (MB) |
|---|---|---|
| 3 columns | 5.36 ms | 3.89 |
| all 200 columns | 711 ms | 259 |
3 columns 0.005 s 3.9 MB of column chunks
all 200 columns 0.711 s 259.3 MB of column chunks
footer statistics, row group 0, c0: min 0, max 999, nulls 0
appending bytes after the footer: the file no longer reads: ArrowInvalidWhat we found
Three columns took 5.36 ms and 3.89 MB of column chunks; all 200 took 711 ms and 259 MB. The bytes fell exactly in proportion (66.7×), the time by more (133×), since a small read also skips per-column setup. The footer gave the first row group's statistics for c0 (min 0, max 999). After bytes were appended, the file no longer read: the file no longer reads: ArrowInvalid.
Read it
The footer is what makes column skipping possible and what makes the file immutable. That is why table formats never append to Parquet files: they write new files and record them in a manifest, and why a writer that dies before the footer leaves a file no reader can use.