@@ -1135,8 +1135,8 @@ impl OptimizerRule for PushDownFilter {
11351135 // multiple window functions, each with potentially different partition keys.
11361136 // Therefore, we need to ensure that any potential partition key returned is used in
11371137 // ALL window functions. Otherwise, filters cannot be pushed by through that column.
1138- fn extract_partition_keys ( func : & WindowFunction ) -> HashSet < Column > {
1139- expr_columns ( & func. params . partition_by )
1138+ fn extract_partition_keys ( func : & WindowFunction ) -> HashSet < & Expr > {
1139+ func. params . partition_by . iter ( ) . collect ( )
11401140 }
11411141
11421142 let potential_partition_keys = window
@@ -1172,21 +1172,23 @@ impl OptimizerRule for PushDownFilter {
11721172 let mut keep_predicates = vec ! [ ] ;
11731173 let mut push_predicates = vec ! [ ] ;
11741174 for expr in predicates {
1175- let cols = expr. column_refs ( ) ;
1176- if cols. iter ( ) . all ( |c| potential_partition_keys. contains ( c) ) {
1175+ // A volatile predicate has to stay above the window: pushing it
1176+ // changes which rows the window function sees.
1177+ if !expr. is_volatile ( )
1178+ && reads_only_partition_keys ( & expr, & potential_partition_keys) ?
1179+ {
11771180 push_predicates. push ( expr) ;
11781181 } else {
11791182 keep_predicates. push ( expr) ;
11801183 }
11811184 }
11821185
11831186 // Unlike with aggregations, there are no cases where we have to replace, e.g.,
1184- // `a+b` with Column(a)+Column(b). This is because partition expressions are not
1185- // available as standalone columns to the user. For example, while an aggregation on
1186- // `a+b` becomes Column(a + b), in a window partition it becomes
1187- // `func() PARTITION BY [a + b] ...`. Thus, filters on expressions always remain in
1188- // place, so we can use `push_predicates` directly. This is consistent with other
1189- // optimizers, such as the one used by Postgres.
1187+ // `a+b` with Column(a+b). This is because partition expressions are not available
1188+ // as standalone columns to the user: while an aggregation on `a+b` becomes
1189+ // Column(a + b), in a window partition it stays `func() PARTITION BY [a + b] ...`.
1190+ // That is why the predicate is matched against the key expressions themselves and
1191+ // can be pushed unchanged.
11901192
11911193 // If we have a filter to push, we push it down to the input of the aggregate
11921194 let result = if let Some ( predicate) = conjunction ( push_predicates) {
@@ -1585,6 +1587,54 @@ fn with_filters(predicates: Vec<Expr>, plan: LogicalPlan) -> LogicalPlan {
15851587 }
15861588}
15871589
1590+ /// Can `expr` be evaluated below a window with these `PARTITION BY` keys?
1591+ ///
1592+ /// A predicate that reads only the partition keys is constant within each
1593+ /// partition, so filtering before the window drops whole partitions and leaves
1594+ /// the surviving rows' window values unchanged. "Reads only the keys" is checked
1595+ /// structurally: every column reference must sit inside a subtree that is equal
1596+ /// to one of the keys. Given `PARTITION BY a, b + c`:
1597+ ///
1598+ /// * `a < 5`, `b + c = 4` and `(b + c) + 1 > 10` can be pushed down
1599+ /// * `d < 5` and `b < 5` cannot (`b` on its own is not a key), and neither can
1600+ /// `c + b = 4` (the match is structural, `c + b` is not `b + c`)
1601+ ///
1602+ /// A predicate containing a subquery is never pushed: what the subquery reads is
1603+ /// not visible from the expression tree.
1604+ fn reads_only_partition_keys (
1605+ expr : & Expr ,
1606+ partition_keys : & HashSet < & Expr > ,
1607+ ) -> Result < bool > {
1608+ let mut reads_something_else = false ;
1609+ expr. apply ( |node| {
1610+ Ok ( if partition_keys. contains ( & node) {
1611+ // the whole key was matched, so whatever it reads is accounted for
1612+ TreeNodeRecursion :: Jump
1613+ } else if reads_beyond_this_node ( node) {
1614+ reads_something_else = true ;
1615+ TreeNodeRecursion :: Stop
1616+ } else {
1617+ TreeNodeRecursion :: Continue
1618+ } )
1619+ } ) ?;
1620+ Ok ( !reads_something_else)
1621+ }
1622+
1623+ /// Does this node read data that walking its children cannot account for? A
1624+ /// column reads itself; a subquery reads whatever its plan reads, which
1625+ /// `Expr::apply` does not visit.
1626+ fn reads_beyond_this_node ( node : & Expr ) -> bool {
1627+ matches ! (
1628+ node,
1629+ Expr :: Column ( _)
1630+ | Expr :: OuterReferenceColumn ( ..)
1631+ | Expr :: ScalarSubquery ( _)
1632+ | Expr :: Exists ( _)
1633+ | Expr :: InSubquery ( _)
1634+ | Expr :: SetComparison ( _)
1635+ )
1636+ }
1637+
15881638fn expr_columns ( exprs : & [ Expr ] ) -> HashSet < Column > {
15891639 exprs
15901640 . iter ( )
@@ -1956,10 +2006,12 @@ mod tests {
19562006 )
19572007 }
19582008
1959- /// verifies that filters on partition expressions are not pushed, as the single expression
1960- /// column is not available to the user, unlike with aggregations
2009+ /// verifies that a filter on an expression partition key is pushed; the
2010+ /// remaining shapes (mixed keys, operand order, subqueries, volatile
2011+ /// predicates, several windows) are covered in
2012+ /// `push_down_filter_regression.slt`
19612013 #[ test]
1962- fn filter_expression_keep_window ( ) -> Result < ( ) > {
2014+ fn filter_expression_move_window ( ) -> Result < ( ) > {
19632015 let table_scan = test_table_scan ( ) ?;
19642016
19652017 let window = Expr :: from ( WindowFunction :: new (
@@ -1975,49 +2027,14 @@ mod tests {
19752027
19762028 let plan = LogicalPlanBuilder :: from ( table_scan)
19772029 . window ( vec ! [ window] ) ?
1978- // unlike with aggregations, single partition column "test.a + test.b" is not available
1979- // to the plan, so we use multiple columns when filtering
1980- . filter ( add ( col ( "a" ) , col ( "b" ) ) . gt ( lit ( 10i64 ) ) ) ?
1981- . build ( ) ?;
1982-
1983- assert_optimized_plan_equal ! (
1984- plan,
1985- @r"
1986- Filter: test.a + test.b > Int64(10)
1987- WindowAggr: windowExpr=[[rank() PARTITION BY [test.a + test.b] ORDER BY [test.c ASC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]
1988- TableScan: test
1989- "
1990- )
1991- }
1992-
1993- /// verifies that filters are not pushed on order by columns (that are not used in partitioning)
1994- #[ test]
1995- fn filter_order_keep_window ( ) -> Result < ( ) > {
1996- let table_scan = test_table_scan ( ) ?;
1997-
1998- let window = Expr :: from ( WindowFunction :: new (
1999- WindowFunctionDefinition :: WindowUDF (
2000- datafusion_functions_window:: rank:: rank_udwf ( ) ,
2001- ) ,
2002- vec ! [ ] ,
2003- ) )
2004- . partition_by ( vec ! [ col( "a" ) ] )
2005- . order_by ( vec ! [ col( "c" ) . sort( true , true ) ] )
2006- . build ( )
2007- . unwrap ( ) ;
2008-
2009- let plan = LogicalPlanBuilder :: from ( table_scan)
2010- . window ( vec ! [ window] ) ?
2011- . filter ( col ( "c" ) . gt ( lit ( 10i64 ) ) ) ?
2030+ . filter ( add ( col ( "a" ) , col ( "b" ) ) . gt ( lit ( 10i64 ) ) ) ? // a + b > 10
20122031 . build ( ) ?;
2013- assert_plan_not_transformed ! ( plan. clone( ) ) ;
20142032
20152033 assert_optimized_plan_equal ! (
20162034 plan,
20172035 @r"
2018- Filter: test.c > Int64(10)
2019- WindowAggr: windowExpr=[[rank() PARTITION BY [test.a] ORDER BY [test.c ASC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]
2020- TableScan: test
2036+ WindowAggr: windowExpr=[[rank() PARTITION BY [test.a + test.b] ORDER BY [test.c ASC NULLS FIRST] ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]
2037+ TableScan: test, full_filters=[test.a + test.b > Int64(10)]
20212038 "
20222039 )
20232040 }
0 commit comments