From c2e6e6028cd00b6f5c628218aba12fad2d20d7df Mon Sep 17 00:00:00 2001 From: qw4990 Date: Thu, 2 Apr 2026 21:23:38 +0800 Subject: [PATCH 1/3] planner: support use_plan_cache binding in hint_only strategy --- pkg/parser/ast/misc.go | 2 +- pkg/planner/core/plan_cache.go | 20 ++++ pkg/planner/core/plan_cache_utils.go | 4 + pkg/planner/optimize.go | 17 +++ pkg/sessionctx/vardef/tidb_vars.go | 7 ++ pkg/sessionctx/variable/session.go | 4 + pkg/sessionctx/variable/sysvar.go | 4 + pkg/util/hint/hint.go | 9 +- pkg/util/hint/hint_processor.go | 41 +++++++ .../r/planner/core/plan_cache.result | 100 ++++++++++++++++++ .../t/planner/core/plan_cache.test | 65 ++++++++++++ 11 files changed, 271 insertions(+), 2 deletions(-) diff --git a/pkg/parser/ast/misc.go b/pkg/parser/ast/misc.go index cc257f7851f7b..4f0dad3fdfb22 100644 --- a/pkg/parser/ast/misc.go +++ b/pkg/parser/ast/misc.go @@ -4102,7 +4102,7 @@ func (n *TableOptimizerHint) Restore(ctx *format.RestoreCtx) error { } // Hints without args except query block. switch n.HintName.L { - case "mpp_1phase_agg", "mpp_2phase_agg", "hash_agg", "stream_agg", "agg_to_cop", "read_consistent_replica", "no_index_merge", "ignore_plan_cache", "limit_to_cop", "straight_join", "merge", "no_decorrelate": + case "mpp_1phase_agg", "mpp_2phase_agg", "hash_agg", "stream_agg", "agg_to_cop", "read_consistent_replica", "no_index_merge", "ignore_plan_cache", "use_plan_cache", "limit_to_cop", "straight_join", "merge", "no_decorrelate": ctx.WritePlain(")") return nil } diff --git a/pkg/planner/core/plan_cache.go b/pkg/planner/core/plan_cache.go index 23ce7512b8f3d..1a78281c14252 100644 --- a/pkg/planner/core/plan_cache.go +++ b/pkg/planner/core/plan_cache.go @@ -20,6 +20,7 @@ import ( "time" "github.com/pingcap/errors" + "github.com/pingcap/tidb/pkg/bindinfo" "github.com/pingcap/tidb/pkg/domain" "github.com/pingcap/tidb/pkg/expression" "github.com/pingcap/tidb/pkg/infoschema" @@ -59,6 +60,19 @@ type PlanCacheKeyTestClone struct{} // PlanCacheKeyEnableInstancePlanCache is only for test. type PlanCacheKeyEnableInstancePlanCache struct{} +const planCacheHintOnlyNoHintReason = "plan cache strategy is hint_only and use_plan_cache hint is absent" + +func containUsePlanCacheHintInPreparedSQLOrBinding(sctx sessionctx.Context, stmt *PlanCacheStmt) bool { + if stmt.HasUsePlanCacheHint { + return true + } + if stmt.PreparedAst == nil || stmt.PreparedAst.Stmt == nil { + return false + } + binding, matched, _ := bindinfo.MatchSQLBindingWithCache(sctx, stmt.PreparedAst.Stmt, &stmt.BindingInfo) + return matched && binding != nil && binding.Hint != nil && binding.Hint.ContainTableHint(hint.HintUsePlanCache) +} + // SetParameterValuesIntoSCtx sets these parameters into session context. func SetParameterValuesIntoSCtx(sctx base.PlanContext, isNonPrep bool, markers []ast.ParamMarkerExpr, params []expression.Expression) error { vars := sctx.GetSessionVars() @@ -202,6 +216,12 @@ func GetPlanFromPlanCache(ctx context.Context, sctx sessionctx.Context, stmtCtx.SetCacheType(contextutil.SessionPrepared) cacheEnabled = sessVars.EnablePreparedPlanCache } + if cacheEnabled && stmt.UncacheableReason == "" && + sessVars.PlanCacheStrategy == vardef.TiDBPlanCacheStrategyHintOnly && + !containUsePlanCacheHintInPreparedSQLOrBinding(sctx, stmt) { + cacheEnabled = false + stmtCtx.WarnSkipPlanCache(planCacheHintOnlyNoHintReason) + } if stmt.StmtCacheable && cacheEnabled { stmtCtx.EnablePlanCache() } diff --git a/pkg/planner/core/plan_cache_utils.go b/pkg/planner/core/plan_cache_utils.go index a8df9017af6f4..7c30409234885 100644 --- a/pkg/planner/core/plan_cache_utils.go +++ b/pkg/planner/core/plan_cache_utils.go @@ -138,6 +138,7 @@ func GeneratePlanCacheStmtWithAST(ctx context.Context, sctx sessionctx.Context, StmtType: stmtctx.GetStmtLabel(ctx, paramStmt), } normalizedSQL, digest := parser.NormalizeDigest(prepared.Stmt.Text()) + hasUsePlanCacheHint := hint.ContainTableHintInStmtNode(paramStmt, hint.HintUsePlanCache) var ( cacheable bool @@ -222,6 +223,7 @@ func GeneratePlanCacheStmtWithAST(ctx context.Context, sctx sessionctx.Context, SnapshotTSEvaluator: ret.SnapshotTSEvaluator, StmtCacheable: cacheable, UncacheableReason: reason, + HasUsePlanCacheHint: hasUsePlanCacheHint, dbName: dbName, tbls: tbls, SchemaVersion: ret.InfoSchema.SchemaMetaVersion(), @@ -736,6 +738,8 @@ type PlanCacheStmt struct { StmtCacheable bool // Whether this stmt is cacheable. UncacheableReason string // Why this stmt is uncacheable. + // HasUsePlanCacheHint indicates whether this stmt contains the use_plan_cache() hint. + HasUsePlanCacheHint bool limits []*ast.Limit hasSubquery bool diff --git a/pkg/planner/optimize.go b/pkg/planner/optimize.go index f06c1cd25af71..9fca0303ed2e2 100644 --- a/pkg/planner/optimize.go +++ b/pkg/planner/optimize.go @@ -52,6 +52,16 @@ import ( "github.com/pingcap/tidb/pkg/util/tracing" ) +const nonPreparedPlanCacheHintOnlyNoHintReason = "plan cache strategy is hint_only and use_plan_cache hint is absent" + +func containUsePlanCacheHintInSQLOrBinding(sctx sessionctx.Context, stmt ast.StmtNode) bool { + if hint.ContainTableHintInStmtNode(stmt, hint.HintUsePlanCache) { + return true + } + binding, matched, _ := bindinfo.MatchSQLBinding(sctx, stmt) + return matched && binding != nil && binding.Hint != nil && binding.Hint.ContainTableHint(hint.HintUsePlanCache) +} + // getPlanFromNonPreparedPlanCache tries to get an available cached plan from the NonPrepared Plan Cache for this stmt. func getPlanFromNonPreparedPlanCache(ctx context.Context, sctx sessionctx.Context, node *resolve.NodeW, is infoschema.InfoSchema) (p base.Plan, ns types.NameSlice, ok bool, err error) { stmtCtx := sctx.GetSessionVars().StmtCtx @@ -65,6 +75,13 @@ func getPlanFromNonPreparedPlanCache(ctx context.Context, sctx sessionctx.Contex sctx.GetSessionVars().InMultiStmts { // in multi-stmt return nil, nil, false, nil } + if sctx.GetSessionVars().PlanCacheStrategy == vardef.TiDBPlanCacheStrategyHintOnly && + !containUsePlanCacheHintInSQLOrBinding(sctx, stmt) { + if !isExplain && stmtCtx.InExplainStmt && stmtCtx.ExplainFormat == types.ExplainFormatPlanCache { + stmtCtx.AppendWarning(errors.NewNoStackErrorf("skip non-prepared plan-cache: %s", nonPreparedPlanCacheHintOnlyNoHintReason)) + } + return nil, nil, false, nil + } ok, reason := core.NonPreparedPlanCacheableWithCtx(sctx.GetPlanCtx(), stmt, is) if !ok { diff --git a/pkg/sessionctx/vardef/tidb_vars.go b/pkg/sessionctx/vardef/tidb_vars.go index fbd7e5ba29995..3cb82cbaaf3ab 100644 --- a/pkg/sessionctx/vardef/tidb_vars.go +++ b/pkg/sessionctx/vardef/tidb_vars.go @@ -948,6 +948,12 @@ const ( TiDBEnableNonPreparedPlanCache = "tidb_enable_non_prepared_plan_cache" // TiDBEnableNonPreparedPlanCacheForDML indicates whether to enable non-prepared plan cache for DML statements. TiDBEnableNonPreparedPlanCacheForDML = "tidb_enable_non_prepared_plan_cache_for_dml" + // TiDBPlanCacheStrategy controls plan cache strategy. + TiDBPlanCacheStrategy = "tidb_plan_cache_strategy" + // TiDBPlanCacheStrategyAll is one strategy value for TiDBPlanCacheStrategy. + TiDBPlanCacheStrategyAll = "all" + // TiDBPlanCacheStrategyHintOnly is one strategy value for TiDBPlanCacheStrategy. + TiDBPlanCacheStrategyHintOnly = "hint_only" // TiDBNonPreparedPlanCacheSize controls the size of non-prepared plan cache. // This variable is deprecated, use tidb_session_plan_cache_size instead. TiDBNonPreparedPlanCacheSize = "tidb_non_prepared_plan_cache_size" @@ -1668,6 +1674,7 @@ const ( DefExecutorConcurrency = 5 DefTiDBEnableNonPreparedPlanCache = false DefTiDBEnableNonPreparedPlanCacheForDML = true + DefTiDBPlanCacheStrategy = TiDBPlanCacheStrategyAll DefTiDBNonPreparedPlanCacheSize = 100 DefTiDBPlanCacheMaxPlanSize = 2 * size.MB DefTiDBInstancePlanCacheMaxMemSize = 100 * size.MB diff --git a/pkg/sessionctx/variable/session.go b/pkg/sessionctx/variable/session.go index 90107e5d227e6..69d0100ef4dc6 100644 --- a/pkg/sessionctx/variable/session.go +++ b/pkg/sessionctx/variable/session.go @@ -1624,6 +1624,9 @@ type SessionVars struct { // EnableNonPreparedPlanCacheForDML indicates whether to enable non-prepared plan cache for DML statements. EnableNonPreparedPlanCacheForDML bool + // PlanCacheStrategy controls plan cache strategy. + PlanCacheStrategy string + // EnableFuzzyBinding indicates whether to enable fuzzy binding. EnableFuzzyBinding bool @@ -2407,6 +2410,7 @@ func NewSessionVars(hctx HookContext) *SessionVars { WindowingUseHighPrecision: true, PrevFoundInPlanCache: vardef.DefTiDBFoundInPlanCache, FoundInPlanCache: vardef.DefTiDBFoundInPlanCache, + PlanCacheStrategy: vardef.DefTiDBPlanCacheStrategy, PrevFoundInBinding: vardef.DefTiDBFoundInBinding, FoundInBinding: vardef.DefTiDBFoundInBinding, SelectLimit: math.MaxUint64, diff --git a/pkg/sessionctx/variable/sysvar.go b/pkg/sessionctx/variable/sysvar.go index e8ffc994bb9a2..339c08de473c3 100644 --- a/pkg/sessionctx/variable/sysvar.go +++ b/pkg/sessionctx/variable/sysvar.go @@ -1560,6 +1560,10 @@ var defaultSysVars = []*SysVar{ s.EnableNonPreparedPlanCacheForDML = TiDBOptOn(val) return nil }}, + {Scope: vardef.ScopeGlobal | vardef.ScopeSession, Name: vardef.TiDBPlanCacheStrategy, Value: vardef.DefTiDBPlanCacheStrategy, Type: vardef.TypeEnum, PossibleValues: []string{vardef.TiDBPlanCacheStrategyAll, vardef.TiDBPlanCacheStrategyHintOnly}, SetSession: func(s *SessionVars, val string) error { + s.PlanCacheStrategy = val + return nil + }}, { Scope: vardef.ScopeGlobal | vardef.ScopeSession, Name: vardef.TiDBOptEnableFuzzyBinding, diff --git a/pkg/util/hint/hint.go b/pkg/util/hint/hint.go index 5d4af787ac81d..2b66ef3ba128a 100644 --- a/pkg/util/hint/hint.go +++ b/pkg/util/hint/hint.go @@ -111,6 +111,8 @@ const ( HintTimeRange = "time_range" // HintIgnorePlanCache is a hint to enforce ignoring plan cache HintIgnorePlanCache = "ignore_plan_cache" + // HintUsePlanCache is a hint to enforce using plan cache. + HintUsePlanCache = "use_plan_cache" // HintLimitToCop is a hint enforce pushing limit or topn to coprocessor. HintLimitToCop = "limit_to_cop" // HintMerge is a hint which can switch turning inline for the CTE. @@ -228,7 +230,9 @@ type StmtHints struct { ResourceGroup string // Do not store plan in either plan cache. IgnorePlanCache bool - WriteSlowLog bool + // Use plan cache under strategy that requires explicit hints. + UsePlanCache bool + WriteSlowLog bool // Hint flags HasAllowInSubqToJoinAndAggHint bool @@ -276,6 +280,7 @@ func (sh *StmtHints) Clone() *StmtHints { ForceNthPlan: sh.ForceNthPlan, ResourceGroup: sh.ResourceGroup, IgnorePlanCache: sh.IgnorePlanCache, + UsePlanCache: sh.UsePlanCache, WriteSlowLog: sh.WriteSlowLog, HasAllowInSubqToJoinAndAggHint: sh.HasAllowInSubqToJoinAndAggHint, HasMemQuotaHint: sh.HasMemQuotaHint, @@ -409,6 +414,8 @@ func ParseStmtHints(hints []*ast.TableOptimizerHint, setVarsOffs = append(setVarsOffs, i) case HintIgnorePlanCache: stmtHints.IgnorePlanCache = true + case HintUsePlanCache: + stmtHints.UsePlanCache = true case HintWriteSlowLog: stmtHints.WriteSlowLog = true } diff --git a/pkg/util/hint/hint_processor.go b/pkg/util/hint/hint_processor.go index ed0b1810933a2..85957d00a0a90 100644 --- a/pkg/util/hint/hint_processor.go +++ b/pkg/util/hint/hint_processor.go @@ -119,6 +119,47 @@ func ExtractTableHintsFromStmtNode(node ast.Node, warnHandler hintWarnHandler) [ } } +func containTableHint(hints []*ast.TableOptimizerHint, hintName string) bool { + for _, hint := range hints { + if hint.HintName.L == hintName { + return true + } + } + return false +} + +// ContainTableHintInStmtNode checks whether the statement contains the target table hint. +func ContainTableHintInStmtNode(node ast.Node, hintName string) bool { + switch x := node.(type) { + case *ast.SelectStmt: + return containTableHint(x.TableHints, hintName) + case *ast.UpdateStmt: + return containTableHint(x.TableHints, hintName) + case *ast.DeleteStmt: + return containTableHint(x.TableHints, hintName) + case *ast.InsertStmt: + if containTableHint(x.TableHints, hintName) { + return true + } + if x.Select == nil { + return false + } + return ContainTableHintInStmtNode(x.Select, hintName) + case *ast.SetOprStmt: + if x.SelectList == nil { + return false + } + for _, s := range x.SelectList.Selects { + if ContainTableHintInStmtNode(s, hintName) { + return true + } + } + return false + default: + return false + } +} + // checkInsertStmtHintDuplicated check whether existed the duplicated hints in both insertStmt and its selectStmt. // If existed, it would send a warning message. func checkInsertStmtHintDuplicated(node ast.Node, warnHandler hintWarnHandler) { diff --git a/tests/integrationtest/r/planner/core/plan_cache.result b/tests/integrationtest/r/planner/core/plan_cache.result index aa576b3f955f8..97e1c5e03fea5 100644 --- a/tests/integrationtest/r/planner/core/plan_cache.result +++ b/tests/integrationtest/r/planner/core/plan_cache.result @@ -2664,6 +2664,106 @@ select @@tidb_plan_cache_skip_stats_on_binding; @@tidb_plan_cache_skip_stats_on_binding 1 drop table if exists t; +create table t(a int, key(a)); +insert into t values (1), (2), (3); +set tidb_plan_cache_strategy = 'hint_only'; +prepare st1 from 'select * from t where a = ?'; +set @a = 1; +execute st1 using @a; +a +1 +execute st1 using @a; +a +1 +select @@last_plan_from_cache; +@@last_plan_from_cache +0 +prepare st2 from 'select /*+ use_plan_cache() */ * from t where a = ?'; +execute st2 using @a; +a +1 +execute st2 using @a; +a +1 +select @@last_plan_from_cache; +@@last_plan_from_cache +1 +set tidb_enable_non_prepared_plan_cache = 1; +select * from t where a = 1; +a +1 +select * from t where a = 1; +a +1 +select @@last_plan_from_cache; +@@last_plan_from_cache +0 +select /*+ use_plan_cache() */ * from t where a = 1; +a +1 +select /*+ use_plan_cache() */ * from t where a = 1; +a +1 +select @@last_plan_from_cache; +@@last_plan_from_cache +1 +set tidb_enable_non_prepared_plan_cache = default; +set tidb_plan_cache_strategy = default; +drop table if exists t; +create table t(a int, key(a)); +insert into t values (1), (2), (3); +set tidb_plan_cache_strategy = 'hint_only'; +prepare st from 'select * from t where a = ?'; +set @a = 1; +execute st using @a; +a +1 +execute st using @a; +a +1 +select @@last_plan_from_cache; +@@last_plan_from_cache +0 +create binding for select * from t where a=1 using select /*+ use_plan_cache() */ * from t where a=1; +execute st using @a; +a +1 +execute st using @a; +a +1 +select @@last_plan_from_binding, @@last_plan_from_cache; +@@last_plan_from_binding @@last_plan_from_cache +1 1 +drop binding for select * from t where a=1; +set tidb_plan_cache_strategy = default; +drop table if exists t; +create table t(a int, key(a)); +insert into t values (1), (2), (3); +set tidb_enable_non_prepared_plan_cache = 1; +set tidb_plan_cache_strategy = 'hint_only'; +select * from t where a = 1; +a +1 +select * from t where a = 1; +a +1 +select @@last_plan_from_cache; +@@last_plan_from_cache +0 +create binding for select * from t where a=1 using select /*+ use_plan_cache() */ * from t where a=1; +select * from t where a = 1; +a +1 +select * from t where a = 1; +a +1 +select @@last_plan_from_binding, @@last_plan_from_cache; +@@last_plan_from_binding @@last_plan_from_cache +1 1 +drop binding for select * from t where a=1; +set tidb_enable_non_prepared_plan_cache = default; +set tidb_plan_cache_strategy = default; +drop table if exists t; CREATE TABLE t (a int(11) DEFAULT NULL, b date DEFAULT NULL); INSERT INTO t VALUES (1, current_date()); PREPARE stmt FROM 'SELECT a FROM t WHERE b=current_date()'; diff --git a/tests/integrationtest/t/planner/core/plan_cache.test b/tests/integrationtest/t/planner/core/plan_cache.test index 248408681dbef..7b58751260539 100644 --- a/tests/integrationtest/t/planner/core/plan_cache.test +++ b/tests/integrationtest/t/planner/core/plan_cache.test @@ -1687,6 +1687,71 @@ set tidb_plan_cache_invalidation_on_fresh_stats = DEFAULT; set tidb_plan_cache_skip_stats_on_binding = DEFAULT; select @@tidb_plan_cache_skip_stats_on_binding; +# TestUsePlanCacheHintWithHintOnlyStrategy +drop table if exists t; +create table t(a int, key(a)); +insert into t values (1), (2), (3); +set tidb_plan_cache_strategy = 'hint_only'; + +## prepared plan cache +prepare st1 from 'select * from t where a = ?'; +set @a = 1; +execute st1 using @a; +execute st1 using @a; +select @@last_plan_from_cache; + +prepare st2 from 'select /*+ use_plan_cache() */ * from t where a = ?'; +execute st2 using @a; +execute st2 using @a; +select @@last_plan_from_cache; + +## non-prepared plan cache +set tidb_enable_non_prepared_plan_cache = 1; +select * from t where a = 1; +select * from t where a = 1; +select @@last_plan_from_cache; + +select /*+ use_plan_cache() */ * from t where a = 1; +select /*+ use_plan_cache() */ * from t where a = 1; +select @@last_plan_from_cache; + +set tidb_enable_non_prepared_plan_cache = default; +set tidb_plan_cache_strategy = default; + +# TestUsePlanCacheHintInBindingWithHintOnlyStrategyPrepared +drop table if exists t; +create table t(a int, key(a)); +insert into t values (1), (2), (3); +set tidb_plan_cache_strategy = 'hint_only'; +prepare st from 'select * from t where a = ?'; +set @a = 1; +execute st using @a; +execute st using @a; +select @@last_plan_from_cache; +create binding for select * from t where a=1 using select /*+ use_plan_cache() */ * from t where a=1; +execute st using @a; +execute st using @a; +select @@last_plan_from_binding, @@last_plan_from_cache; +drop binding for select * from t where a=1; +set tidb_plan_cache_strategy = default; + +# TestUsePlanCacheHintInBindingWithHintOnlyStrategyNonPrepared +drop table if exists t; +create table t(a int, key(a)); +insert into t values (1), (2), (3); +set tidb_enable_non_prepared_plan_cache = 1; +set tidb_plan_cache_strategy = 'hint_only'; +select * from t where a = 1; +select * from t where a = 1; +select @@last_plan_from_cache; +create binding for select * from t where a=1 using select /*+ use_plan_cache() */ * from t where a=1; +select * from t where a = 1; +select * from t where a = 1; +select @@last_plan_from_binding, @@last_plan_from_cache; +drop binding for select * from t where a=1; +set tidb_enable_non_prepared_plan_cache = default; +set tidb_plan_cache_strategy = default; + # TestIssue45086 drop table if exists t; CREATE TABLE t (a int(11) DEFAULT NULL, b date DEFAULT NULL); From 17f7912f4440527850271fc5fb732a72cacd2fd0 Mon Sep 17 00:00:00 2001 From: qw4990 Date: Thu, 2 Apr 2026 21:34:21 +0800 Subject: [PATCH 2/3] planner: avoid duplicate binding match in plan cache key path --- pkg/planner/core/plan_cache.go | 28 ++++++++++++++++++---------- pkg/planner/core/plan_cache_utils.go | 14 +++++++++++++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/pkg/planner/core/plan_cache.go b/pkg/planner/core/plan_cache.go index 1a78281c14252..b261e4bf8baea 100644 --- a/pkg/planner/core/plan_cache.go +++ b/pkg/planner/core/plan_cache.go @@ -62,15 +62,14 @@ type PlanCacheKeyEnableInstancePlanCache struct{} const planCacheHintOnlyNoHintReason = "plan cache strategy is hint_only and use_plan_cache hint is absent" -func containUsePlanCacheHintInPreparedSQLOrBinding(sctx sessionctx.Context, stmt *PlanCacheStmt) bool { +func containUsePlanCacheHintInPreparedSQLOrBinding(stmt *PlanCacheStmt, matchedBinding *bindinfo.Binding, bindingMatched bool) bool { if stmt.HasUsePlanCacheHint { return true } - if stmt.PreparedAst == nil || stmt.PreparedAst.Stmt == nil { - return false - } - binding, matched, _ := bindinfo.MatchSQLBindingWithCache(sctx, stmt.PreparedAst.Stmt, &stmt.BindingInfo) - return matched && binding != nil && binding.Hint != nil && binding.Hint.ContainTableHint(hint.HintUsePlanCache) + return bindingMatched && + matchedBinding != nil && + matchedBinding.Hint != nil && + matchedBinding.Hint.ContainTableHint(hint.HintUsePlanCache) } // SetParameterValuesIntoSCtx sets these parameters into session context. @@ -209,6 +208,10 @@ func GetPlanFromPlanCache(ctx context.Context, sctx sessionctx.Context, sessVars := sctx.GetSessionVars() stmtCtx := sessVars.StmtCtx cacheEnabled := false + var ( + matchedBinding *bindinfo.Binding + bindingMatched bool + ) if isNonPrepared { stmtCtx.SetCacheType(contextutil.SessionNonPrepared) cacheEnabled = sessVars.EnableNonPreparedPlanCache // plan-cache might be disabled after prepare. @@ -218,9 +221,14 @@ func GetPlanFromPlanCache(ctx context.Context, sctx sessionctx.Context, } if cacheEnabled && stmt.UncacheableReason == "" && sessVars.PlanCacheStrategy == vardef.TiDBPlanCacheStrategyHintOnly && - !containUsePlanCacheHintInPreparedSQLOrBinding(sctx, stmt) { - cacheEnabled = false - stmtCtx.WarnSkipPlanCache(planCacheHintOnlyNoHintReason) + !stmt.HasUsePlanCacheHint { + if stmt.PreparedAst != nil { + matchedBinding, bindingMatched, _ = bindinfo.MatchSQLBindingWithCache(sctx, stmt.PreparedAst.Stmt, &stmt.BindingInfo) + } + if !containUsePlanCacheHintInPreparedSQLOrBinding(stmt, matchedBinding, bindingMatched) { + cacheEnabled = false + stmtCtx.WarnSkipPlanCache(planCacheHintOnlyNoHintReason) + } } if stmt.StmtCacheable && cacheEnabled { stmtCtx.EnablePlanCache() @@ -232,7 +240,7 @@ func GetPlanFromPlanCache(ctx context.Context, sctx sessionctx.Context, var cacheKey, binding, reason string var cacheable bool if stmtCtx.UseCache() { - cacheKey, binding, cacheable, reason, err = NewPlanCacheKey(sctx, stmt) + cacheKey, binding, cacheable, reason, err = newPlanCacheKeyWithMatchedBinding(sctx, stmt, matchedBinding, bindingMatched) if err != nil { return nil, nil, err } diff --git a/pkg/planner/core/plan_cache_utils.go b/pkg/planner/core/plan_cache_utils.go index 7c30409234885..8e101ed9b6442 100644 --- a/pkg/planner/core/plan_cache_utils.go +++ b/pkg/planner/core/plan_cache_utils.go @@ -312,7 +312,19 @@ func hashInt64Uint64Map(b []byte, m map[int64]uint64) []byte { // differentiate the cache key. In other cases, it will be 0. // All information that might affect the plan should be considered in this function. func NewPlanCacheKey(sctx sessionctx.Context, stmt *PlanCacheStmt) (key, binding string, cacheable bool, reason string, err error) { - if matchedBinding, matched, _ := bindinfo.MatchSQLBindingWithCache(sctx, stmt.PreparedAst.Stmt, &stmt.BindingInfo); matched { + return newPlanCacheKeyWithMatchedBinding(sctx, stmt, nil, false) +} + +func newPlanCacheKeyWithMatchedBinding( + sctx sessionctx.Context, + stmt *PlanCacheStmt, + matchedBinding *bindinfo.Binding, + bindingMatched bool, +) (key, binding string, cacheable bool, reason string, err error) { + if !bindingMatched && stmt.PreparedAst != nil { + matchedBinding, bindingMatched, _ = bindinfo.MatchSQLBindingWithCache(sctx, stmt.PreparedAst.Stmt, &stmt.BindingInfo) + } + if bindingMatched && matchedBinding != nil { // Record the matched binding SQL so the plan cache key reflects the effective hints. binding = matchedBinding.BindSQL } From b41d65b0179eff8fb9a88b24b6a8e9fb77f868ac Mon Sep 17 00:00:00 2001 From: qw4990 Date: Tue, 7 Apr 2026 20:14:17 +0800 Subject: [PATCH 3/3] fixup --- tests/integrationtest/r/planner/core/plan_cache.result | 7 +++++++ tests/integrationtest/t/planner/core/plan_cache.test | 2 ++ 2 files changed, 9 insertions(+) diff --git a/tests/integrationtest/r/planner/core/plan_cache.result b/tests/integrationtest/r/planner/core/plan_cache.result index 97e1c5e03fea5..7d69907b7f441 100644 --- a/tests/integrationtest/r/planner/core/plan_cache.result +++ b/tests/integrationtest/r/planner/core/plan_cache.result @@ -2698,6 +2698,13 @@ a select @@last_plan_from_cache; @@last_plan_from_cache 0 +explain format='plan_cache' select * from t where a = 1; +id estRows task access object operator info +IndexReader_6 10.00 root index:IndexRangeScan_5 +└─IndexRangeScan_5 10.00 cop[tikv] table:t, index:a(a) range:[1,1], keep order:false, stats:pseudo +show warnings; +Level Code Message +Warning 1105 skip non-prepared plan-cache: plan cache strategy is hint_only and use_plan_cache hint is absent select /*+ use_plan_cache() */ * from t where a = 1; a 1 diff --git a/tests/integrationtest/t/planner/core/plan_cache.test b/tests/integrationtest/t/planner/core/plan_cache.test index 7b58751260539..d27f3a3d0d42d 100644 --- a/tests/integrationtest/t/planner/core/plan_cache.test +++ b/tests/integrationtest/t/planner/core/plan_cache.test @@ -1710,6 +1710,8 @@ set tidb_enable_non_prepared_plan_cache = 1; select * from t where a = 1; select * from t where a = 1; select @@last_plan_from_cache; +explain format='plan_cache' select * from t where a = 1; +show warnings; select /*+ use_plan_cache() */ * from t where a = 1; select /*+ use_plan_cache() */ * from t where a = 1;