practice

Row Group Sizing

also called Row Group Tuning, Parquet Block Size

Choosing how many rows a columnar file groups into one statistics-bearing unit, which sets both the finest granularity a query can skip and the memory a writer and reader need.

parquetstatisticspruningmemorycompaction

A columnar file is not one long column. It is a sequence of row groups, each holding a slice of the rows with every column stored separately inside it, and each carrying min/max statistics per column. The row group is the unit of skipping: an engine either reads a row group or it does not, and a predicate that matches one row in it reads the whole thing.

That makes row group size the setting that decides how much of a selective query's work is wasted. It also decides memory: a writer buffers a full row group before flushing it, and a reader materialises the column chunks it needs from one.

The default in most writers is on the order of 128 MB. Whether that is right depends entirely on whether reads are selective.

Why it matters

Two queries over the same table behave in opposite ways as row groups grow. A full scan gets cheaper, because there is less per-group overhead and better compression. A selective lookup gets more expensive, because the smallest thing it can skip is now larger.

The effect is easy to quantify. With 128 MB row groups and a predicate matching 0.1% of rows spread randomly, nearly every group qualifies and pruning saves nothing; with the data sorted on that predicate, the same query may touch 2 groups out of 2,000. Sort order and row group size are one decision, not two.

Implementation patterns

  • Size from the selective query, not the scan. If selective reads matter, 32–128 MB with a sort key aligned to the predicate beats 1 GB with no sort.
  • Sort or cluster on write. Statistics prune nothing when values are scattered, because every group's min/max spans the whole domain.
  • Watch writer memory. Concurrent writers each buffer a row group, so 16 writers at 512 MB is 8 GB of heap before anything else.
  • Keep the page size sensible (typically 1 MB): pages are the unit of decompression, and very large pages waste work on narrow reads.
  • Measure bytes scanned per query class, not just total. The number that moves when this is wrong is the p95 of the selective class.

Industry example

Datadog's engineering writeups on Husky describe a custom columnar format modelled on Parquet but with one row group and many pages per fragment, chosen so compaction can stream a fragment from a single GET request and discover columns as it goes, with bounded row groups to keep merge memory predictable. The design reasoning is explicit: row group structure was chosen for how the compactor and the query engine read, not as a default inherited from a library.

Failure scenarios

  • Out-of-memory in the writer after raising the row group size, appearing first on the highest-parallelism job rather than on the one that was changed.
  • Selective queries degrading silently after a compaction change: totals improve, p95 does not, and nobody looks at the split.
  • Statistics rendered useless by a random sort order, so every group is read and the file format appears not to prune at all.
  • A high-cardinality string column dominating a group's size, so the group holds far fewer rows than expected and pruning granularity is worse than the setting implies.

Trade-offs

Larger row groups buy scan throughput, fewer object requests and better compression ratios; they pay in pruning granularity and writer memory. Smaller groups buy precise skipping and low memory; they pay in per-group overhead, more metadata and worse compression. The exchange rate is set by your sort order: with a good sort key, larger groups cost less than the theory suggests, because the matching rows are adjacent anyway.

When not to use it

Do not tune row groups on a table that is always scanned whole. The default is fine, and the engineering time is better spent on partitioning or on the query. Tuning here is also the wrong answer when the real problem is thousands of kilobyte-sized files: fix file sizes first, because row group settings do nothing for a file smaller than one row group.

Interview question

Q: After a compaction change, your platform's total bytes scanned per day fell 30% and the support team says single-customer lookups got slower. Explain the mechanism and tell me what you would change without reverting the compaction.

What a strong answer covers: row group and file granularity as the skipping unit; why larger units help scans and hurt lookups; adding or restoring a sort or cluster key on the lookup predicate as the fix that keeps both gains; measuring the two query classes separately; recognising that a secondary index or a separate serving store is the answer if lookups carry a latency SLO rather than a cost target.

Quick check

Quiz: Why does a 0.1%-selectivity query sometimes read the entire table despite per-group statistics? — Because unsorted data gives every row group a min/max spanning the whole domain, so none can be skipped.

Flashcard: What single property makes column statistics useful? — Clustering of the values in the predicate column, so that most groups can be excluded by their min/max range.