Skip to content

[Parquet] Improve Auto RowSelection construction for scattered predicate results - #10852

Merged
alamb merged 6 commits into
apache:mainfrom
hhhizzz:fix/issue-10776-capped-auto-selection
Sep 18, 2026
Merged

alamb merged 6 commits into
apache:mainfrom
hhhizzz:fix/issue-10776-capped-auto-selection

Conversation

@hhhizzz

@hhhizzz hhhizzz commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

For the first predicate, when there is no existing RowSelection, ReadPlanBuilder::with_predicate_options currently materializes all predicate results as RLE selectors. RowSelectionPolicy::Auto may then decide that the selection is too fragmented, convert those selectors back to a bitmap, and use the mask strategy.

For scattered selections such as ClickBench Q25, this creates a large temporary Vec<RowSelector> even though the final strategy becomes certain as soon as the normalized run count crosses Auto's threshold.

Keeping the first fragmented predicate mask-backed also benefits subsequent predicates: they use the existing mask construction path and mask conjunction instead of rebuilding another large selector vector.

What changes are included in this PR?

  • Add an internal RowSelection::from_filters_auto constructor that builds normalized selectors only while the selector strategy is still possible.
  • Share one auto_min_mask_runs helper between selector-backed strategy resolution, mask-backed strategy resolution, and capped construction. This keeps the strict comparison, threshold 0/1, and saturating overflow behavior in one place.
  • Once the minimum mask run count is reached, drop the partial selector allocation before constructing the mask directly from the predicate BooleanArrays.
  • Fall back to the existing from_filters path when no attainable run count can select Mask, avoiding a per-selector cutoff check for threshold 0 and 1.
  • Add from_filters_mask as the semantic mask constructor, including a single-filter fast path that reuses the filter's BooleanBuffer.
  • Use capped construction only for a first predicate with no pre-existing selection and RowSelectionPolicy::Auto. Existing selections and explicit Mask/Selectors policies retain their existing paths.
  • Preserve unresolved Auto in prepare_selection_for_page_skipping when there is no selection yet. There are no selection-driven pages to skip in that state, and resolving early would force Selectors before the first predicate result is available.

Are these changes tested?

Yes.

Correctness coverage includes:

  • deterministic 31/32 run-length threshold boundaries and shared-helper boundary checks;
  • cross-filter run merging, empty filters, and trailing skips;
  • eight edge row counts crossed with all eight thresholds and four deterministic selection patterns;
  • a separate 512-case fixed-seed matrix spanning eight thresholds, eight selectivities, four named run shapes, aligned and non-byte-aligned buffers, and multiple BooleanArray splits;
  • LIMIT/padding behavior and async page-skipping preparation.

Focused construction benchmark over 4,194,304 rows, split into 512 BooleanArrays of 8,192 rows each (median of three Criterion rounds):

Shape Previous Auto Capped Auto Change
Q25-like 15% scattered 14.675 ms 506.9 µs 28.95x faster
Alternating run-1 48.595 ms 513.8 µs 94.57x faster
Exact run-32 boundary 431.4 µs 445.9 µs +3.35%
Clustered run-128 165.8 µs 154.5 µs -6.77%
Sparse run-32 66.2 µs 65.5 µs -1.04%

An unconditional mask-first implementation was rejected because it regressed selector-friendly run-128 by 30.7% and sparse input by 107.6%.

Three paired async-reader rounds with PageIndex disabled showed:

  • Q25-like: -26.02%
  • alternating run-1: -48.91%
  • run-32: -1.87%
  • run-128: -0.29%
  • sparse: -0.30%
  • all-selected: +1.45%

The focused construction harness was kept separate in accordance with the repository guidance for benchmark additions. These results isolate selection construction and async reader behavior; they are not presented as a full ClickBench Q25 wall-time measurement.

Local validation:

  • cargo fmt --all -- --check: passed
  • cargo test -p parquet --lib -- --skip file::writer::tests::test_int96_interop: 1,305 passed, 0 failed, 1 filtered
  • cargo test -p parquet --test arrow_reader --features async -- --skip bad_data::test_invalid_files: 123 passed, 0 failed, 1 ignored, 1 filtered
  • cargo clippy -p parquet --all-targets --all-features -- -D warnings: passed

