forked from pingcap/tidb
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathplan_cache_utils.go
More file actions
836 lines (749 loc) · 30.5 KB
/
plan_cache_utils.go
File metadata and controls
836 lines (749 loc) · 30.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
// 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"
"sort"
"strconv"
"time"
"github.com/pingcap/errors"
"github.com/pingcap/tidb/pkg/bindinfo"
"github.com/pingcap/tidb/pkg/config"
"github.com/pingcap/tidb/pkg/domain"
"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/core/base"
"github.com/pingcap/tidb/pkg/planner/core/resolve"
"github.com/pingcap/tidb/pkg/planner/core/rule"
"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/table"
"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/dbterror/plannererrors"
"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/logutil"
"github.com/pingcap/tidb/pkg/util/size"
"github.com/pingcap/tidb/pkg/util/zeropool"
atomic2 "go.uber.org/atomic"
"go.uber.org/zap"
)
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)
)
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<? and b>?'.
// 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, base.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, plannererrors.ErrPrepareDDL
}
switch stmt := paramStmt.(type) {
case *ast.ImportIntoStmt, *ast.LoadDataStmt, *ast.PrepareStmt, *ast.ExecuteStmt, *ast.DeallocateStmt, *ast.NonTransactionalDMLStmt:
return nil, nil, 0, plannererrors.ErrUnsupportedPs
case *ast.SelectStmt:
if stmt.SelectIntoOpt != nil {
return nil, nil, 0, plannererrors.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, plannererrors.ErrPsManyParam
}
ret := &PreprocessorReturn{InfoSchema: is} // is can be nil, and
nodeW := resolve.NewNodeW(paramStmt)
err := Preprocess(ctx, sctx, nodeW, 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: stmtctx.GetStmtLabel(ctx, paramStmt),
}
normalizedSQL, digest := parser.NormalizeDigest(prepared.Stmt.Text())
var (
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.GetPlanCtx(), paramStmt, ret.InfoSchema)
} else {
cacheable = true // it is already checked here
}
if !cacheable && fixcontrol.GetBoolWithDefault(vars.OptimizerFixControl, fixcontrol.Fix49736, false) {
sctx.GetSessionVars().StmtCtx.AppendWarning(errors.NewNoStackErrorf("force plan-cache: may use risky cached plan: %s", reason))
cacheable = true
reason = ""
}
if !cacheable {
sctx.GetSessionVars().StmtCtx.AppendWarning(errors.NewNoStackError("skip prepared plan-cache: " + reason))
}
}
// For prepared statements like `prepare st from 'select * from t where a<?'`,
// parameters are unknown here, so regard them all as NULL.
// For non-prepared statements, all parameters are already initialized at `ParameterizeAST`, so no need to set NULL.
if isPrepStmt {
for i := range extractor.markers {
param := extractor.markers[i].(*driver.ParamMarkerExpr)
param.Datum.SetNull()
param.InExecute = false
}
}
var p base.Plan
destBuilder, _ := NewPlanBuilder().Init(sctx.GetPlanCtx(), ret.InfoSchema, hint.NewQBHintHandler(nil))
p, err = destBuilder.Build(ctx, nodeW)
if err != nil {
return nil, nil, 0, err
}
if cacheable && destBuilder.optFlag&rule.FlagPartitionProcessor > 0 {
// dynamic prune mode is not used, could be that global statistics not yet available!
cacheable = false
reason = "static partition prune mode used"
sctx.GetSessionVars().StmtCtx.AppendWarning(errors.NewNoStackError("skip prepared plan-cache: " + reason))
}
// Collect information for metadata lock.
dbName := make([]model.CIStr, 0, len(vars.StmtCtx.MDLRelatedTableIDs))
tbls := make([]table.Table, 0, len(vars.StmtCtx.MDLRelatedTableIDs))
relateVersion := make(map[int64]uint64, len(vars.StmtCtx.MDLRelatedTableIDs))
for id := range vars.StmtCtx.MDLRelatedTableIDs {
tbl, ok := is.TableByID(ctx, id)
if !ok {
logutil.BgLogger().Error("table not found in info schema", zap.Int64("tableID", id))
return nil, nil, 0, errors.New("table not found in info schema")
}
db, ok := is.SchemaByID(tbl.Meta().DBID)
if !ok {
logutil.BgLogger().Error("database not found in info schema", zap.Int64("dbID", tbl.Meta().DBID))
return nil, nil, 0, errors.New("database not found in info schema")
}
dbName = append(dbName, db.Name)
tbls = append(tbls, tbl)
relateVersion[id] = tbl.Meta().Revision
}
preparedObj := &PlanCacheStmt{
PreparedAst: prepared,
ResolveCtx: nodeW.GetResolveContext(),
StmtDB: vars.CurrentDB,
StmtText: paramSQL,
VisitInfos: destBuilder.GetVisitInfo(),
NormalizedSQL: normalizedSQL,
SQLDigest: digest,
ForUpdateRead: destBuilder.GetIsForUpdateRead(),
SnapshotTSEvaluator: ret.SnapshotTSEvaluator,
StmtCacheable: cacheable,
UncacheableReason: reason,
dbName: dbName,
tbls: tbls,
SchemaVersion: ret.InfoSchema.SchemaMetaVersion(),
RelateVersion: relateVersion,
Params: extractor.markers,
}
stmtProcessor := &planCacheStmtProcessor{ctx: ctx, is: is, stmt: preparedObj}
paramStmt.Accept(stmtProcessor)
if err = checkPreparedPriv(ctx, sctx, preparedObj, ret.InfoSchema); err != nil {
return nil, nil, 0, err
}
return preparedObj, p, paramCount, nil
}
func hashInt64Uint64Map(b []byte, m map[int64]uint64) []byte {
keys := make([]int64, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
return keys[i] < keys[j]
})
for _, k := range keys {
v := m[k]
b = codec.EncodeInt(b, k)
b = codec.EncodeUint(b, v)
}
return b
}
// NewPlanCacheKey creates the plan cache key for this statement.
// 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.
// 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) {
binding, ignored := bindinfo.MatchSQLBindingForPlanCache(sctx, stmt.PreparedAst.Stmt, &stmt.BindingInfo)
if ignored {
return "", binding, false, "ignore plan cache by binding", nil
}
// In rc or for update read, we need the latest schema version to decide whether we need to
// rebuild the plan. So we set this value in rc or for update read. In other cases, let it be 0.
var latestSchemaVersion int64
if sctx.GetSessionVars().IsIsolation(ast.ReadCommitted) || stmt.ForUpdateRead {
// In Rc or ForUpdateRead, we should check if the information schema has been changed since
// last time. If it changed, we should rebuild the plan. Here, we use a different and more
// up-to-date schema version which can lead plan cache miss and thus, the plan will be rebuilt.
latestSchemaVersion = domain.GetDomain(sctx).InfoSchema().SchemaMetaVersion()
}
// rebuild key to exclude kv.TiFlash when stmt is not read only
vars := sctx.GetSessionVars()
if _, isolationReadContainTiFlash := vars.IsolationReadEngines[kv.TiFlash]; isolationReadContainTiFlash && !IsReadOnly(stmt.PreparedAst.Stmt, vars) {
delete(vars.IsolationReadEngines, kv.TiFlash)
defer func() {
vars.IsolationReadEngines[kv.TiFlash] = struct{}{}
}()
}
if stmt.StmtText == "" {
return "", "", false, "", errors.New("no statement text")
}
if stmt.SchemaVersion == 0 && !intest.InTest {
return "", "", false, "", errors.New("Schema version uninitialized")
}
if stmt.hasSubquery && !vars.EnablePlanCacheForSubquery {
return "", "", false, "the switch 'tidb_enable_plan_cache_for_subquery' is off", nil
}
if len(stmt.limits) > 0 && !vars.EnablePlanCacheForParamLimit {
return "", "", false, "the switch 'tidb_enable_plan_cache_for_param_limit' is off", nil
}
stmtDB := stmt.StmtDB
if stmtDB == "" {
stmtDB = vars.CurrentDB
}
timezoneOffset := 0
if vars.TimeZone != nil {
_, timezoneOffset = time.Now().In(vars.TimeZone).Zone()
}
connCharset, connCollation := vars.GetCharsetInfo()
// not allow to share the same plan among different users for safety.
var userName, hostName string
if sctx.GetSessionVars().User != nil { // might be nil if in test
userName = sctx.GetSessionVars().User.AuthUsername
hostName = sctx.GetSessionVars().User.AuthHostname
}
// the user might switch the prune mode dynamically
pruneMode := sctx.GetSessionVars().PartitionPruneMode.Load()
// precalculate the length of the hash buffer, note each time add an element to the buffer, need
// to update hashLen accordingly
// basic informations
hashLen := len(userName) + len(hostName) + len(stmtDB) + len(stmt.StmtText)
// schemaVersion + relateVersion + pruneMode
hashLen += 8 + len(stmt.RelateVersion)*16 + len(pruneMode)
// latestSchemaVersion + sqlMode + timeZoneOffset + isolationReadEngines + selectLimit
hashLen += 8 + 8 + 8 + 4 /*len(kv.TiDB.Name())*/ + 4 /*len(kv.TiKV.Name())*/ + 7 /*len(kv.TiFlash.Name())*/ + 8
// binding + connCharset + connCollation + inRestrictedSQL + readOnly + superReadOnly + exprPushdownBlacklistReloadTimeStamp + hasSubquery + foreignKeyChecks
hashLen += len(binding) + len(connCharset) + len(connCollation) + 3*5 + 8 + 2
if len(stmt.limits) > 0 {
// '|' + each limit count/offset takes 8 bytes + '|'
hashLen += 2 + len(stmt.limits)*2*8
}
if vars.GetSessionVars().PlanCacheInvalidationOnFreshStats {
// statsVerHash
hashLen += 8
}
// dirty tables
hashLen += 8 * len(vars.StmtCtx.TblInfo2UnionScan)
// txn status
hashLen += 6
hash := make([]byte, 0, hashLen)
// hashInitCap is not used, just for test purposes
hashInitCap := cap(hash)
hash = append(hash, userName...)
hash = append(hash, hostName...)
hash = append(hash, stmtDB...)
hash = append(hash, stmt.StmtText...)
hash = codec.EncodeInt(hash, stmt.SchemaVersion)
hash = hashInt64Uint64Map(hash, stmt.RelateVersion)
hash = append(hash, pruneMode...)
// Only be set in rc or for update read and leave it default otherwise.
// In Rc or ForUpdateRead, we should check whether the information schema has been changed when using plan cache.
// If it changed, we should rebuild the plan. lastUpdatedSchemaVersion help us to decide whether we should rebuild
// the plan in rc or for update read.
hash = codec.EncodeInt(hash, latestSchemaVersion)
hash = codec.EncodeInt(hash, int64(vars.SQLMode))
hash = codec.EncodeInt(hash, int64(timezoneOffset))
if _, ok := vars.IsolationReadEngines[kv.TiDB]; ok {
hash = append(hash, kv.TiDB.Name()...)
}
if _, ok := vars.IsolationReadEngines[kv.TiKV]; ok {
hash = append(hash, kv.TiKV.Name()...)
}
if _, ok := vars.IsolationReadEngines[kv.TiFlash]; ok {
hash = append(hash, kv.TiFlash.Name()...)
}
hash = codec.EncodeInt(hash, int64(vars.SelectLimit))
hash = append(hash, binding...)
hash = append(hash, connCharset...)
hash = append(hash, connCollation...)
hash = append(hash, strconv.FormatBool(vars.InRestrictedSQL)...)
hash = append(hash, strconv.FormatBool(variable.RestrictedReadOnly.Load())...)
hash = append(hash, strconv.FormatBool(variable.VarTiDBSuperReadOnly.Load())...)
// expr-pushdown-blacklist can affect query optimization, so we need to consider it in plan cache.
hash = codec.EncodeInt(hash, expression.ExprPushDownBlackListReloadTimeStamp.Load())
// whether this query has sub-query
hash = append(hash, bool2Byte(stmt.hasSubquery))
// this variable might affect the plan
hash = append(hash, bool2Byte(vars.ForeignKeyChecks))
// "limit ?" can affect the cached plan: "limit 1" and "limit 10000" should use different plans.
if len(stmt.limits) > 0 {
hash = append(hash, '|')
for _, node := range stmt.limits {
for _, valNode := range []ast.ExprNode{node.Count, node.Offset} {
if valNode == nil {
continue
}
if param, isParam := valNode.(*driver.ParamMarkerExpr); isParam {
typeExpected, val := CheckParamTypeInt64orUint64(param)
if !typeExpected {
return "", "", false, "unexpected value after LIMIT", nil
}
if val > MaxCacheableLimitCount {
return "", "", false, "limit count is too large", nil
}
hash = codec.EncodeUint(hash, val)
}
}
}
hash = append(hash, '|')
}
// stats ver can affect cached plan
if sctx.GetSessionVars().PlanCacheInvalidationOnFreshStats {
var statsVerHash uint64
for _, t := range stmt.tables {
statsVerHash += getLatestVersionFromStatsTable(sctx, t.Meta(), t.Meta().ID) // use '+' as the hash function for simplicity
}
hash = codec.EncodeUint(hash, statsVerHash)
}
// handle dirty tables
dirtyTables := vars.StmtCtx.TblInfo2UnionScan
if len(dirtyTables) > 0 {
// Get int64 slice from pool
dirtyTableIDs := dirtyTableIDsPool.Get()[:0]
if cap(dirtyTableIDs) < len(dirtyTables) {
dirtyTableIDs = make([]int64, 0, len(dirtyTables))
}
for t, dirty := range dirtyTables {
if !dirty {
continue
}
dirtyTableIDs = append(dirtyTableIDs, t.ID)
}
slices.Sort(dirtyTableIDs)
for _, id := range dirtyTableIDs {
hash = codec.EncodeInt(hash, id)
}
// Return slice to pool
dirtyTableIDsPool.Put(dirtyTableIDs)
}
// txn status
hash = append(hash, '|')
hash = append(hash, bool2Byte(vars.InTxn()))
hash = append(hash, bool2Byte(vars.IsAutocommit()))
hash = append(hash, bool2Byte(config.GetGlobalConfig().PessimisticTxn.PessimisticAutoCommit.Load()))
hash = append(hash, bool2Byte(vars.StmtCtx.ForShareLockEnabledByNoop))
hash = append(hash, bool2Byte(vars.SharedLockPromotion))
if intest.InTest {
if cap(hash) != hashInitCap {
panic("unexpected hash buffer realloc in NewPlanCacheKey")
}
}
return string(hack.String(hash)), binding, true, "", nil
}
func bool2Byte(flag bool) byte {
if flag {
return '1'
}
return '0'
}
// PlanCacheValue stores the cached Statement and StmtNode.
type PlanCacheValue struct {
Plan base.Plan // not-read-only, session might update it before reusing
OutputColumns types.NameSlice // read-only
memoryUsage int64 // read-only
testKey int64 // test-only
paramTypes []*types.FieldType // read-only, all parameters' types, different parameters may share same plan
stmtHints *hint.StmtHints // read-only, hints which set session variables
}
// CloneForInstancePlanCache clones a PlanCacheValue for instance plan cache.
// Since PlanCacheValue.Plan is not read-only, to solve the concurrency problem when sharing the same PlanCacheValue
// across multiple sessions, we need to clone the PlanCacheValue for each session.
func (v *PlanCacheValue) CloneForInstancePlanCache(ctx context.Context, newCtx base.PlanContext) (*PlanCacheValue, bool) {
clonedPlan, ok := v.Plan.CloneForPlanCache(newCtx)
if !ok {
return nil, false
}
if intest.InTest && ctx.Value(PlanCacheKeyTestClone{}) != nil {
ctx.Value(PlanCacheKeyTestClone{}).(func(plan, cloned base.Plan))(v.Plan, clonedPlan)
}
cloned := new(PlanCacheValue)
*cloned = *v
cloned.Plan = clonedPlan
return cloned, true
}
// 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 base.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.OutputColumns))*size.SizeOfPointer +
size.SizeOfMap + size.SizeOfInt64*2
if v.paramTypes != nil {
sum += int64(cap(v.paramTypes)) * size.SizeOfPointer
for _, ft := range v.paramTypes {
sum += ft.MemoryUsage()
}
}
for _, name := range v.OutputColumns {
sum += name.MemoryUsage()
}
v.memoryUsage = sum
return
}
// dirtyTableIDsPool is a pool for int64 slices used in NewPlanCacheKey.
var dirtyTableIDsPool = zeropool.New[[]int64](func() []int64 {
return make([]int64, 0, 8)
})
// NewPlanCacheValue creates a SQLCacheValue.
func NewPlanCacheValue(plan base.Plan, names []*types.FieldName,
paramTypes []*types.FieldType, stmtHints *hint.StmtHints) *PlanCacheValue {
userParamTypes := make([]*types.FieldType, len(paramTypes))
for i, tp := range paramTypes {
userParamTypes[i] = tp.Clone()
}
return &PlanCacheValue{
Plan: plan,
OutputColumns: names,
paramTypes: userParamTypes,
stmtHints: stmtHints.Clone(),
}
}
// planCacheStmtProcessor records all query features which may affect plan selection.
type planCacheStmtProcessor struct {
ctx context.Context
is infoschema.InfoSchema
stmt *PlanCacheStmt
}
// Enter implements Visitor interface.
func (f *planCacheStmtProcessor) Enter(in ast.Node) (out ast.Node, skipChildren bool) {
switch node := in.(type) {
case *ast.Limit:
f.stmt.limits = append(f.stmt.limits, node)
case *ast.SubqueryExpr, *ast.ExistsSubqueryExpr:
f.stmt.hasSubquery = true
case *ast.TableName:
t, err := f.is.TableByName(f.ctx, node.Schema, node.Name)
if err == nil {
f.stmt.tables = append(f.stmt.tables, t)
}
}
return in, false
}
// Leave implements Visitor interface.
func (*planCacheStmtProcessor) Leave(in ast.Node) (out ast.Node, ok bool) {
return in, true
}
// PointGetExecutorCache caches the PointGetExecutor to further improve its performance.
// Don't forget to reset this executor when the prior plan is invalid.
type PointGetExecutorCache struct {
ColumnInfos any
// 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 any
}
// PlanCacheStmt store prepared ast from PrepareExec and other related fields
type PlanCacheStmt struct {
PreparedAst *ast.Prepared
ResolveCtx *resolve.Context
StmtDB string // which DB the statement will be processed over
VisitInfos []visitInfo
Params []ast.ParamMarkerExpr
PointGet PointGetExecutorCache
// below fields are for PointGet short path
SchemaVersion int64
// RelateVersion stores the true cache plan table schema version, since each table schema can be updated separately in transaction.
RelateVersion map[int64]uint64
StmtCacheable bool // Whether this stmt is cacheable.
UncacheableReason string // Why this stmt is uncacheable.
limits []*ast.Limit
hasSubquery bool
tables []table.Table // to capture table stats changes
NormalizedSQL string
NormalizedPlan string
SQLDigest *parser.Digest
PlanDigest *parser.Digest
ForUpdateRead bool
SnapshotTSEvaluator func(context.Context, sessionctx.Context) (uint64, error)
BindingInfo bindinfo.BindingMatchInfo
// the different between NormalizedSQL, NormalizedSQL4PC and StmtText:
// for the query `select * from t where a>1 and b<?`, then
// NormalizedSQL: select * from `t` where `a` > ? 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
// dbName and tbls are used to add metadata lock.
dbName []model.CIStr
tbls []table.Table
}
// PrepareStmtCacheEntry is stored in the session-level prepare dedup cache.
// It holds the result of a full Prepare flow so that subsequent Prepares of
// the same SQL text can skip the expensive Preprocess + PlanBuilder.Build.
type PrepareStmtCacheEntry struct {
Stmt *PlanCacheStmt
Fields []*resolve.ResultField
ParamCount int
}
// ExtractAndSortParamMarkers extracts ParamMarkerExpr nodes from the AST,
// sorts them by position, assigns their order indices, and initialises each
// marker's Datum to NULL (matching the behaviour of GeneratePlanCacheStmtWithAST
// for prepared statements where the actual parameter values are not yet known).
func ExtractAndSortParamMarkers(stmtNode ast.StmtNode) []ast.ParamMarkerExpr {
var extractor paramMarkerExtractor
stmtNode.Accept(&extractor)
slices.SortFunc(extractor.markers, func(i, j ast.ParamMarkerExpr) int {
return cmp.Compare(i.(*driver.ParamMarkerExpr).Offset, j.(*driver.ParamMarkerExpr).Offset)
})
for i, m := range extractor.markers {
m.SetOrder(i)
p := m.(*driver.ParamMarkerExpr)
p.Datum.SetNull()
p.InExecute = false
}
return extractor.markers
}
// CollectPlanCacheStmtInfo walks the AST to populate the limits, hasSubquery,
// and tables fields of stmt. It must be called on the fresh AST after a
// re-parse so that limit nodes and table references point into the new tree.
func CollectPlanCacheStmtInfo(ctx context.Context, is infoschema.InfoSchema, stmt *PlanCacheStmt, stmtNode ast.StmtNode) {
processor := &planCacheStmtProcessor{ctx: ctx, is: is, stmt: stmt}
stmtNode.Accept(processor)
}
// DBName returns the dbName field (used for metadata lock during Execute).
func (s *PlanCacheStmt) DBName() []model.CIStr { return s.dbName }
// Tbls returns the tbls field (used for metadata lock during Execute).
func (s *PlanCacheStmt) Tbls() []table.Table { return s.tbls }
// SetDBNameAndTbls sets the dbName and tbls fields, cloning the input slices
// so that this PlanCacheStmt owns independent backing arrays. This is required
// because planCachePreprocess replaces tbls[i] in-place during Execute, and
// sharing the backing array with a cached template would cause cross-stmt contamination.
func (s *PlanCacheStmt) SetDBNameAndTbls(dbName []model.CIStr, tbls []table.Table) {
s.dbName = slices.Clone(dbName)
s.tbls = slices.Clone(tbls)
}
// 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, plannererrors.ErrStmtNotFound
}
// 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(expected, actual any) bool {
if expected == nil || actual == nil {
return true // no need to compare types
}
tpsExpected := expected.([]*types.FieldType)
tpsActual := actual.([]*types.FieldType)
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 base.PlanContext, 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
}
// parseParamTypes get parameters' types in PREPARE statement
func parseParamTypes(sctx sessionctx.Context, params []expression.Expression) (paramTypes []*types.FieldType) {
ectx := sctx.GetExprCtx().GetEvalCtx()
paramTypes = make([]*types.FieldType, 0, len(params))
for _, param := range params {
if c, ok := param.(*expression.Constant); ok { // from binary protocol
paramTypes = append(paramTypes, c.GetType(ectx))
continue
}
// from text protocol, there must be a GetVar function
name := param.(*expression.ScalarFunction).GetArgs()[0].StringWithCtx(ectx, errors.RedactLogDisable)
tp, ok := sctx.GetSessionVars().GetUserVarType(name)
if !ok {
tp = types.NewFieldType(mysql.TypeNull)
}
paramTypes = append(paramTypes, tp)
}
return
}