Skip to content

Defer dictionary page decode on the skip path - #11168

Merged
alamb merged 23 commits into
apache:mainfrom
zhuqi-lucas:lazy-dict-skip
Oct 15, 2026
Merged

alamb merged 23 commits into
apache:mainfrom
zhuqi-lucas:lazy-dict-skip

Conversation

@zhuqi-lucas

@zhuqi-lucas zhuqi-lucas commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

GenericColumnReader::skip_records decompresses a column chunk's dictionary page as soon as it reaches it, even when every remaining page is then skipped and no value from the chunk is ever decoded. Skipping values needs only the RLE index cursor and skipping whole pages needs nothing, so dictionary contents are only ever needed by value decoding. On zstd-compressed data with large dictionaries this is a large, avoidable cost, and it is most visible with pushdown_filters/RowSelection and with predicate caching, where output columns are consumed largely by skipping.

A local timing test on a zstd column whose dictionary holds 40k distinct strings (14,800 compressed bytes):

whole-chunk skip, dictionary never decoded 84 µs
partial skip that pays the deferred dictionary + first page 4.76 ms

No regression on the plain read path. The read path gains one matches! on the page encoding and one always-None take() per data page; arrow_reader's dictionary-encoded read benchmarks are all within the noise threshold against main:

dictionary-encoded read change vs main
Int32, mandatory +0.43%
Int32, optional +0.58%
Int32, optional, half NULLs +0.72%
UInt8, mandatory +0.28%
UInt8, optional +0.35%

(Run locally with --features arrow,test_common,experimental; CodSpeed does not run on fork PRs.)

What changes are included in this PR?

The deferral is created on the skip path and consumed on the read path; that
crossing is the whole change:

flowchart TD
  A["skip_records reaches a dictionary page"] --> G{"supports_deferred_dictionary()?"}
  G -->|"false (trait default)"| H["read_dictionary_page()<br/>eager, unchanged from main"]
  G -->|"true"| B["record data_start + header<br/>no decompress"]
  B -.->|"deferred, may never fire"| C["read_records: a data page needs values"]
  C --> D["take_deferred_dictionary()"]
  D --> E["get_bytes(data_start)<br/>decode_page, set_dict"]
  B ==>|"chunk ends first"| F["never decompressed"]
Loading
  • The skip path records the dictionary page's location (DeferredDictionaryPage) instead of decoding it. A data page that turns out to need the dictionary installs it then, paying the decompression exactly once; a chunk skipped end to end never pays it.
  • PageReader::supports_deferred_dictionary, a new trait method defaulting to false, decides whether the skip path may skip past a dictionary at all. A reader that does not opt in keeps the eager path exactly as before — skipping past a dictionary it cannot hand back would simply lose it. SerializedPageReader answers per state: it can always defer while walking page headers, and declines in the offset-index state when the metadata recorded no dictionary_page_offset, because there a dictionary inlined ahead of the first data page is indistinguishable from a data page location.
  • PageReader::take_deferred_dictionary, a new trait method defaulting to Ok(None), returns a dictionary the reader skipped past.
  • Whether a page is a dictionary is judged by its actual header type, not the dictionary_page_offset metadata flag, so files that inline the dictionary without recording an offset (older parquet-mr writers, e.g. the one behind test_read_nested_lists) are handled correctly.

On the re-read. The deferred install is a ChunkReader::get_read at the
recorded offset, which is already part of the trait. It never adds I/O relative
to main: these are the same bytes the eager path would have read, either read
later or, when the chunk is skipped end to end, not at all. On the async path it
is not a fetch at all — the column chunk is already buffered, so the read is an
in-memory slice.

Are these changes tested?

Yes. The deferral itself:

  • whole-chunk skip never decodes the dictionary (corrupting the dictionary body leaves the skip green);
  • a partial skip pays it (the same corruption then fails);
  • skip-then-read returns correct values;
  • alternating skip and read — the shape a RowSelection produces — triggers the deferral more than once per chunk;
  • the plain read path is unchanged;
  • a timing test prints the avoided cost.

Page readers that cannot defer:

  • a reader forwarding only the pre-existing trait methods still gets its dictionary, through both a partial and a whole-chunk skip. Removing the supports_deferred_dictionary gate makes it panic in set_data;
  • a reader forwarding take_deferred_dictionary but not supports_deferred_dictionary — the easy mistake when adding both to an existing wrapper — stays correct by falling back to the eager path.

The probe's answer in all three SerializedPageReader states, including the offset-index state without a dictionary_page_offset, where it declines. The existing test_read_nested_lists (nested column + RowSelection) caught the offset-less-dictionary case during development and still covers it.

Are there any user-facing changes?

No format change, and no behavior change for existing PageReader implementations: deferral is opt-in through supports_deferred_dictionary, which defaults to false, so a reader that does not implement it keeps the eager dictionary read it had before. PageReader gains two defaulted trait methods; an implementation that wraps another PageReader should forward both to benefit, and stays correct if it forwards neither. Forwarding only take_deferred_dictionary without supports_deferred_dictionary is also safe — the skip path simply stays eager.

