Skip to content

GH-55292: [C++][Dataset] Prune Parquet row groups for is_in predicates with large value sets - #55293

Draft
rok wants to merge 2 commits into
apache:mainfrom
rok:GH-55292-is-in-row-group-pruning
Draft

rok wants to merge 2 commits into
apache:mainfrom
rok:GH-55292-is-in-row-group-pruning

Conversation

@rok

@rok rok commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Rationale for this change

Since #46859, is_in predicates with more than 50 values are not simplified against Parquet row group statistics at all, so such filters read every row group. See #55292 for the background. The same problem was reported and fixed in Polars in pola-rs/polars#29923.

What changes are included in this PR?

  • The is_in kernel state keeps a sorted, null-free copy of the value set when it has more than 50 values. An already sorted set is reused as is after a single O(N) check; otherwise it is sorted lazily on first use, so plain kernel calls don't pay for it. Primitive sets are sorted with std::sort on a copy, which is about 13x faster than sort_indices + take for 10M shuffled values.
  • SimplifyWithGuarantee applies all inequality guarantees on a field to a large is_in call at once by binary searching the sorted set. If no value can match, the call becomes false, otherwise it is left alone (no rebinding). Using both row group bounds together is what lets us skip row groups that lie inside the hull of the value set but hold none of its values.
  • Bounds are brought into the field type exactly and then put through the same casts as the argument. Strict bounds through non-injective (floating point) casts are weakened to inclusive ones.
  • Comparison::StripOrderPreservingCasts no longer strips unsigned to signed casts that may wrap. This also fixes the existing comparison simplifier.
  • Sets of up to 50 values keep using the existing filter and rebind path.

Are these changes tested?

New Expression.SimplifyIsInLargeValueSet and Expression.SimplifyLargeIsInThenExecute tests, plus a Parquet dataset test that prunes row groups inside the hull of the value set.

Scan timings on a 49M row file with 47 row groups (release build, file in page cache): filters with 100 to 100K ids go from ~100 ms (the whole file) to 4 to 8 ms, 1M ids from ~300 ms to 26 to 42 ms, 10M ids from ~450 ms to ~260 ms. A value set that touches every row group costs the same as before.

Are there any user-facing changes?

No API changes. is_in filters with more than 50 values prune Parquet row groups again.

Was AI used for this PR?

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

The code was drafted with an AI assistant; I reviewed, tested and benchmarked it and iterated on the design.

rok added 2 commits October 27, 2026 22:33
… sets using a sorted 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.
…d of via sort_indices + 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.
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #55292 has been automatically assigned in GitHub to PR creator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant