Parquet Row Group and Statistics

Mental model for Parquet row groups, column chunks, footer statistics, predicate pruning, and write/read trade-offs.

Parquet row group và statistics: columnar không có nghĩa là tự skip được data

Đặt vấn đề

Trong lakehouse, có những query nhìn rất selective:

SELECT amount
FROM events
WHERE event_date = '2026-06-27'
  AND country = 'VN';

Nhưng Spark/Trino vẫn scan nhiều data, tốn I/O và latency cao. Lý do thường không nằm ở SQL, mà nằm ở physical layout: file có cho engine đủ metadata để bỏ qua phần chắc chắn không liên quan hay không.

Vì sao “columnar là nhanh” chưa đủ

CSV/JSON lưu record nối tiếp nhau, nên muốn filter thường phải đọc/parse nhiều dòng:

1,VN,100
2,US,200
3,VN,300

Parquet thì là columnar format, nhưng không phải chỉ có một cục dữ liệu theo từng cột cho cả file. Nó chia file thành các row groups. Bên trong mỗi row group, dữ liệu mới được lưu theo từng column chunk:

Parquet file
├── Row group 1: rows 1 → 1,000,000
│   ├── Column chunk: user_id
│   ├── Column chunk: event_date
│   ├── Column chunk: country
│   └── Column chunk: amount
├── Row group 2: rows 1,000,001 → 2,000,000
│   ├── Column chunk: user_id
│   ├── Column chunk: event_date
│   ├── Column chunk: country
│   └── Column chunk: amount
└── Footer metadata

Vậy nên mental model đúng là:

Parquet = chia ngang thành row-group batches,
          rồi trong mỗi batch lưu dọc theo column chunks.

row group nghe giống row-based, nhưng nó không làm Parquet thành row-based. Row group là đơn vị batch/skip/parallelism/metadata. Bên trong nó vẫn là columnar.

Columnar layout trả lời câu hỏi:

Query cần đọc cột nào?

Ví dụ query chỉ cần country để filter và amount để output thì engine có thể không đọc user_id, device, payload, v.v.

Nhưng columnar layout chưa tự trả lời câu hỏi:

Trong cột đó, đoạn nào chắc chắn không match filter để bỏ qua trước khi decompress/read data thật?

Đó là chỗ Parquet footer statistics phát huy tác dụng.

Parquet footer không chỉ ghi partition column. Nó chứa schema và metadata cho từng row group / column chunk. Thường mỗi column chunk có thể có statistics như min, max, null_count, offset, compressed size, encoding, compression codec:

Parquet file
└── Footer metadata
    ├── schema
    ├── row groups
    │   ├── row_group_1
    │   │   ├── num_rows
    │   │   ├── total_byte_size
    │   │   ├── column_chunk: event_date
    │   │   │   ├── file_offset
    │   │   │   ├── compressed_size
    │   │   │   └── statistics: min/max/null_count
    │   │   ├── column_chunk: country
    │   │   │   ├── file_offset
    │   │   │   ├── compressed_size
    │   │   │   └── statistics: min/max/null_count
    │   │   └── column_chunk: amount
    │   │       ├── file_offset
    │   │       ├── compressed_size
    │   │       └── statistics: min/max/null_count
    │   └── row_group_2
    │       └── ...

Partition column là metadata ở tầng table/path/catalog, ví dụ:

s3://bucket/events/event_date=2026-06-27/part-0001.parquet

Còn Parquet footer là metadata ở tầng file.

Các tầng pruning có thể phối hợp với nhau:

TầngMetadata nằm ở đâuSkip được gì
Partition pruningpath/table format/metastorefolder/partition/file
File-level statsDelta/Iceberg/Hudi metadatafile
Parquet row group statsParquet footer trong filerow group
Page index/stats nếu cóParquet page metadatapage nhỏ hơn row group
Column pruningschema + projectioncolumn không cần đọc

Cơ chế skip bằng row group statistics

Giả sử footer có stats:

row_group_17:
  event_date min = 2026-06-01
  event_date max = 2026-06-03
  country min = "JP"
  country max = "US"

Query:

WHERE event_date = '2026-06-27'

Engine đọc footer, thấy row_group_17 chỉ chứa date từ 2026-06-01 tới 2026-06-03, nên bỏ qua luôn row group đó. Không cần đọc column chunk, không cần decompress page, không cần parse rows.

