@@ -247,32 +247,50 @@ impl SpjNormalForm {
247247 /// This is useful for rewriting queries to use materialized views.
248248 pub fn rewrite_from (
249249 & self ,
250- mut other : & Self ,
250+ other : & Self ,
251251 qualifier : TableReference ,
252252 source : Arc < dyn TableSource > ,
253253 ) -> Result < Option < LogicalPlan > > {
254254 log:: trace!( "rewriting from {qualifier}" ) ;
255+
256+ // Cache columns() result to avoid repeated Vec allocation in the loop.
257+ // DFSchema::columns() creates a new Vec on each call.
258+ let output_columns = self . output_schema . columns ( ) ;
259+
255260 let mut new_output_exprs = Vec :: with_capacity ( self . output_exprs . len ( ) ) ;
256261 // check that our output exprs are sub-expressions of the other one's output exprs
257262 for ( i, output_expr) in self . output_exprs . iter ( ) . enumerate ( ) {
258- let new_output_expr = other
259- . predicate
260- . normalize_expr ( output_expr. clone ( ) )
261- . rewrite ( & mut other) ?
262- . data ;
263-
264- // Check that all references to the original tables have been replaced.
265- // All remaining column expressions should be unqualified, which indicates
266- // that they refer to the output of the sub-plan (in this case the view)
267- if new_output_expr
268- . column_refs ( )
269- . iter ( )
270- . any ( |c| c. relation . is_some ( ) )
271- {
272- return Ok ( None ) ;
273- }
263+ // Fast path for simple Column expressions (most common case).
264+ // This avoids the expensive normalize_expr transform for columns.
265+ let new_output_expr = if let Expr :: Column ( col) = output_expr {
266+ let normalized_col = other. predicate . normalize_column ( col) ;
267+ match other. find_output_column ( & normalized_col) {
268+ Some ( rewritten) => rewritten,
269+ None => return Ok ( None ) , // Column not found, can't rewrite
270+ }
271+ } else {
272+ // Slow path: complex expressions need full transform
273+ let new_output_expr = other
274+ . predicate
275+ . normalize_expr ( output_expr. clone ( ) )
276+ . rewrite ( & mut & * other) ?
277+ . data ;
278+
279+ // Check that all references to the original tables have been replaced.
280+ // All remaining column expressions should be unqualified, which indicates
281+ // that they refer to the output of the sub-plan (in this case the view)
282+ if new_output_expr
283+ . column_refs ( )
284+ . iter ( )
285+ . any ( |c| c. relation . is_some ( ) )
286+ {
287+ return Ok ( None ) ;
288+ }
289+ new_output_expr
290+ } ;
274291
275- let column = & self . output_schema . columns ( ) [ i] ;
292+ // Use cached columns instead of calling .columns() on each iteration
293+ let column = & output_columns[ i] ;
276294 new_output_exprs. push (
277295 new_output_expr. alias_qualified ( column. relation . clone ( ) , column. name . clone ( ) ) ,
278296 ) ;
@@ -299,7 +317,7 @@ impl SpjNormalForm {
299317 . into_iter ( )
300318 . chain ( range_filters)
301319 . chain ( residual_filters)
302- . map ( |expr| expr. rewrite ( & mut other) . unwrap ( ) . data )
320+ . map ( |expr| expr. rewrite ( & mut & * other) . unwrap ( ) . data )
303321 . reduce ( |a, b| a. and ( b) ) ;
304322
305323 if all_filters
@@ -318,6 +336,20 @@ impl SpjNormalForm {
318336
319337 builder. project ( new_output_exprs) ?. build ( ) . map ( Some )
320338 }
339+
340+ /// Fast path: find a column in output_exprs and return rewritten expression.
341+ /// This avoids full tree traversal for simple column lookups.
342+ #[ inline]
343+ fn find_output_column ( & self , col : & Column ) -> Option < Expr > {
344+ self . output_exprs
345+ . iter ( )
346+ . position ( |e| matches ! ( e, Expr :: Column ( c) if c == col) )
347+ . map ( |idx| {
348+ Expr :: Column ( Column :: new_unqualified (
349+ self . output_schema . field ( idx) . name ( ) . clone ( ) ,
350+ ) )
351+ } )
352+ }
321353}
322354
323355/// Stores information on filters from a Select-Project-Join plan.
@@ -431,6 +463,17 @@ impl Predicate {
431463 . and_then ( |& idx| self . eq_classes . get ( idx) )
432464 }
433465
466+ /// Fast path: normalize a single Column without full tree traversal.
467+ /// This is O(1) lookup instead of O(n) transform.
468+ #[ inline]
469+ fn normalize_column ( & self , col : & Column ) -> Column {
470+ if let Some ( eq_class) = self . class_for_column ( col) {
471+ eq_class. columns . first ( ) . unwrap ( ) . clone ( )
472+ } else {
473+ col. clone ( )
474+ }
475+ }
476+
434477 /// Add a new column equivalence
435478 fn add_equivalence ( & mut self , c1 : & Column , c2 : & Column ) -> Result < ( ) > {
436479 match (
@@ -792,6 +835,15 @@ impl Predicate {
792835 /// Rewrite all expressions in terms of their normal representatives
793836 /// with respect to this predicate's equivalence classes.
794837 fn normalize_expr ( & self , e : Expr ) -> Expr {
838+ // Fast path: if it's a simple Column, avoid full transform traversal.
839+ // Even though transform() handles Column efficiently, the machinery setup
840+ // (closures, iterators, Transformed<T> wrappers) has overhead that adds up
841+ // when called thousands of times (e.g., 41 columns × 5-7 MVs × every query).
842+ // Direct HashMap lookup + clone is significantly faster.
843+ if let Expr :: Column ( ref c) = e {
844+ return Expr :: Column ( self . normalize_column ( c) ) ;
845+ }
846+
795847 e. transform ( & |e| {
796848 let c = match e {
797849 Expr :: Column ( c) => c,
@@ -1320,4 +1372,75 @@ mod test {
13201372
13211373 Ok ( ( ) )
13221374 }
1375+
1376+ #[ tokio:: test]
1377+ async fn test_normalize_column_fast_path ( ) -> Result < ( ) > {
1378+ let ctx = SessionContext :: new ( ) ;
1379+
1380+ ctx. sql ( "CREATE TABLE t (a INT, b INT, c INT)" )
1381+ . await ?
1382+ . collect ( )
1383+ . await ?;
1384+
1385+ // Query with column equivalence: a = b
1386+ let plan = ctx
1387+ . sql ( "SELECT a, b, c FROM t WHERE a = b" )
1388+ . await ?
1389+ . into_optimized_plan ( ) ?;
1390+
1391+ let normal_form = SpjNormalForm :: new ( & plan) ?;
1392+
1393+ // Verify that columns are normalized correctly
1394+ // a and b should be in the same equivalence class
1395+ assert_eq ! ( normal_form. output_exprs( ) . len( ) , 3 ) ;
1396+
1397+ Ok ( ( ) )
1398+ }
1399+
1400+ #[ tokio:: test]
1401+ async fn test_rewrite_from_with_many_columns ( ) -> Result < ( ) > {
1402+ let ctx = SessionContext :: new ( ) ;
1403+
1404+ // Create a wide table to test the columns() caching optimization
1405+ ctx. sql (
1406+ "CREATE TABLE wide_table (
1407+ c0 INT, c1 INT, c2 INT, c3 INT, c4 INT,
1408+ c5 INT, c6 INT, c7 INT, c8 INT, c9 INT
1409+ )" ,
1410+ )
1411+ . await ?
1412+ . collect ( )
1413+ . await ?;
1414+
1415+ let base_plan = ctx
1416+ . sql ( "SELECT * FROM wide_table WHERE c0 >= 0" )
1417+ . await ?
1418+ . into_optimized_plan ( ) ?;
1419+
1420+ let query_plan = ctx
1421+ . sql ( "SELECT c0, c1, c2 FROM wide_table WHERE c0 >= 10" )
1422+ . await ?
1423+ . into_optimized_plan ( ) ?;
1424+
1425+ let base_nf = SpjNormalForm :: new ( & base_plan) ?;
1426+ let query_nf = SpjNormalForm :: new ( & query_plan) ?;
1427+
1428+ // Create MV table
1429+ ctx. sql ( "CREATE TABLE mv AS SELECT * FROM wide_table WHERE c0 >= 0" )
1430+ . await ?
1431+ . collect ( )
1432+ . await ?;
1433+
1434+ let table_ref = TableReference :: bare ( "mv" ) ;
1435+ let provider = ctx. table_provider ( table_ref. clone ( ) ) . await ?;
1436+
1437+ // Test that rewrite_from works correctly with cached columns
1438+ let result = query_nf. rewrite_from ( & base_nf, table_ref, provider_as_source ( provider) ) ?;
1439+
1440+ assert ! ( result. is_some( ) ) ;
1441+ let rewritten = result. unwrap ( ) ;
1442+ assert_eq ! ( rewritten. schema( ) . fields( ) . len( ) , 3 ) ;
1443+
1444+ Ok ( ( ) )
1445+ }
13231446}
0 commit comments