Apache Parquet Format

Parquet is the default storage format of the modern data lake: Spark, Trino, DuckDB, Snowflake external tables, Delta Lake and Iceberg all read it natively. This page explains the file layout that makes it fast, shows how readers skip data, and gives runnable PyArrow and DuckDB examples.

Apache Parquet is a columnar storage file format optimized for use with data processing systems like Apache Hadoop, Apache Spark, and cloud-based data lakes. It is highly efficient for large-scale data storage and retrieval, especially for analytic workloads.

Key Features of Parquet:

Benefits of Using Parquet:

Use Cases:


1. File layout

Parquet file layout Row group 1 ~128 MB of rows Row group 2 ~128 MB of rows Column chunks one per column Pages encoded + compressed values Footer schema, row-group and column statistics Readers fetch the footer first, then only the column chunks they need.

A Parquet file is split horizontally into row groups, and each row group stores every column contiguously as a column chunk. Column chunks are divided into pages, the unit of encoding and compression. The footer at the end of the file holds the schema and, for each column chunk, statistics such as min, max and null count. Because the footer is at the end, writers can stream data and only finalize metadata when they close the file.

Within a page, values are encoded before compression. Dictionary encoding replaces repeated strings with small integer ids, run-length and bit-packing encodings shrink low-cardinality and boolean columns, and delta encodings help sorted integers and timestamps. Compression codecs (Snappy, ZSTD, GZIP) then apply on top.

2. How readers skip data

Query pruning with Parquet SELECT a, b projection Read footer min/max stats Skip row groups predicate pushdown Decode 2 columns vectorized

Columnar layout gives projection pushdown: a query that touches two columns of a 100-column table reads roughly 2 percent of the bytes. Footer statistics give predicate pushdown: if a row group's max order_date is earlier than the filter, the reader skips it without decompressing anything. Pruning works best when data is sorted or clustered on the filter column, so that each row group covers a narrow range of values.

3. Worked example

import pyarrow as pa
import pyarrow.parquet as pq
import pyarrow.dataset as ds

table = pa.table({
    "region": ["us", "eu", "us", "apac"] * 250_000,
    "amount": list(range(1_000_000)),
})
# Write partitioned by region, ZSTD compression, ~100k rows per row group
pq.write_to_dataset(table, root_path="sales", partition_cols=["region"],
                    compression="zstd", row_group_size=100_000)

# Read only one column, only the "us" partition, only rows matching a filter
dataset = ds.dataset("sales", format="parquet", partitioning="hive")
us = dataset.to_table(columns=["amount"],
                      filter=(ds.field("region") == "us") & (ds.field("amount") > 900_000))
print(us.num_rows)

# Inspect footer metadata
f = pq.ParquetFile(next(iter(dataset.files)))
print(f.metadata.num_row_groups, f.metadata.row_group(0).column(0).statistics)
-- DuckDB queries the same files directly
SELECT region, sum(amount)
FROM read_parquet('sales/*/*.parquet', hive_partitioning = true)
GROUP BY region;

4. Parquet versus other formats

FormatLayoutBest for
ParquetColumnarAnalytics, wide tables, data lakes
ORCColumnarHive-centric stacks
AvroRowKafka messages, write-heavy ingestion, schema evolution
CSV / JSONRow, textInterchange, small files, human inspection

Parquet files are immutable. Updates, deletes and ACID transactions come from a table format layered on top, such as Delta Lake or Apache Iceberg.

5. Pitfalls