The two filtered tests require fixtures absent from the local parquet-testing checkout (int96_timestamp_order.parquet and the bad_data/variants fixture). The PR's GitHub parquet test, compilation, and Clippy checks pass without filtering.

Are there any user-facing changes?

No public interface changes. The new constructors and cutoff helper are crate-private, and logical row-selection semantics and explicit policy behavior are unchanged.

@github-actions github-actions Bot added the parquet Changes to the parquet crate label Aug 26, 2026
@mkleen

mkleen commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

run benchmarks

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5423660022-1962-t5wvj 6.12.85+ #1 SMP Sat Jun 27 09:31:30 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 fix/issue-10776-capped-auto-selection (d087f0e) to c62002b (merge-base) diff

Run configuration
run benchmark sql_planner

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


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

Benchmark for this request failed before finishing (Kubernetes reason: BackoffLimitExceeded).

Benchmarks requested: ``

Kubernetes message
Job has reached the specified backoff limit

File an issue against this benchmark runner

@mkleen

mkleen commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

run benchmarks

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5423727827-1963-dlsmn 6.12.85+ #1 SMP Sat Jun 27 09:31:30 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 fix/issue-10776-capped-auto-selection (d087f0e) to c62002b (merge-base) diff

Run configuration
run benchmark sql_planner

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


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

Benchmark for this request failed before finishing (Kubernetes reason: BackoffLimitExceeded).

Benchmarks requested: ``

Kubernetes message
Job has reached the specified backoff limit

File an issue against this benchmark runner

@mkleen

mkleen commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

run benchmarks

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5425045915-1964-r58bv 6.12.85+ #1 SMP Sat Jun 27 09:31:30 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 fix/issue-10776-capped-auto-selection (d087f0e) to c62002b (merge-base) diff

Run configuration
run benchmark sql_planner

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


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

Benchmark for this request failed before finishing (Kubernetes reason: BackoffLimitExceeded).

Benchmarks requested: ``

Kubernetes message
Job has reached the specified backoff limit

File an issue against this benchmark runner

@hhhizzz

hhhizzz commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

I'm not sure how to run whole bench in this PR, So I run them on my dev server, here's the main results:

End-to-end benchmark results

I ran three cyclic-order rounds against Arrow main using the same DataFusion integration harness. The first iteration of each query was excluded, and the table reports the cross-round median of steady-state query times.

Suite Main OFF Main ON Candidate ON Candidate vs Main ON Query geomean
ClickBench 41.394 s 35.932 s 36.280 s +0.97% -4.76%
TPC-H SF10 9.549 s 8.265 s 7.992 s -3.31% -5.02%
TPC-DS SF10 73.218 s 46.051 s 44.450 s -3.48% -4.12%

Selected queries:

Suite Query Main OFF Main ON Candidate ON Candidate vs Main ON Candidate vs OFF
ClickBench Q25 180.4 ms 283.5 ms 172.8 ms -39.0% -4.2%
ClickBench Q12 552.0 ms 671.4 ms 567.9 ms -15.4% +2.9%
ClickBench Q23 7,738.4 ms 857.3 ms 853.4 ms -0.5% -89.0%
ClickBench Q21 834.5 ms 1,592.3 ms 1,570.2 ms -1.4% +88.2%
TPC-H Q6 122.7 ms 324.0 ms 239.0 ms -26.2% +94.8%
TPC-H Q18 3,136.8 ms 871.1 ms 859.1 ms -1.4% -72.6%
TPC-DS Q9 279.3 ms 1,198.3 ms 468.6 ms -60.9% +67.8%
TPC-DS Q72 31,044.1 ms 1,376.0 ms 1,255.3 ms -8.8% -96.0%
TPC-DS Q14 1,777.4 ms 1,622.4 ms 1,801.8 ms +11.1% +1.4%
TPC-DS Q50 280.9 ms 500.2 ms 553.4 ms +10.6% +97.1%

The candidate fixes the target Q25 regression, preserves the major pushdown wins such as ClickBench Q23, TPC-H Q18, and TPC-DS Q72, and reduces several other Main-ON regressions. It is not a pure monotonic improvement: regressions remain on queries such as ClickBench Q21 and TPC-DS Q14/Q50, and pushdown is still not universally faster than OFF. All query row counts matched across every arm and round.