Flow gần đúng:

SQL filter

engine đọc Parquet footer

check statistics từng row group / column chunk

loại row group chắc chắn không match

chỉ đọc column chunks/pages còn lại

Ví dụ mini: cùng là Parquet nhưng scan khác nhau

Giả sử bảng event 1TB, query thường theo ngày.

Layout A: dữ liệu được ghi/cluster gần theo thời gian:

row_group_1:  2026-06-01 → 2026-06-01
row_group_2:  2026-06-02 → 2026-06-02
row_group_3:  2026-06-03 → 2026-06-03
...

Query ngày 2026-06-27 có thể skip gần hết row groups.

Layout B: mỗi row group đều chứa data cả tháng:

row_group_1:  2026-06-01 → 2026-06-30
row_group_2:  2026-06-01 → 2026-06-30
row_group_3:  2026-06-01 → 2026-06-30
...

Query ngày 2026-06-27 nhìn thì selective, nhưng min/max của row group nào cũng overlap. Engine không dám skip, nên vẫn phải đọc nhiều.

Warning

OPTIMIZE, compaction, sorting, clustering, Z-order hoặc liquid clustering không chỉ để “file đẹp hơn”. Mục tiêu sâu hơn là làm physical layout khiến metadata pruning có ích hơn: min/max hẹp hơn, file/row group liên quan ít hơn, scan ít I/O hơn.

Vậy ghi Parquet có chậm và nặng hơn không?

Có. Ghi Parquet thường nặng hơn row dump vì writer phải làm nhiều việc:

buffer rows
→ tách theo column
→ encode
→ compress
→ tính statistics
→ ghi column chunks/pages
→ ghi footer metadata

Nhưng statistics không phải overhead vô lý. Đây là kiểu trả thêm lúc write để tiết kiệm lúc read.

ViệcTốn lúc write?Đổi lại
Tách data thành column chunksđọc ít cột hơn
Encoding dictionary/RLE/deltafile nhỏ hơn, scan nhanh hơn
Compressiongiảm storage + I/O
Min/max/null_count statsCó nhẹskip row group/page
Footer metadataCó nhẹplanning/pruning
Sort/cluster trước khi ghiCó thể rất tốnstats hữu ích hơn nhiều

Tính min/max/null_count thường không phải phần đắt nhất, vì writer vốn đã đi qua values khi encode/compress. Phần đắt hơn thường là compression, shuffle/sort/cluster, compaction, small-file handling, và commit protocol của table format trên object storage.

Mental model:

stats rẻ,
layout tốt mới đắt.

Nếu data trộn lung tung, stats có thể tồn tại nhưng vô dụng:

country min=AD max=ZW
event_date min=2026-01-01 max=2026-12-31

Nhìn có metadata, nhưng filter nào cũng overlap nên skip chẳng được bao nhiêu.

Pitfall / Trade-off

Row group không phải càng nhỏ càng tốt.

Thiết kếLợiHại
Row group nhỏskip fine-grained hơnnhiều metadata, nhiều seek/read request, planning overhead cao
Row group lớnmetadata gọn, sequential read tốt hơnpruning thô hơn, dễ đọc dư

Parquet hợp nhất với workload kiểu:

write once / append batches, read many times

Nếu workload cần ingest cực nhanh, latency ghi rất thấp, và đọc ít, người ta thường không ép mọi thứ vào optimized Parquet ngay từ đầu. Pattern phổ biến hơn:

Kafka / CDC

raw landing: JSON/Avro/log-like, append nhanh

compaction / optimize job

curated Parquet + Delta/Iceberg/Hudi

BI / Spark / Trino đọc nhiều

My Summary

Parquet không phải row-based. Nó là columnar format được chia thành row groups. Row group là batch nhiều rows để quản lý metadata, skip, parallelism và read planning; bên trong mỗi row group vẫn lưu theo column chunks.

Điểm quan trọng không phải “Parquet có stats nên file nặng hơn”, mà là:

columnar giúp đọc ít cột,
statistics giúp đọc ít vùng dữ liệu,
layout tốt giúp statistics thật sự có ích.

Trade-off nằm ở chỗ write path phức tạp hơn, nhưng analytics workload thường đọc nhiều hơn ghi. Với data platform, chi phí thêm ở write/optimize thường đáng nếu nó giảm scan I/O, query latency và compute cost ở downstream.

On this page