RFC: Adaptive bounded streaming of large column chunks in the async Parquet reader#10410
Draft
adriangb wants to merge 4 commits into
Draft
RFC: Adaptive bounded streaming of large column chunks in the async Parquet reader#10410adriangb wants to merge 4 commits into
adriangb wants to merge 4 commits into
Conversation
…arquet reader Adds an opt-in mode (ArrowReaderBuilder::with_bounded_streaming) where row groups whose projected column chunks exceed a threshold are decoded in bounded windows: window boundaries fall on selected-row multiples of batch_size, each window decodes through the existing sparse-selection machinery, and large chunks are fetched with a single ranged request each (AsyncFileReader::get_bytes_stream, default impl materializes) whose body feeds the decoder through a bounded buffer. Decode of earlier windows overlaps transfer of later ones, and peak residency is bounded by the window size instead of the row group size. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGnT9oETcFMydc9cp95HLG
…-decoder drivers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGnT9oETcFMydc9cp95HLG
… by projected column count - AsyncFileReader::get_bytes_stream now returns an Option of a 'static future so streamed requests can be opened concurrently with each other and with the materialized batch (previously opens were serialized on the reader borrow, paying one request latency per stream). None (the default) falls back to materialization. - Decode windows now have a floor of min_window_bytes_per_column (default 2MiB) per projected column so per-window fixed costs (array reader construction, boundary-page and dictionary-page re-decode) stay amortized; projections wide enough that one window covers the row group fall back to the non-windowed path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGnT9oETcFMydc9cp95HLG
adriangb
force-pushed
the
bounded-column-streaming
branch
from
July 22, 2026 14:14
1e436df to
2c09b00
Compare
adriangb
added a commit
to pydantic/datafusion
that referenced
this pull request
Jul 22, 2026
Makes the PR buildable by CI and benchmark infrastructure until the parquet changes are released. Must be removed before merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGnT9oETcFMydc9cp95HLG
Each decode window builds fresh column readers, which previously re-read and re-decompressed the chunk's dictionary page per window. The first window now stores the decompressed page in a per-row-group cache; later windows skip the page in the underlying reader (free when page locations are available) and reuse the cached one. Reduces the windowed-decode overhead on dictionary-encoded string columns from ~25% to ~5% in ClickBench single-file microbenchmarks (Q22). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MGnT9oETcFMydc9cp95HLG
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
None yet — opened as a draft RFC + prototype for discussion (design + benchmarks below). Happy to file a tracking issue if there is interest in the direction.
Rationale for this change
ParquetPushDecoder(and thereforeParquetRecordBatchStream) materializes every projected byte of a row group before decode starts. For wide-value columns this is painful: a measured production case has 100 MB single-row-group files whose projected columns are ~97 MB, so peak reader residency ≈ whole row group × concurrent partitions, and decode cannot start until the last byte of the slowest range arrives.Object store GETs return a body stream, so one ranged GET over a huge chunk can be consumed incrementally: streaming does not require more requests. This PR adds an opt-in adaptive bounded streaming mode:
batch_size— so the emitted batch sequence is byte-identical to the materialized path (property-tested).RowSelection→scan_ranges→ColumnChunkData::Sparse→ParquetRecordBatchReader); no changes toSerializedPageReader,ArrayReader,RecordReader, or batch assembly.One fundamental tradeoff, stated up front: bounded windowed decode needs per-large-column byte progress (batches zip columns), so adjacent large columns cannot share one GET. Strict request-count equality with today's maximally-coalesced plan is impossible in exactly the wide-projection case; instead the planner guarantees a provable bounded-inflation invariant: small columns never gain requests, groups without large chunks issue exactly today's GETs, and every extra GET transfers ≥
stream_thresholdbytes.What changes are included in this PR?
BoundedStreamingOptions+ArrowReaderBuilder::with_bounded_streaming(default off; everything is unchanged unless enabled).AsyncFileReader::get_bytes_stream(range) -> Option<BoxFuture<'static, Result<BoxStream<'static, Result<Bytes>>>>>(defaultNone= unsupported → materialize fallback, so every existing implementor keeps working).ParquetObjectReaderimplements it viaget_opts.DecodingWindowsstate + window planner (reader_builder/windows.rs),ParquetPushDecoder::upcoming_fetch_plan()(full remaining need + streamable spans, so callers can plan coalesced fetches up front whileNeedsDatastays the "now" backpressure signal). Dictionary pages and window-boundary pages are ref-counted and pinned across the windows that share them.AdaptiveFetcher(used byParquetRecordBatchStream, exported with an asyncfetch_moreAPI for external push-decoder drivers like DataFusion): replicatesobject_storerange coalescing to reproduce today's request plan, materializes small groups throughget_byte_rangesunchanged, opens streamed requests concurrently, drains bodies via bounded channels with backpressure, aborts promptly on drop.Companion DataFusion draft (wiring + benchmarks): apache/datafusion PRs linked below.
Benchmarks (via DataFusion
benchmarks/, details in the companion PR)Production shape (8 × 94 MB single-row-group files, 92 MB blob column, full-scan checksum query):
TPC-H SF1 / TPC-DS SF1 / ClickBench without page indexes: no request inflation, wall neutral within noise (streaming never engages below the threshold / without an offset index).
Known issue (why this is a draft): on ClickBench rewritten with page indexes, queries over large
RLE_DICTIONARYstring chunks regress ~+14% total (e.g. Q22 +38%): every decode window currently re-decodes the chunk's dictionary page. Fix identified but not implemented: cache decoded dictionaries across the windows of a row group.SELECT *-style projections show the predicted bounded inflation (each extra GET ≥ 16 MiB).Are these changes tested?
Yes: mixed tiny/huge projections with request accounting, streamed chunks +
RowSelectionfuzz (byte-identical output vs. the materialized path across random selections/batch sizes/projections), offset/limit, cancel mid-stream. The existing parquet suite is green with the option off and on.Are there any user-facing changes?
New opt-in API only (
BoundedStreamingOptions,with_bounded_streaming,get_bytes_stream,upcoming_fetch_plan,AdaptiveFetcher). Default behavior is unchanged. When enabled,next_row_group()yields one reader per window rather than per row group (documented).🤖 Generated with Claude Code
https://claude.ai/code/session_01MGnT9oETcFMydc9cp95HLG