@mkleen

mkleen commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

run benchmarks

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5436757746-2013-tsxkl 6.12.85+ #1 SMP Sat Jun 27 09:31:30 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 fix/issue-10776-capped-auto-selection (4ea9518) to c62002b (merge-base) diff

Run configuration
run benchmark sql_planner

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


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

Benchmark for this request failed before finishing (Kubernetes reason: BackoffLimitExceeded).

Benchmarks requested: ``

Kubernetes message
Job has reached the specified backoff limit

File an issue against this benchmark runner

@alamb

alamb commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

run benchmarks

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5438378850-2015-srbdq 6.12.85+ #1 SMP Sat Jun 27 09:31:30 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 fix/issue-10776-capped-auto-selection (4ea9518) to c62002b (merge-base) diff

Run configuration
run benchmark sql_planner

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


File an issue against this benchmark runner

@adriangbot

Copy link
Copy Markdown

Benchmark for this request failed before finishing (Kubernetes reason: BackoffLimitExceeded).

Benchmarks requested: ``

Kubernetes message
Job has reached the specified backoff limit

File an issue against this benchmark runner

@haohuaijin

Copy link
Copy Markdown
Contributor

run benchmark arrow_reader_clickbench

@adriangbot

Copy link
Copy Markdown

🤖 Arrow criterion benchmark running (GKE) | trigger
Instance: c4a-highmem-16 (12 vCPU / 65 GiB) | Linux bench-c5440018056-2020-b88kq 6.12.85+ #1 SMP Sat Jun 27 09:31:30 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 fix/issue-10776-capped-auto-selection (4ea9518) to c62002b (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 fix/issue-10776-capped-auto-selection (4ea9518) to c62002b (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                                             fix_issue-10776-capped-auto-selection    main
-----                                             -------------------------------------    ----
arrow_reader_clickbench/async/Q1                  1.00   1089.0±5.18µs        ? ?/sec      1.00   1085.8±7.53µs        ? ?/sec
arrow_reader_clickbench/async/Q10                 1.01      6.0±0.05ms        ? ?/sec      1.00      6.0±0.04ms        ? ?/sec
arrow_reader_clickbench/async/Q11                 1.00      7.1±0.04ms        ? ?/sec      1.00      7.1±0.05ms        ? ?/sec
arrow_reader_clickbench/async/Q12                 1.00     12.9±0.09ms        ? ?/sec      1.04     13.4±0.07ms        ? ?/sec
arrow_reader_clickbench/async/Q13                 1.00     15.8±0.13ms        ? ?/sec      1.02     16.1±0.13ms        ? ?/sec
arrow_reader_clickbench/async/Q14                 1.00     14.4±0.09ms        ? ?/sec      1.04     15.1±0.09ms        ? ?/sec
arrow_reader_clickbench/async/Q19                 1.00      3.1±0.03ms        ? ?/sec      1.00      3.1±0.02ms        ? ?/sec
arrow_reader_clickbench/async/Q20                 1.00    93.8±12.20ms        ? ?/sec      1.02     95.8±7.22ms        ? ?/sec
arrow_reader_clickbench/async/Q21                 1.29    106.1±7.63ms        ? ?/sec      1.00     81.9±0.42ms        ? ?/sec
arrow_reader_clickbench/async/Q22                 1.00    141.9±2.35ms        ? ?/sec      1.01    142.6±3.80ms        ? ?/sec
arrow_reader_clickbench/async/Q23                 1.00    254.0±1.80ms        ? ?/sec      1.00    254.0±2.78ms        ? ?/sec
arrow_reader_clickbench/async/Q24                 1.00     17.7±0.18ms        ? ?/sec      1.04     18.5±0.15ms        ? ?/sec
arrow_reader_clickbench/async/Q27                 1.00     57.8±0.65ms        ? ?/sec      1.01     58.2±0.37ms        ? ?/sec
arrow_reader_clickbench/async/Q28                 1.00     57.8±0.68ms        ? ?/sec      1.00     57.8±0.55ms        ? ?/sec
arrow_reader_clickbench/async/Q30                 1.00     17.1±0.11ms        ? ?/sec      1.04     17.7±0.10ms        ? ?/sec
arrow_reader_clickbench/async/Q36                 1.04     15.4±0.31ms        ? ?/sec      1.00     14.9±0.19ms        ? ?/sec
arrow_reader_clickbench/async/Q37                 1.03      5.1±0.02ms        ? ?/sec      1.00      5.0±0.02ms        ? ?/sec
arrow_reader_clickbench/async/Q38                 1.03     14.0±0.32ms        ? ?/sec      1.00     13.6±0.20ms        ? ?/sec
arrow_reader_clickbench/async/Q39                 1.02     25.2±0.50ms        ? ?/sec      1.00     24.8±0.46ms        ? ?/sec
arrow_reader_clickbench/async/Q40                 1.00      5.3±0.06ms        ? ?/sec      1.01      5.4±0.05ms        ? ?/sec
arrow_reader_clickbench/async/Q41                 1.00      4.7±0.03ms        ? ?/sec      1.00      4.7±0.04ms        ? ?/sec
arrow_reader_clickbench/async/Q42                 1.03      3.5±0.02ms        ? ?/sec      1.00      3.4±0.02ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q1     1.01   1070.3±4.87µs        ? ?/sec      1.00   1064.1±5.03µs        ? ?/sec
arrow_reader_clickbench/async_object_store/Q10    1.01      5.9±0.04ms        ? ?/sec      1.00      5.9±0.05ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q11    1.01      7.0±0.06ms        ? ?/sec      1.00      6.9±0.04ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q12    1.00     12.9±0.07ms        ? ?/sec      1.04     13.4±0.10ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q13    1.00     15.4±0.10ms        ? ?/sec      1.04     16.0±0.16ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q14    1.00     14.3±0.09ms        ? ?/sec      1.04     14.9±0.08ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q19    1.01      3.0±0.03ms        ? ?/sec      1.00      3.0±0.02ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q20    1.00     72.9±0.64ms        ? ?/sec      1.00     72.6±0.55ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q21    1.00     81.7±0.71ms        ? ?/sec      1.00     81.6±0.65ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q22    1.01    101.4±0.74ms        ? ?/sec      1.00    100.8±1.40ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q23    1.00    222.5±0.72ms        ? ?/sec      1.10    245.2±0.73ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q24    1.00     17.6±0.16ms        ? ?/sec      1.04     18.3±0.18ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q27    1.00     57.7±0.68ms        ? ?/sec      1.00     57.6±0.60ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q28    1.00     56.8±0.70ms        ? ?/sec      1.01     57.4±0.55ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q30    1.00     16.8±0.14ms        ? ?/sec      1.04     17.5±0.12ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q36    1.01     14.7±0.29ms        ? ?/sec      1.00     14.5±0.21ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q37    1.03      5.1±0.02ms        ? ?/sec      1.00      4.9±0.02ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q38    1.02     13.3±0.41ms        ? ?/sec      1.00     13.1±0.25ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q39    1.01     24.1±0.40ms        ? ?/sec      1.00     23.9±0.42ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q40    1.00      5.2±0.06ms        ? ?/sec      1.01      5.2±0.05ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q41    1.01      4.5±0.04ms        ? ?/sec      1.00      4.5±0.02ms        ? ?/sec
arrow_reader_clickbench/async_object_store/Q42    1.03      3.4±0.02ms        ? ?/sec      1.00      3.3±0.02ms        ? ?/sec
arrow_reader_clickbench/sync/Q1                   1.01    919.5±1.47µs        ? ?/sec      1.00    911.9±3.90µs        ? ?/sec
arrow_reader_clickbench/sync/Q10                  1.01      4.7±0.02ms        ? ?/sec      1.00      4.6±0.01ms        ? ?/sec
arrow_reader_clickbench/sync/Q11                  1.01      5.7±0.02ms        ? ?/sec      1.00      5.6±0.02ms        ? ?/sec
arrow_reader_clickbench/sync/Q12                  1.00     20.2±0.06ms        ? ?/sec      1.03     20.9±0.13ms        ? ?/sec
arrow_reader_clickbench/sync/Q13                  1.00     23.3±0.16ms        ? ?/sec      1.01     23.6±0.07ms        ? ?/sec
arrow_reader_clickbench/sync/Q14                  1.00     21.6±0.06ms        ? ?/sec      1.03     22.3±0.12ms        ? ?/sec
arrow_reader_clickbench/sync/Q19                  1.02      2.7±0.03ms        ? ?/sec      1.00      2.7±0.02ms        ? ?/sec
arrow_reader_clickbench/sync/Q20                  1.01    124.1±0.23ms        ? ?/sec      1.00    123.3±0.20ms        ? ?/sec
arrow_reader_clickbench/sync/Q21                  1.03     98.9±0.23ms        ? ?/sec      1.00     95.6±1.12ms        ? ?/sec
arrow_reader_clickbench/sync/Q22                  1.01    146.9±0.52ms        ? ?/sec      1.00    145.8±0.43ms        ? ?/sec
arrow_reader_clickbench/sync/Q23                  1.00   304.4±10.71ms        ? ?/sec      1.01    307.7±9.86ms        ? ?/sec
arrow_reader_clickbench/sync/Q24                  1.00     25.5±0.16ms        ? ?/sec      1.02     25.9±0.07ms        ? ?/sec
arrow_reader_clickbench/sync/Q27                  1.00    110.3±0.31ms        ? ?/sec      1.00    109.9±0.44ms        ? ?/sec
arrow_reader_clickbench/sync/Q28                  1.00    107.1±0.35ms        ? ?/sec      1.00    107.0±0.38ms        ? ?/sec
arrow_reader_clickbench/sync/Q30                  1.00     17.3±0.04ms        ? ?/sec      1.02     17.7±0.06ms        ? ?/sec
arrow_reader_clickbench/sync/Q36                  1.00     22.2±0.11ms        ? ?/sec      1.00     22.2±0.05ms        ? ?/sec
arrow_reader_clickbench/sync/Q37                  1.00      6.7±0.03ms        ? ?/sec      1.00      6.7±0.01ms        ? ?/sec
arrow_reader_clickbench/sync/Q38                  1.01     11.6±0.09ms        ? ?/sec      1.00     11.4±0.04ms        ? ?/sec
arrow_reader_clickbench/sync/Q39                  1.01     21.0±0.12ms        ? ?/sec      1.00     20.8±0.05ms        ? ?/sec
arrow_reader_clickbench/sync/Q40                  1.01      4.9±0.03ms        ? ?/sec      1.00      4.8±0.01ms        ? ?/sec
arrow_reader_clickbench/sync/Q41                  1.01      5.5±0.05ms        ? ?/sec      1.00      5.5±0.03ms        ? ?/sec
arrow_reader_clickbench/sync/Q42                  1.00      4.3±0.03ms        ? ?/sec      1.01      4.3±0.03ms        ? ?/sec

