1919
2020use std:: sync:: Arc ;
2121
22+ use arrow:: datatypes:: SchemaRef ;
2223use datafusion_common:: Result ;
2324use datafusion_common:: config:: ConfigOptions ;
2425use datafusion_common:: tree_node:: { Transformed , TransformedResult , TreeNode } ;
2526use datafusion_common:: { JoinSide , JoinType , internal_datafusion_err} ;
26- use datafusion_physical_expr:: PhysicalExpr ;
2727use datafusion_physical_expr:: expressions:: { Column , LambdaExpr , Literal } ;
28+ use datafusion_physical_expr:: { PhysicalExpr , PhysicalExprSimplifier } ;
2829use datafusion_physical_plan:: filter:: FilterExec ;
29- use datafusion_physical_plan:: joins:: NestedLoopJoinExec ;
30- use datafusion_physical_plan:: joins:: utils :: ColumnIndex ;
30+ use datafusion_physical_plan:: joins:: utils :: { ColumnIndex , JoinFilter } ;
31+ use datafusion_physical_plan:: joins:: { CrossJoinExec , NestedLoopJoinExec } ;
3132use datafusion_physical_plan:: placeholder_row:: PlaceholderRowExec ;
3233use datafusion_physical_plan:: projection:: { ProjectionExec , ProjectionExpr } ;
3334use datafusion_physical_plan:: { ExecutionPlan , ExecutionPlanProperties } ;
3435
3536use crate :: PhysicalOptimizerRule ;
3637
37- /// Rewrites an inner [`NestedLoopJoinExec`] whose input provably produces one row
38- /// of literals into a [`FilterExec`] over the other input, with the literals
39- /// substituted into the join filter, under a [`ProjectionExec`] that restores the
40- /// join's output.
38+ /// Rewrites an inner [`NestedLoopJoinExec`] or a [`CrossJoinExec`] whose input
39+ /// provably produces one row of literals into a [`FilterExec`] over the other
40+ /// input, with the literals substituted into the join filter, under a
41+ /// [`ProjectionExec`] that restores the join's output.
4142///
4243/// For example, [`AggregateStatistics`] folds the scalar subquery in
4344///
@@ -70,10 +71,15 @@ impl PhysicalOptimizerRule for EliminateConstantJoin {
7071 _config : & ConfigOptions ,
7172 ) -> Result < Arc < dyn ExecutionPlan > > {
7273 plan. transform_up ( |plan| {
73- let Some ( join) = plan. downcast_ref :: < NestedLoopJoinExec > ( ) else {
74- return Ok ( Transformed :: no ( plan) ) ;
74+ let rewritten = if let Some ( join) = plan. downcast_ref :: < NestedLoopJoinExec > ( )
75+ {
76+ eliminate_nested_loop_join ( join) ?
77+ } else if let Some ( join) = plan. downcast_ref :: < CrossJoinExec > ( ) {
78+ eliminate ( join. left ( ) , join. right ( ) , None , None , & join. schema ( ) ) ?
79+ } else {
80+ None
7581 } ;
76- Ok ( match eliminate ( join ) ? {
82+ Ok ( match rewritten {
7783 Some ( rewritten) => Transformed :: yes ( rewritten) ,
7884 None => Transformed :: no ( plan) ,
7985 } )
@@ -90,8 +96,11 @@ impl PhysicalOptimizerRule for EliminateConstantJoin {
9096 }
9197}
9298
93- /// Rewrites `join` over its non-constant input, if the other input is a constant row.
94- fn eliminate ( join : & NestedLoopJoinExec ) -> Result < Option < Arc < dyn ExecutionPlan > > > {
99+ /// Rewrites `join` if it is an inner join against a constant row and its filter
100+ /// has no lambdas.
101+ fn eliminate_nested_loop_join (
102+ join : & NestedLoopJoinExec ,
103+ ) -> Result < Option < Arc < dyn ExecutionPlan > > > {
95104 if join. join_type ( ) != & JoinType :: Inner {
96105 return Ok ( None ) ;
97106 }
@@ -104,17 +113,35 @@ fn eliminate(join: &NestedLoopJoinExec) -> Result<Option<Arc<dyn ExecutionPlan>>
104113 {
105114 return Ok ( None ) ;
106115 }
116+ eliminate (
117+ join. left ( ) ,
118+ join. right ( ) ,
119+ join. filter ( ) ,
120+ join. projection ( ) . as_deref ( ) ,
121+ & join. schema ( ) ,
122+ )
123+ }
124+
125+ /// Rewrites an inner join of `left_input` and `right_input` over its non-constant
126+ /// input, if the other input is a constant row.
127+ fn eliminate (
128+ left_input : & Arc < dyn ExecutionPlan > ,
129+ right_input : & Arc < dyn ExecutionPlan > ,
130+ filter : Option < & JoinFilter > ,
131+ projection : Option < & [ usize ] > ,
132+ schema : & SchemaRef ,
133+ ) -> Result < Option < Arc < dyn ExecutionPlan > > > {
107134 // Each input's columns as expressions over the non-constant input.
108- let ( left, right, input) = if let Some ( row) = constant_row ( join . left ( ) ) {
109- ( row, columns ( join . right ( ) ) , join . right ( ) )
110- } else if let Some ( row) = constant_row ( join . right ( ) ) {
111- ( columns ( join . left ( ) ) , row, join . left ( ) )
135+ let ( left, right, input) = if let Some ( row) = constant_row ( left_input ) {
136+ ( row, columns ( right_input ) , right_input )
137+ } else if let Some ( row) = constant_row ( right_input ) {
138+ ( columns ( left_input ) , row, left_input )
112139 } else {
113140 return Ok ( None ) ;
114141 } ;
115142
116143 let mut input = Arc :: clone ( input) ;
117- if let Some ( filter) = join . filter ( ) {
144+ if let Some ( filter) = filter {
118145 let predicate = Arc :: clone ( filter. expression ( ) )
119146 . transform_up ( |expr| {
120147 let Some ( column) = expr. downcast_ref :: < Column > ( ) else {
@@ -141,14 +168,13 @@ fn eliminate(join: &NestedLoopJoinExec) -> Result<Option<Arc<dyn ExecutionPlan>>
141168 }
142169
143170 let mut outputs = left. into_iter ( ) . chain ( right) . collect :: < Vec < _ > > ( ) ;
144- if let Some ( projection) = join . projection ( ) {
171+ if let Some ( projection) = projection {
145172 outputs = projection
146173 . iter ( )
147174 . map ( |& index| outputs. get ( index) . cloned ( ) )
148175 . collect :: < Option < _ > > ( )
149176 . ok_or_else ( || internal_datafusion_err ! ( "Join projection out of range" ) ) ?;
150177 }
151- let schema = join. schema ( ) ;
152178 let exprs = outputs
153179 . into_iter ( )
154180 . zip ( schema. fields ( ) )
@@ -158,13 +184,13 @@ fn eliminate(join: &NestedLoopJoinExec) -> Result<Option<Arc<dyn ExecutionPlan>>
158184 } ) ;
159185 // Keep the join's metadata, which may come from the constant input.
160186 Ok ( Some ( Arc :: new (
161- ProjectionExec :: try_new_with_schema_metadata ( exprs, input, & schema) ?,
187+ ProjectionExec :: try_new_with_schema_metadata ( exprs, input, schema) ?,
162188 ) ) )
163189}
164190
165191/// Returns the literal values of the one row `plan` produces, if it is a stack of
166- /// projections over a single-partition [`PlaceholderRowExec`] that project only
167- /// literals and columns of the projection below .
192+ /// projections over a single-partition [`PlaceholderRowExec`] whose expressions
193+ /// evaluate to literals at planning time .
168194fn constant_row ( plan : & Arc < dyn ExecutionPlan > ) -> Option < Vec < Arc < dyn PhysicalExpr > > > {
169195 let projection = plan. downcast_ref :: < ProjectionExec > ( ) ?;
170196 let input = projection. input ( ) ;
@@ -178,15 +204,30 @@ fn constant_row(plan: &Arc<dyn ExecutionPlan>) -> Option<Vec<Arc<dyn PhysicalExp
178204 } else {
179205 constant_row ( input) ?
180206 } ;
207+ // The logical simplifier already folds constant expressions, but those over a
208+ // value `AggregateStatistics` folded, such as `min(ts) + 1`, are only constant
209+ // now. Volatile expressions and ones that fail to evaluate are not folded, and
210+ // are rejected: the filter would evaluate them for every row instead of once.
211+ let schema = input. schema ( ) ;
212+ let simplifier = PhysicalExprSimplifier :: new ( & schema) ;
181213 projection
182214 . expr ( )
183215 . iter ( )
184216 . map ( |ProjectionExpr { expr, .. } | {
185- if expr. is :: < Literal > ( ) {
186- return Some ( Arc :: clone ( expr) ) ;
187- }
188- let column = expr. downcast_ref :: < Column > ( ) ?;
189- input_row. get ( column. index ( ) ) . cloned ( )
217+ let expr = Arc :: clone ( expr)
218+ . transform_up ( |expr| {
219+ let value = expr
220+ . downcast_ref :: < Column > ( )
221+ . and_then ( |column| input_row. get ( column. index ( ) ) ) ;
222+ Ok ( match value {
223+ Some ( value) => Transformed :: yes ( Arc :: clone ( value) ) ,
224+ None => Transformed :: no ( expr) ,
225+ } )
226+ } )
227+ . data ( )
228+ . and_then ( |expr| simplifier. simplify ( expr) )
229+ . ok ( ) ?;
230+ expr. is :: < Literal > ( ) . then_some ( expr)
190231 } )
191232 . collect ( )
192233}
0 commit comments