One observable difference for a reader that does opt in: a corrupt dictionary page is no longer guaranteed to raise an error. Today the page is decompressed the moment the skip path reaches it, so a corrupt one fails even for a column chunk that goes on to decode nothing. With the deferral, a chunk skipped end to end never touches those bytes and the error never surfaces. Any chunk that decodes a value still hits it, just later. This is not a correctness change — no wrong data is returned either way — but it does mean corruption in a never-read dictionary now goes unreported. skipping_a_whole_chunk_never_decodes_the_dictionary asserts exactly this, using a deliberately corrupted dictionary body as the probe for "was it decompressed".

`GenericColumnReader::skip_records` decompressed a column chunk's
dictionary page the moment it reached it, even when every remaining page
was then skipped and no value was ever decoded. On zstd-compressed data
with large dictionaries this is a large, avoidable cost: skipping values
needs only the RLE index cursor, and skipping whole pages needs nothing,
so dictionary contents are only ever needed by value decoding.

The skip path now records the dictionary page's location instead of
decoding it (`DeferredDictionaryPage`), and a data page that turns out to
need the dictionary installs it then, paying the decompression exactly
once. A chunk skipped end to end never pays it. The re-read is a random
`ChunkReader::get_read` at the recorded offset, which the trait already
guarantees and which is an in-memory slice on the async path.

`PageReader::take_deferred_dictionary` is a new trait method defaulting
to `Ok(None)`, so a reader that never defers is unchanged, and the eager
path (a dictionary page arriving through `get_next_page`) is untouched.
Whether a page is a dictionary is judged by its actual header type, not
the `dictionary_page_offset` metadata flag, so files that inline the
dictionary without recording an offset (older parquet-mr writers, as in
`test_read_nested_lists`) are handled correctly.

Tests: whole-chunk skip never decodes the dictionary (corrupting the
dictionary body leaves the skip green), a partial skip pays it (the same
corruption fails), skip-then-read returns correct values, and the plain
read path is unchanged. A timing test prints the avoided cost.
@github-actions github-actions Bot added the parquet Changes to the parquet crate label Sep 22, 2026
@zhuqi-lucas

Copy link
Copy Markdown
Contributor Author

run benchmarks arrow_reader_clickbench

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5778360152-2579-x6pjk 6.12.94+ #1 SMP Wed Aug 19 07:47:20 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing lazy-dict-skip (021c7bf) to 6ea6c12 (merge-base) diff

Run configuration
run benchmark arrow_reader_clickbench

BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench arrow_reader_clickbench
Results will be posted here when complete


File an issue against this benchmark runner

The three select-only cases skip the leading data page, which now defers
the dictionary page rather than decoding it up front, so the dictionary
is read when the following data page is first decoded rather than before
it. Same requests and same bytes, order swapped. Snapshots updated and
annotated; caught by `cargo test -p parquet` (the workspace test target)
where the earlier `--lib` run did not reach these integration tests.
@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing lazy-dict-skip (021c7bf) to 6ea6c12 (merge-base) diff