Resource Usage

base (merge-base)

Metric Value
Wall time 790.2s
Peak memory 286.2 MiB
Avg memory 129.4 MiB
CPU user 701.0s
CPU sys 85.8s
Peak spill 0 B

branch

Metric Value
Wall time 785.2s
Peak memory 286.2 MiB
Avg memory 133.2 MiB
CPU user 690.7s
CPU sys 87.7s
Peak spill 0 B

File an issue against this benchmark runner

@haohuaijin

haohuaijin commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Thanks @hhhizzz, look good to me. This is a good improvement for the first-predicate path. One remaining limitation is that when the selection stays selector-backed, later predicates still fully materialize selectors via from_filters. A follow-up can extend capped construction to those predicates(becuase when page index are available and page pruning is effective, datafusion usually provides an existing RowSelection).

for the regression in TPC-DS Q14/Q50, maybe we can run the benchmark once again to check if this a noisy, because both Q14/Q50 are join-dominated, the pr should not affect so much. also i can help test, if you can share how to run the test .

@hhhizzz

hhhizzz commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @hhhizzz, look good to me. This is a good improvement for the first-predicate path. One remaining limitation is that when the selection stays selector-backed, later predicates still fully materialize selectors via from_filters. A follow-up can extend capped construction to those predicates(becuase when page index are available and page pruning is effective, datafusion usually provides an existing RowSelection).

