Repository navigation
Conversation
rok
force-pushed
the
gh-1440-is-in-sorted-pruning
branch
2 times, most recently
from
October 27, 2026 20:05
8307bd7 to
f6953c2
Compare
|
Thanks for opening a pull request! This pull request has been automatically converted to a draft because its title doesn't match Arrow's required format. If this is not a minor PR, could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project. Then could you also rename the pull request title in the following format? or After updating the title, you can mark the pull request as ready for review. See also: |
…rted value set Since apacheGH-46777 is_in predicates with more than 50 values are not simplified against row group statistics at all, because filtering the value set and rebinding the call for every guarantee was prohibitively expensive. Large value sets are now simplified with a binary search instead: - The is_in kernel init keeps a copy of the value set sorted in the order of the comparison kernels, with nulls removed. An already sorted value set is reused as is after a single O(N) check; an unsorted one is sorted lazily and thread-safely on the first simplification, so eager kernel calls never pay for sorting. Value sets containing NaN are not searchable, since statistics cannot bound NaN. - SimplifyWithGuarantee applies all inequality guarantees on a field to a large is_in call at once, narrowing an index range into the sorted value set with lower/upper bound searches. If the range is empty the call simplifies to false (subject to the null matching behavior), otherwise it is left unchanged. This is exact for any number of values, costs O(log N) per guarantee and never rebinds. Applying both bounds together is what allows pruning a row group inside the hull of the value set but holding none of its values, which no per-bound check can do. Value sets up to 50 values keep using the existing filter-and-rebind path.
…dices + take Sorting a copy of the values with std::sort is about 14x faster than the generic sort_indices + take path for a shuffled set of 10M int64 values (0.19s vs 2.6s). Nulls are dropped while copying and a set containing NaN is reported as not searchable before sorting, since NaN would break the strict weak ordering std::sort requires. Non-primitive types (binary, decimal) keep using the generic path.
rok
force-pushed
the
gh-1440-is-in-sorted-pruning
branch
from
October 27, 2026 20:43
f6953c2 to
9f25a9a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rationale for this change
apache#43761 (apacheGH-43187) added
is_inpredicate simplification against row group statistics by filtering the value set and rebinding the call for every guarantee. That made large value sets prohibitively slow (apache#46777), so apache#46859 disabled the simplification above 50 values. Since thenis_inpredicates with hundreds or millions of values cannot prune any row groups, even though that is exactly the case where pruning pays off most (see G-Research/gr-oss#1440 and the equivalent Polars change pola-rs/polars#29923).The closed PR apache#43256 already explored a sorted value set with binary search; this PR picks that up with the constraints raised in its review: no flag on
SetLookupOptions, no mutation of the options, the sorted copy is kept in the kernel state and written at most once, and the result is exact rather than hull based.What changes are included in this PR?
Sorted value set in the
is_inkernel state (scalar_set_lookup_internal.h,scalar_set_lookup.cc)is_inandindex_inget separate init functions. Foris_inwith more thankIsInSimplificationMaxValueSet(50) values of a searchable type (integers, floats, temporal types, decimals, binary-like types), the init keeps a copy of the value set sorted in the order of the comparison kernels, with nulls removed.std::sort, which is about 14x faster thansort_indices+takefor 10M shuffled int64 values. Binary and decimal sets use the generic path.SetLookupStateBasemoves to a header shared between the kernel (libarrow_compute) and expression simplification (libarrow); everything in it is header-only.Simplification against all inequalities at once (
expression.cc)SimplifyWithGuaranteecollects every inequality conjunct of the guarantee and applies all of them to each largeis_incall in one pass, narrowing an index range into the sorted value set with lower/upper bound searches. If the range is empty the call simplifies tofalse, subject to the same null matching rules as the small set path; otherwise it is left unchanged, so nothing is rebound.Are these changes tested?
Expression.SimplifyIsInLargeValueSetcovers hull pruning, gap pruning, redundant bounds, guarantees on other fields, casts in both directions, unsorted and duplicated sets, strings (sorted, unsorted, with nulls), floats with and without NaN, dictionaries (not searchable), the full null matching matrix including a guarantee that is nullable on one bound only, equality-or-null guarantees, and composition with other predicates.TestParquetFileFormatScan.PredicatePushdownRowGroupFragmentsLargeIsInprunes 30 of 80 row groups that lie inside the hull of a 60 value set but hold none of its values, with sorted, shuffled, duplicated and int32 value sets, and combined with another predicate.arrow-compute-*,arrow-acero-*andarrow-dataset-*test suites at the base commit and at each commit of this PR: all 43 suites pass at each of the three points (basee7b0e50dfa, commit 1, commit 2).End-to-end scan sweep. Real Parquet scans through
arrow::dataset(ScannerBuilder::Filter+ToTable, threads on, min of 3 runs, files in page cache, Apple Silicon, release build) at the base commit (main), commit 1 (sorted value set + interval search) and commit 2 (+std::sortfast path). The big file follows apache#46777: 47 row groups of 1Mi rows with a sorted int64idcolumn (1..49.3M) and a float64 column. The small file follows gr-oss#1440: 4096 ids x 8 rows in 4096-row row groups. Value set layouts: sorted contiguous ids 1..N; the same ids shuffled; two clusters of N/2 ids at the start and at the end of the data, whose hull spans every row group so only an exact check can prune anything; and N ids spread evenly over the data, where every row group holds a match and nothing can be pruned (the no-benefit case, which shows the overhead).49.3M rows, 47 row groups of 1Mi rows (apache#46777 layout)
32K rows, 8 row groups of 4096 rows (gr-oss#1440 layout)
Reading: up to 50 values nothing changes. Above that,
mainreads every row group and the scan costs the full file regardless of N, while this PR reads only the row groups that can hold a match: 16-30x faster for sets up to 100K ids, 7-11x at 1M, 1.8x at 10M where 10 of 47 row groups genuinely contain matches and the hash table build dominates. The spread layout and the unfiltered scan vary by the same few milliseconds between runs, i.e. the pruning attempt costs nothing measurable when it cannot prune. The one place commit 1 alone was a regression, a 10M shuffled set (3.0 s vs 0.47 s, from the generic sort), is what commit 2 fixes (0.46 s).Simplification-only microbenchmark (bind + SimplifyWithGuarantee over 47 row group guarantees, no I/O)
Sorted contiguous ids 1..N
The same ids, shuffled
Two clusters: N/2 ids at the start and N/2 at the end of the data (hull spans every row group)
Are there any user-facing changes?
No API changes.
is_infilters with more than 50 values now prune Parquet row groups again, exactly.Follow-ups not included here: a benchmark for
SimplifyWithGuaranteewithis_in, and distributingor(and(ge, le), is_null)row group guarantees so that nullable columns get inequality simplification at all.