Run configuration
run benchmark arrow_reader_clickbench
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                             lazy-dict-skip                         main
-----                                             --------------                         ----
arrow_reader_clickbench/async/Q1                  1.00   1043.9±4.20µs        ? ?/sec    1.00   1047.6±6.38µs        ? ?/sec
arrow_reader_clickbench/async/Q10                 1.02      6.2±0.17ms        ? ?/sec    1.00      6.1±0.16ms        ? ?/sec
arrow_reader_clickbench/async/Q11                 1.01      7.2±0.16ms        ? ?/sec    1.00      7.2±0.17ms        ? ?/sec
arrow_reader_clickbench/async/Q12                 1.01     13.1±0.18ms        ? ?/sec    1.00     13.0±0.20ms        ? ?/sec
arrow_reader_clickbench/async/Q13                 1.01     16.1±0.23ms        ? ?/sec    1.00     15.9±0.23ms        ? ?/sec
arrow_reader_clickbench/async/Q14                 1.01     14.7±0.21ms        ? ?/sec    1.00     14.5±0.19ms        ? ?/sec
arrow_reader_clickbench/async/Q19                 1.01      3.1±0.05ms        ? ?/sec    1.00      3.1±0.05ms        ? ?/sec
arrow_reader_clickbench/async/Q20                 1.00     74.0±0.50ms        ? ?/sec    1.25    92.9±10.06ms        ? ?/sec
arrow_reader_clickbench/async/Q21                 1.00     82.4±0.60ms        ? ?/sec    1.31    107.7±9.32ms        ? ?/sec
arrow_reader_clickbench/async/Q22                 1.00    108.3±4.87ms        ? ?/sec    1.04   112.9±10.45ms        ? ?/sec
arrow_reader_clickbench/async/Q23                 1.01    256.1±3.02ms        ? ?/sec    1.00    254.1±2.32ms        ? ?/sec
arrow_reader_clickbench/async/Q24                 1.01     18.5±0.23ms        ? ?/sec    1.00     18.3±0.29ms        ? ?/sec
arrow_reader_clickbench/async/Q27                 1.01     59.0±0.55ms        ? ?/sec    1.00     58.6±0.64ms        ? ?/sec
arrow_reader_clickbench/async/Q28                 1.01     58.2±0.64ms        ? ?/sec    1.00     57.7±0.68ms        ? ?/sec
arrow_reader_clickbench/async/Q30                 1.01     17.1±0.11ms        ? ?/sec    1.00     16.9±0.12ms        ? ?/sec
arrow_reader_clickbench/async/Q36                 1.01     15.6±0.33ms        ? ?/sec    1.00     15.4±0.36ms        ? ?/sec
arrow_reader_clickbench/async/Q37                 1.00      5.2±0.06ms        ? ?/sec    1.00      5.2±0.07ms        ? ?/sec
arrow_reader_clickbench/async/Q38                 1.00     14.1±0.35ms        ? ?/sec    1.02     14.4±0.43ms        ? ?/sec
arrow_reader_clickbench/async/Q39                 1.02     26.1±0.75ms        ? ?/sec    1.00     25.6±0.67ms        ? ?/sec
arrow_reader_clickbench/async/Q40                 1.01      5.5±0.10ms        ? ?/sec    1.00      5.4±0.13ms        ? ?/sec
arrow_reader_clickbench/async/Q41                 1.00      4.7±0.06ms        ? ?/sec    1.00      4.7±0.06ms        ? ?/sec
arrow_reader_clickbench/async/Q42                 1.00      3.5±0.04ms        ? ?/sec    1.00      3.5±0.04ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q1     1.01   1031.3±5.71µs        ? ?/sec    1.00   1025.9±2.85µs        ? ?/sec
arrow_reader_clickbench/async_object_store/Q10    1.00      6.0±0.16ms        ? ?/sec    1.01      6.0±0.16ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q11    1.00      7.1±0.18ms        ? ?/sec    1.00      7.1±0.18ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q12    1.01     13.0±0.20ms        ? ?/sec    1.00     13.0±0.17ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q13    1.00     15.6±0.25ms        ? ?/sec    1.00     15.7±0.26ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q14    1.00     14.5±0.22ms        ? ?/sec    1.00     14.5±0.22ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q19    1.00      2.9±0.04ms        ? ?/sec    1.00      2.9±0.04ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q20    1.00     72.7±0.65ms        ? ?/sec    1.01     73.1±0.76ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q21    1.00     81.5±0.66ms        ? ?/sec    1.01     82.0±1.72ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q22    1.00     97.9±0.95ms        ? ?/sec    1.03    101.2±1.41ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q23    1.03    232.0±1.89ms        ? ?/sec    1.00    225.0±7.05ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q24    1.01     18.1±0.22ms        ? ?/sec    1.00     18.0±0.28ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q27    1.00     57.8±0.71ms        ? ?/sec    1.00     57.6±0.71ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q28    1.01     57.3±0.70ms        ? ?/sec    1.00     57.0±0.66ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q30    1.01     16.8±0.12ms        ? ?/sec    1.00     16.6±0.13ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q36    1.02     15.3±0.37ms        ? ?/sec    1.00     15.0±0.33ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q37    1.00      5.1±0.07ms        ? ?/sec    1.00      5.1±0.06ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q38    1.00     13.6±0.35ms        ? ?/sec    1.00     13.6±0.37ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q39    1.00     24.5±0.53ms        ? ?/sec    1.00     24.5±0.65ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q40    1.01      5.3±0.12ms        ? ?/sec    1.00      5.2±0.10ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q41    1.00      4.6±0.09ms        ? ?/sec    1.00      4.6±0.08ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q42    1.00      3.4±0.04ms        ? ?/sec    1.00      3.4±0.03ms        ? ?/sec
arrow_reader_clickbench/sync/Q1                   1.00    890.6±1.49µs        ? ?/sec    1.00    894.2±1.72µs        ? ?/sec
arrow_reader_clickbench/sync/Q10                  1.01      4.6±0.06ms        ? ?/sec    1.00      4.6±0.05ms        ? ?/sec
arrow_reader_clickbench/sync/Q11                  1.00      5.6±0.05ms        ? ?/sec    1.00      5.6±0.05ms        ? ?/sec
arrow_reader_clickbench/sync/Q12                  1.01     20.6±0.40ms        ? ?/sec    1.00     20.4±0.13ms        ? ?/sec
arrow_reader_clickbench/sync/Q13                  1.01     23.4±0.42ms        ? ?/sec    1.00     23.2±0.15ms        ? ?/sec
arrow_reader_clickbench/sync/Q14                  1.01     22.1±0.60ms        ? ?/sec    1.00     21.7±0.14ms        ? ?/sec
arrow_reader_clickbench/sync/Q19                  1.01      2.7±0.06ms        ? ?/sec    1.00      2.7±0.05ms        ? ?/sec
arrow_reader_clickbench/sync/Q20                  1.01    125.0±0.87ms        ? ?/sec    1.00    124.2±0.63ms        ? ?/sec
arrow_reader_clickbench/sync/Q21                  1.03     97.9±0.59ms        ? ?/sec    1.00     95.3±0.47ms        ? ?/sec
arrow_reader_clickbench/sync/Q22                  1.00    144.0±1.07ms        ? ?/sec    1.01    145.9±0.95ms        ? ?/sec
arrow_reader_clickbench/sync/Q23                  1.00    290.4±7.76ms        ? ?/sec    1.06   307.7±12.46ms        ? ?/sec
arrow_reader_clickbench/sync/Q24                  1.01     25.7±0.21ms        ? ?/sec    1.00     25.5±0.24ms        ? ?/sec
arrow_reader_clickbench/sync/Q27                  1.00    110.3±0.62ms        ? ?/sec    1.01    111.0±0.66ms        ? ?/sec
arrow_reader_clickbench/sync/Q28                  1.00    106.9±0.65ms        ? ?/sec    1.00    106.4±0.61ms        ? ?/sec
arrow_reader_clickbench/sync/Q30                  1.01     17.2±0.13ms        ? ?/sec    1.00     17.0±0.14ms        ? ?/sec
arrow_reader_clickbench/sync/Q36                  1.00     22.7±0.20ms        ? ?/sec    1.00     22.6±0.19ms        ? ?/sec
arrow_reader_clickbench/sync/Q37                  1.00      6.6±0.05ms        ? ?/sec    1.00      6.7±0.05ms        ? ?/sec
arrow_reader_clickbench/sync/Q38                  1.00     11.8±0.05ms        ? ?/sec    1.00     11.8±0.07ms        ? ?/sec
arrow_reader_clickbench/sync/Q39                  1.00     21.4±0.17ms        ? ?/sec    1.00     21.4±0.16ms        ? ?/sec
arrow_reader_clickbench/sync/Q40                  1.01      5.0±0.07ms        ? ?/sec    1.00      5.0±0.08ms        ? ?/sec
arrow_reader_clickbench/sync/Q41                  1.00      5.5±0.05ms        ? ?/sec    1.00      5.5±0.06ms        ? ?/sec
arrow_reader_clickbench/sync/Q42                  1.00      4.4±0.05ms        ? ?/sec    1.00      4.4±0.05ms        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 785.2s
Peak memory 286.5 MiB
Avg memory 136.1 MiB
CPU user 692.6s
CPU sys 87.1s
Peak spill 0 B

