Repository navigation
Description
Part of #6946.
What users get
Today, ParquetPushDecoder returns no rows of a row group until the caller has pushed all bytes that the row group reads. For a large row group on object storage, the first batch waits for the full row group, and the decoder holds the full row group in memory.
With this EPIC, a caller can opt in to getting each batch as soon as its pages are pushed:
let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(metadata)?
.with_fetch_granularity(FetchGranularity::Batch) // new, opt-in
.build()?;
// `NeedsData` asks only for the pages of the next batch.Prototype measurement (one row group of 84 MB, 50 ms per request, caller reads 8 MB ahead):
| Mode | Time to the first batch | Peak buffered bytes |
|---|---|---|
Today (FetchGranularity::RowGroup) |
451 ms | 84 MB |
FetchGranularity::Batch |
89 ms | 8.1 MB |
DataFusion can use this mode to scan large row groups with less memory (apache/datafusion#24086).
Terms
| Term | Meaning |
|---|---|
Push decoder (ParquetPushDecoder) |
A Parquet decoder that does no I/O. try_decode returns NeedsData(ranges); the caller fetches the byte ranges and pushes them. |
| Row group, column chunk, page | A row group holds one column chunk per column. A column chunk is a sequence of pages (an optional dictionary page, then data pages). |
| Offset index | Parquet metadata that gives the file offset, size and first row of each data page. |
Predicate (RowFilter) |
A filter that the decoder evaluates while it decodes, to skip rows of other columns. |
| Read-ahead | The caller fetches ranges before the decoder asks for them. |
Plan
#11223 (+4,156 lines, now closed) was too large to review in one step. The PRs below replace it. Each PR is useful without the PRs after it, and no PR adds a temporary error or fallback that a later PR removes.
| PR | What users get | Size | State |
|---|---|---|---|
| #11218 | A test that shows the current behavior | Merged | |
| #11235 | The decoder releases the bytes of a finished row group (fix for #11233) | Merged | |
| #11237 | Faster lookups when a caller pushes many buffers | Merged | |
| #11484 | A file with a bad offset index gives an error, not a panic | Merged | |
| #11485 | Tests only: the push decoder against the sync reader, with proptest |
+636 | Ready for review |
| #11238 | FetchGranularity::Batch: the first batch of a row group as soon as its pages are pushed |
+2,364 (about 1,100 are tests) | Draft until #11485 merges |
| #11487 | Batch mode: no empty decode after each batch | +70 | Draft until #11238 merges |
| #11240 | Batch mode: filtered scans decode the predicate columns one time, peak memory of about one batch, and dense row selections use the faster mask read | +690 | Draft until #11487 merges |
main ── 11485 tests ── 11238 batch mode ── 11487 ── 11240 batch-mode optimizations
Review order:
- Ready now: test(parquet): differential tests of
ParquetPushDecoderagainst the sync reader #11485. - Then: feat(parquet): let ParquetPushDecoder decode a batch as soon as its pages are pushed #11238. It has two commits that can be reviewed in sequence or as one diff.
- Then: perf(parquet): skip the empty decode after each batch in
FetchGranularity::Batch#11487 and feat(parquet): predicate cache, page release and selection policy in the batch-granular push decoder #11240.
Tangential
- ParquetPushDecoder does not release pushed buffers that are larger than the requested ranges #11233 (fix: fix(parquet): release the bytes of a row group when the push decoder is done with it #11235): the decoder does not release a pushed buffer that is larger than the requested ranges. This is a bug in the default mode. feat(parquet): let ParquetPushDecoder decode a batch as soon as its pages are pushed #11238 uses the fix.
Activity
feat(parquet): release each page when the batch-granular push decoder…
feat(parquet): use the predicate cache in the batch-granular push dec…
feat(parquet): release each page when the batch-granular push decoder…
feat(parquet): use the predicate cache in the batch-granular push dec…
perf(parquet): use the configured selection policy in batch windows
feat(parquet): release each page when the batch-granular push decoder…
feat(parquet): use the predicate cache in the batch-granular push dec…
perf(parquet): use the configured selection policy in batch windows
perf(parquet): use the configured selection policy in batch windows
adriangb commentedon Sep 27, 2026
Note
Out of date: see #11234 (comment)
@alamb, as you asked on #11223 (comment), I split #11223 (+4,156 lines) into the PRs below. This comment gives the reason for each cut and a suggested review order.
How the split works
mainfix or speed up the current decoder. Each PR on top of the core adds one improvement to batch mode.Suggested review order
into_builder().build()releases the pushed bytes of the row groups that the new decoder skipsPushBuffers:Nbuf/100000rangesgoes from 574 ms to 35 msFetchGranularity::Batch: the first batch of a row group as soon as its pages are pushedRowSelectionPolicyin each windowStep 2: how to read #11238
It is the largest PR, because the core cannot be smaller without a temporary fallback. About half of it is tests. Suggested reading order:
FetchGranularityinpush_decoder/mod.rs: the behavior that users see, and the I/O contract.IncrementalRowGroupandStageinreader_builder/incremental.rs: the design.IncrementalRowGroup::stepandColumnChunkPages: the engine.reader_builder/mod.rs,remaining.rsandpush_decoder/mod.rs, which is small.incremental_tests.rs: most tests compare batch mode with the default mode on the same scan.The PR description also records two decisions from review: windows stay
batch_sizerows of the row group, and the caller decides how far to read ahead.Merge notes
util/push_buffers.rs. The PR that merges last needs a small rebase: remove the duplicatemerge_rangesand test helper, and update the maximum buffer length of #11237 inrelease_rangesandretain_ranges.#11223 stays open as a reference for the full change. I will close it when the stack merges.
adriangb commentedon Sep 27, 2026
(sorry for the ping above andrew, i told it to post a summary so i could read it and then I was going to ping you after vetting, i always try to not have an ai ping a human, appoligies. that said i've now read the comment above and it is reasonable 😄)
cc @sunchao
feat(parquet): let ParquetPushDecoder decode a batch as soon as its p…
feat(parquet): release each page when the batch-granular push decoder…
feat(parquet): use the predicate cache in the batch-granular push dec…
perf(parquet): use the configured selection policy in batch windows
28 remaining items
feat(parquet): release each page when the batch-granular push decoder…
perf(parquet): use the configured selection policy in batch windows
feat(parquet): use the predicate cache in the batch-granular push dec…
feat(parquet): release each page when the batch-granular push decoder…
perf(parquet): use the configured selection policy in batch windows
test(parquet): differential tests of `ParquetPushDecoder` against the…
feat(parquet): use the predicate cache in the batch-granular push dec…
feat(parquet): release each page when the batch-granular push decoder…
perf(parquet): use the configured selection policy in batch windows
feat(parquet): use the predicate cache in the batch-granular push dec…
feat(parquet): release each page when the batch-granular push decoder…
perf(parquet): use the configured selection policy in batch windows
feat(parquet): use the predicate cache in the batch-granular push dec…
feat(parquet): release each page when the batch-granular push decoder…
perf(parquet): use the configured selection policy in batch windows