From bd298fb21ab212358069e24f14dc9bc46c73594e Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Tue, 3 Mar 2026 15:24:27 +0800 Subject: [PATCH 01/11] planner: deep copy AST-owned FieldTypes when building expressions --- pkg/planner/core/BUILD.bazel | 1 + pkg/planner/core/expression_rewriter.go | 40 +++++++++++---------- pkg/planner/core/expression_test.go | 46 +++++++++++++++++++++++++ pkg/planner/core/planbuilder.go | 6 ++-- pkg/planner/core/planbuilder_test.go | 35 +++++++++++++++++++ 5 files changed, 106 insertions(+), 22 deletions(-) diff --git a/pkg/planner/core/BUILD.bazel b/pkg/planner/core/BUILD.bazel index df7c8ec4808a0..aa7e9692318a1 100644 --- a/pkg/planner/core/BUILD.bazel +++ b/pkg/planner/core/BUILD.bazel @@ -297,6 +297,7 @@ go_test( "//pkg/testkit/testsetup", "//pkg/testkit/testutil", "//pkg/types", + "//pkg/types/parser_driver", "//pkg/util", "//pkg/util/chunk", "//pkg/util/collate", diff --git a/pkg/planner/core/expression_rewriter.go b/pkg/planner/core/expression_rewriter.go index ef02666c31001..0b044fad94612 100644 --- a/pkg/planner/core/expression_rewriter.go +++ b/pkg/planner/core/expression_rewriter.go @@ -172,7 +172,7 @@ func buildSimpleExpr(ctx expression.BuildContext, node ast.ExprNode, opts ...exp } if ft := options.TargetFieldType; ft != nil { - expr = expression.BuildCastFunction(ctx, expr, ft) + expr = expression.BuildCastFunction(ctx, expr, ft.DeepCopy()) } return expr, err @@ -1612,7 +1612,8 @@ func (er *expressionRewriter) Leave(originInNode ast.Node) (retNode ast.Node, ok return retNode, false } - castFunction, err := expression.BuildCastFunctionWithCheck(er.sctx, arg, v.Tp, false, v.ExplicitCharSet) + targetTp := v.Tp.DeepCopy() + castFunction, err := expression.BuildCastFunctionWithCheck(er.sctx, arg, targetTp, false, v.ExplicitCharSet) if err != nil { er.err = err return retNode, false @@ -1633,7 +1634,8 @@ func (er *expressionRewriter) Leave(originInNode ast.Node) (retNode ast.Node, ok er.ctxNameStk[len(er.ctxNameStk)-1] = types.EmptyName case *ast.JSONSumCrc32Expr: arg := er.ctxStack[len(er.ctxStack)-1] - jsonSumFunction, err := expression.BuildJSONSumCrc32FunctionWithCheck(er.sctx, arg, v.Tp) + targetTp := v.Tp.DeepCopy() + jsonSumFunction, err := expression.BuildJSONSumCrc32FunctionWithCheck(er.sctx, arg, targetTp) if err != nil { er.err = err return retNode, false @@ -1651,7 +1653,7 @@ func (er *expressionRewriter) Leave(originInNode ast.Node) (retNode ast.Node, ok er.rowToScalarFunc(v) case *ast.PatternInExpr: if v.Sel == nil { - er.inToExpression(len(v.List), v.Not, &v.Type) + er.inToExpression(len(v.List), v.Not, v.Type.DeepCopy()) } case *ast.PositionExpr: withPlanCtx(func(planCtx *exprRewriterPlanCtx) { @@ -1913,7 +1915,7 @@ func (er *expressionRewriter) unaryOpToExpression(v *ast.UnaryOperationExpr) { er.err = expression.ErrOperandColumns.GenWithStackByArgs(1) return } - er.ctxStack[stkLen-1], er.err = er.newFunction(op, &v.Type, er.ctxStack[stkLen-1]) + er.ctxStack[stkLen-1], er.err = er.newFunction(op, v.Type.DeepCopy(), er.ctxStack[stkLen-1]) er.ctxNameStk[stkLen-1] = types.EmptyName } @@ -1965,7 +1967,7 @@ func (er *expressionRewriter) isNullToExpression(v *ast.IsNullExpr) { er.err = expression.ErrOperandColumns.GenWithStackByArgs(1) return } - function := er.notToExpression(v.Not, ast.IsNull, &v.Type, er.ctxStack[stkLen-1]) + function := er.notToExpression(v.Not, ast.IsNull, v.Type.DeepCopy(), er.ctxStack[stkLen-1]) er.ctxStackPop(1) er.ctxStackAppend(function, types.EmptyName) } @@ -2005,7 +2007,7 @@ func (er *expressionRewriter) isTrueToScalarFunc(v *ast.IsTruthExpr) { er.err = expression.ErrOperandColumns.GenWithStackByArgs(1) return } - function := er.notToExpression(v.Not, op, &v.Type, er.ctxStack[stkLen-1]) + function := er.notToExpression(v.Not, op, v.Type.DeepCopy(), er.ctxStack[stkLen-1]) er.ctxStackPop(1) er.ctxStackAppend(function, types.EmptyName) } @@ -2196,7 +2198,7 @@ func (er *expressionRewriter) caseToExpression(v *ast.CaseExpr) { // else clause args = er.ctxStack[stkLen-argsLen:] } - function, err := er.newFunction(ast.Case, &v.Type, args...) + function, err := er.newFunction(ast.Case, v.Type.DeepCopy(), args...) if err != nil { er.err = err return @@ -2244,7 +2246,7 @@ func (er *expressionRewriter) patternLikeOrIlikeToExpression(v *ast.PatternLikeO funcName = ast.Ilike } types.DefaultTypeForValue(int(v.Escape), fieldType, char, col) - function = er.notToExpression(v.Not, funcName, &v.Type, + function = er.notToExpression(v.Not, funcName, v.Type.DeepCopy(), er.ctxStack[l-2], er.ctxStack[l-1], &expression.Constant{Value: types.NewIntDatum(int64(v.Escape)), RetType: fieldType}) } @@ -2258,7 +2260,7 @@ func (er *expressionRewriter) regexpToScalarFunc(v *ast.PatternRegexpExpr) { if er.err != nil { return } - function := er.notToExpression(v.Not, ast.Regexp, &v.Type, er.ctxStack[l-2], er.ctxStack[l-1]) + function := er.notToExpression(v.Not, ast.Regexp, v.Type.DeepCopy(), er.ctxStack[l-2], er.ctxStack[l-1]) er.ctxStackPop(2) er.ctxStackAppend(function, types.EmptyName) } @@ -2346,21 +2348,21 @@ func (er *expressionRewriter) betweenToExpression(v *ast.BetweenExpr) { rexp = expression.BuildCastCollationFunction(er.sctx, rexp, coll, enumOrSetRealTypeIsStr) var l, r expression.Expression - l, er.err = expression.NewFunction(er.sctx, ast.GE, &v.Type, expr, lexp) + l, er.err = expression.NewFunction(er.sctx, ast.GE, v.Type.DeepCopy(), expr, lexp) if er.err != nil { return } - r, er.err = expression.NewFunction(er.sctx, ast.LE, &v.Type, expr, rexp) + r, er.err = expression.NewFunction(er.sctx, ast.LE, v.Type.DeepCopy(), expr, rexp) if er.err != nil { return } - function, err := er.newFunction(ast.LogicAnd, &v.Type, l, r) + function, err := er.newFunction(ast.LogicAnd, v.Type.DeepCopy(), l, r) if err != nil { er.err = err return } if v.Not { - function, err = er.newFunction(ast.UnaryNot, &v.Type, function) + function, err = er.newFunction(ast.UnaryNot, v.Type.DeepCopy(), function) if err != nil { er.err = err return @@ -2434,7 +2436,7 @@ func (er *expressionRewriter) rewriteFuncCall(v *ast.FuncCallExpr) bool { RetType: nullTp, } // if(param1 = param2, NULL, param1) - funcIf, err := er.newFunction(ast.If, &v.Type, funcCompare, paramNull, param1) + funcIf, err := er.newFunction(ast.If, v.Type.DeepCopy(), funcCompare, paramNull, param1) if err != nil { er.err = err return true @@ -2495,7 +2497,7 @@ func (er *expressionRewriter) funcCallToExpressionWithPlanCtx(planCtx *exprRewri err = groupingFunc.Function.(*expression.BuiltinGroupingImplSig).SetMetadata(planCtx.rollExpand.GroupingMode, planCtx.rollExpand.GenerateGroupingMarks(resolvedCols)) return groupingFunc, err } - function, er.err = er.newFunctionWithInit(v.FnName.L, &v.Type, init, newArg) + function, er.err = er.newFunctionWithInit(v.FnName.L, v.Type.DeepCopy(), init, newArg) er.ctxStackAppend(function, types.EmptyName) } default: @@ -2522,15 +2524,15 @@ func (er *expressionRewriter) funcCallToExpression(v *ast.FuncCallExpr) { // When the expression is unix_timestamp and the number of argument is not zero, // we deal with it as normal expression. if v.FnName.L == ast.UnixTimestamp && len(v.Args) != 0 { - function, er.err = er.newFunction(v.FnName.L, &v.Type, args...) + function, er.err = er.newFunction(v.FnName.L, v.Type.DeepCopy(), args...) er.ctxStackAppend(function, types.EmptyName) } else { - function, er.err = expression.NewFunctionBase(er.sctx, v.FnName.L, &v.Type, args...) + function, er.err = expression.NewFunctionBase(er.sctx, v.FnName.L, v.Type.DeepCopy(), args...) c := &expression.Constant{Value: types.NewDatum(nil), RetType: function.GetType(er.sctx.GetEvalCtx()).Clone(), DeferredExpr: function} er.ctxStackAppend(c, types.EmptyName) } } else { - function, er.err = er.newFunction(v.FnName.L, &v.Type, args...) + function, er.err = er.newFunction(v.FnName.L, v.Type.DeepCopy(), args...) er.ctxStackAppend(function, types.EmptyName) } } diff --git a/pkg/planner/core/expression_test.go b/pkg/planner/core/expression_test.go index 3da5b7a37704c..766e7c6f1b3e5 100644 --- a/pkg/planner/core/expression_test.go +++ b/pkg/planner/core/expression_test.go @@ -202,6 +202,52 @@ func TestCast(t *testing.T) { require.Equal(t, types.KindNull, v.Kind()) } +func TestCastRetTypeDoesNotShareASTFieldType(t *testing.T) { + targetTp := types.NewFieldType(mysql.TypeLonglong) + targetTp.AddFlag(mysql.NotNullFlag) + + ctx := coretestsdk.MockContext() + defer func() { + do := domain.GetDomain(ctx) + do.StatsHandle().Close() + }() + + tbl := &model.TableInfo{ + Name: ast.NewCIStr("t"), + Columns: []*model.ColumnInfo{ + { + Name: ast.NewCIStr("a"), + Offset: 0, + State: model.StatePublic, + FieldType: *types.NewFieldType(mysql.TypeLonglong), + }, + }, + } + expr := parseExpr(t, "cast(a as signed)").(*ast.FuncCastExpr) + expr.Tp = targetTp + + built1, err := buildExpr(t, ctx, expr, expression.WithTableInfo("", tbl)) + require.NoError(t, err) + sf1, ok := built1.(*expression.ScalarFunction) + require.True(t, ok) + require.NotSame(t, targetTp, sf1.RetType) + require.True(t, mysql.HasNotNullFlag(targetTp.GetFlag())) + + sf1.RetType.SetType(mysql.TypeString) + sf1.RetType.AddFlag(mysql.UnsignedFlag) + + built2, err := buildExpr(t, ctx, expr, expression.WithTableInfo("", tbl)) + require.NoError(t, err) + sf2, ok := built2.(*expression.ScalarFunction) + require.True(t, ok) + require.NotSame(t, sf1.RetType, sf2.RetType) + require.Equal(t, mysql.TypeLonglong, targetTp.GetType()) + require.True(t, mysql.HasNotNullFlag(targetTp.GetFlag())) + require.Equal(t, mysql.TypeLonglong, sf2.RetType.GetType()) + require.False(t, mysql.HasNotNullFlag(sf2.RetType.GetFlag())) + require.False(t, mysql.HasUnsignedFlag(sf2.RetType.GetFlag())) +} + func TestPatternIn(t *testing.T) { tests := []testCase{ { diff --git a/pkg/planner/core/planbuilder.go b/pkg/planner/core/planbuilder.go index 7b6ed624f3a7f..e3ee2bfa57672 100644 --- a/pkg/planner/core/planbuilder.go +++ b/pkg/planner/core/planbuilder.go @@ -750,7 +750,7 @@ func (b *PlanBuilder) buildSet(ctx context.Context, v *ast.SetStmt) (base.Plan, if vars.ExtendValue != nil { assign.ExtendValue = &expression.Constant{ Value: vars.ExtendValue.(*driver.ValueExpr).Datum, - RetType: &vars.ExtendValue.(*driver.ValueExpr).Type, + RetType: vars.ExtendValue.(*driver.ValueExpr).Type.DeepCopy(), } } p.VarAssigns = append(p.VarAssigns, assign) @@ -4351,7 +4351,7 @@ func (b PlanBuilder) getInsertColExpr(ctx context.Context, insertPlan *physicalo case *driver.ValueExpr: outExpr = &expression.Constant{ Value: x.Datum, - RetType: &x.Type, + RetType: x.Type.DeepCopy(), } case *driver.ParamMarkerExpr: outExpr, err = expression.ParamMarkerExpression(b.ctx.GetExprCtx(), x, false) @@ -5185,7 +5185,7 @@ func (b *PlanBuilder) convertValue(valueItem ast.ExprNode, mockTablePlan base.Lo case *driver.ValueExpr: expr = &expression.Constant{ Value: x.Datum, - RetType: &x.Type, + RetType: x.Type.DeepCopy(), } default: expr, _, err = b.rewrite(context.TODO(), valueItem, mockTablePlan, nil, true) diff --git a/pkg/planner/core/planbuilder_test.go b/pkg/planner/core/planbuilder_test.go index f2820282be327..741845f82fabc 100644 --- a/pkg/planner/core/planbuilder_test.go +++ b/pkg/planner/core/planbuilder_test.go @@ -42,7 +42,9 @@ import ( "github.com/pingcap/tidb/pkg/planner/property" "github.com/pingcap/tidb/pkg/planner/util" "github.com/pingcap/tidb/pkg/planner/util/coretestsdk" + "github.com/pingcap/tidb/pkg/table" "github.com/pingcap/tidb/pkg/types" + driver "github.com/pingcap/tidb/pkg/types/parser_driver" "github.com/pingcap/tidb/pkg/util/dbterror/plannererrors" "github.com/pingcap/tidb/pkg/util/hint" "github.com/pingcap/tidb/pkg/util/mock" @@ -161,6 +163,39 @@ func TestRewriterPool(t *testing.T) { builder.rewriterCounter-- } +func TestGetInsertColExprDeepCopiesValueExprFieldType(t *testing.T) { + ctx := coretestsdk.MockContext() + defer func() { + domain.GetDomain(ctx).StatsHandle().Close() + }() + builder, _ := NewPlanBuilder().Init(ctx, nil, hint.NewQBHintHandler(nil)) + + valueExpr, ok := ast.NewValueExpr(1, "", "").(*driver.ValueExpr) + require.True(t, ok) + valueExpr.Type.AddFlag(mysql.NotNullFlag) + + col := &table.Column{ + ColumnInfo: &model.ColumnInfo{ + Name: ast.NewCIStr("a"), + FieldType: *types.NewFieldType(mysql.TypeLonglong), + }, + } + expr, err := builder.getInsertColExpr(context.TODO(), &physicalop.Insert{}, nil, col, valueExpr, nil) + require.NoError(t, err) + + constExpr, ok := expr.(*expression.Constant) + require.True(t, ok) + require.NotSame(t, valueExpr.GetType(), constExpr.RetType) + require.Equal(t, mysql.TypeLonglong, valueExpr.Type.GetType()) + require.True(t, mysql.HasNotNullFlag(valueExpr.Type.GetFlag())) + + constExpr.RetType.SetType(mysql.TypeString) + constExpr.RetType.DelFlag(mysql.NotNullFlag) + + require.Equal(t, mysql.TypeLonglong, valueExpr.Type.GetType()) + require.True(t, mysql.HasNotNullFlag(valueExpr.Type.GetFlag())) +} + func TestDisableFold(t *testing.T) { // Functions like BENCHMARK() shall not be folded into result 0, // but normal outer function with constant args should be folded. From c1845091cca03ed6fa146633e02e283060c4a607 Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Tue, 3 Mar 2026 15:59:44 +0800 Subject: [PATCH 02/11] planner: document read-only AST references in planner plans --- .../operator/logicalop/logical_datasource.go | 6 +++++- .../core/operator/logicalop/logical_lock.go | 2 ++ .../core/operator/logicalop/logical_show.go | 20 ++++++++++--------- .../core/operator/physicalop/physical_lock.go | 1 + 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pkg/planner/core/operator/logicalop/logical_datasource.go b/pkg/planner/core/operator/logicalop/logical_datasource.go index ea010f0b2ea4f..c7899396b3aeb 100644 --- a/pkg/planner/core/operator/logicalop/logical_datasource.go +++ b/pkg/planner/core/operator/logicalop/logical_datasource.go @@ -49,6 +49,8 @@ import ( type DataSource struct { LogicalSchemaProducer `hash64-equals:"true"` + // AstIndexHints keeps the original AST hints for later access-path selection. + // It is treated as read-only after DataSource build and may be shared by plans rebuilt from the same AST. AstIndexHints []*ast.IndexHint IndexHints []h.HintedIndex Table table.Table @@ -56,6 +58,7 @@ type DataSource struct { Columns []*model.ColumnInfo DBName ast.CIStr + // TableAsName points to the AST alias and is only used as read-only metadata after plan build. TableAsName *ast.CIStr `hash64-equals:"true"` // IndexMergeHints are the hint for indexmerge. IndexMergeHints []h.HintedIndex @@ -84,7 +87,8 @@ type DataSource struct { // The data source may be a partition, rather than a real table. PartitionDefIdx *int PhysicalTableID int64 `hash64-equals:"true"` - PartitionNames []ast.CIStr + // PartitionNames records the explicit partition list from AST and is treated as read-only after plan build. + PartitionNames []ast.CIStr // handleCol represents the handle column for the datasource, either the // int primary key column or extra handle column. diff --git a/pkg/planner/core/operator/logicalop/logical_lock.go b/pkg/planner/core/operator/logicalop/logical_lock.go index f6b0dda0c93cc..2a3221e4f7d2c 100644 --- a/pkg/planner/core/operator/logicalop/logical_lock.go +++ b/pkg/planner/core/operator/logicalop/logical_lock.go @@ -26,6 +26,8 @@ import ( type LogicalLock struct { BaseLogicalPlan `hash64-equals:"true"` + // Lock points to AST lock metadata. Preprocess may normalize the AST before planning, + // but after LogicalLock is built this field is treated as read-only. Lock *ast.SelectLockInfo `hash64-equals:"true"` TblID2Handle map[int64][]util.HandleCols diff --git a/pkg/planner/core/operator/logicalop/logical_show.go b/pkg/planner/core/operator/logicalop/logical_show.go index c59f57b26043a..61322616b77b4 100644 --- a/pkg/planner/core/operator/logicalop/logical_show.go +++ b/pkg/planner/core/operator/logicalop/logical_show.go @@ -37,11 +37,12 @@ type LogicalShow struct { // ShowContents stores the contents for the `SHOW` statement. type ShowContents struct { - Tp ast.ShowStmtType // Databases/Tables/Columns/.... - DBName string - Table *resolve.TableNameW // Used for showing columns. - Partition ast.CIStr // Use for showing partition - Column *ast.ColumnName // Used for `desc table column`. + Tp ast.ShowStmtType // Databases/Tables/Columns/.... + DBName string + Table *resolve.TableNameW // Used for showing columns. + Partition ast.CIStr // Use for showing partition + // Column points to the AST selector for `desc table column` and is treated as read-only after plan build. + Column *ast.ColumnName IndexName ast.CIStr ResourceGroupName string // Used for showing resource group Flag int // Some flag parsed from sql, such as FULL. @@ -51,10 +52,11 @@ type ShowContents struct { CountWarningsOrErrors bool // Used for showing count(*) warnings | errors Full bool - IfNotExists bool // Used for `show create database if not exists`. - GlobalScope bool // Used by show variables. - Extended bool // Used for `show extended columns from ...` - Limit *ast.Limit // Used for limit Result Set row number. + IfNotExists bool // Used for `show create database if not exists`. + GlobalScope bool // Used by show variables. + Extended bool // Used for `show extended columns from ...` + // Limit points to the AST SHOW limit clause. It is only read during planner build and should stay immutable. + Limit *ast.Limit ImportJobID *int64 // Used for SHOW LOAD DATA JOB ImportGroupKey string // Used for SHOW IMPORT GROUP diff --git a/pkg/planner/core/operator/physicalop/physical_lock.go b/pkg/planner/core/operator/physicalop/physical_lock.go index bd2d37bcffad1..8558d00105163 100644 --- a/pkg/planner/core/operator/physicalop/physical_lock.go +++ b/pkg/planner/core/operator/physicalop/physical_lock.go @@ -33,6 +33,7 @@ import ( type PhysicalLock struct { BasePhysicalPlan + // Lock shares the read-only AST lock metadata forwarded from LogicalLock. Lock *ast.SelectLockInfo `plan-cache-clone:"shallow"` TblID2Handle map[int64][]util.HandleCols From d7b27b5e0cd7e6fa9b606238d4455c23183f414e Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Thu, 5 Mar 2026 16:01:06 +0800 Subject: [PATCH 03/11] planner: isolate per-build state for shared AST rebuild --- pkg/planner/core/logical_plan_builder.go | 13 ++-- pkg/planner/core/planbuilder.go | 19 ++++++ pkg/sessionctx/stmtctx/BUILD.bazel | 4 +- pkg/sessionctx/stmtctx/stmtctx.go | 48 +++++++++++++ pkg/sessionctx/stmtctx/stmtctx_test.go | 86 ++++++++++++++++++++++++ pkg/util/context/plancache.go | 35 ++++++++++ pkg/util/hint/hint_query_block.go | 68 +++++++++++++++---- 7 files changed, 254 insertions(+), 19 deletions(-) diff --git a/pkg/planner/core/logical_plan_builder.go b/pkg/planner/core/logical_plan_builder.go index f1221e42f12e7..586442c891043 100644 --- a/pkg/planner/core/logical_plan_builder.go +++ b/pkg/planner/core/logical_plan_builder.go @@ -3685,7 +3685,7 @@ func (b *PlanBuilder) addAliasName(ctx context.Context, selectStmt *ast.SelectSt } func (b *PlanBuilder) pushTableHints(hints []*ast.TableOptimizerHint, currentLevel int) { - hints = b.hintProcessor.GetCurrentStmtHints(hints, currentLevel) + hints = b.hintProcessor.GetCurrentStmtHints(hints, currentLevel, b.hintState) sessionVars := b.ctx.GetSessionVars() currentDB := sessionVars.CurrentDB warnHandler := sessionVars.StmtCtx @@ -4495,7 +4495,7 @@ func (b *PlanBuilder) buildDataSource(ctx context.Context, tn *ast.TableName, as // Because of the nested views, so we should check the left table list in hint when build the data source from the view inside the current view. currentQBNameMap4View[qbName] = viewQBNameHintTable[1:] currentViewHints[qbName] = b.hintProcessor.ViewQBNameToHints[qbName] - b.hintProcessor.ViewQBNameUsed[qbName] = struct{}{} + b.hintProcessor.MarkViewQBNameUsed(qbName, b.hintState) } } return b.BuildDataSourceFromView(ctx, dbName, tableInfo, currentQBNameMap4View, currentViewHints) @@ -4995,18 +4995,21 @@ func (b *PlanBuilder) BuildDataSourceFromView(ctx context.Context, dbName ast.CI hintProcessor.ViewQBNameToTable = qbNameMap4View hintProcessor.ViewQBNameToHints = viewHints - hintProcessor.ViewQBNameUsed = make(map[string]struct{}) - hintProcessor.QBOffsetToHints = currentQbHints hintProcessor.QBNameToSelOffset = currentQbNameMap + hintState := hintProcessor.NewBuildState() + hintState.QBOffsetToHints = currentQbHints originHintProcessor := b.hintProcessor + originHintState := b.hintState originPlannerSelectBlockAsName := b.ctx.GetSessionVars().PlannerSelectBlockAsName.Load() b.hintProcessor = hintProcessor + b.hintState = hintState newPlannerSelectBlockAsName := make([]ast.HintTable, hintProcessor.MaxSelectStmtOffset()+1) b.ctx.GetSessionVars().PlannerSelectBlockAsName.Store(&newPlannerSelectBlockAsName) defer func() { - b.hintProcessor.HandleUnusedViewHints() + b.hintProcessor.SetWarns(b.hintProcessor.HandleUnusedViewHints(b.hintState, nil)) b.hintProcessor = originHintProcessor + b.hintState = originHintState b.ctx.GetSessionVars().PlannerSelectBlockAsName.Store(originPlannerSelectBlockAsName) }() nodeW := resolve.NewNodeWWithCtx(selectNode, b.resolveCtx) diff --git a/pkg/planner/core/planbuilder.go b/pkg/planner/core/planbuilder.go index e3ee2bfa57672..15d01fbbb1289 100644 --- a/pkg/planner/core/planbuilder.go +++ b/pkg/planner/core/planbuilder.go @@ -250,7 +250,10 @@ type PlanBuilder struct { // finish building the subquery or CTE. handleHelper *handleColHelper + // read-only meta derived from ast node. hintProcessor *hint.QBHintHandler + // mutable state of QBHint when building. + hintState *hint.QBHintBuildState // qbOffset is the offsets of current processing select stmts. qbOffset []int @@ -389,6 +392,11 @@ func GetDBTableInfo(visitInfo []visitInfo) []stmtctx.TableEntry { return tables } +// GetHintState gets the HintState from the PlanBuilder. +func (b *PlanBuilder) GetHintState() *hint.QBHintBuildState { + return b.hintState +} + // GetOptFlag gets the OptFlag of the PlanBuilder. func (b *PlanBuilder) GetOptFlag() uint64 { if b.isSampling { @@ -466,6 +474,9 @@ func (b *PlanBuilder) Init(sctx base.PlanContext, is infoschema.InfoSchema, proc b.ctx = sctx b.is = is b.hintProcessor = processor + if processor != nil { + b.hintState = processor.NewBuildState() + } b.isForUpdateRead = sctx.GetSessionVars().IsPessimisticReadConsistency() b.noDecorrelate = sctx.GetSessionVars().EnableNoDecorrelateInSelect if savedBlockNames == nil { @@ -507,6 +518,14 @@ func (b *PlanBuilder) ResetForReuse() *PlanBuilder { return b } +// HandleUnusedViewHints appends warnings for unused view hints in the current build. +func (b *PlanBuilder) HandleUnusedViewHints() { + if b.hintProcessor == nil { + return + } + b.hintProcessor.SetWarns(b.hintProcessor.HandleUnusedViewHints(b.hintState, nil)) +} + // Build builds the ast node to a Plan. func (b *PlanBuilder) Build(ctx context.Context, node *resolve.NodeW) (base.Plan, error) { err := b.checkSEMStmt(node.Node) diff --git a/pkg/sessionctx/stmtctx/BUILD.bazel b/pkg/sessionctx/stmtctx/BUILD.bazel index f9513e8187b6d..58c2bbd7a4110 100644 --- a/pkg/sessionctx/stmtctx/BUILD.bazel +++ b/pkg/sessionctx/stmtctx/BUILD.bazel @@ -44,10 +44,12 @@ go_test( ], embed = [":stmtctx"], flaky = True, - shard_count = 15, + shard_count = 17, deps = [ "//pkg/errctx", "//pkg/kv", + "//pkg/meta/model", + "//pkg/parser/ast", "//pkg/sessionctx/variable", "//pkg/testkit", "//pkg/testkit/testfailpoint", diff --git a/pkg/sessionctx/stmtctx/stmtctx.go b/pkg/sessionctx/stmtctx/stmtctx.go index a8ef4e288c58d..e3afa56f0bc1c 100644 --- a/pkg/sessionctx/stmtctx/stmtctx.go +++ b/pkg/sessionctx/stmtctx/stmtctx.go @@ -69,6 +69,21 @@ func AllocateTaskID() uint64 { // SQLWarn relates a sql warning and it's level. type SQLWarn = contextutil.SQLWarn +// LogicalPlanBuildState stores the statement-scoped planner state that is mutated while +// building a logical plan from AST. +type LogicalPlanBuildState struct { + warnings []SQLWarn + extraWarnings []SQLWarn + tables []TableEntry + tableStats map[int64]any + lockTableIDs map[int64]struct{} + tblInfo2UnionScan map[*model.TableInfo]bool + useDynamicPruneMode bool + viewDepth int32 + colRefFromUpdatePlan intset.FastIntSet + planCacheTracker contextutil.PlanCacheTrackerState +} + // ReferenceCount indicates the reference count of StmtCtx. type ReferenceCount int32 @@ -583,6 +598,39 @@ func (sc *StatementContext) Reset() bool { return true } +// SaveLogicalPlanBuildState captures the statement-scoped planner state before building +// another logical plan candidate from the same AST. +func (sc *StatementContext) SaveLogicalPlanBuildState() LogicalPlanBuildState { + return LogicalPlanBuildState{ + warnings: slices.Clone(sc.GetWarnings()), + extraWarnings: slices.Clone(sc.GetExtraWarnings()), + tables: slices.Clone(sc.Tables), + tableStats: maps.Clone(sc.TableStats), + lockTableIDs: maps.Clone(sc.LockTableIDs), + tblInfo2UnionScan: maps.Clone(sc.TblInfo2UnionScan), + useDynamicPruneMode: sc.UseDynamicPruneMode, + viewDepth: sc.ViewDepth, + colRefFromUpdatePlan: sc.ColRefFromUpdatePlan.Copy(), + planCacheTracker: sc.PlanCacheTracker.Save(), + } +} + +// RestoreLogicalPlanBuildState restores the statement-scoped planner state after a +// discarded logical plan build attempt. +func (sc *StatementContext) RestoreLogicalPlanBuildState(state LogicalPlanBuildState) { + sc.SetWarnings(slices.Clone(state.warnings)) + sc.SetExtraWarnings(slices.Clone(state.extraWarnings)) + sc.Tables = slices.Clone(state.tables) + sc.TableStats = maps.Clone(state.tableStats) + sc.LockTableIDs = maps.Clone(state.lockTableIDs) + sc.TblInfo2UnionScan = maps.Clone(state.tblInfo2UnionScan) + sc.UseDynamicPruneMode = state.useDynamicPruneMode + sc.ViewDepth = state.viewDepth + sc.ColRefFromUpdatePlan.CopyFrom(state.colRefFromUpdatePlan) + sc.PlanCacheTracker.Restore(state.planCacheTracker) + sc.RangeFallbackHandler = contextutil.NewRangeFallbackHandler(&sc.PlanCacheTracker, sc) +} + // CtxID returns the context id of the statement func (sc *StatementContext) CtxID() uint64 { return sc.ctxID diff --git a/pkg/sessionctx/stmtctx/stmtctx_test.go b/pkg/sessionctx/stmtctx/stmtctx_test.go index b7494004a4ae7..e6115eaa339c5 100644 --- a/pkg/sessionctx/stmtctx/stmtctx_test.go +++ b/pkg/sessionctx/stmtctx/stmtctx_test.go @@ -28,6 +28,8 @@ import ( "github.com/pingcap/errors" "github.com/pingcap/tidb/pkg/errctx" "github.com/pingcap/tidb/pkg/kv" + "github.com/pingcap/tidb/pkg/meta/model" + "github.com/pingcap/tidb/pkg/parser/ast" "github.com/pingcap/tidb/pkg/sessionctx/stmtctx" "github.com/pingcap/tidb/pkg/sessionctx/variable" "github.com/pingcap/tidb/pkg/testkit" @@ -219,6 +221,90 @@ func TestMarshalSQLWarn(t *testing.T) { tk.MustQuery("show warnings").Check(rows) } +func TestLogicalPlanBuildStateRestore(t *testing.T) { + sc := stmtctx.NewStmtCtx() + sc.AppendWarning(errors.New("baseline warning")) + sc.AppendExtraWarning(errors.New("baseline extra warning")) + sc.Tables = []stmtctx.TableEntry{{DB: "test", Table: "t"}} + sc.TableStats = map[int64]any{42: "baseline stats"} + sc.LockTableIDs = map[int64]struct{}{1: {}} + tblInfo := &model.TableInfo{ID: 42} + sc.TblInfo2UnionScan = map[*model.TableInfo]bool{tblInfo: true} + sc.UseDynamicPruneMode = true + sc.ViewDepth = 2 + sc.ColRefFromUpdatePlan.Insert(7) + sc.SetCacheType(contextutil.SessionNonPrepared) + sc.EnablePlanCache() + + state := sc.SaveLogicalPlanBuildState() + + sc.AppendWarning(errors.New("candidate warning")) + sc.AppendExtraWarning(errors.New("candidate extra warning")) + sc.Tables = []stmtctx.TableEntry{{DB: "candidate", Table: "t2"}} + sc.TableStats = map[int64]any{99: "candidate stats"} + sc.LockTableIDs[2] = struct{}{} + sc.TblInfo2UnionScan = map[*model.TableInfo]bool{{ID: 99}: false} + sc.UseDynamicPruneMode = false + sc.ViewDepth = 9 + sc.ColRefFromUpdatePlan.Insert(9) + sc.SetSkipPlanCache("candidate reason") + + sc.RestoreLogicalPlanBuildState(state) + + warnings := sc.GetWarnings() + require.Len(t, warnings, 1) + require.Equal(t, "baseline warning", warnings[0].Err.Error()) + + extraWarnings := sc.GetExtraWarnings() + require.Len(t, extraWarnings, 1) + require.Equal(t, "baseline extra warning", extraWarnings[0].Err.Error()) + + require.Equal(t, []stmtctx.TableEntry{{DB: "test", Table: "t"}}, sc.Tables) + require.Equal(t, map[int64]any{42: "baseline stats"}, sc.TableStats) + require.Equal(t, map[int64]struct{}{1: {}}, sc.LockTableIDs) + require.Equal(t, map[*model.TableInfo]bool{tblInfo: true}, sc.TblInfo2UnionScan) + require.True(t, sc.UseDynamicPartitionPrune()) + require.Equal(t, int32(2), sc.ViewDepth) + require.True(t, sc.ColRefFromUpdatePlan.Has(7)) + require.False(t, sc.ColRefFromUpdatePlan.Has(9)) + require.True(t, sc.UseCache()) + require.Empty(t, sc.PlanCacheUnqualified()) +} + +func TestQBHintHandlerBuildState(t *testing.T) { + handler := hint.NewQBHintHandler(nil) + handler.QBNameToSelOffset = map[string]int{"qb_1": 1} + handler.ViewQBNameToTable = map[string][]ast.HintTable{ + "view_qb": {{TableName: ast.NewCIStr("t")}}, + } + handler.ViewQBNameToHints = map[string][]*ast.TableOptimizerHint{ + "view_qb": {{HintName: ast.NewCIStr("merge_join")}}, + } + handler.Enter(&ast.SelectStmt{}) + handler.Enter(&ast.SelectStmt{}) + state := handler.NewBuildState() + hints := handler.GetCurrentStmtHints([]*ast.TableOptimizerHint{ + {HintName: ast.NewCIStr("use_index"), QBName: ast.NewCIStr("qb_1")}, + }, 1, state) + handler.MarkViewQBNameUsed("view_qb", state) + + require.Len(t, hints, 1) + require.Equal(t, "use_index", hints[0].HintName.L) + + require.Equal(t, 2, handler.MaxSelectStmtOffset()) + require.Equal(t, map[string]int{"qb_1": 1}, handler.QBNameToSelOffset) + require.Equal(t, map[string][]*ast.TableOptimizerHint{ + "view_qb": {{HintName: ast.NewCIStr("merge_join")}}, + }, handler.ViewQBNameToHints) + require.Equal(t, map[string][]ast.HintTable{ + "view_qb": {{TableName: ast.NewCIStr("t")}}, + }, handler.ViewQBNameToTable) + require.Equal(t, map[int][]*ast.TableOptimizerHint{ + 1: {{HintName: ast.NewCIStr("use_index"), QBName: ast.NewCIStr("qb_1")}}, + }, state.QBOffsetToHints) + require.Equal(t, map[string]struct{}{"view_qb": {}}, state.ViewQBNameUsed) +} + func TestApproxRuntimeInfo(t *testing.T) { var n = rand.Intn(19000) + 1000 var valRange = rand.Int31n(10000) + 1000 diff --git a/pkg/util/context/plancache.go b/pkg/util/context/plancache.go index 9cac5ec81012e..7117ee572b0d2 100644 --- a/pkg/util/context/plancache.go +++ b/pkg/util/context/plancache.go @@ -45,6 +45,15 @@ type PlanCacheTracker struct { warnHandler WarnAppender } +// PlanCacheTrackerState stores the mutable planning-time state of a PlanCacheTracker. +type PlanCacheTrackerState struct { + UseCache bool + CacheType PlanCacheType + PlanCacheUnqualified string + ForcePlanCache bool + AlwaysWarnSkipCache bool +} + // WarnSkipPlanCache output the reason why this query can't hit the plan cache. func (h *PlanCacheTracker) WarnSkipPlanCache(reason string) { h.mu.Lock() @@ -122,6 +131,32 @@ func (h *PlanCacheTracker) EnablePlanCache() { h.useCache = true } +// Save captures the mutable planning-time state of the tracker. +func (h *PlanCacheTracker) Save() PlanCacheTrackerState { + h.mu.Lock() + defer h.mu.Unlock() + + return PlanCacheTrackerState{ + UseCache: h.useCache, + CacheType: h.cacheType, + PlanCacheUnqualified: h.planCacheUnqualified, + ForcePlanCache: h.forcePlanCache, + AlwaysWarnSkipCache: h.alwaysWarnSkipCache, + } +} + +// Restore restores the mutable planning-time state of the tracker. +func (h *PlanCacheTracker) Restore(state PlanCacheTrackerState) { + h.mu.Lock() + defer h.mu.Unlock() + + h.useCache = state.UseCache + h.cacheType = state.CacheType + h.planCacheUnqualified = state.PlanCacheUnqualified + h.forcePlanCache = state.ForcePlanCache + h.alwaysWarnSkipCache = state.AlwaysWarnSkipCache +} + // UseCache returns whether to use plan cache. func (h *PlanCacheTracker) UseCache() bool { h.mu.Lock() diff --git a/pkg/util/hint/hint_query_block.go b/pkg/util/hint/hint_query_block.go index eb85910b59827..44aa156320f2b 100644 --- a/pkg/util/hint/hint_query_block.go +++ b/pkg/util/hint/hint_query_block.go @@ -30,18 +30,23 @@ import ( // In both cases, the `use_index` hint doesn't take effect directly, since a specific qb_name is specified, and this // QBHintHandler is used to handle this cases. type QBHintHandler struct { - QBNameToSelOffset map[string]int // map[QBName]SelectOffset - QBOffsetToHints map[int][]*ast.TableOptimizerHint // map[QueryBlockOffset]Hints + QBNameToSelOffset map[string]int // map[QBName]SelectOffset // Used for the view's hint ViewQBNameToTable map[string][]ast.HintTable // map[QBName]HintedTable ViewQBNameToHints map[string][]*ast.TableOptimizerHint // map[QBName]Hints - ViewQBNameUsed map[string]struct{} // map[QBName]Used warnHandler hintWarnHandler selectStmtOffset int } +// QBHintBuildState stores the per-build runtime state for a QBHintHandler. +// The handler itself only keeps AST-derived metadata and can be shared safely. +type QBHintBuildState struct { + QBOffsetToHints map[int][]*ast.TableOptimizerHint // map[QueryBlockOffset]Hints + ViewQBNameUsed map[string]struct{} // map[QBName]Used +} + // hintWarnHandler is used to handle the warning when parsing hints. type hintWarnHandler interface { SetHintWarning(warn string) @@ -55,6 +60,20 @@ func NewQBHintHandler(warnHandler hintWarnHandler) *QBHintHandler { } } +// NewBuildState creates the per-build runtime state for the handler. +func (p *QBHintHandler) NewBuildState() *QBHintBuildState { + if p == nil { + return nil + } + + state := &QBHintBuildState{} + if len(p.ViewQBNameToTable) == 0 { + return state + } + state.ViewQBNameUsed = make(map[string]struct{}, len(p.ViewQBNameToTable)) + return state +} + // MaxSelectStmtOffset returns the current stmt offset. func (p *QBHintHandler) MaxSelectStmtOffset() int { return p.selectStmtOffset @@ -132,7 +151,6 @@ func (p *QBHintHandler) handleViewHints(hints []*ast.TableOptimizerHint, offset usedHints[i] = true if p.ViewQBNameToTable == nil { p.ViewQBNameToTable = make(map[string][]ast.HintTable) - p.ViewQBNameUsed = make(map[string]struct{}) } qbName := hint.QBName.L if qbName == "" { @@ -201,15 +219,21 @@ func (p *QBHintHandler) handleViewHints(hints []*ast.TableOptimizerHint, offset } // HandleUnusedViewHints handle the unused view hints. -func (p *QBHintHandler) HandleUnusedViewHints() { +func (p *QBHintHandler) HandleUnusedViewHints(state *QBHintBuildState, warn []string) []string { + if state == nil { + return warn + } + + warn = warn[:0] if p.ViewQBNameToTable != nil { for qbName := range p.ViewQBNameToTable { - _, ok := p.ViewQBNameUsed[qbName] + _, ok := state.ViewQBNameUsed[qbName] if !ok && p.warnHandler != nil { - p.warnHandler.SetHintWarning(fmt.Sprintf("The qb_name hint %s is unused, please check whether the table list in the qb_name hint %s is correct", qbName, qbName)) + warn = append(warn, fmt.Sprintf("The qb_name hint %s is unused, please check whether the table list in the qb_name hint %s is correct", qbName, qbName)) } } } + return warn } const ( @@ -242,6 +266,13 @@ func (p *QBHintHandler) getBlockOffset(blockName ast.CIStr) int { return -1 } +// SetWarns set the warning from a list of strings. +func (p *QBHintHandler) SetWarns(warns []string) { + for _, one := range warns { + p.warnHandler.SetHintWarning(one) + } +} + // GetHintOffset gets the offset of stmt that the hints take effects. func (p *QBHintHandler) GetHintOffset(qbName ast.CIStr, currentOffset int) int { if qbName.L != "" { @@ -279,9 +310,12 @@ func (p *QBHintHandler) isHint4View(hint *ast.TableOptimizerHint) bool { } // GetCurrentStmtHints extracts all hints that take effects at current stmt. -func (p *QBHintHandler) GetCurrentStmtHints(hints []*ast.TableOptimizerHint, currentOffset int) []*ast.TableOptimizerHint { - if p.QBOffsetToHints == nil { - p.QBOffsetToHints = make(map[int][]*ast.TableOptimizerHint) +func (p *QBHintHandler) GetCurrentStmtHints(hints []*ast.TableOptimizerHint, currentOffset int, state *QBHintBuildState) []*ast.TableOptimizerHint { + if state == nil { + state = &QBHintBuildState{} + } + if state.QBOffsetToHints == nil { + state.QBOffsetToHints = make(map[int][]*ast.TableOptimizerHint) } for _, hint := range hints { if hint.HintName.L == hintQBName { @@ -295,11 +329,19 @@ func (p *QBHintHandler) GetCurrentStmtHints(hints []*ast.TableOptimizerHint, cur } continue } - if !slices.Contains(p.QBOffsetToHints[offset], hint) { - p.QBOffsetToHints[offset] = append(p.QBOffsetToHints[offset], hint) + if !slices.Contains(state.QBOffsetToHints[offset], hint) { + state.QBOffsetToHints[offset] = append(state.QBOffsetToHints[offset], hint) } } - return p.QBOffsetToHints[currentOffset] + return state.QBOffsetToHints[currentOffset] +} + +// MarkViewQBNameUsed records that the named view qb hint was used in the current build. +func (*QBHintHandler) MarkViewQBNameUsed(qbName string, state *QBHintBuildState) { + if state == nil || state.ViewQBNameUsed == nil { + return + } + state.ViewQBNameUsed[qbName] = struct{}{} } // GenerateQBName builds QBName from offset. From 25da30959e31df258823cd98654927886e040932 Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Thu, 5 Mar 2026 16:23:51 +0800 Subject: [PATCH 04/11] planner: fix optimize unused view hint handling call --- pkg/planner/optimize.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/planner/optimize.go b/pkg/planner/optimize.go index 405efe1b525e2..4200aa0698e9c 100644 --- a/pkg/planner/optimize.go +++ b/pkg/planner/optimize.go @@ -460,10 +460,10 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW // build logical plan hintProcessor := hint.NewQBHintHandler(sctx.GetSessionVars().StmtCtx) node.Node.Accept(hintProcessor) - defer hintProcessor.HandleUnusedViewHints() builder := planBuilderPool.Get().(*core.PlanBuilder) defer planBuilderPool.Put(builder.ResetForReuse()) builder.Init(sctx, is, hintProcessor) + defer builder.HandleUnusedViewHints() p, err := buildLogicalPlan(ctx, sctx, node, builder) if err != nil { return nil, nil, 0, err From ef4207aba7907a10891b51bde5661b9dccf3a9cf Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Thu, 5 Mar 2026 16:42:43 +0800 Subject: [PATCH 05/11] sessionctx: keep plan cache build state in stmtctx --- pkg/sessionctx/stmtctx/stmtctx.go | 16 +++++++++++++--- pkg/util/context/plancache.go | 31 ++++++++----------------------- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/pkg/sessionctx/stmtctx/stmtctx.go b/pkg/sessionctx/stmtctx/stmtctx.go index e3afa56f0bc1c..80c0600b786bb 100644 --- a/pkg/sessionctx/stmtctx/stmtctx.go +++ b/pkg/sessionctx/stmtctx/stmtctx.go @@ -81,7 +81,12 @@ type LogicalPlanBuildState struct { useDynamicPruneMode bool viewDepth int32 colRefFromUpdatePlan intset.FastIntSet - planCacheTracker contextutil.PlanCacheTrackerState + // plan cache related stuff + planCacheUseCache bool + planCacheType contextutil.PlanCacheType + planCacheUnqualified string + planCacheForce bool + planCacheAlwaysWarn bool } // ReferenceCount indicates the reference count of StmtCtx. @@ -601,6 +606,7 @@ func (sc *StatementContext) Reset() bool { // SaveLogicalPlanBuildState captures the statement-scoped planner state before building // another logical plan candidate from the same AST. func (sc *StatementContext) SaveLogicalPlanBuildState() LogicalPlanBuildState { + planCacheUseCache, planCacheType, planCacheUnqualified, planCacheForce, planCacheAlwaysWarn := sc.PlanCacheTracker.Save() return LogicalPlanBuildState{ warnings: slices.Clone(sc.GetWarnings()), extraWarnings: slices.Clone(sc.GetExtraWarnings()), @@ -611,7 +617,11 @@ func (sc *StatementContext) SaveLogicalPlanBuildState() LogicalPlanBuildState { useDynamicPruneMode: sc.UseDynamicPruneMode, viewDepth: sc.ViewDepth, colRefFromUpdatePlan: sc.ColRefFromUpdatePlan.Copy(), - planCacheTracker: sc.PlanCacheTracker.Save(), + planCacheUseCache: planCacheUseCache, + planCacheType: planCacheType, + planCacheUnqualified: planCacheUnqualified, + planCacheForce: planCacheForce, + planCacheAlwaysWarn: planCacheAlwaysWarn, } } @@ -627,7 +637,7 @@ func (sc *StatementContext) RestoreLogicalPlanBuildState(state LogicalPlanBuildS sc.UseDynamicPruneMode = state.useDynamicPruneMode sc.ViewDepth = state.viewDepth sc.ColRefFromUpdatePlan.CopyFrom(state.colRefFromUpdatePlan) - sc.PlanCacheTracker.Restore(state.planCacheTracker) + sc.PlanCacheTracker.Restore(state.planCacheUseCache, state.planCacheType, state.planCacheUnqualified, state.planCacheForce, state.planCacheAlwaysWarn) sc.RangeFallbackHandler = contextutil.NewRangeFallbackHandler(&sc.PlanCacheTracker, sc) } diff --git a/pkg/util/context/plancache.go b/pkg/util/context/plancache.go index 7117ee572b0d2..d6c46cd4db81e 100644 --- a/pkg/util/context/plancache.go +++ b/pkg/util/context/plancache.go @@ -45,15 +45,6 @@ type PlanCacheTracker struct { warnHandler WarnAppender } -// PlanCacheTrackerState stores the mutable planning-time state of a PlanCacheTracker. -type PlanCacheTrackerState struct { - UseCache bool - CacheType PlanCacheType - PlanCacheUnqualified string - ForcePlanCache bool - AlwaysWarnSkipCache bool -} - // WarnSkipPlanCache output the reason why this query can't hit the plan cache. func (h *PlanCacheTracker) WarnSkipPlanCache(reason string) { h.mu.Lock() @@ -132,29 +123,23 @@ func (h *PlanCacheTracker) EnablePlanCache() { } // Save captures the mutable planning-time state of the tracker. -func (h *PlanCacheTracker) Save() PlanCacheTrackerState { +func (h *PlanCacheTracker) Save() (useCache bool, cacheType PlanCacheType, planCacheUnqualified string, forcePlanCache bool, alwaysWarnSkipCache bool) { h.mu.Lock() defer h.mu.Unlock() - return PlanCacheTrackerState{ - UseCache: h.useCache, - CacheType: h.cacheType, - PlanCacheUnqualified: h.planCacheUnqualified, - ForcePlanCache: h.forcePlanCache, - AlwaysWarnSkipCache: h.alwaysWarnSkipCache, - } + return h.useCache, h.cacheType, h.planCacheUnqualified, h.forcePlanCache, h.alwaysWarnSkipCache } // Restore restores the mutable planning-time state of the tracker. -func (h *PlanCacheTracker) Restore(state PlanCacheTrackerState) { +func (h *PlanCacheTracker) Restore(useCache bool, cacheType PlanCacheType, planCacheUnqualified string, forcePlanCache bool, alwaysWarnSkipCache bool) { h.mu.Lock() defer h.mu.Unlock() - h.useCache = state.UseCache - h.cacheType = state.CacheType - h.planCacheUnqualified = state.PlanCacheUnqualified - h.forcePlanCache = state.ForcePlanCache - h.alwaysWarnSkipCache = state.AlwaysWarnSkipCache + h.useCache = useCache + h.cacheType = cacheType + h.planCacheUnqualified = planCacheUnqualified + h.forcePlanCache = forcePlanCache + h.alwaysWarnSkipCache = alwaysWarnSkipCache } // UseCache returns whether to use plan cache. From 82d97da18e29eb04fa685ae0760943c0e3c5650e Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Thu, 5 Mar 2026 16:46:51 +0800 Subject: [PATCH 06/11] . Signed-off-by: AilinKid <314806019@qq.com> --- pkg/util/hint/hint_query_block.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/util/hint/hint_query_block.go b/pkg/util/hint/hint_query_block.go index 44aa156320f2b..0262e1b53e6a3 100644 --- a/pkg/util/hint/hint_query_block.go +++ b/pkg/util/hint/hint_query_block.go @@ -268,6 +268,9 @@ func (p *QBHintHandler) getBlockOffset(blockName ast.CIStr) int { // SetWarns set the warning from a list of strings. func (p *QBHintHandler) SetWarns(warns []string) { + if p == nil || p.warnHandler == nil || len(warns) == 0 { + return + } for _, one := range warns { p.warnHandler.SetHintWarning(one) } From 36d9021fe67c0c2300f7884bea06bbe6a781e9b4 Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Fri, 6 Mar 2026 15:51:31 +0800 Subject: [PATCH 07/11] planner: add step2 multi-build path for shared AST --- pkg/planner/optimize.go | 146 ++++++++++++++++++++++++++++++---------- 1 file changed, 112 insertions(+), 34 deletions(-) diff --git a/pkg/planner/optimize.go b/pkg/planner/optimize.go index 4200aa0698e9c..335aad5a4b8ce 100644 --- a/pkg/planner/optimize.go +++ b/pkg/planner/optimize.go @@ -438,6 +438,23 @@ var planBuilderPool = sync.Pool{ }, } +type logicalPlanBuildBaseline struct { + stmtCtxState stmtctx.LogicalPlanBuildState + plannerSelectBlockAsName *[]ast.HintTable +} + +func captureLogicalPlanBuildBaseline(sessVars *variable.SessionVars) logicalPlanBuildBaseline { + return logicalPlanBuildBaseline{ + stmtCtxState: sessVars.StmtCtx.SaveLogicalPlanBuildState(), + plannerSelectBlockAsName: sessVars.PlannerSelectBlockAsName.Load(), + } +} + +func restoreLogicalPlanBuildBaseline(sessVars *variable.SessionVars, baseline logicalPlanBuildBaseline) { + sessVars.StmtCtx.RestoreLogicalPlanBuildState(baseline.stmtCtxState) + sessVars.PlannerSelectBlockAsName.Store(baseline.plannerSelectBlockAsName) +} + // optimizeCnt is a global variable only used for test. var optimizeCnt int @@ -457,53 +474,114 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW }) sessVars := sctx.GetSessionVars() - // build logical plan + // Build the logical plan from the raw AST. The hint processor only keeps + // AST-derived metadata; per-build state is allocated inside PlanBuilder. hintProcessor := hint.NewQBHintHandler(sctx.GetSessionVars().StmtCtx) node.Node.Accept(hintProcessor) - builder := planBuilderPool.Get().(*core.PlanBuilder) - defer planBuilderPool.Put(builder.ResetForReuse()) - builder.Init(sctx, is, hintProcessor) - defer builder.HandleUnusedViewHints() - p, err := buildLogicalPlan(ctx, sctx, node, builder) - if err != nil { - return nil, nil, 0, err - } + // build multi logical plan from raw AST. + var ( + buildRound = 1 + needBaseline = buildRound > 1 + bestCost = math.MaxFloat64 + bestPlan base.PhysicalPlan + bestNames types.NameSlice + bestState logicalPlanBuildBaseline + checked bool + ) + var baseline logicalPlanBuildBaseline + if needBaseline { + baseline = captureLogicalPlanBuildBaseline(sessVars) + } activeRoles := sessVars.ActiveRoles - // Check privilege. Maybe it's better to move this to the Preprocess, but - // we need the table information to check privilege, which is collected - // into the visitInfo in the logical plan builder. - if pm := privilege.GetPrivilegeManager(sctx); pm != nil { - visitInfo := core.VisitInfo4PrivCheck(ctx, is, node.Node, builder.GetVisitInfo()) - if err := core.CheckPrivilege(activeRoles, pm, visitInfo); err != nil { - return nil, nil, 0, err + beginOpt := time.Now() + for i := range buildRound { + if needBaseline && i > 0 { + restoreLogicalPlanBuildBaseline(sessVars, baseline) } - } - if err := core.CheckTableLock(sctx, is, builder.GetVisitInfo()); err != nil { - return nil, nil, 0, err - } + var ( + p base.Plan + names types.NameSlice + nonLogical bool + ) + err := func() error { + builder := planBuilderPool.Get().(*core.PlanBuilder) + defer planBuilderPool.Put(builder.ResetForReuse()) - if err := core.CheckTableMode(node); err != nil { - return nil, nil, 0, err - } + builder.Init(sctx, is, hintProcessor) - names := p.OutputNames() + var err error + // todo: you can customize each round's special builder (like semi join rewrite or not by signal) + p, err = buildLogicalPlan(ctx, sctx, node, builder) + if err != nil { + return err + } - // Handle the non-logical plan statement. - logic, isLogicalPlan := p.(base.LogicalPlan) - if !isLogicalPlan { - return p, names, 0, nil - } + names = p.OutputNames() + if !checked { + // Keep privilege and lock checks fail-fast. These depend on visitInfo + // produced by the logical build, but not on the later cost winner. + if pm := privilege.GetPrivilegeManager(sctx); pm != nil { + visitInfo := core.VisitInfo4PrivCheck(ctx, is, node.Node, builder.GetVisitInfo()) + if err := core.CheckPrivilege(activeRoles, pm, visitInfo); err != nil { + return err + } + } - core.RecheckCTE(logic) + if err := core.CheckTableLock(sctx, is, builder.GetVisitInfo()); err != nil { + return err + } - beginOpt := time.Now() - finalPlan, cost, err := core.DoOptimize(ctx, sctx, builder.GetOptFlag(), logic) - // TODO: capture plan replayer here if it matches sql and plan digest + if err := core.CheckTableMode(node); err != nil { + return err + } + checked = true + } + // Handle the non-logical plan statement. + logic, isLogicalPlan := p.(base.LogicalPlan) + if !isLogicalPlan { + builder.HandleUnusedViewHints() + nonLogical = true + return nil + } + + core.RecheckCTE(logic) + + // todo: also you can customize each round's special logical opt flag here (like decorrelate rule or not) + finalPlan, cost, err := core.DoOptimize(ctx, sctx, builder.GetOptFlag(), logic) + if err != nil { + return err + } + builder.HandleUnusedViewHints() + + if bestPlan == nil || cost < bestCost { + bestCost = cost + bestPlan = finalPlan + bestNames = names + if needBaseline { + bestState = captureLogicalPlanBuildBaseline(sessVars) + } + } + return nil + }() + if err != nil { + return nil, nil, 0, err + } + if nonLogical { + // keep compatible with the old. + return p, names, 0, nil + } + } + if bestPlan == nil { + return nil, nil, 0, errors.New("failed to build logical plan") + } + if needBaseline { + restoreLogicalPlanBuildBaseline(sessVars, bestState) + } sessVars.DurationOptimizer.Total = time.Since(beginOpt) - return finalPlan, names, cost, err + return bestPlan, bestNames, bestCost, nil } // OptimizeExecStmt to handle the "execute" statement From 2bd86780e59349b90da508e3856bc4c56a3f56b9 Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Tue, 10 Mar 2026 10:53:42 +0800 Subject: [PATCH 08/11] planner: address step2 review comments --- pkg/planner/optimize.go | 190 ++++++++++++++++++++++------------------ 1 file changed, 104 insertions(+), 86 deletions(-) diff --git a/pkg/planner/optimize.go b/pkg/planner/optimize.go index 335aad5a4b8ce..57392369a7d34 100644 --- a/pkg/planner/optimize.go +++ b/pkg/planner/optimize.go @@ -438,21 +438,93 @@ var planBuilderPool = sync.Pool{ }, } -type logicalPlanBuildBaseline struct { +type logicalPlanBuildCtx struct { stmtCtxState stmtctx.LogicalPlanBuildState plannerSelectBlockAsName *[]ast.HintTable } -func captureLogicalPlanBuildBaseline(sessVars *variable.SessionVars) logicalPlanBuildBaseline { - return logicalPlanBuildBaseline{ +func saveLogicalPlanBuildCtx(sessVars *variable.SessionVars) logicalPlanBuildCtx { + return logicalPlanBuildCtx{ stmtCtxState: sessVars.StmtCtx.SaveLogicalPlanBuildState(), plannerSelectBlockAsName: sessVars.PlannerSelectBlockAsName.Load(), } } -func restoreLogicalPlanBuildBaseline(sessVars *variable.SessionVars, baseline logicalPlanBuildBaseline) { - sessVars.StmtCtx.RestoreLogicalPlanBuildState(baseline.stmtCtxState) - sessVars.PlannerSelectBlockAsName.Store(baseline.plannerSelectBlockAsName) +func restoreLogicalPlanBuildCtx(sessVars *variable.SessionVars, logicalPlanCtx logicalPlanBuildCtx) { + sessVars.StmtCtx.RestoreLogicalPlanBuildState(logicalPlanCtx.stmtCtxState) + sessVars.PlannerSelectBlockAsName.Store(logicalPlanCtx.plannerSelectBlockAsName) +} + +func buildAndOptimizeLogicalPlanRound( + ctx context.Context, + sctx planctx.PlanContext, + node *resolve.NodeW, + is infoschema.InfoSchema, + hintProcessor *hint.QBHintHandler, + checked *bool, + needRestoreLogicalPlanCtx bool, + bestPlan *base.PhysicalPlan, + bestNames *types.NameSlice, + bestCost *float64, + bestLogicalPlanCtx *logicalPlanBuildCtx, +) (base.Plan, types.NameSlice, bool, error) { + builder := planBuilderPool.Get().(*core.PlanBuilder) + defer planBuilderPool.Put(builder.ResetForReuse()) + + builder.Init(sctx, is, hintProcessor) + + // todo: you can customize each round's special builder (like semi join rewrite or not by signal) + p, err := buildLogicalPlan(ctx, sctx, node, builder) + if err != nil { + return nil, nil, false, err + } + names := p.OutputNames() + + if !*checked { + // Keep privilege and lock checks fail-fast. These depend on visitInfo + // produced by the logical build, but not on the later cost winner. + if pm := privilege.GetPrivilegeManager(sctx); pm != nil { + visitInfo := core.VisitInfo4PrivCheck(ctx, is, node.Node, builder.GetVisitInfo()) + if err := core.CheckPrivilege(sctx.GetSessionVars().ActiveRoles, pm, visitInfo); err != nil { + return nil, nil, false, err + } + } + + if err := core.CheckTableLock(sctx, is, builder.GetVisitInfo()); err != nil { + return nil, nil, false, err + } + + if err := core.CheckTableMode(node); err != nil { + return nil, nil, false, err + } + *checked = true + } + + // Handle the non-logical plan statement. + logic, isLogicalPlan := p.(base.LogicalPlan) + if !isLogicalPlan { + builder.HandleUnusedViewHints() + return p, names, true, nil + } + + core.RecheckCTE(logic) + + // todo: also you can customize each round's special logical opt flag here (like decorrelate rule or not) + finalPlan, cost, err := core.DoOptimize(ctx, sctx, builder.GetOptFlag(), logic) + if err != nil { + return nil, nil, false, err + } + builder.HandleUnusedViewHints() + + if *bestPlan == nil || cost < *bestCost { + *bestCost = cost + *bestPlan = finalPlan + *bestNames = names + if needRestoreLogicalPlanCtx { + *bestLogicalPlanCtx = saveLogicalPlanBuildCtx(sctx.GetSessionVars()) + } + } + return p, names, false, nil } // optimizeCnt is a global variable only used for test. @@ -481,91 +553,37 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW // build multi logical plan from raw AST. var ( - buildRound = 1 - needBaseline = buildRound > 1 - bestCost = math.MaxFloat64 - bestPlan base.PhysicalPlan - bestNames types.NameSlice - bestState logicalPlanBuildBaseline - checked bool + buildRound = 1 + needRestoreLogicalPlanCtx = buildRound > 1 + bestCost = math.MaxFloat64 + bestPlan base.PhysicalPlan + bestNames types.NameSlice + bestLogicalPlanCtx logicalPlanBuildCtx + checked bool ) - var baseline logicalPlanBuildBaseline - if needBaseline { - baseline = captureLogicalPlanBuildBaseline(sessVars) + var initialLogicalPlanCtx logicalPlanBuildCtx + if needRestoreLogicalPlanCtx { + initialLogicalPlanCtx = saveLogicalPlanBuildCtx(sessVars) } - activeRoles := sessVars.ActiveRoles beginOpt := time.Now() for i := range buildRound { - if needBaseline && i > 0 { - restoreLogicalPlanBuildBaseline(sessVars, baseline) + if needRestoreLogicalPlanCtx && i > 0 { + restoreLogicalPlanBuildCtx(sessVars, initialLogicalPlanCtx) } - var ( - p base.Plan - names types.NameSlice - nonLogical bool + p, names, nonLogical, err := buildAndOptimizeLogicalPlanRound( + ctx, + sctx, + node, + is, + hintProcessor, + &checked, + needRestoreLogicalPlanCtx, + &bestPlan, + &bestNames, + &bestCost, + &bestLogicalPlanCtx, ) - err := func() error { - builder := planBuilderPool.Get().(*core.PlanBuilder) - defer planBuilderPool.Put(builder.ResetForReuse()) - - builder.Init(sctx, is, hintProcessor) - - var err error - // todo: you can customize each round's special builder (like semi join rewrite or not by signal) - p, err = buildLogicalPlan(ctx, sctx, node, builder) - if err != nil { - return err - } - - names = p.OutputNames() - if !checked { - // Keep privilege and lock checks fail-fast. These depend on visitInfo - // produced by the logical build, but not on the later cost winner. - if pm := privilege.GetPrivilegeManager(sctx); pm != nil { - visitInfo := core.VisitInfo4PrivCheck(ctx, is, node.Node, builder.GetVisitInfo()) - if err := core.CheckPrivilege(activeRoles, pm, visitInfo); err != nil { - return err - } - } - - if err := core.CheckTableLock(sctx, is, builder.GetVisitInfo()); err != nil { - return err - } - - if err := core.CheckTableMode(node); err != nil { - return err - } - checked = true - } - - // Handle the non-logical plan statement. - logic, isLogicalPlan := p.(base.LogicalPlan) - if !isLogicalPlan { - builder.HandleUnusedViewHints() - nonLogical = true - return nil - } - - core.RecheckCTE(logic) - - // todo: also you can customize each round's special logical opt flag here (like decorrelate rule or not) - finalPlan, cost, err := core.DoOptimize(ctx, sctx, builder.GetOptFlag(), logic) - if err != nil { - return err - } - builder.HandleUnusedViewHints() - - if bestPlan == nil || cost < bestCost { - bestCost = cost - bestPlan = finalPlan - bestNames = names - if needBaseline { - bestState = captureLogicalPlanBuildBaseline(sessVars) - } - } - return nil - }() if err != nil { return nil, nil, 0, err } @@ -577,8 +595,8 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW if bestPlan == nil { return nil, nil, 0, errors.New("failed to build logical plan") } - if needBaseline { - restoreLogicalPlanBuildBaseline(sessVars, bestState) + if needRestoreLogicalPlanCtx { + restoreLogicalPlanBuildCtx(sessVars, bestLogicalPlanCtx) } sessVars.DurationOptimizer.Total = time.Since(beginOpt) return bestPlan, bestNames, bestCost, nil From bc1b396a1e279a1f40172c9236fd687ed3293911 Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Tue, 10 Mar 2026 12:56:11 +0800 Subject: [PATCH 09/11] planner: address coderabbit comments in step2 optimize --- pkg/planner/optimize.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/planner/optimize.go b/pkg/planner/optimize.go index 57392369a7d34..c1bef1aeae2a8 100644 --- a/pkg/planner/optimize.go +++ b/pkg/planner/optimize.go @@ -441,18 +441,27 @@ var planBuilderPool = sync.Pool{ type logicalPlanBuildCtx struct { stmtCtxState stmtctx.LogicalPlanBuildState plannerSelectBlockAsName *[]ast.HintTable + mapScalarSubQ []any + mapHashCode2UniqueID map[string]int + rewritePhaseInfo variable.RewritePhaseInfo } func saveLogicalPlanBuildCtx(sessVars *variable.SessionVars) logicalPlanBuildCtx { return logicalPlanBuildCtx{ stmtCtxState: sessVars.StmtCtx.SaveLogicalPlanBuildState(), plannerSelectBlockAsName: sessVars.PlannerSelectBlockAsName.Load(), + mapScalarSubQ: sessVars.MapScalarSubQ, + mapHashCode2UniqueID: sessVars.MapHashCode2UniqueID4ExtendedCol, + rewritePhaseInfo: sessVars.RewritePhaseInfo, } } func restoreLogicalPlanBuildCtx(sessVars *variable.SessionVars, logicalPlanCtx logicalPlanBuildCtx) { sessVars.StmtCtx.RestoreLogicalPlanBuildState(logicalPlanCtx.stmtCtxState) sessVars.PlannerSelectBlockAsName.Store(logicalPlanCtx.plannerSelectBlockAsName) + sessVars.MapScalarSubQ = logicalPlanCtx.mapScalarSubQ + sessVars.MapHashCode2UniqueID4ExtendedCol = logicalPlanCtx.mapHashCode2UniqueID + sessVars.RewritePhaseInfo = logicalPlanCtx.rewritePhaseInfo } func buildAndOptimizeLogicalPlanRound( @@ -470,6 +479,7 @@ func buildAndOptimizeLogicalPlanRound( ) (base.Plan, types.NameSlice, bool, error) { builder := planBuilderPool.Get().(*core.PlanBuilder) defer planBuilderPool.Put(builder.ResetForReuse()) + defer builder.HandleUnusedViewHints() builder.Init(sctx, is, hintProcessor) @@ -503,7 +513,6 @@ func buildAndOptimizeLogicalPlanRound( // Handle the non-logical plan statement. logic, isLogicalPlan := p.(base.LogicalPlan) if !isLogicalPlan { - builder.HandleUnusedViewHints() return p, names, true, nil } @@ -514,7 +523,6 @@ func buildAndOptimizeLogicalPlanRound( if err != nil { return nil, nil, false, err } - builder.HandleUnusedViewHints() if *bestPlan == nil || cost < *bestCost { *bestCost = cost @@ -545,6 +553,10 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW topsql.MockHighCPULoad(sctx.GetSessionVars().StmtCtx.OriginalSQL, sqlPrefixes, 10) }) sessVars := sctx.GetSessionVars() + beginOpt := time.Now() + defer func() { + sessVars.DurationOptimizer.Total = time.Since(beginOpt) + }() // Build the logical plan from the raw AST. The hint processor only keeps // AST-derived metadata; per-build state is allocated inside PlanBuilder. @@ -565,7 +577,6 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW if needRestoreLogicalPlanCtx { initialLogicalPlanCtx = saveLogicalPlanBuildCtx(sessVars) } - beginOpt := time.Now() for i := range buildRound { if needRestoreLogicalPlanCtx && i > 0 { restoreLogicalPlanBuildCtx(sessVars, initialLogicalPlanCtx) @@ -598,7 +609,6 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW if needRestoreLogicalPlanCtx { restoreLogicalPlanBuildCtx(sessVars, bestLogicalPlanCtx) } - sessVars.DurationOptimizer.Total = time.Since(beginOpt) return bestPlan, bestNames, bestCost, nil } From 7bb71495dfc21bbb322239372f21a3addb78889e Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Tue, 10 Mar 2026 15:13:00 +0800 Subject: [PATCH 10/11] planner: add TODO for multi-round view-hint warnings --- pkg/planner/optimize.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/planner/optimize.go b/pkg/planner/optimize.go index c1bef1aeae2a8..b2aa4a0efcf28 100644 --- a/pkg/planner/optimize.go +++ b/pkg/planner/optimize.go @@ -479,6 +479,7 @@ func buildAndOptimizeLogicalPlanRound( ) (base.Plan, types.NameSlice, bool, error) { builder := planBuilderPool.Get().(*core.PlanBuilder) defer planBuilderPool.Put(builder.ResetForReuse()) + // TODO: when buildRound > 1, only emit unused view-hint warnings for the winner build. defer builder.HandleUnusedViewHints() builder.Init(sctx, is, hintProcessor) From f89fdd9e867390edfe7ba8f8f37d4f2302cbf888 Mon Sep 17 00:00:00 2001 From: AilinKid <314806019@qq.com> Date: Tue, 10 Mar 2026 15:53:13 +0800 Subject: [PATCH 11/11] planner: keep Optimize_time scoped to DoOptimize phase --- pkg/planner/optimize.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/planner/optimize.go b/pkg/planner/optimize.go index b2aa4a0efcf28..816c5d82deb0d 100644 --- a/pkg/planner/optimize.go +++ b/pkg/planner/optimize.go @@ -471,6 +471,8 @@ func buildAndOptimizeLogicalPlanRound( is infoschema.InfoSchema, hintProcessor *hint.QBHintHandler, checked *bool, + optimizeStarted *bool, + beginOpt *time.Time, needRestoreLogicalPlanCtx bool, bestPlan *base.PhysicalPlan, bestNames *types.NameSlice, @@ -520,6 +522,10 @@ func buildAndOptimizeLogicalPlanRound( core.RecheckCTE(logic) // todo: also you can customize each round's special logical opt flag here (like decorrelate rule or not) + if !*optimizeStarted { + *optimizeStarted = true + *beginOpt = time.Now() + } finalPlan, cost, err := core.DoOptimize(ctx, sctx, builder.GetOptFlag(), logic) if err != nil { return nil, nil, false, err @@ -554,9 +560,14 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW topsql.MockHighCPULoad(sctx.GetSessionVars().StmtCtx.OriginalSQL, sqlPrefixes, 10) }) sessVars := sctx.GetSessionVars() - beginOpt := time.Now() + var ( + beginOpt time.Time + optimizeStarted bool + ) defer func() { - sessVars.DurationOptimizer.Total = time.Since(beginOpt) + if optimizeStarted { + sessVars.DurationOptimizer.Total = time.Since(beginOpt) + } }() // Build the logical plan from the raw AST. The hint processor only keeps @@ -590,6 +601,8 @@ func optimize(ctx context.Context, sctx planctx.PlanContext, node *resolve.NodeW is, hintProcessor, &checked, + &optimizeStarted, + &beginOpt, needRestoreLogicalPlanCtx, &bestPlan, &bestNames,