for the regression in TPC-DS Q14/Q50, maybe we can run the benchmark once again to check if this a noisy, because both Q14/Q50 are join-dominated, the pr should not affect so much. also i can help test, if you can share how to run the test .

I have a bunch of local environment to compile the datafusion and arrow to do the bench. Let me run a few more time quickly.

@hhhizzz

hhhizzz commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @hhhizzz, look good to me. This is a good improvement for the first-predicate path. One remaining limitation is that when the selection stays selector-backed, later predicates still fully materialize selectors via from_filters. A follow-up can extend capped construction to those predicates(becuase when page index are available and page pruning is effective, datafusion usually provides an existing RowSelection).

for the regression in TPC-DS Q14/Q50, maybe we can run the benchmark once again to check if this a noisy, because both Q14/Q50 are join-dominated, the pr should not affect so much. also i can help test, if you can share how to run the test .

Thanks for pointing this out. I investigated the apparent regressions further.

In a new four focused 10-iteration rounds, neither regression reproduced:

  • Q14: -3.7% (noisy/overlapping)
  • Q50: -0.5%
  • Arrow async Q21: -1.0%

Profiles showed no new candidate hotspot, and the Q14/Q50 direct-output counters were zero. The earlier regressions were most likely caused by insufficient iterations and benchmark noise. I’ll treat that table as experimental context rather than PR-only evidence.

