Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/parser/ast/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
30 changes: 29 additions & 1 deletion pkg/planner/core/plan_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -60,6 +61,18 @@ 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(stmt *PlanCacheStmt, matchedBinding *bindinfo.Binding, bindingMatched bool) bool {
if stmt.HasUsePlanCacheHint {
return true
}
return bindingMatched &&
matchedBinding != nil &&
matchedBinding.Hint != nil &&
matchedBinding.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()
Expand Down Expand Up @@ -199,13 +212,28 @@ 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.
} else {
stmtCtx.SetCacheType(contextutil.SessionPrepared)
cacheEnabled = sessVars.EnablePreparedPlanCache
}
if cacheEnabled && stmt.UncacheableReason == "" &&
sessVars.PlanCacheStrategy == vardef.TiDBPlanCacheStrategyHintOnly &&
!stmt.HasUsePlanCacheHint {
Copy link
Copy Markdown
Contributor

@AilinKid AilinKid Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!stmt.HasUsePlanCacheHint may overlap with !containUsePlanCacheHintInPreparedSQLOrBinding check below?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, stmt.HasUsePlanCacheHint is from the statement directly, but containUsePlanCacheHintInPreparedSQLOrBinding is from the binding.

prepare st1 from "select /*+ use_plan_cache() */ * from t1";
prepare st2 from "select * from t2";
create global binding using select /*+ use_plan_cache() */ * from t2

In the case above, st1.HasUsePlanCacheHint is true, but st2.HashUsePlanCacheHint is false, st2 has to get the use_plan_cache from binding, which is what containUsePlanCacheHintInPreparedSQLOrBinding for.

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()
}
Expand All @@ -216,7 +244,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
}
Expand Down
18 changes: 17 additions & 1 deletion pkg/planner/core/plan_cache_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -310,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
}
Expand Down Expand Up @@ -736,6 +750,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
Expand Down
17 changes: 17 additions & 0 deletions pkg/planner/optimize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add warning test about this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, the new test case has been added:
image

}
return nil, nil, false, nil
}

ok, reason := core.NonPreparedPlanCacheableWithCtx(sctx.GetPlanCtx(), stmt, is)
if !ok {
Expand Down
7 changes: 7 additions & 0 deletions pkg/sessionctx/vardef/tidb_vars.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1668,6 +1674,7 @@ const (
DefExecutorConcurrency = 5
DefTiDBEnableNonPreparedPlanCache = false
DefTiDBEnableNonPreparedPlanCacheForDML = true
DefTiDBPlanCacheStrategy = TiDBPlanCacheStrategyAll
DefTiDBNonPreparedPlanCacheSize = 100
DefTiDBPlanCacheMaxPlanSize = 2 * size.MB
DefTiDBInstancePlanCacheMaxMemSize = 100 * size.MB
Expand Down
4 changes: 4 additions & 0 deletions pkg/sessionctx/variable/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions pkg/sessionctx/variable/sysvar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion pkg/util/hint/hint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
41 changes: 41 additions & 0 deletions pkg/util/hint/hint_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
107 changes: 107 additions & 0 deletions tests/integrationtest/r/planner/core/plan_cache.result
Original file line number Diff line number Diff line change
Expand Up @@ -2664,6 +2664,113 @@ 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
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
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()';
Expand Down
Loading
Loading