branch

Metric Value
Wall time 780.2s
Peak memory 285.2 MiB
Avg memory 156.4 MiB
CPU user 694.8s
CPU sys 82.6s
Peak spill 0 B

File an issue against this benchmark runner

@zhuqi-lucas
zhuqi-lucas marked this pull request as ready for review September 23, 2026 02:51
Copilot AI lite review requested due to automatic review settings September 23, 2026 02:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical compatibility gaps can drop dictionaries for existing readers, and offset-less leading dictionaries are not preserved in the page-index path.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 High severity

Open (3)
What changed in this PR

Defers Parquet dictionary-page decompression during row skipping until dictionary-encoded values are decoded.

Changes:

  • Tracks and lazily loads skipped dictionary pages.
  • Adds deferred-dictionary support to page and column readers.
  • Updates I/O expectations and regression tests.
File Changes
parquet/​tests/​arrow_reader/​io/​sync_reader.rs Updates expected request ordering.
parquet/​src/​file/​serialized_reader.rs Tracks and lazily decodes dictionary pages.
parquet/​src/​column/​reader.rs Defers dictionaries during skips and installs them for decoding.
parquet/​src/​column/​page.rs Adds the deferred-dictionary reader hook.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread parquet/src/column/page.rs
Comment thread parquet/src/column/reader.rs Outdated
Comment thread parquet/src/file/serialized_reader.rs
skip_records skipped past a dictionary page for every PageReader, but only
SerializedPageReader retains one. Any other implementation lost it, and the
defaulted take_deferred_dictionary returned Ok(None), so the next
dictionary-encoded data page reached set_data with no decoder and panicked.

Add PageReader::supports_deferred_dictionary, defaulting to false, and keep
the eager read_dictionary_page path for readers that do not opt in.

SerializedPageReader answers per state. The offset-index state can only
represent a dictionary whose offset the metadata recorded: dictionary_page is
synthesised from the gap between byte_range().0 and the first page location,
and those coincide when dictionary_page_offset is absent, so a dictionary
inlined ahead of the first data page is indistinguishable from a data page
location. It declines deferral there.

Tests cover a non-deferring reader through both a partial and a whole-chunk
skip, and the probe's answer in all three states.
@zhuqi-lucas
zhuqi-lucas marked this pull request as draft September 23, 2026 03:15
Alternating skip and read is the shape a RowSelection produces and exercises
the deferral more than once per chunk; nothing covered it.

A wrapper that forwards take_deferred_dictionary but not
supports_deferred_dictionary is the easy mistake when adding both methods to
an existing implementation. It stays correct — the probe reports false, the
skip path reads eagerly, and the forwarded method never has anything to
return — and that is now asserted rather than only claimed in the docs.

Also notes on the offset-index probe test that the case it guards is unlikely
to be reachable: a writer old enough to omit dictionary_page_offset predates
the offset index, so it would not emit page locations either.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Two moderate issues remain, along with a documentation nit.

