@@ -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.
16451644pub 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`].
16491653pub 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