Skip to content
About
Main content

The footer pays for column skipping

Measure your machineNeeds Python with PyArrow

Problem

stores each column's data in separate chunks and writes a 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 of 200 columns, how many times faster is it than reading all 200?

Run it

  1. 1Install uv once. It fetches Python and the lab's packages for you.

    Terminal
    curl -LsSf https://astral.sh/uv/install.sh | sh
  2. 2Get the script. It is the whole lab: read it, then download it.

    the-footer-pays-for-column-skipping.py44 linesDownload
    View 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)
    
  3. 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
Knobs
Knob · defaultWhat it changes
--rows1,000,000 Rows.
--cols200 Columns.

Our run

Column reads

Column reads
ReadTimeColumn chunks (MB)
3 columns5.36 ms3.89
all 200 columns711 ms259
Reference runApple M2 · 8 cores · 8 GB
Output · uv run the-footer-pays-for-column-skipping.py
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: ArrowInvalid
macOS 26.6.2 · Python 3.13.3 · numpy 2.5.4 · pyarrow 26.0.0 · run 2026-10-20

What 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 never append to Parquet files: they write new files and record them in a , and why a writer that dies before the footer leaves a file no reader can use.