Review effort: Lite
Findings: None

Resolved since last review (3)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Validate compressed page size after accounting for header length

parquet/​src/​file/​serialized_reader.rs:1184

On this branch, remaining_bytes still includes header_len, but compressed_page_size is validated against the unadjusted total before subtracting header_len + data_page_size. A malformed header with compressed_page_size <= remaining_bytes but header_len + compressed_page_size > remaining_bytes can therefore underflow the u64 (or panic in debug) instead of returning the invalid-page error that get_next_page returns. Validate against *remaining_bytes - header_len as u64 after verify_page_header_len.

The diagram on DeferredDictionaryPage had NotSeen -> Deferred happening
unconditionally on a skip, which stopped being true once the transition was
gated on supports_deferred_dictionary. A reader that cannot defer now takes a
third path straight to Decoded, and that path is the one keeping existing
PageReader implementations correct, so it belongs in the picture.
zhuqi-lucas added a commit to zhuqi-lucas/arrow-datafusion that referenced this pull request Sep 24, 2026
… merge

Patches every arrow-rs 60.0.0 crate to zhuqi-lucas/arrow-rs@f18aaab so the
DataFusion benchmarks can measure deferring dictionary page decode on the
Parquet skip path end to end, through pushdown_filters and the predicate
cache, before the upstream PR is marked ready for review.
@zhuqi-lucas

Copy link
Copy Markdown
Contributor Author

run benchmarks arrow_reader_clickbench

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5808976850-2680-nbqbz 6.12.94+ #1 SMP Wed Aug 19 07:47:20 UTC 2026 aarch64 GNU/Linux

CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected

Comparing lazy-dict-skip (8687610) to 486867f (merge-base) diff

Run configuration
run benchmark arrow_reader_clickbench

BENCH_COMMAND=cargo bench --features=arrow,async,test_common,experimental,object_store --bench arrow_reader_clickbench
Results will be posted here when complete


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark completed (GKE) | trigger

Instance: c4a-highmem-16 (12 vCPU / 65 GiB)

Comparing lazy-dict-skip (8687610) to 486867f (merge-base) diff

Run configuration
run benchmark arrow_reader_clickbench
CPU Details (lscpu)
Architecture:                            aarch64
CPU op-mode(s):                          64-bit
Byte Order:                              Little Endian
CPU(s):                                  16
On-line CPU(s) list:                     0-15
Vendor ID:                               ARM
Model name:                              Neoverse-V2
Model:                                   1
Thread(s) per core:                      1
Core(s) per cluster:                     16
Socket(s):                               -
Cluster(s):                              1
Stepping:                                r0p1
BogoMIPS:                                2000.00
Flags:                                   fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 dgh rng bti
L1d cache:                               1 MiB (16 instances)
L1i cache:                               1 MiB (16 instances)
L2 cache:                                32 MiB (16 instances)
L3 cache:                                80 MiB (1 instance)
NUMA node(s):                            1
NUMA node0 CPU(s):                       0-15
Vulnerability Gather data sampling:      Not affected
Vulnerability Indirect target selection: Not affected
Vulnerability Itlb multihit:             Not affected
Vulnerability L1tf:                      Not affected
Vulnerability Mds:                       Not affected
Vulnerability Meltdown:                  Not affected
Vulnerability Mmio stale data:           Not affected
Vulnerability Reg file data sampling:    Not affected
Vulnerability Retbleed:                  Not affected
Vulnerability Spec rstack overflow:      Not affected
Vulnerability Spec store bypass:         Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:                Mitigation; __user pointer sanitization
Vulnerability Spectre v2:                Mitigation; CSV2, BHB
Vulnerability Srbds:                     Not affected
Vulnerability Tsa:                       Not affected
Vulnerability Tsx async abort:           Not affected
Vulnerability Vmscape:                   Not affected
Details

