// Copyright 2017 PingCAP, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package core import ( "cmp" "context" "math" "slices" "strconv" "time" "unsafe" "github.com/pingcap/errors" "github.com/pingcap/tidb/pkg/expression" "github.com/pingcap/tidb/pkg/infoschema" "github.com/pingcap/tidb/pkg/kv" "github.com/pingcap/tidb/pkg/parser" "github.com/pingcap/tidb/pkg/parser/ast" "github.com/pingcap/tidb/pkg/parser/model" "github.com/pingcap/tidb/pkg/parser/mysql" "github.com/pingcap/tidb/pkg/planner/util" "github.com/pingcap/tidb/pkg/planner/util/fixcontrol" "github.com/pingcap/tidb/pkg/sessionctx" "github.com/pingcap/tidb/pkg/sessionctx/stmtctx" "github.com/pingcap/tidb/pkg/sessionctx/variable" "github.com/pingcap/tidb/pkg/types" driver "github.com/pingcap/tidb/pkg/types/parser_driver" "github.com/pingcap/tidb/pkg/util/codec" "github.com/pingcap/tidb/pkg/util/hack" "github.com/pingcap/tidb/pkg/util/hint" "github.com/pingcap/tidb/pkg/util/intest" "github.com/pingcap/tidb/pkg/util/kvcache" utilpc "github.com/pingcap/tidb/pkg/util/plancache" "github.com/pingcap/tidb/pkg/util/size" atomic2 "go.uber.org/atomic" ) const ( // MaxCacheableLimitCount is the max limit count for cacheable query. MaxCacheableLimitCount = 10000 ) var ( // PreparedPlanCacheMaxMemory stores the max memory size defined in the global config "performance-server-memory-quota". PreparedPlanCacheMaxMemory = *atomic2.NewUint64(math.MaxUint64) // ExtractSelectAndNormalizeDigest extract the select statement and normalize it. ExtractSelectAndNormalizeDigest func(stmtNode ast.StmtNode, specifiledDB string, forBinding bool) (ast.StmtNode, string, string, error) ) type paramMarkerExtractor struct { markers []ast.ParamMarkerExpr } func (*paramMarkerExtractor) Enter(in ast.Node) (ast.Node, bool) { return in, false } func (e *paramMarkerExtractor) Leave(in ast.Node) (ast.Node, bool) { if x, ok := in.(*driver.ParamMarkerExpr); ok { e.markers = append(e.markers, x) } return in, true } // GeneratePlanCacheStmtWithAST generates the PlanCacheStmt structure for this AST. // paramSQL is the corresponding parameterized sql like 'select * from t where a?'. // paramStmt is the Node of paramSQL. func GeneratePlanCacheStmtWithAST(ctx context.Context, sctx sessionctx.Context, isPrepStmt bool, paramSQL string, paramStmt ast.StmtNode, is infoschema.InfoSchema) (*PlanCacheStmt, Plan, int, error) { vars := sctx.GetSessionVars() var extractor paramMarkerExtractor paramStmt.Accept(&extractor) // DDL Statements can not accept parameters if _, ok := paramStmt.(ast.DDLNode); ok && len(extractor.markers) > 0 { return nil, nil, 0, ErrPrepareDDL } switch stmt := paramStmt.(type) { case *ast.ImportIntoStmt, *ast.LoadDataStmt, *ast.PrepareStmt, *ast.ExecuteStmt, *ast.DeallocateStmt, *ast.NonTransactionalDMLStmt: return nil, nil, 0, ErrUnsupportedPs case *ast.SelectStmt: if stmt.SelectIntoOpt != nil { return nil, nil, 0, ErrUnsupportedPs } } // Prepare parameters should NOT over 2 bytes(MaxUint16) // https://dev.mysql.com/doc/internals/en/com-stmt-prepare-response.html#packet-COM_STMT_PREPARE_OK. if len(extractor.markers) > math.MaxUint16 { return nil, nil, 0, ErrPsManyParam } ret := &PreprocessorReturn{InfoSchema: is} // is can be nil, and err := Preprocess(ctx, sctx, paramStmt, InPrepare, WithPreprocessorReturn(ret)) if err != nil { return nil, nil, 0, err } // The parameter markers are appended in visiting order, which may not // be the same as the position order in the query string. We need to // sort it by position. slices.SortFunc(extractor.markers, func(i, j ast.ParamMarkerExpr) int { return cmp.Compare(i.(*driver.ParamMarkerExpr).Offset, j.(*driver.ParamMarkerExpr).Offset) }) paramCount := len(extractor.markers) for i := 0; i < paramCount; i++ { extractor.markers[i].SetOrder(i) } prepared := &ast.Prepared{ Stmt: paramStmt, StmtType: ast.GetStmtLabel(paramStmt), Params: extractor.markers, SchemaVersion: ret.InfoSchema.SchemaMetaVersion(), } normalizedSQL, digest := parser.NormalizeDigest(prepared.Stmt.Text()) var ( normalizedSQL4PC, digest4PC string selectStmtNode ast.StmtNode cacheable bool reason string ) if (isPrepStmt && !vars.EnablePreparedPlanCache) || // prepared statement (!isPrepStmt && !vars.EnableNonPreparedPlanCache) { // non-prepared statement cacheable = false reason = "plan cache is disabled" } else { if isPrepStmt { cacheable, reason = IsASTCacheable(ctx, sctx, paramStmt, ret.InfoSchema) } else { cacheable = true // it is already checked here } if !cacheable { sctx.GetSessionVars().StmtCtx.AppendWarning(errors.Errorf("skip prepared plan-cache: " + reason)) } selectStmtNode, normalizedSQL4PC, digest4PC, err = ExtractSelectAndNormalizeDigest(paramStmt, vars.CurrentDB, false) if err != nil || selectStmtNode == nil { normalizedSQL4PC = "" digest4PC = "" } } // For prepared statements like `prepare st from 'select * from t where a 0 { return key.memoryUsage } sum = emptyPlanCacheKeySize + int64(len(key.database)+len(key.stmtText)+len(key.bindSQL)+len(key.connCollation)) + int64(len(key.isolationReadEngines))*size.SizeOfUint8 + int64(cap(key.hash)) key.memoryUsage = sum return } // SetPstmtIDSchemaVersion implements PstmtCacheKeyMutator interface to change pstmtID and schemaVersion of cacheKey. // so we can reuse Key instead of new every time. func SetPstmtIDSchemaVersion(key kvcache.Key, stmtText string, schemaVersion int64, isolationReadEngines map[kv.StoreType]struct{}) { psStmtKey, isPsStmtKey := key.(*planCacheKey) if !isPsStmtKey { return } psStmtKey.stmtText = stmtText psStmtKey.schemaVersion = schemaVersion psStmtKey.isolationReadEngines = make(map[kv.StoreType]struct{}) for k, v := range isolationReadEngines { psStmtKey.isolationReadEngines[k] = v } psStmtKey.hash = psStmtKey.hash[:0] } // NewPlanCacheKey creates a new planCacheKey object. // Note: lastUpdatedSchemaVersion will only be set in the case of rc or for update read in order to // differentiate the cache key. In other cases, it will be 0. func NewPlanCacheKey(sessionVars *variable.SessionVars, stmtText, stmtDB string, schemaVersion int64, lastUpdatedSchemaVersion int64, bindSQL string, exprBlacklistTS int64) (kvcache.Key, error) { if stmtText == "" { return nil, errors.New("no statement text") } if schemaVersion == 0 && !intest.InTest { return nil, errors.New("Schema version uninitialized") } if stmtDB == "" { stmtDB = sessionVars.CurrentDB } timezoneOffset := 0 if sessionVars.TimeZone != nil { _, timezoneOffset = time.Now().In(sessionVars.TimeZone).Zone() } _, connCollation := sessionVars.GetCharsetInfo() key := &planCacheKey{ database: stmtDB, connID: sessionVars.ConnectionID, stmtText: stmtText, schemaVersion: schemaVersion, lastUpdatedSchemaVersion: lastUpdatedSchemaVersion, sqlMode: sessionVars.SQLMode, timezoneOffset: timezoneOffset, isolationReadEngines: make(map[kv.StoreType]struct{}), selectLimit: sessionVars.SelectLimit, bindSQL: bindSQL, connCollation: connCollation, inRestrictedSQL: sessionVars.InRestrictedSQL, restrictedReadOnly: variable.RestrictedReadOnly.Load(), TiDBSuperReadOnly: variable.VarTiDBSuperReadOnly.Load(), exprBlacklistTS: exprBlacklistTS, } for k, v := range sessionVars.IsolationReadEngines { key.isolationReadEngines[k] = v } return key, nil } // PlanCacheValue stores the cached Statement and StmtNode. type PlanCacheValue struct { Plan Plan OutPutNames []*types.FieldName TblInfo2UnionScan map[*model.TableInfo]bool memoryUsage int64 // matchOpts stores some fields help to choose a suitable plan matchOpts *utilpc.PlanCacheMatchOpts // stmtHints stores the hints which set session variables, because the hints won't be processed using cached plan. stmtHints *stmtctx.StmtHints } // unKnownMemoryUsage represent the memory usage of uncounted structure, maybe need implement later // 100 KiB is approximate consumption of a plan from our internal tests const unKnownMemoryUsage = int64(50 * size.KB) // MemoryUsage return the memory usage of PlanCacheValue func (v *PlanCacheValue) MemoryUsage() (sum int64) { if v == nil { return } if v.memoryUsage > 0 { return v.memoryUsage } switch x := v.Plan.(type) { case PhysicalPlan: sum = x.MemoryUsage() case *Insert: sum = x.MemoryUsage() case *Update: sum = x.MemoryUsage() case *Delete: sum = x.MemoryUsage() default: sum = unKnownMemoryUsage } sum += size.SizeOfInterface + size.SizeOfSlice*2 + int64(cap(v.OutPutNames))*size.SizeOfPointer + size.SizeOfMap + int64(len(v.TblInfo2UnionScan))*(size.SizeOfPointer+size.SizeOfBool) + size.SizeOfInt64*2 if v.matchOpts != nil { sum += int64(cap(v.matchOpts.ParamTypes)) * size.SizeOfPointer for _, ft := range v.matchOpts.ParamTypes { sum += ft.MemoryUsage() } } for _, name := range v.OutPutNames { sum += name.MemoryUsage() } v.memoryUsage = sum return } // NewPlanCacheValue creates a SQLCacheValue. func NewPlanCacheValue(plan Plan, names []*types.FieldName, srcMap map[*model.TableInfo]bool, matchOpts *utilpc.PlanCacheMatchOpts, stmtHints *stmtctx.StmtHints) *PlanCacheValue { dstMap := make(map[*model.TableInfo]bool) for k, v := range srcMap { dstMap[k] = v } userParamTypes := make([]*types.FieldType, len(matchOpts.ParamTypes)) for i, tp := range matchOpts.ParamTypes { userParamTypes[i] = tp.Clone() } return &PlanCacheValue{ Plan: plan, OutPutNames: names, TblInfo2UnionScan: dstMap, matchOpts: matchOpts, stmtHints: stmtHints.Clone(), } } // PlanCacheQueryFeatures records all query features which may affect plan selection. type PlanCacheQueryFeatures struct { limits []*ast.Limit hasSubquery bool tables []*ast.TableName // to capture table stats changes } // Enter implements Visitor interface. func (f *PlanCacheQueryFeatures) Enter(in ast.Node) (out ast.Node, skipChildren bool) { switch node := in.(type) { case *ast.Limit: f.limits = append(f.limits, node) case *ast.SubqueryExpr, *ast.ExistsSubqueryExpr: f.hasSubquery = true case *ast.TableName: f.tables = append(f.tables, node) } return in, false } // Leave implements Visitor interface. func (*PlanCacheQueryFeatures) Leave(in ast.Node) (out ast.Node, ok bool) { return in, true } // PlanCacheStmt store prepared ast from PrepareExec and other related fields type PlanCacheStmt struct { PreparedAst *ast.Prepared StmtDB string // which DB the statement will be processed over VisitInfos []visitInfo ColumnInfos interface{} // Executor is only used for point get scene. // Notice that we should only cache the PointGetExecutor that have a snapshot with MaxTS in it. // If the current plan is not PointGet or does not use MaxTS optimization, this value should be nil here. Executor interface{} StmtCacheable bool // Whether this stmt is cacheable. UncacheableReason string // Why this stmt is uncacheable. QueryFeatures *PlanCacheQueryFeatures NormalizedSQL string NormalizedPlan string SQLDigest *parser.Digest PlanDigest *parser.Digest ForUpdateRead bool SnapshotTSEvaluator func(sessionctx.Context) (uint64, error) NormalizedSQL4PC string SQLDigest4PC string // the different between NormalizedSQL, NormalizedSQL4PC and StmtText: // for the query `select * from t where a>1 and b ? and `b` < ? --> constants are normalized to '?', // NormalizedSQL4PC: select * from `test` . `t` where `a` > ? and `b` < ? --> schema name is added, // StmtText: select * from t where a>1 and b just format the original query; StmtText string } // GetPreparedStmt extract the prepared statement from the execute statement. func GetPreparedStmt(stmt *ast.ExecuteStmt, vars *variable.SessionVars) (*PlanCacheStmt, error) { if stmt.PrepStmt != nil { return stmt.PrepStmt.(*PlanCacheStmt), nil } if stmt.Name != "" { prepStmt, err := vars.GetPreparedStmtByName(stmt.Name) if err != nil { return nil, err } stmt.PrepStmt = prepStmt return prepStmt.(*PlanCacheStmt), nil } return nil, ErrStmtNotFound } // GetMatchOpts get options to fetch plan or generate new plan // we can add more options here func GetMatchOpts(sctx sessionctx.Context, is infoschema.InfoSchema, stmt *PlanCacheStmt, params []expression.Expression) (*utilpc.PlanCacheMatchOpts, error) { var statsVerHash uint64 var limitOffsetAndCount []uint64 if stmt.QueryFeatures != nil { for _, node := range stmt.QueryFeatures.tables { t, err := is.TableByName(node.Schema, node.Name) if err != nil { // CTE in this case continue } statsVerHash += getLatestVersionFromStatsTable(sctx, t.Meta(), t.Meta().ID) // use '+' as the hash function for simplicity } for _, node := range stmt.QueryFeatures.limits { if node.Count != nil { if count, isParamMarker := node.Count.(*driver.ParamMarkerExpr); isParamMarker { typeExpected, val := CheckParamTypeInt64orUint64(count) if !typeExpected { sctx.GetSessionVars().StmtCtx.SetSkipPlanCache(errors.New("unexpected value after LIMIT")) break } if val > MaxCacheableLimitCount { sctx.GetSessionVars().StmtCtx.SetSkipPlanCache(errors.New("limit count is too large")) break } limitOffsetAndCount = append(limitOffsetAndCount, val) } } if node.Offset != nil { if offset, isParamMarker := node.Offset.(*driver.ParamMarkerExpr); isParamMarker { typeExpected, val := CheckParamTypeInt64orUint64(offset) if !typeExpected { sctx.GetSessionVars().StmtCtx.SetSkipPlanCache(errors.New("unexpected value after LIMIT")) break } limitOffsetAndCount = append(limitOffsetAndCount, val) } } } } return &utilpc.PlanCacheMatchOpts{ LimitOffsetAndCount: limitOffsetAndCount, HasSubQuery: stmt.QueryFeatures.hasSubquery, StatsVersionHash: statsVerHash, ParamTypes: parseParamTypes(sctx, params), ForeignKeyChecks: sctx.GetSessionVars().ForeignKeyChecks, }, nil } // CheckTypesCompatibility4PC compares FieldSlice with []*types.FieldType // Currently this is only used in plan cache to check whether the types of parameters are compatible. // If the types of parameters are compatible, we can use the cached plan. // tpsExpected is types from cached plan func checkTypesCompatibility4PC(tpsExpected, tpsActual []*types.FieldType) bool { if len(tpsExpected) != len(tpsActual) { return false } for i := range tpsActual { // We only use part of logic of `func (ft *FieldType) Equal(other *FieldType)` here because (1) only numeric and // string types will show up here, and (2) we don't need flen and decimal to be matched exactly to use plan cache tpEqual := (tpsExpected[i].GetType() == tpsActual[i].GetType()) || (tpsExpected[i].GetType() == mysql.TypeVarchar && tpsActual[i].GetType() == mysql.TypeVarString) || (tpsExpected[i].GetType() == mysql.TypeVarString && tpsActual[i].GetType() == mysql.TypeVarchar) if !tpEqual || tpsExpected[i].GetCharset() != tpsActual[i].GetCharset() || tpsExpected[i].GetCollate() != tpsActual[i].GetCollate() || (tpsExpected[i].EvalType() == types.ETInt && mysql.HasUnsignedFlag(tpsExpected[i].GetFlag()) != mysql.HasUnsignedFlag(tpsActual[i].GetFlag())) { return false } // When the type is decimal, we should compare the Flen and Decimal. // We can only use the plan when both Flen and Decimal should less equal than the cached one. // We assume here that there is no correctness problem when the precision of the parameters is less than the precision of the parameters in the cache. if tpEqual && tpsExpected[i].GetType() == mysql.TypeNewDecimal && !(tpsExpected[i].GetFlen() >= tpsActual[i].GetFlen() && tpsExpected[i].GetDecimal() >= tpsActual[i].GetDecimal()) { return false } } return true } func isSafePointGetPath4PlanCache(sctx sessionctx.Context, path *util.AccessPath) bool { // PointGet might contain some over-optimized assumptions, like `a>=1 and a<=1` --> `a=1`, but // these assumptions may be broken after parameters change. if isSafePointGetPath4PlanCacheScenario1(path) { return true } // TODO: enable this fix control switch by default after more test cases are added. if sctx != nil && sctx.GetSessionVars() != nil && sctx.GetSessionVars().OptimizerFixControl != nil { fixControlOK := fixcontrol.GetBoolWithDefault(sctx.GetSessionVars().GetOptimizerFixControlMap(), fixcontrol.Fix44830, false) if fixControlOK && (isSafePointGetPath4PlanCacheScenario2(path) || isSafePointGetPath4PlanCacheScenario3(path)) { return true } } return false } func isSafePointGetPath4PlanCacheScenario1(path *util.AccessPath) bool { // safe scenario 1: each column corresponds to a single EQ, `a=1 and b=2 and c=3` --> `[1, 2, 3]` if len(path.Ranges) <= 0 || path.Ranges[0].Width() != len(path.AccessConds) { return false } for _, accessCond := range path.AccessConds { f, ok := accessCond.(*expression.ScalarFunction) if !ok || f.FuncName.L != ast.EQ { // column = constant return false } } return true } func isSafePointGetPath4PlanCacheScenario2(path *util.AccessPath) bool { // safe scenario 2: this Batch or PointGet is simply from a single IN predicate, `key in (...)` if len(path.Ranges) <= 0 || len(path.AccessConds) != 1 { return false } f, ok := path.AccessConds[0].(*expression.ScalarFunction) if !ok || f.FuncName.L != ast.In { return false } return len(path.Ranges) == len(f.GetArgs())-1 // no duplicated values in this in-list for safety. } func isSafePointGetPath4PlanCacheScenario3(path *util.AccessPath) bool { // safe scenario 3: this Batch or PointGet is simply from a simple DNF like `key=? or key=? or key=?` if len(path.Ranges) <= 0 || len(path.AccessConds) != 1 { return false } f, ok := path.AccessConds[0].(*expression.ScalarFunction) if !ok || f.FuncName.L != ast.LogicOr { return false } dnfExprs := expression.FlattenDNFConditions(f) if len(path.Ranges) != len(dnfExprs) { // no duplicated values in this in-list for safety. // e.g. `k=1 or k=2 or k=1` --> [[1, 1], [2, 2]] return false } for _, expr := range dnfExprs { f, ok := expr.(*expression.ScalarFunction) if !ok { return false } switch f.FuncName.L { case ast.EQ: // (k=1 or k=2) --> [k=1, k=2] case ast.LogicAnd: // ((k1=1 and k2=1) or (k1=2 and k2=2)) --> [k1=1 and k2=1, k2=2 and k2=2] cnfExprs := expression.FlattenCNFConditions(f) if path.Ranges[0].Width() != len(cnfExprs) { // not all key columns are specified return false } for _, expr := range cnfExprs { // k1=1 and k2=1 f, ok := expr.(*expression.ScalarFunction) if !ok || f.FuncName.L != ast.EQ { return false } } default: return false } } return true }