@haohuaijin haohuaijin 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.

LGTM

@hhhizzz

hhhizzz commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Hi @alamb, Just wondering if you have time to take a look this week?

@alamb

alamb commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Hi @hhhizzz -- I will try -- I am soryr I have been out last week

Resolve the PageIndex API conflict by preserving both sets of test imports and passing the column count to the first-predicate page-skipping test.

Validated with parquet all-targets/all-features Clippy, all-features library and arrow_reader tests (1572 passed), cargo fmt, and typos.

Codex assisted with the test import and argument conflict resolution.
@hhhizzz

hhhizzz commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Hi @alamb, just following up on this PR. I've merged the latest main and resolved the conflicts, and the Parquet CI checks are passing. Would you have a chance to review it this week? Happy to provide any additional details or benchmarks. Thanks for your time!

@alamb

alamb commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

I am so sorry -- I will do it first thing tomorrow morning. Put it on my list

@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 @hhhizzz and @haohuaijin . I sincerely apologize for the delay in review -- I just needed to find enough contiguous time to review this carefully and such time is hard to come by when i was catching up from other things.

I went through this PR quite carefully and it makes sense to me, and I verified that it does indeed improve the performance of the reproducer in #10776

Here is my test script q24.sql

Details
set datafusion.execution.parquet.pushdown_filters = true;
CREATE EXTERNAL TABLE hits_raw STORED AS PARQUET LOCATION '/Users/andrewlamb/Software/datafusion/benchmarks/data/hits_partitioned/';
CREATE VIEW hits AS SELECT * EXCEPT ("EventDate"), CAST(CAST("EventDate" AS INTEGER) AS DATE) AS "EventDate" FROM hits_raw;

SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;
SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10;

Then I ran the query like this (5x, 75 runs total)

datafusion-cli -f q24.sql  | grep  Elapsed

The results look like this

binary min max avg median
baseline (arrow-60, PR 25335) 0.100 0.113 0.104 0.103
adaptive-selector (+ this PR) 0.089 0.107 0.093 0.092

Average change: -10.1% (negative = adaptive-selector faster). Median change: -10.7%.

Details

