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/casetest/plancache/BUILD.bazel b/pkg/planner/core/casetest/plancache/BUILD.bazel index 25f20e76c21e6..5875bbd4cbb68 100644 --- a/pkg/planner/core/casetest/plancache/BUILD.bazel +++ b/pkg/planner/core/casetest/plancache/BUILD.bazel @@ -17,7 +17,7 @@ go_test( "//pkg/planner/core:plan_clone_utils.go", ], flaky = True, - shard_count = 45, + shard_count = 47, deps = [ "//pkg/expression", "//pkg/infoschema", diff --git a/pkg/planner/core/casetest/plancache/plan_cache_suite_test.go b/pkg/planner/core/casetest/plancache/plan_cache_suite_test.go index 6c7dbd47527a3..c57acda82de8f 100644 --- a/pkg/planner/core/casetest/plancache/plan_cache_suite_test.go +++ b/pkg/planner/core/casetest/plancache/plan_cache_suite_test.go @@ -1811,6 +1811,95 @@ func runPreparedPlanCacheForUpdateInTxn(t *testing.T, tk *testkit.TestKit) { tk.MustExec(`deallocate prepare st`) } +func TestPreparedPlanCacheHintOnlyWithoutUsePlanCacheHint(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + + preparedCache := tk.MustQuery("select @@session.tidb_enable_prepared_plan_cache").Rows()[0][0] + planCachePolicy := tk.MustQuery("select @@session.tidb_plan_cache_policy").Rows()[0][0] + defer func() { + tk.MustExec(fmt.Sprintf("set @@session.tidb_enable_prepared_plan_cache=%v", preparedCache)) + tk.MustExec(fmt.Sprintf("set @@session.tidb_plan_cache_policy=%q", planCachePolicy)) + }() + tk.MustExec(`set @@session.tidb_enable_prepared_plan_cache=1`) + tk.MustExec(`set @@session.tidb_plan_cache_policy='hint_only'`) + + tableName := "t_prepare_hint_only_without_use_hint" + tk.MustExec(fmt.Sprintf("drop table if exists %s", tableName)) + tk.MustExec(fmt.Sprintf("create table %s (a int)", tableName)) + tk.MustExec(fmt.Sprintf("insert into %s values (1)", tableName)) + + tk.MustExec(fmt.Sprintf("prepare st from 'select 1 from %s where a = ?'", tableName)) + tk.MustExec("set @a=1") + tk.MustExec("execute st using @a") + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) + tk.MustExec("execute st using @a") + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) + + tk.MustExec(fmt.Sprintf( + "create global binding for select 1 from %s where a = ? using select /*+ use_plan_cache() */ 1 from %s where a = ?", + tableName, tableName, + )) + tk.MustExec("execute st using @a") + tk.MustQuery("select @@last_plan_from_binding, @@last_plan_from_cache").Check(testkit.Rows("1 0")) + tk.MustExec("execute st using @a") + tk.MustQuery("select @@last_plan_from_binding, @@last_plan_from_cache").Check(testkit.Rows("1 1")) + tk.MustExec("execute st using @a") + tk.MustQuery("select @@last_plan_from_binding, @@last_plan_from_cache").Check(testkit.Rows("1 1")) +} + +func TestPreparedPlanCacheHintOnlyWithBinding(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + + preparedCache := tk.MustQuery("select @@session.tidb_enable_prepared_plan_cache").Rows()[0][0] + planCachePolicy := tk.MustQuery("select @@session.tidb_plan_cache_policy").Rows()[0][0] + defer func() { + tk.MustExec(fmt.Sprintf("set @@session.tidb_enable_prepared_plan_cache=%v", preparedCache)) + tk.MustExec(fmt.Sprintf("set @@session.tidb_plan_cache_policy=%q", planCachePolicy)) + }() + tk.MustExec(`set @@session.tidb_enable_prepared_plan_cache=1`) + tk.MustExec(`set @@session.tidb_plan_cache_policy='hint_only'`) + + tableName := "t_prepare_hint_only_binding" + tk.MustExec(fmt.Sprintf("drop table if exists %s", tableName)) + tk.MustExec(fmt.Sprintf("create table %s (pk int, a int, primary key(pk))", tableName)) + tk.MustExec(fmt.Sprintf("insert into %s values (1, 1), (2, 2)", tableName)) + + tk.MustExec(fmt.Sprintf("prepare st from 'select * from %s where pk >= ?'", tableName)) + tk.MustExec("set @a=1") + tk.MustExec("execute st using @a") + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) + tk.MustExec("execute st using @a") + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) + tk.MustExec("execute st using @a") + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) + + tk.MustExec(fmt.Sprintf( + "CREATE BINDING FOR select * from %s where pk >= ? USING select /*+ use_plan_cache() */ * from %s where pk >= ?", + tableName, tableName, + )) + tk.MustExec("execute st using @a") + tk.MustQuery("select @@last_plan_from_binding, @@last_plan_from_cache").Check(testkit.Rows("1 0")) + tk.MustExec("execute st using @a") + tk.MustQuery("select @@last_plan_from_binding, @@last_plan_from_cache").Check(testkit.Rows("1 1")) + tk.MustExec("execute st using @a") + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("1")) + + tk.MustExec(fmt.Sprintf( + "CREATE BINDING FOR select * from %s where pk >= ? USING select /*+ ignore_plan_cache() */ * from %s where pk >= ?", + tableName, tableName, + )) + tk.MustExec("execute st using @a") + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) + tk.MustExec("execute st using @a") + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) + tk.MustExec("execute st using @a") + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) +} + func TestNonPreparedPlanCacheSupportsFeatures(t *testing.T) { store := testkit.CreateMockStore(t) tk := testkit.NewTestKit(t, store) diff --git a/pkg/planner/core/plan_cache.go b/pkg/planner/core/plan_cache.go index 23ce7512b8f3d..8486762497903 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" @@ -194,6 +195,7 @@ func GetPlanFromPlanCache(ctx context.Context, sctx sessionctx.Context, sessVars := sctx.GetSessionVars() stmtCtx := sessVars.StmtCtx + var matchedBinding *bindinfo.Binding cacheEnabled := false if isNonPrepared { stmtCtx.SetCacheType(contextutil.SessionNonPrepared) @@ -203,7 +205,14 @@ func GetPlanFromPlanCache(ctx context.Context, sctx sessionctx.Context, cacheEnabled = sessVars.EnablePreparedPlanCache } if stmt.StmtCacheable && cacheEnabled { - stmtCtx.EnablePlanCache() + if sessVars.PlanCachePolicy == vardef.PlanCachePolicyHintOnly && !stmt.UsePlanCacheHint { + matchedBinding = matchSQLBindingWithCache(sctx, stmt) + } + if allowPlanCacheByPolicy(sctx, stmt, matchedBinding) { + stmtCtx.EnablePlanCache() + } else { + stmtCtx.WarnSkipPlanCache("the switch 'tidb_plan_cache_policy' is set to hint_only and no USE_PLAN_CACHE() hint is found") + } } if stmt.UncacheableReason != "" { stmtCtx.WarnSkipPlanCache(stmt.UncacheableReason) @@ -212,7 +221,10 @@ 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) + if matchedBinding == nil { + matchedBinding = matchSQLBindingWithCache(sctx, stmt) + } + cacheKey, binding, cacheable, reason, err = newPlanCacheKeyWithMatchedBinding(sctx, stmt, matchedBinding) if err != nil { return nil, nil, err } @@ -238,6 +250,30 @@ func GetPlanFromPlanCache(ctx context.Context, sctx sessionctx.Context, return generateNewPlan(ctx, sctx, isNonPrepared, is, stmt, cacheKey, binding, paramTypes) } +func allowPlanCacheByPolicy(sctx sessionctx.Context, stmt *PlanCacheStmt, matchedBinding *bindinfo.Binding) bool { + if sctx.GetSessionVars().PlanCachePolicy != vardef.PlanCachePolicyHintOnly { + return true + } + if stmt.UsePlanCacheHint { + return true + } + return bindingHasUsePlanCacheHint(matchedBinding) +} + +func matchSQLBindingWithCache(sctx sessionctx.Context, stmt *PlanCacheStmt) *bindinfo.Binding { + matchedBinding, matched, _ := bindinfo.MatchSQLBindingWithCache(sctx, stmt.PreparedAst.Stmt, &stmt.BindingInfo) + if !matched { + return nil + } + return matchedBinding +} + +func bindingHasUsePlanCacheHint(matchedBinding *bindinfo.Binding) bool { + return matchedBinding != nil && + matchedBinding.Hint != nil && + matchedBinding.Hint.ContainTableHint(hint.HintUsePlanCache) +} + func clonePlanForInstancePlanCache(ctx context.Context, sctx sessionctx.Context, stmt *PlanCacheStmt, plan base.Plan) (clonedPlan base.Plan, ok bool) { defer func(begin time.Time) { diff --git a/pkg/planner/core/plan_cache_utils.go b/pkg/planner/core/plan_cache_utils.go index a8df9017af6f4..5fc7e4d4687fe 100644 --- a/pkg/planner/core/plan_cache_utils.go +++ b/pkg/planner/core/plan_cache_utils.go @@ -92,6 +92,7 @@ func (e *paramMarkerExtractor) Leave(in ast.Node) (ast.Node, bool) { func GeneratePlanCacheStmtWithAST(ctx context.Context, sctx sessionctx.Context, isPrepStmt bool, paramSQL string, paramStmt ast.StmtNode, is infoschema.InfoSchema) (*PlanCacheStmt, base.Plan, int, error) { vars := sctx.GetSessionVars() + usePlanCacheHint := hasUsePlanCacheHint(paramStmt) var extractor paramMarkerExtractor paramStmt.Accept(&extractor) @@ -222,6 +223,7 @@ func GeneratePlanCacheStmtWithAST(ctx context.Context, sctx sessionctx.Context, SnapshotTSEvaluator: ret.SnapshotTSEvaluator, StmtCacheable: cacheable, UncacheableReason: reason, + UsePlanCacheHint: usePlanCacheHint, dbName: dbName, tbls: tbls, SchemaVersion: ret.InfoSchema.SchemaMetaVersion(), @@ -238,6 +240,15 @@ func GeneratePlanCacheStmtWithAST(ctx context.Context, sctx sessionctx.Context, return preparedObj, p, paramCount, nil } +func hasUsePlanCacheHint(stmt ast.StmtNode) bool { + for _, h := range hint.ExtractTableHintsFromStmtNode(stmt, nil) { + if h.HintName.L == hint.HintUsePlanCache { + return true + } + } + return false +} + // tableIDSlicePool is a pool for int64 slices used in hashInt64Uint64Map. var tableIDSlicePool = zeropool.New[[]int64](func() []int64 { return make([]int64, 0, 8) @@ -310,7 +321,15 @@ 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 { + matchedBinding, matched, _ := bindinfo.MatchSQLBindingWithCache(sctx, stmt.PreparedAst.Stmt, &stmt.BindingInfo) + if !matched { + matchedBinding = nil + } + return newPlanCacheKeyWithMatchedBinding(sctx, stmt, matchedBinding) +} + +func newPlanCacheKeyWithMatchedBinding(sctx sessionctx.Context, stmt *PlanCacheStmt, matchedBinding *bindinfo.Binding) (key, binding string, cacheable bool, reason string, err error) { + if matchedBinding != nil { // Record the matched binding SQL so the plan cache key reflects the effective hints. binding = matchedBinding.BindSQL } @@ -736,6 +755,7 @@ type PlanCacheStmt struct { StmtCacheable bool // Whether this stmt is cacheable. UncacheableReason string // Why this stmt is uncacheable. + UsePlanCacheHint bool // Whether this stmt contains the USE_PLAN_CACHE() hint. limits []*ast.Limit hasSubquery bool diff --git a/pkg/sessionctx/vardef/tidb_vars.go b/pkg/sessionctx/vardef/tidb_vars.go index 26414cb98d6a6..f461f40c40797 100644 --- a/pkg/sessionctx/vardef/tidb_vars.go +++ b/pkg/sessionctx/vardef/tidb_vars.go @@ -953,6 +953,8 @@ const ( TiDBNonPreparedPlanCacheSize = "tidb_non_prepared_plan_cache_size" // TiDBPlanCacheMaxPlanSize controls the maximum size of a plan that can be cached. TiDBPlanCacheMaxPlanSize = "tidb_plan_cache_max_plan_size" + // TiDBPlanCachePolicy controls how plan cache is enabled. + TiDBPlanCachePolicy = "tidb_plan_cache_policy" // TiDBPlanCacheInvalidationOnFreshStats controls if plan cache will be invalidated automatically when // related stats are analyzed after the plan cache is generated. TiDBPlanCacheInvalidationOnFreshStats = "tidb_plan_cache_invalidation_on_fresh_stats" @@ -1670,6 +1672,7 @@ const ( DefTiDBEnableNonPreparedPlanCacheForDML = true DefTiDBNonPreparedPlanCacheSize = 100 DefTiDBPlanCacheMaxPlanSize = 2 * size.MB + DefTiDBPlanCachePolicy = PlanCachePolicyAll DefTiDBInstancePlanCacheMaxMemSize = 100 * size.MB MinTiDBInstancePlanCacheMemSize = 100 * size.MB DefTiDBInstancePlanCacheReservedPercentage = 0.1 @@ -2121,6 +2124,11 @@ const ( // StrategyCustom is a choice of variable TiDBPipelinedDmlResourcePolicy, StrategyCustom = "custom" + // PlanCachePolicyAll means all cacheable statements can use plan cache. + PlanCachePolicyAll = "all" + // PlanCachePolicyHintOnly means only statements with the USE_PLAN_CACHE() hint can use plan cache. + PlanCachePolicyHintOnly = "hint_only" + // IndexLookUpPushDownPolicyHintOnly indicates only use the hint to decide whether to push down the index lookup or not. IndexLookUpPushDownPolicyHintOnly = "hint-only" // IndexLookUpPushDownPolicyAffinityForce indicates to force push down the index lookup for table with affinity options. diff --git a/pkg/sessionctx/variable/session.go b/pkg/sessionctx/variable/session.go index 90107e5d227e6..4b15d57c1721a 100644 --- a/pkg/sessionctx/variable/session.go +++ b/pkg/sessionctx/variable/session.go @@ -1642,6 +1642,9 @@ type SessionVars struct { // PlanCacheMaxPlanSize controls the maximum size of a plan that can be cached. PlanCacheMaxPlanSize uint64 + // PlanCachePolicy controls how plan cache is enabled. + PlanCachePolicy string + // SessionPlanCacheSize controls the size of session plan cache. SessionPlanCacheSize uint64 @@ -2455,6 +2458,7 @@ func NewSessionVars(hctx HookContext) *SessionVars { RegardNULLAsPoint: vardef.DefTiDBRegardNULLAsPoint, AllowProjectionPushDown: vardef.DefOptEnableProjectionPushDown, SkipMissingPartitionStats: vardef.DefTiDBSkipMissingPartitionStats, + PlanCachePolicy: vardef.DefTiDBPlanCachePolicy, IndexLookUpPushDownPolicy: vardef.DefTiDBIndexLookUpPushDownPolicy, OptPartialOrderedIndexForTopN: vardef.DefTiDBOptPartialOrderedIndexForTopN, } diff --git a/pkg/sessionctx/variable/setvar_affect.go b/pkg/sessionctx/variable/setvar_affect.go index 258de00d355dd..596ae1342a44f 100644 --- a/pkg/sessionctx/variable/setvar_affect.go +++ b/pkg/sessionctx/variable/setvar_affect.go @@ -114,6 +114,7 @@ var isHintUpdatableVerified = map[string]struct{}{ "tidb_enable_prepared_plan_cache": {}, "tidb_enable_non_prepared_plan_cache": {}, "tidb_plan_cache_max_plan_size": {}, + "tidb_plan_cache_policy": {}, "tidb_opt_range_max_size": {}, "tidb_opt_advanced_join_hint": {}, "tidb_opt_prefix_index_single_scan": {}, diff --git a/pkg/sessionctx/variable/sysvar.go b/pkg/sessionctx/variable/sysvar.go index e8ffc994bb9a2..250efd87a111f 100644 --- a/pkg/sessionctx/variable/sysvar.go +++ b/pkg/sessionctx/variable/sysvar.go @@ -1587,6 +1587,10 @@ var defaultSysVars = []*SysVar{ } return err }}, + {Scope: vardef.ScopeGlobal | vardef.ScopeSession, Name: vardef.TiDBPlanCachePolicy, Value: vardef.DefTiDBPlanCachePolicy, Type: vardef.TypeEnum, PossibleValues: []string{vardef.PlanCachePolicyAll, vardef.PlanCachePolicyHintOnly}, SetSession: func(s *SessionVars, val string) error { + s.PlanCachePolicy = val + return nil + }}, {Scope: vardef.ScopeGlobal | vardef.ScopeSession, Name: vardef.TiDBSessionPlanCacheSize, Aliases: []string{vardef.TiDBPrepPlanCacheSize}, Value: strconv.FormatUint(uint64(vardef.DefTiDBSessionPlanCacheSize), 10), Type: vardef.TypeUnsigned, MinValue: 1, MaxValue: 100000, SetSession: func(s *SessionVars, val string) error { uVal, err := strconv.ParseUint(val, 10, 64) if err == nil { diff --git a/pkg/util/hint/hint.go b/pkg/util/hint/hint.go index 5d4af787ac81d..9cfdebfe01cb3 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 enabling 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 + // Force statement to use plan cache. + 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 }