group                                             lazy-dict-skip                         main
-----                                             --------------                         ----
arrow_reader_clickbench/async/Q1                  1.02   1067.6±3.66µs        ? ?/sec    1.00   1049.1±3.73µs        ? ?/sec
arrow_reader_clickbench/async/Q10                 1.00      6.1±0.10ms        ? ?/sec    1.01      6.2±0.08ms        ? ?/sec
arrow_reader_clickbench/async/Q11                 1.00      7.2±0.09ms        ? ?/sec    1.02      7.3±0.09ms        ? ?/sec
arrow_reader_clickbench/async/Q12                 1.00     12.9±0.11ms        ? ?/sec    1.01     13.1±0.09ms        ? ?/sec
arrow_reader_clickbench/async/Q13                 1.01     16.0±0.15ms        ? ?/sec    1.00     15.9±0.14ms        ? ?/sec
arrow_reader_clickbench/async/Q14                 1.00     14.5±0.09ms        ? ?/sec    1.01     14.6±0.12ms        ? ?/sec
arrow_reader_clickbench/async/Q19                 1.00      3.1±0.03ms        ? ?/sec    1.01      3.1±0.03ms        ? ?/sec
arrow_reader_clickbench/async/Q20                 1.00     74.1±0.49ms        ? ?/sec    1.15     85.5±8.31ms        ? ?/sec
arrow_reader_clickbench/async/Q21                 1.00     82.4±0.68ms        ? ?/sec    1.32    109.0±0.71ms        ? ?/sec
arrow_reader_clickbench/async/Q22                 1.00    113.5±2.35ms        ? ?/sec    1.25    141.4±0.97ms        ? ?/sec
arrow_reader_clickbench/async/Q23                 1.00    256.9±5.18ms        ? ?/sec    1.02    262.2±7.40ms        ? ?/sec
arrow_reader_clickbench/async/Q24                 1.00     18.1±0.12ms        ? ?/sec    1.01     18.3±0.21ms        ? ?/sec
arrow_reader_clickbench/async/Q27                 1.00     58.7±0.55ms        ? ?/sec    1.00     58.9±0.67ms        ? ?/sec
arrow_reader_clickbench/async/Q28                 1.00     57.7±0.60ms        ? ?/sec    1.03     59.2±0.69ms        ? ?/sec
arrow_reader_clickbench/async/Q30                 1.00     17.0±0.10ms        ? ?/sec    1.01     17.2±0.15ms        ? ?/sec
arrow_reader_clickbench/async/Q36                 1.00     15.3±0.34ms        ? ?/sec    1.03     15.8±0.40ms        ? ?/sec
arrow_reader_clickbench/async/Q37                 1.00      5.2±0.04ms        ? ?/sec    1.02      5.3±0.03ms        ? ?/sec
arrow_reader_clickbench/async/Q38                 1.00     14.0±0.32ms        ? ?/sec    1.01     14.2±0.33ms        ? ?/sec
arrow_reader_clickbench/async/Q39                 1.00     25.5±0.61ms        ? ?/sec    1.01     25.8±0.68ms        ? ?/sec
arrow_reader_clickbench/async/Q40                 1.00      5.5±0.09ms        ? ?/sec    1.02      5.6±0.10ms        ? ?/sec
arrow_reader_clickbench/async/Q41                 1.00      4.8±0.05ms        ? ?/sec    1.02      4.8±0.04ms        ? ?/sec
arrow_reader_clickbench/async/Q42                 1.00      3.5±0.03ms        ? ?/sec    1.00      3.5±0.02ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q1     1.01   1037.9±3.54µs        ? ?/sec    1.00   1030.8±5.35µs        ? ?/sec
arrow_reader_clickbench/async_object_store/Q10    1.00      5.9±0.07ms        ? ?/sec    1.02      6.0±0.06ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q11    1.00      7.1±0.10ms        ? ?/sec    1.03      7.3±0.09ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q12    1.00     13.0±0.14ms        ? ?/sec    1.01     13.1±0.10ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q13    1.00     15.6±0.09ms        ? ?/sec    1.00     15.7±0.16ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q14    1.00     14.5±0.11ms        ? ?/sec    1.00     14.5±0.11ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q19    1.01      3.0±0.03ms        ? ?/sec    1.00      3.0±0.03ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q20    1.00     73.1±0.61ms        ? ?/sec    1.00     73.2±0.78ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q21    1.01     82.6±0.70ms        ? ?/sec    1.00     82.1±0.83ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q22    1.00     99.6±0.74ms        ? ?/sec    1.02    101.3±0.79ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q23    1.00    232.1±1.77ms        ? ?/sec    1.01    234.2±1.42ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q24    1.00     18.0±0.18ms        ? ?/sec    1.00     18.0±0.15ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q27    1.00     57.9±0.71ms        ? ?/sec    1.01     58.3±0.77ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q28    1.00     57.3±0.61ms        ? ?/sec    1.01     57.8±0.82ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q30    1.00     16.7±0.09ms        ? ?/sec    1.01     16.9±0.23ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q36    1.00     15.1±0.26ms        ? ?/sec    1.00     15.1±0.31ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q37    1.01      5.2±0.03ms        ? ?/sec    1.00      5.1±0.03ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q38    1.00     13.6±0.26ms        ? ?/sec    1.02     14.0±0.34ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q39    1.00     24.6±0.53ms        ? ?/sec    1.01     24.8±0.60ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q40    1.01      5.4±0.09ms        ? ?/sec    1.00      5.3±0.08ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q41    1.03      4.7±0.04ms        ? ?/sec    1.00      4.6±0.03ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q42    1.01      3.4±0.03ms        ? ?/sec    1.00      3.4±0.02ms        ? ?/sec
arrow_reader_clickbench/sync/Q1                   1.02    904.6±2.23µs        ? ?/sec    1.00    889.9±2.08µs        ? ?/sec
arrow_reader_clickbench/sync/Q10                  1.02      4.7±0.03ms        ? ?/sec    1.00      4.6±0.02ms        ? ?/sec
arrow_reader_clickbench/sync/Q11                  1.03      5.7±0.03ms        ? ?/sec    1.00      5.6±0.04ms        ? ?/sec
arrow_reader_clickbench/sync/Q12                  1.01     20.7±0.43ms        ? ?/sec    1.00     20.5±0.05ms        ? ?/sec
arrow_reader_clickbench/sync/Q13                  1.00     23.4±0.41ms        ? ?/sec    1.00     23.5±0.14ms        ? ?/sec
arrow_reader_clickbench/sync/Q14                  1.01     22.1±0.64ms        ? ?/sec    1.00     21.9±0.08ms        ? ?/sec
arrow_reader_clickbench/sync/Q19                  1.01      2.7±0.04ms        ? ?/sec    1.00      2.7±0.03ms        ? ?/sec
arrow_reader_clickbench/sync/Q20                  1.01    127.9±0.70ms        ? ?/sec    1.00    126.5±0.61ms        ? ?/sec
arrow_reader_clickbench/sync/Q21                  1.03     99.3±0.39ms        ? ?/sec    1.00     96.7±0.35ms        ? ?/sec
arrow_reader_clickbench/sync/Q22                  1.00    148.5±0.72ms        ? ?/sec    1.00    147.8±0.67ms        ? ?/sec
arrow_reader_clickbench/sync/Q23                  1.00    295.3±9.35ms        ? ?/sec    1.06   314.0±10.74ms        ? ?/sec
arrow_reader_clickbench/sync/Q24                  1.01     26.0±0.20ms        ? ?/sec    1.00     25.7±0.07ms        ? ?/sec
arrow_reader_clickbench/sync/Q27                  1.01    112.2±0.58ms        ? ?/sec    1.00    111.2±0.27ms        ? ?/sec
arrow_reader_clickbench/sync/Q28                  1.00    107.8±0.35ms        ? ?/sec    1.01    108.6±0.46ms        ? ?/sec
arrow_reader_clickbench/sync/Q30                  1.02     17.4±0.07ms        ? ?/sec    1.00     17.1±0.08ms        ? ?/sec
arrow_reader_clickbench/sync/Q36                  1.01     23.0±0.04ms        ? ?/sec    1.00     22.8±0.06ms        ? ?/sec
arrow_reader_clickbench/sync/Q37                  1.02      6.8±0.02ms        ? ?/sec    1.00      6.6±0.02ms        ? ?/sec
arrow_reader_clickbench/sync/Q38                  1.01     11.9±0.02ms        ? ?/sec    1.00     11.7±0.02ms        ? ?/sec
arrow_reader_clickbench/sync/Q39                  1.01     21.5±0.07ms        ? ?/sec    1.00     21.3±0.12ms        ? ?/sec
arrow_reader_clickbench/sync/Q40                  1.02      5.1±0.02ms        ? ?/sec    1.00      5.0±0.04ms        ? ?/sec
arrow_reader_clickbench/sync/Q41                  1.02      5.7±0.03ms        ? ?/sec    1.00      5.6±0.03ms        ? ?/sec
arrow_reader_clickbench/sync/Q42                  1.03      4.5±0.03ms        ? ?/sec    1.00      4.4±0.03ms        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 790.2s
Peak memory 288.4 MiB
Avg memory 133.3 MiB
CPU user 689.2s
CPU sys 96.1s
Peak spill 0 B

