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,300Parquet 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 metadataVậ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.
Footer metadata ghi gì?
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.parquetCòn Parquet footer là metadata ở tầng file.
Các tầng pruning có thể phối hợp với nhau:
| Tầng | Metadata nằm ở đâu | Skip được gì |
|---|---|---|
| Partition pruning | path/table format/metastore | folder/partition/file |
| File-level stats | Delta/Iceberg/Hudi metadata | file |
| Parquet row group stats | Parquet footer trong file | row group |
| Page index/stats nếu có | Parquet page metadata | page nhỏ hơn row group |
| Column pruning | schema + projection | column 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ạiVí 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 metadataNhư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ệc | Tốn lúc write? | Đổi lại |
|---|---|---|
| Tách data thành column chunks | Có | đọc ít cột hơn |
| Encoding dictionary/RLE/delta | Có | file nhỏ hơn, scan nhanh hơn |
| Compression | Có | giảm storage + I/O |
| Min/max/null_count stats | Có nhẹ | skip row group/page |
| Footer metadata | Có nhẹ | planning/pruning |
| Sort/cluster trước khi ghi | Có thể rất tốn | stats 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-31Nhì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ợi | Hại |
|---|---|---|
| Row group nhỏ | skip fine-grained hơn | nhiều metadata, nhiều seek/read request, planning overhead cao |
| Row group lớn | metadata gọn, sequential read tốt hơn | pruning thô hơn, dễ đọc dư |
Parquet hợp nhất với workload kiểu:
write once / append batches, read many timesNế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ềuMy 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.