Skip to content

Commit 65f827d

Browse files
committed
refactor: separate compact IN-list pruning threshold from the default cap
1 parent 925d7f8 commit 65f827d

2 files changed

Lines changed: 56 additions & 17 deletions

File tree

‎datafusion/pruning/benches/string_in_list_pruning.rs‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,10 @@ use datafusion_common::{Column, ScalarValue};
4747
use datafusion_expr_common::operator::Operator;
4848
use datafusion_physical_expr::PhysicalExprRef;
4949
use datafusion_physical_expr::expressions::{BinaryExpr, col, in_list, lit};
50-
use datafusion_pruning::{
51-
MAX_IN_LIST_SIZE, PruningPredicate, PruningPredicateBuilder, PruningStatistics,
52-
};
50+
use datafusion_pruning::{PruningPredicate, PruningPredicateBuilder, PruningStatistics};
5351

5452
const DOMAIN_SIZES: [usize; 9] = [1, 2, 4, 8, 16, 20, 21, 256, 1024];
55-
const CONTAINER_COUNTS: [usize; 3] = [16, 256, 4096];
53+
const CONTAINER_COUNTS: [usize; 4] = [1, 16, 256, 4096];
5654
const BASELINE_CONTAINER_COUNT: usize = 4096;
5755

5856
fn value(index: usize) -> String {
@@ -332,7 +330,12 @@ fn assert_equivalent_results(case: &BenchmarkCase, statistics: &IntervalStatisti
332330
.not_in_list_with_null_predicate
333331
.prune(statistics)
334332
.unwrap();
335-
if case.size + 1 > MAX_IN_LIST_SIZE {
333+
if case
334+
.in_list_with_null_predicate
335+
.predicate_expr()
336+
.to_string()
337+
.contains("IN_SET_INTERSECTS")
338+
{
336339
// Compact pruning applies filter semantics: NULL does not change which
337340
// rows can satisfy IN, while NOT IN (..., NULL) can never be true.
338341
assert_eq!(in_with_null, expected);
@@ -435,11 +438,11 @@ fn criterion_benchmark(criterion: &mut Criterion) {
435438
criterion.benchmark_group("string_in_list_pruning/not_in_distributions");
436439
distributions.throughput(Throughput::Elements(BASELINE_CONTAINER_COUNT as u64));
437440
let singleton = IntervalStatistics::uniform_singleton();
438-
for case in cases.iter().filter(|case| case.size > 20) {
439-
let compact = case.not_in_list_predicate.prune(&singleton).unwrap();
440-
assert!(compact.iter().all(|keep| !keep));
441+
for case in &cases {
442+
let actual = case.not_in_list_predicate.prune(&singleton).unwrap();
443+
assert!(actual.iter().all(|keep| !keep));
441444
assert_eq!(
442-
compact,
445+
actual,
443446
case.expanded_and_predicate.prune(&singleton).unwrap()
444447
);
445448
for (name, predicate) in [

‎datafusion/pruning/src/pruning_predicate.rs‎

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,14 +1636,18 @@ fn build_compact_in_list_expr(
16361636
}
16371637

16381638
/// Default maximum number of entries in an `IN (...)` list eligible for
1639-
/// statistics pruning. Eligible literal lists of supported ordered types above
1640-
/// this threshold use a compact sorted domain instead of per-value min/max
1641-
/// checks, for both `IN` and `NOT IN`.
1639+
/// statistics pruning. See [`PruningPredicateBuilder::with_max_in_list_size`]
1640+
/// for the representation used within this cap.
16421641
/// Callers can raise the cap via [`PredicateRewriter::with_max_in_list_size`], and
16431642
/// query engines can wire it from the
16441643
/// `datafusion.execution.parquet.max_in_list_size` config option.
16451644
pub const MAX_IN_LIST_SIZE: usize = 20;
16461645

1646+
// Keep the representation threshold independent of the configurable pruning cap.
1647+
// Small lists can be faster with vectorized per-value comparisons than with
1648+
// compact per-container searches, particularly for large statistics batches.
1649+
const MIN_COMPACT_IN_LIST_SIZE: usize = 21;
1650+
16471651
/// Rewrite a predicate expression in terms of statistics (min/max/null_counts)
16481652
/// for use as a [`PruningPredicate`].
16491653
pub struct PredicateRewriter {
@@ -1770,11 +1774,8 @@ fn build_predicate_expression(
17701774
}
17711775
}
17721776
if let Some(in_list) = expr.downcast_ref::<phys_expr::InListExpr>() {
1773-
// Keep the existing expression shape for lists of at most 20 values.
1774-
// This lower bound is a scope/compatibility choice, not a measured
1775-
// performance threshold; compact pruning is opt-in via a raised cap.
17761777
// The compact form covers both `IN` and `NOT IN`.
1777-
if in_list.list().len() > MAX_IN_LIST_SIZE
1778+
if in_list.list().len() >= MIN_COMPACT_IN_LIST_SIZE
17781779
&& in_list.list().len() <= max_in_list_size
17791780
&& let Some(pruning_expr) =
17801781
build_compact_in_list_expr(in_list, schema, required_columns, properties)
@@ -4247,7 +4248,7 @@ mod tests {
42474248
predicate.required_columns().single_column().unwrap().name(),
42484249
"c1"
42494250
);
4250-
if count > MAX_IN_LIST_SIZE {
4251+
if count >= MIN_COMPACT_IN_LIST_SIZE {
42514252
// The expression and statistics schema do not grow with the domain.
42524253
assert_eq!(predicate.required_columns.columns.len(), 4);
42534254
let mut nodes = 0;
@@ -4385,6 +4386,41 @@ mod tests {
43854386
Ok(())
43864387
}
43874388

4389+
#[test]
4390+
fn string_in_list_representation_respects_cap_and_threshold() -> Result<()> {
4391+
let schema = Arc::new(Schema::new(vec![Field::new("c1", DataType::Utf8, true)]));
4392+
for count in [1, 2, 4, 8, 20, 21] {
4393+
let values = (0..count)
4394+
.map(|i| lit(format!("a{i:03}")))
4395+
.collect::<Vec<_>>();
4396+
for (negated, marker) in
4397+
[(false, "IN_SET_INTERSECTS"), (true, "NOT_IN_SET_MAY_MATCH")]
4398+
{
4399+
let expr = logical2physical(
4400+
&col("c1").in_list(values.clone(), negated),
4401+
&schema,
4402+
);
4403+
for limit in [0, count - 1, count, 32] {
4404+
let predicate = PruningPredicateBuilder::new()
4405+
.with_file_schema(Arc::clone(&schema))
4406+
.with_max_in_list_size(limit)
4407+
.try_build(Arc::clone(&expr))?;
4408+
assert_eq!(
4409+
predicate.predicate_expr().to_string().contains(marker),
4410+
count >= 21 && count <= limit,
4411+
"count={count}, limit={limit}, negated={negated}"
4412+
);
4413+
assert_eq!(
4414+
is_always_true(predicate.predicate_expr()),
4415+
count > limit,
4416+
"count={count}, limit={limit}, negated={negated}"
4417+
);
4418+
}
4419+
}
4420+
}
4421+
Ok(())
4422+
}
4423+
43884424
#[test]
43894425
fn large_string_in_list_respects_configured_limit() -> Result<()> {
43904426
let schema = Arc::new(Schema::new(vec![Field::new("c1", DataType::Utf8, true)]));

0 commit comments

Comments
 (0)