branch

Metric Value
Wall time 780.2s
Peak memory 285.3 MiB
Avg memory 155.6 MiB
CPU user 690.7s
CPU sys 85.3s
Peak spill 0 B

File an issue against this benchmark runner

@zhuqi-lucas
zhuqi-lucas marked this pull request as ready for review September 24, 2026 07:02
@zhuqi-lucas

zhuqi-lucas commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

cc @etseidl @alamb @adriangb this new design for skip page logic ready for a look. It defers dictionary page decode on the skip path so a column chunk that is skipped end to end never decompresses its dictionary; a chunk that is read after a partial skip pays it on the first data page instead.

And some benchmark is pretty good, and also clickbench here show good result:

arrow_reader_clickbench/async/Q21                 1.00     82.4±0.68ms        ? ?/sec    1.32    109.0±0.71ms        ? ?/sec
arrow_reader_clickbench/async/Q22                 1.00    113.5±2.35ms        ? ?/sec    1.25    141.4±0.97ms        ? ?/sec

End to end through DataFusion (apache/datafusion#25686, clickbench_1 / partitioned / pushdown) also no regression, and small improvement. result: apache/datafusion#25686 (comment)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The only remaining feedback is a non-blocking documentation nit.

Review effort: Lite
Findings: 1 Low severity

Open (1)

Comment thread parquet/src/column/page.rs Outdated
zhuqi-lucas and others added 4 commits September 29, 2026 14:04
Co-authored-by: Ed Seidl <etseidl@users.noreply.github.com>
Co-authored-by: Ed Seidl <etseidl@users.noreply.github.com>
read_page_header_len_from_bytes moved out of the SerializedPageReader impl
and became a free function, but only one of its two call sites was updated;
the other still went through Self:: and stopped compiling.
@zhuqi-lucas

Copy link
Copy Markdown
Contributor Author

Thanks @etseidl — applied all of them, the docs read much better trimmed. One heads-up: applying the suggestions moved read_page_header_len_from_bytes out of the SerializedPageReader impl and updated only one of its two call sites, so the branch went red for a moment. Fixed in d2b1294.

You are right about the corrupt dictionary, and it is sharper than "may not trigger an error" — it definitely will not, for any chunk that decodes nothing. Today the page is decompressed the moment the skip path reaches it, so corruption fails even when the chunk goes on to decode zero values; with the deferral those bytes are never touched. A chunk that does decode something still hits the error, just later. No wrong data is returned either way, but corruption in a never-read dictionary now goes unreported.

It is also the mechanism behind two of the tests: corrupt_dictionary_body zero-fills the dictionary page body, and the same corrupted file makes skipping_a_whole_chunk_never_decodes_the_dictionary pass while a_partial_skip_pays_the_deferred_dictionary fails — "does this error" is the probe for "was it decompressed". I have written the behaviour into the user-facing changes section.

# Conflicts:
#	parquet/src/file/serialized_reader.rs
Copilot caught that `supports_deferred_dictionary` opened by describing a
method that decodes and returns a page, while it only returns a bool. The
paragraph belongs to `take_deferred_dictionary`, which previously had two
lines and no explanation of why deferring is safe.
@alamb

alamb commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Is this one ready to merge?


#[test]
fn test_read_single_row_filter() {
// Note: the skip that precedes the selected rows now defers the dictionary

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the comment here that explains the changes in this PR will be confusing in the future

Maybe a comment saying that "DictionaryPages are deferred until first read, see #11154" would help, but the fact that in the past the behavior was different is likely not relevant in the future

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @alamb — agreed, the before/after framing stops meaning anything once this merges. Reworded all three to state what the reader does, and to explain the ordering the snapshot actually shows: the dictionary trails the data page wherever a skip precedes it, and leads it where none does. Issue link kept.

The notes described the before/after of this PR, which stops meaning
anything once it is merged. Describe what the reader does instead, and
explain the ordering the snapshot actually shows: a dictionary trails the
data page wherever a skip precedes it, and leads it where none does.
@zhuqi-lucas

zhuqi-lucas commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @alamb — yes, ready from my side. Since @etseidl's approval the only change is the comment reword you asked for above.

One behaviour change worth naming before merge: a corrupt dictionary in a chunk that is never read no longer errors, since those bytes are never touched. No wrong data is returned — anything that decodes a value still decodes the dictionary and still errors. Covered by skipping_a_whole_chunk_never_decodes_the_dictionary and a_partial_skip_pays_the_deferred_dictionary.

@alamb

alamb commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

Thanks -- I'll try and get this reviewed in the next few days

…ict-skip

# Conflicts:
#	parquet/src/column/reader.rs
@alamb

alamb commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

I merged up from main to resolve a conflict

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @zhuqi-lucas -- this makes sense to me

I had a question about one of the tests, but otherwise looks like a great change (and nice performance win!)

Comment thread parquet/src/column/page.rs Outdated
/// [`Self::take_deferred_dictionary`].
///
/// Implementations returning `true` must retain dictionary pages consumed by
/// [`Self::skip_next_page`]. The default preserves compatibility with existing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor:

The default preserves compatibility with existing
/// readers by requiring dictionaries to be decoded eagerly.

I think this is a comment that will not add value after this PR is merged as it explains what happens prior to this PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, removed it.

Comment thread parquet/src/column/page.rs Outdated
Comment thread parquet/src/file/serialized_reader.rs Outdated
assert_eq!(values, vec![3, 4, 5]);
}

/// Prints the cost the deferral avoids, on a chunk shaped like a real

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems more like a benchmark. Why do we leave it as a ignored test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, thanks. Moved it to a criterion bench (benches/dictionary_skip.rs). Skipping a whole chunk takes ~614µs on main and ~1.1µs with this PR.

"Row Group 0, column 'a': DictionaryPage (1617 bytes, 1 requests) [data]",
"Row Group 0, column 'a': DataPage(1) (126 bytes , 1 requests) [data]",
"Row Group 0, column 'b': DictionaryPage (1617 bytes, 1 requests) [data]",
"Row Group 0, column 'a': DictionaryPage (1617 bytes, 1 requests) [data]",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is cool to see the different IO patterns

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! The dictionary page is now fetched only when a data page actually needs it.

- Move `lazy_dictionary_timings` (an ignored test that only printed
  timings) to a criterion benchmark, `benches/dictionary_skip.rs`.
- Drop the doc sentence about the pre-change default.
- Say that `take_deferred_dictionary` returns the decoded dictionary `Page`.
@zhuqi-lucas

Copy link
Copy Markdown
Contributor Author

Thank you @zhuqi-lucas -- this makes sense to me

I had a question about one of the tests, but otherwise looks like a great change (and nice performance win!)

Thanks @alamb for review, addressed all comments now.

@alamb alamb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thnk you @zhuqi-lucas and @etseidl

@alamb
alamb added this pull request to the merge queue Oct 15, 2026
Merged via the queue into apache:main with commit 8c9d694 Oct 15, 2026
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

parquet Changes to the parquet crate performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parquet: skip_records eagerly decompresses dictionary pages for column chunks it never decodes

5 participants