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.
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.
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.
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;
| Format | Layout | Best for |
|---|---|---|
| Parquet | Columnar | Analytics, wide tables, data lakes |
| ORC | Columnar | Hive-centric stacks |
| Avro | Row | Kafka messages, write-heavy ingestion, schema evolution |
| CSV / JSON | Row, text | Interchange, 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.