@@ -48,58 +48,77 @@ func extractJoinGroup(p base.LogicalPlan) *joinGroupResult {
4848// 3. Build current node's colExprMap and return
4949// This approach is consistent with rule_eliminate_projection.go.
5050func extractJoinGroupImpl (p base.LogicalPlan ) * joinGroupResult {
51- // Check if we should try to inline projections
52- if proj , ok := p .(* logicalop.LogicalProjection ); ok &&
53- p .SCtx () != nil && p .SCtx ().GetSessionVars ().TiDBOptJoinReorderProj {
54- // Only inline projections whose child is a join; otherwise the projection is treated as an atomic leaf.
55- if _ , isJoin := proj .Children ()[0 ].(* logicalop.LogicalJoin ); isJoin {
56- // Fast safety checks first to avoid unnecessary recursion.
57- if canInlineProjectionBasic (proj ) {
58- childResult := extractJoinGroupImpl (proj .Children ()[0 ])
59- // The single-leaf check needs the extracted join group and child's colExprMap.
60- if canInlineProjection (proj , childResult ) {
61- // Build current Projection's colExprMap.
62- colExprMap := make (map [int64 ]expression.Expression , len (proj .Schema ().Columns ))
63- for i , col := range proj .Schema ().Columns {
64- expr := proj .Exprs [i ]
65-
66- // Use child's colExprMap to substitute column references in current expression.
67- if len (childResult .colExprMap ) > 0 {
68- expr = substituteColsInExpr (expr , childResult .colExprMap )
51+ // NOTE: We only support extracting join groups through a single Selection/Projection layer for now.
52+ // TODO: Support stacked unary operators like Projection->Selection->Join or Selection->Projection->Join.
53+ if selection , isSelection := p .(* logicalop.LogicalSelection ); isSelection {
54+ // If its child is a join, add the selection conditions to otherConds and continue extracting
55+ // the join group from the child.
56+ //
57+ // Join reorder may distribute/push down conditions during constructing the new join tree.
58+ // For volatile or side-effect expressions, moving them can change evaluation times/orders,
59+ // thus may change query results, so we skip reordering through Selection in such cases.
60+ if p .SCtx () != nil && p .SCtx ().GetSessionVars ().TiDBOptJoinReorderThroughSel &&
61+ ! slices .ContainsFunc (selection .Conditions , expression .IsMutableEffectsExpr ) {
62+ child := selection .Children ()[0 ]
63+ if _ , isChildJoin := child .(* logicalop.LogicalJoin ); isChildJoin {
64+ childResult := extractJoinGroup (child )
65+ childResult .otherConds = append (childResult .otherConds , selection .Conditions ... )
66+ return childResult
67+ }
68+ }
69+ } else if proj , ok := p .(* logicalop.LogicalProjection ); ok {
70+ // Check if we should try to inline projections
71+ if p .SCtx () != nil && p .SCtx ().GetSessionVars ().TiDBOptJoinReorderThroughProj {
72+ // Only inline projections whose child is a join; otherwise the projection is treated as an atomic leaf.
73+ if _ , isJoin := proj .Children ()[0 ].(* logicalop.LogicalJoin ); isJoin {
74+ // Fast safety checks first to avoid unnecessary recursion.
75+ if canInlineProjectionBasic (proj ) {
76+ childResult := extractJoinGroupImpl (proj .Children ()[0 ])
77+ // The single-leaf check needs the extracted join group and child's colExprMap.
78+ if canInlineProjection (proj , childResult ) {
79+ // Build current Projection's colExprMap.
80+ colExprMap := make (map [int64 ]expression.Expression , len (proj .Schema ().Columns ))
81+ for i , col := range proj .Schema ().Columns {
82+ expr := proj .Exprs [i ]
83+
84+ // Use child's colExprMap to substitute column references in current expression.
85+ if len (childResult .colExprMap ) > 0 {
86+ expr = substituteColsInExpr (expr , childResult .colExprMap )
87+ }
88+
89+ // For pass-through columns (where the projection expression is just a Column reference
90+ // that has been substituted to the underlying expression), we need to check if the output
91+ // column's UniqueID matches the input column's UniqueID. This can happen when a projection
92+ // simply passes through a column without transformation (e.g., SELECT dt1.key_a FROM ...).
93+ // In such cases, the output column may share the same UniqueID as the input column.
94+ //
95+ // If expr is a Column and its UniqueID equals col.UniqueID, this is a pass-through scenario.
96+ // We should NOT add it to colExprMap because:
97+ // 1. It would create a self-referential mapping (UniqueID -> Column with same UniqueID)
98+ // 2. It could conflict with child's colExprMap entry for the same UniqueID
99+ //
100+ // The child's colExprMap will be merged later and will provide the proper mapping for this UniqueID.
101+ if colExpr , isCol := expr .(* expression.Column ); isCol && colExpr .UniqueID == col .UniqueID {
102+ // Skip pass-through columns - they will get their mapping from child's colExprMap.
103+ continue
104+ }
105+
106+ colExprMap [col .UniqueID ] = expr
69107 }
70108
71- // For pass-through columns (where the projection expression is just a Column reference
72- // that has been substituted to the underlying expression), we need to check if the output
73- // column's UniqueID matches the input column's UniqueID. This can happen when a projection
74- // simply passes through a column without transformation (e.g., SELECT dt1.key_a FROM ...).
75- // In such cases, the output column may share the same UniqueID as the input column.
76- //
77- // If expr is a Column and its UniqueID equals col.UniqueID, this is a pass-through scenario.
78- // We should NOT add it to colExprMap because:
79- // 1. It would create a self-referential mapping (UniqueID -> Column with same UniqueID)
80- // 2. It could conflict with child's colExprMap entry for the same UniqueID
81- //
82- // The child's colExprMap will be merged later and will provide the proper mapping for this UniqueID.
83- if colExpr , isCol := expr .(* expression.Column ); isCol && colExpr .UniqueID == col .UniqueID {
84- // Skip pass-through columns - they will get their mapping from child's colExprMap.
85- continue
109+ // Merge child's colExprMap entries.
110+ // Note: A key conflict here should not happen because each projection layer
111+ // produces new UniqueIDs. If it occurs, we skip the child's entry and keep
112+ // the current projection's entry, which is the correct behavior.
113+ for k , v := range childResult .colExprMap {
114+ if _ , exists := colExprMap [k ]; ! exists {
115+ colExprMap [k ] = v
116+ }
86117 }
87118
88- colExprMap [col .UniqueID ] = expr
89- }
90-
91- // Merge child's colExprMap entries.
92- // Note: A key conflict here should not happen because each projection layer
93- // produces new UniqueIDs. If it occurs, we skip the child's entry and keep
94- // the current projection's entry, which is the correct behavior.
95- for k , v := range childResult .colExprMap {
96- if _ , exists := colExprMap [k ]; ! exists {
97- colExprMap [k ] = v
98- }
119+ childResult .colExprMap = colExprMap
120+ return childResult
99121 }
100-
101- childResult .colExprMap = colExprMap
102- return childResult
103122 }
104123 }
105124 }
@@ -115,21 +134,6 @@ func extractJoinGroupImpl(p base.LogicalPlan) *joinGroupResult {
115134 hasOuterJoin bool
116135 )
117136
118- // Check if the current plan is a Selection. If its child is a join, add the selection conditions
119- // to otherConds and continue extracting the join group from the child.
120- // Join reorder may distribute/push down conditions during constructing the new join tree.
121- // For volatile or side-effect expressions, moving them can change evaluation times/orders
122- // thus may change query results, so we skip reordering through Selection in such cases.
123- if selection , isSelection := p .(* logicalop.LogicalSelection ); isSelection && p .SCtx ().GetSessionVars ().TiDBOptJoinReorderThroughSel &&
124- ! slices .ContainsFunc (selection .Conditions , expression .IsMutableEffectsExpr ) {
125- child := selection .Children ()[0 ]
126- if _ , isChildJoin := child .(* logicalop.LogicalJoin ); isChildJoin {
127- childResult := extractJoinGroup (child )
128- childResult .otherConds = append (childResult .otherConds , selection .Conditions ... )
129- return childResult
130- }
131- }
132-
133137 join , isJoin := p .(* logicalop.LogicalJoin )
134138 if isJoin && join .PreferJoinOrder {
135139 // When there is a leading hint, the hint may not take effect for other reasons.
@@ -593,12 +597,12 @@ func (s *JoinReOrderSolver) optimizeRecursive(ctx base.PlanContext, p base.Logic
593597 origJoinRoot := p
594598 originalSchema := p .Schema ()
595599 fallbackOnErr := func (err error ) (base.LogicalPlan , error ) {
596- if ctx .GetSessionVars ().TiDBOptJoinReorderProj {
600+ if ctx .GetSessionVars ().TiDBOptJoinReorderThroughProj {
597601 // This optimization is best-effort. If anything goes wrong, fallback to the
598602 // original behavior (treat projections as atomic leaves) to keep correctness.
599- saved := ctx .GetSessionVars ().TiDBOptJoinReorderProj
600- ctx .GetSessionVars ().TiDBOptJoinReorderProj = false
601- defer func () { ctx .GetSessionVars ().TiDBOptJoinReorderProj = saved }()
603+ saved := ctx .GetSessionVars ().TiDBOptJoinReorderThroughProj
604+ ctx .GetSessionVars ().TiDBOptJoinReorderThroughProj = false
605+ defer func () { ctx .GetSessionVars ().TiDBOptJoinReorderThroughProj = saved }()
602606 return s .optimizeRecursive (ctx , origJoinRoot )
603607 }
604608 return nil , err
0 commit comments