datafusion-cli-arrow-60 (baseline: apache/datafusion#25335, arrow/parquet 60.0.0)

run min max avg
1 0.101 0.111 0.104
2 0.100 0.110 0.104
3 0.100 0.110 0.103
4 0.101 0.113 0.105
5 0.101 0.108 0.104
all 5 runs (75 queries) 0.100 0.113 0.104

datafusion-cli-adaptive-selector (baseline + #10852)

run min max avg
1 0.090 0.099 0.094
2 0.090 0.107 0.094
3 0.090 0.102 0.092
4 0.090 0.100 0.094
5 0.089 0.099 0.093
all 5 runs (75 queries) 0.089 0.107 0.093

I left some suggested comments, but nothing I think is required

My agent also suggested we could use RowSelection::from_filters_auto in more places (example below). I will try this out locally and see if it makes any difference.

Details
diff --git a/parquet/src/arrow/arrow_reader/read_plan.rs b/parquet/src/arrow/arrow_reader/read_plan.rs
index 5ebfbc4890..2dda5ce618 100644
--- a/parquet/src/arrow/arrow_reader/read_plan.rs
+++ b/parquet/src/arrow/arrow_reader/read_plan.rs
@@ -273,12 +273,28 @@ impl ReadPlanBuilder {
             return Ok(self);
         }
         let raw = match (self.selection.as_ref(), self.row_selection_policy) {
+            // A mask-backed prior selection stays mask-backed: `and_then` then
+            // takes the mask/mask path and never materializes selectors.
             (Some(selection), _) if selection.as_mask().is_some() => {
                 RowSelection::from_filters_mask(&filters)
             }
+            // Selector-backed prior selection (e.g. from page index pruning)
+            // under Auto: stop materializing selectors as soon as the predicate
+            // result is fragmented enough that Auto would choose a mask.
+            // `and_then` then streams the mask as runs, so the fragmented
+            // predicate result is never held as a second selector vector.
+            (Some(_selection), RowSelectionPolicy::Auto { threshold }) => {
+                RowSelection::from_filters_auto(&filters, threshold)
+            }
+            // No prior selection under Auto: the backing chosen here is the
+            // one `build` will resolve to, so later predicates and `build`
+            // never need to convert it.
             (None, RowSelectionPolicy::Auto { threshold }) => {
                 RowSelection::from_filters_auto(&filters, threshold)
             }
+            // The final backing is known to be a mask, so build it directly
+            // rather than materializing selectors that `build` would convert.
+            (None, RowSelectionPolicy::Mask) => RowSelection::from_filters_mask(&filters),
             _ => RowSelection::from_filters(&filters),
         };
         self.selection = match self.selection.take() {

}
}

/// Returns the minimum normalized run count for which Auto prefers a mask.

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.

Suggested change
/// Returns the minimum normalized run count for which Auto prefers a mask.
/// Returns the minimum normalized run count for which the Auto selection policy
/// would prefer a mask to RLE.


/// Returns the minimum normalized run count for which Auto prefers a mask.
///
/// This matches `total_rows < run_count.saturating_mul(threshold)`. For totals

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.

What is the relevance of matching total_rows < run_count.saturating_mul(threshold)? It seems like this comment may be trying to explain how the code was changed in this PR but will not be relevant after this PR merges.

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.

Maybe it is trying to say that this implements the definition of the Auto policy threshold, namely that

"average selector length below which masks are preferred" and average run length below threshold means total_rows / run_count < threshold ?

///
/// This matches `total_rows < run_count.saturating_mul(threshold)`. For totals
/// below `usize::MAX`, the first matching run count is
/// `floor(total_rows / threshold) + 1`. `None` means no attainable run count

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.

maybe this would be clearer if it said "Returns None when selectors are always a better choice"

@hhhizzz

hhhizzz commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @alamb for the careful review and for checking the performance improvement! I've applied the documentation suggestions in 8e1d1e4, clarifying the average-run-length threshold and when the policy prefers selectors.

I'll try the additional construction-path changes you suggested shortly and verify their correctness and performance.

@alamb

alamb commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

I'll try the additional construction-path changes you suggested shortly and verify their correctness and performance.

FWIW I did not see any difference in my tests (I tried the clickbench queries locally with filter pushdown turned on(

@alamb
alamb merged commit d506e14 into apache:main Sep 18, 2026
31 checks passed
@alamb

alamb commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Thank you again @hhhizzz and @haohuaijin -- and I really apologize for the delay. I am slowly working through my backlog and I hope to be able to review #10859 later this afternoon

etseidl pushed a commit to etseidl/arrow-rs that referenced this pull request Sep 19, 2026
…ate results (apache#10852)

# Which issue does this PR close?

- Closes apache#10776.

# Rationale for this change

For the first predicate, when there is no existing `RowSelection`,
`ReadPlanBuilder::with_predicate_options` currently materializes all
predicate results as RLE selectors. `RowSelectionPolicy::Auto` may then
decide that the selection is too fragmented, convert those selectors
back to a bitmap, and use the mask strategy.

For scattered selections such as ClickBench Q25, this creates a large
temporary `Vec<RowSelector>` even though the final strategy becomes
certain as soon as the normalized run count crosses Auto's threshold.

Keeping the first fragmented predicate mask-backed also benefits
subsequent predicates: they use the existing mask construction path and
mask conjunction instead of rebuilding another large selector vector.

# What changes are included in this PR?

- Add an internal `RowSelection::from_filters_auto` constructor that
builds normalized selectors only while the selector strategy is still
possible.
- Share one `auto_min_mask_runs` helper between selector-backed strategy
resolution, mask-backed strategy resolution, and capped construction.
This keeps the strict comparison, threshold `0`/`1`, and saturating
overflow behavior in one place.
- Once the minimum mask run count is reached, drop the partial selector
allocation before constructing the mask directly from the predicate
`BooleanArray`s.
- Fall back to the existing `from_filters` path when no attainable run
count can select Mask, avoiding a per-selector cutoff check for
threshold `0` and `1`.
- Add `from_filters_mask` as the semantic mask constructor, including a
single-filter fast path that reuses the filter's `BooleanBuffer`.
- Use capped construction only for a first predicate with no
pre-existing selection and `RowSelectionPolicy::Auto`. Existing
selections and explicit Mask/Selectors policies retain their existing
paths.
- Preserve unresolved Auto in `prepare_selection_for_page_skipping` when
there is no selection yet. There are no selection-driven pages to skip
in that state, and resolving early would force Selectors before the
first predicate result is available.

# Are these changes tested?

Yes.

Correctness coverage includes:

- deterministic `31`/`32` run-length threshold boundaries and
shared-helper boundary checks;
- cross-filter run merging, empty filters, and trailing skips;
- eight edge row counts crossed with all eight thresholds and four
deterministic selection patterns;
- a separate 512-case fixed-seed matrix spanning eight thresholds, eight
selectivities, four named run shapes, aligned and non-byte-aligned
buffers, and multiple `BooleanArray` splits;
- LIMIT/padding behavior and async page-skipping preparation.

Focused construction benchmark over 4,194,304 rows, split into 512
`BooleanArray`s of 8,192 rows each (median of three Criterion rounds):

| Shape | Previous Auto | Capped Auto | Change |
|---|---:|---:|---:|
| Q25-like 15% scattered | 14.675 ms | 506.9 µs | 28.95x faster |
| Alternating run-1 | 48.595 ms | 513.8 µs | 94.57x faster |
| Exact run-32 boundary | 431.4 µs | 445.9 µs | +3.35% |
| Clustered run-128 | 165.8 µs | 154.5 µs | -6.77% |
| Sparse run-32 | 66.2 µs | 65.5 µs | -1.04% |

An unconditional mask-first implementation was rejected because it
regressed selector-friendly run-128 by 30.7% and sparse input by 107.6%.

Three paired async-reader rounds with PageIndex disabled showed:

- Q25-like: -26.02%
- alternating run-1: -48.91%
- run-32: -1.87%
- run-128: -0.29%
- sparse: -0.30%
- all-selected: +1.45%

The focused construction harness was kept separate in accordance with
the repository guidance for benchmark additions. These results isolate
selection construction and async reader behavior; they are not presented
as a full ClickBench Q25 wall-time measurement.

Local validation:

- `cargo fmt --all -- --check`: passed
- `cargo test -p parquet --lib -- --skip
file::writer::tests::test_int96_interop`: 1,305 passed, 0 failed, 1
filtered
- `cargo test -p parquet --test arrow_reader --features async -- --skip
bad_data::test_invalid_files`: 123 passed, 0 failed, 1 ignored, 1
filtered
- `cargo clippy -p parquet --all-targets --all-features -- -D warnings`:
passed

The two filtered tests require fixtures absent from the local
`parquet-testing` checkout (`int96_timestamp_order.parquet` and the
`bad_data/variants` fixture). The PR's GitHub `parquet` test,
compilation, and Clippy checks pass without filtering.

# Are there any user-facing changes?

No public interface changes. The new constructors and cutoff helper are
crate-private, and logical row-selection semantics and explicit policy
behavior are unchanged.
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: Improve performance of mask/selection construction in ReadPlanBuilder::with_predicate_options

6 participants