Skip to content

Commit e091ee5

Browse files
authored
planner: plan cache supports Batch/PointGet converted from (primary keys) in ((...), ...) (#44838) (#49380)
close #44830
1 parent 266d8f7 commit e091ee5

File tree

5 files changed

+161
-24
lines changed

5 files changed

+161
-24
lines changed

planner/core/find_best_task.go

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,7 @@ func (ds *DataSource) findBestTask(prop *property.PhysicalProperty, planCounter
10641064

10651065
// Batch/PointGet plans may be over-optimized, like `a>=1(?) and a<=1(?)` --> `a=1` --> PointGet(a=1).
10661066
// For safety, prevent these plans from the plan cache here.
1067-
if !pointGetTask.invalid() && expression.MaybeOverOptimized4PlanCache(ds.ctx, candidate.path.AccessConds) && !ds.isSafePointGetPlan4PlanCache(candidate.path) {
1067+
if !pointGetTask.invalid() && expression.MaybeOverOptimized4PlanCache(ds.ctx, candidate.path.AccessConds) && !isSafePointGetPath4PlanCache(ds.ctx, candidate.path) {
10681068
ds.ctx.GetSessionVars().StmtCtx.SetSkipPlanCache(errors.New("Batch/PointGet plans may be over-optimized"))
10691069
}
10701070

@@ -1147,26 +1147,6 @@ func (ds *DataSource) findBestTask(prop *property.PhysicalProperty, planCounter
11471147
return
11481148
}
11491149

1150-
func (ds *DataSource) isSafePointGetPlan4PlanCache(path *util.AccessPath) bool {
1151-
// PointGet might contain some over-optimized assumptions, like `a>=1 and a<=1` --> `a=1`, but
1152-
// these assumptions may be broken after parameters change.
1153-
1154-
// safe scenario 1: each column corresponds to a single EQ, `a=1 and b=2 and c=3` --> `[1, 2, 3]`
1155-
if len(path.Ranges) > 0 && path.Ranges[0].Width() == len(path.AccessConds) {
1156-
for _, accessCond := range path.AccessConds {
1157-
f, ok := accessCond.(*expression.ScalarFunction)
1158-
if !ok {
1159-
return false
1160-
}
1161-
if f.FuncName.L != ast.EQ {
1162-
return false
1163-
}
1164-
}
1165-
return true
1166-
}
1167-
return false
1168-
}
1169-
11701150
func (ds *DataSource) convertToIndexMergeScan(prop *property.PhysicalProperty, candidate *candidatePath, _ *physicalOptimizeOp) (task task, err error) {
11711151
if prop.IsFlashProp() || prop.TaskTp == property.CopSingleReadTaskType || !prop.IsSortItemEmpty() {
11721152
return invalidTask, nil

planner/core/plan_cache.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ func rebuildRange(p Plan) error {
448448
if err != nil {
449449
return err
450450
}
451-
if !isSafeRange(x.AccessConditions, ranges, false, nil) {
451+
if len(ranges.Ranges) != 1 || !isSafeRange(x.AccessConditions, ranges, false, nil) {
452452
return errors.New("rebuild to get an unsafe range")
453453
}
454454
for i := range x.IndexValues {
@@ -470,7 +470,7 @@ func rebuildRange(p Plan) error {
470470
if err != nil {
471471
return err
472472
}
473-
if !isSafeRange(x.AccessConditions, &ranger.DetachRangeResult{
473+
if len(ranges) != 1 || !isSafeRange(x.AccessConditions, &ranger.DetachRangeResult{
474474
Ranges: ranges,
475475
AccessConds: accessConds,
476476
RemainedConds: remainingConds,
@@ -539,7 +539,7 @@ func rebuildRange(p Plan) error {
539539
if err != nil {
540540
return err
541541
}
542-
if len(ranges) != len(x.Handles) && !isSafeRange(x.AccessConditions, &ranger.DetachRangeResult{
542+
if len(ranges) != len(x.Handles) || !isSafeRange(x.AccessConditions, &ranger.DetachRangeResult{
543543
Ranges: ranges,
544544
AccessConds: accessConds,
545545
RemainedConds: remainingConds,

planner/core/plan_cache_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,72 @@ func TestIssue43311(t *testing.T) {
8484
tk.MustQuery(`execute st using @a, @b`).Check(testkit.Rows()) // empty
8585
}
8686

87+
func TestIssue44830(t *testing.T) {
88+
store := testkit.CreateMockStore(t)
89+
tk := testkit.NewTestKit(t, store)
90+
tk.MustExec(`use test`)
91+
tk.MustExec(`set @@tidb_opt_fix_control = "44830:ON"`)
92+
tk.MustExec(`create table t (a int, primary key(a))`)
93+
tk.MustExec(`create table t1 (a int, b int, primary key(a, b))`) // multiple-column primary key
94+
tk.MustExec(`insert into t values (1), (2), (3)`)
95+
tk.MustExec(`insert into t1 values (1, 1), (2, 2), (3, 3)`)
96+
tk.MustExec(`set @a=1, @b=2, @c=3`)
97+
98+
// single-column primary key cases
99+
tk.MustExec(`prepare st from 'select * from t where 1=1 and a in (?, ?, ?)'`)
100+
tk.MustQuery(`execute st using @a, @b, @c`).Sort().Check(testkit.Rows("1", "2", "3"))
101+
tk.MustQuery(`execute st using @a, @b, @c`).Sort().Check(testkit.Rows("1", "2", "3"))
102+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("1"))
103+
tk.MustQuery(`execute st using @a, @b, @b`).Sort().Check(testkit.Rows("1", "2"))
104+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) // range length changed
105+
tk.MustQuery(`execute st using @b, @b, @b`).Sort().Check(testkit.Rows("2"))
106+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) // range length changed
107+
tk.MustQuery(`execute st using @a, @b, @c`).Sort().Check(testkit.Rows("1", "2", "3"))
108+
tk.MustQuery(`execute st using @a, @b, @b`).Sort().Check(testkit.Rows("1", "2"))
109+
tk.MustQuery(`execute st using @a, @b, @b`).Sort().Check(testkit.Rows("1", "2"))
110+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) // contain duplicated values in the in-list
111+
112+
// multi-column primary key cases
113+
tk.MustExec(`prepare st from 'select * from t1 where 1=1 and (a, b) in ((?, ?), (?, ?), (?, ?))'`)
114+
tk.MustQuery(`execute st using @a, @a, @b, @b, @c, @c`).Sort().Check(testkit.Rows("1 1", "2 2", "3 3"))
115+
tk.MustQuery(`execute st using @a, @a, @b, @b, @c, @c`).Sort().Check(testkit.Rows("1 1", "2 2", "3 3"))
116+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("1"))
117+
tk.MustQuery(`execute st using @a, @a, @b, @b, @b, @b`).Sort().Check(testkit.Rows("1 1", "2 2"))
118+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) // range length changed
119+
tk.MustQuery(`execute st using @b, @b, @b, @b, @b, @b`).Sort().Check(testkit.Rows("2 2"))
120+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) // range length changed
121+
tk.MustQuery(`execute st using @b, @b, @b, @b, @c, @c`).Sort().Check(testkit.Rows("2 2", "3 3"))
122+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) // range length changed
123+
tk.MustQuery(`execute st using @a, @a, @a, @a, @a, @a`).Sort().Check(testkit.Rows("1 1"))
124+
tk.MustQuery(`execute st using @a, @a, @a, @a, @a, @a`).Sort().Check(testkit.Rows("1 1"))
125+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) // contain duplicated values in the in-list
126+
tk.MustQuery(`execute st using @a, @a, @b, @b, @b, @b`).Sort().Check(testkit.Rows("1 1", "2 2"))
127+
tk.MustQuery(`execute st using @a, @a, @b, @b, @b, @b`).Sort().Check(testkit.Rows("1 1", "2 2"))
128+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) // contain duplicated values in the in-list
129+
}
130+
131+
func TestIssue44830NonPrep(t *testing.T) {
132+
store := testkit.CreateMockStore(t)
133+
tk := testkit.NewTestKit(t, store)
134+
tk.MustExec(`use test`)
135+
tk.MustExec(`set @@tidb_enable_non_prepared_plan_cache=1`)
136+
tk.MustExec(`set @@tidb_opt_fix_control = "44830:ON"`)
137+
tk.MustExec(`create table t1 (a int, b int, primary key(a, b))`) // multiple-column primary key
138+
tk.MustExec(`insert into t1 values (1, 1), (2, 2), (3, 3)`)
139+
tk.MustExec(`set @a=1, @b=2, @c=3`)
140+
141+
tk.MustQuery(`select * from t1 where 1=1 and (a, b) in ((1, 1), (2, 2), (3, 3))`).Sort().Check(testkit.Rows("1 1", "2 2", "3 3"))
142+
tk.MustQuery(`select * from t1 where 1=1 and (a, b) in ((1, 1), (2, 2), (3, 3))`).Sort().Check(testkit.Rows("1 1", "2 2", "3 3"))
143+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("1"))
144+
tk.MustQuery(`select * from t1 where 1=1 and (a, b) in ((1, 1), (2, 2), (2, 2))`).Sort().Check(testkit.Rows("1 1", "2 2"))
145+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0"))
146+
tk.MustQuery(`select * from t1 where 1=1 and (a, b) in ((2, 2), (2, 2), (2, 2))`).Sort().Check(testkit.Rows("2 2"))
147+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0"))
148+
tk.MustQuery(`select * from t1 where 1=1 and (a, b) in ((1, 1), (1, 1), (1, 1))`).Sort().Check(testkit.Rows("1 1"))
149+
tk.MustQuery(`select * from t1 where 1=1 and (a, b) in ((1, 1), (1, 1), (1, 1))`).Sort().Check(testkit.Rows("1 1"))
150+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0"))
151+
}
152+
87153
func TestPlanCacheSizeSwitch(t *testing.T) {
88154
store := testkit.CreateMockStore(t)
89155
tk := testkit.NewTestKit(t, store)

planner/core/plan_cache_utils.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/pingcap/tidb/parser/ast"
3030
"github.com/pingcap/tidb/parser/model"
3131
"github.com/pingcap/tidb/parser/mysql"
32+
"github.com/pingcap/tidb/planner/util"
3233
"github.com/pingcap/tidb/sessionctx"
3334
"github.com/pingcap/tidb/sessionctx/stmtctx"
3435
"github.com/pingcap/tidb/sessionctx/variable"
@@ -577,3 +578,90 @@ func checkTypesCompatibility4PC(tpsExpected, tpsActual []*types.FieldType) bool
577578
}
578579
return true
579580
}
581+
582+
func isSafePointGetPath4PlanCache(sctx sessionctx.Context, path *util.AccessPath) bool {
583+
// PointGet might contain some over-optimized assumptions, like `a>=1 and a<=1` --> `a=1`, but
584+
// these assumptions may be broken after parameters change.
585+
586+
if isSafePointGetPath4PlanCacheScenario1(path) {
587+
return true
588+
}
589+
590+
// TODO: enable this fix control switch by default after more test cases are added.
591+
if sctx != nil && sctx.GetSessionVars() != nil && sctx.GetSessionVars().OptimizerFixControl != nil {
592+
v, ok := sctx.GetSessionVars().OptimizerFixControl[variable.TiDBOptFixControl44830]
593+
if ok && variable.TiDBOptOn(v) && (isSafePointGetPath4PlanCacheScenario2(path) || isSafePointGetPath4PlanCacheScenario3(path)) {
594+
return true
595+
}
596+
}
597+
598+
return false
599+
}
600+
601+
func isSafePointGetPath4PlanCacheScenario1(path *util.AccessPath) bool {
602+
// safe scenario 1: each column corresponds to a single EQ, `a=1 and b=2 and c=3` --> `[1, 2, 3]`
603+
if len(path.Ranges) <= 0 || path.Ranges[0].Width() != len(path.AccessConds) {
604+
return false
605+
}
606+
for _, accessCond := range path.AccessConds {
607+
f, ok := accessCond.(*expression.ScalarFunction)
608+
if !ok || f.FuncName.L != ast.EQ { // column = constant
609+
return false
610+
}
611+
}
612+
return true
613+
}
614+
615+
func isSafePointGetPath4PlanCacheScenario2(path *util.AccessPath) bool {
616+
// safe scenario 2: this Batch or PointGet is simply from a single IN predicate, `key in (...)`
617+
if len(path.Ranges) <= 0 || len(path.AccessConds) != 1 {
618+
return false
619+
}
620+
f, ok := path.AccessConds[0].(*expression.ScalarFunction)
621+
if !ok || f.FuncName.L != ast.In {
622+
return false
623+
}
624+
return len(path.Ranges) == len(f.GetArgs())-1 // no duplicated values in this in-list for safety.
625+
}
626+
627+
func isSafePointGetPath4PlanCacheScenario3(path *util.AccessPath) bool {
628+
// safe scenario 3: this Batch or PointGet is simply from a simple DNF like `key=? or key=? or key=?`
629+
if len(path.Ranges) <= 0 || len(path.AccessConds) != 1 {
630+
return false
631+
}
632+
f, ok := path.AccessConds[0].(*expression.ScalarFunction)
633+
if !ok || f.FuncName.L != ast.LogicOr {
634+
return false
635+
}
636+
637+
dnfExprs := expression.FlattenDNFConditions(f)
638+
if len(path.Ranges) != len(dnfExprs) {
639+
// no duplicated values in this in-list for safety.
640+
// e.g. `k=1 or k=2 or k=1` --> [[1, 1], [2, 2]]
641+
return false
642+
}
643+
644+
for _, expr := range dnfExprs {
645+
f, ok := expr.(*expression.ScalarFunction)
646+
if !ok {
647+
return false
648+
}
649+
switch f.FuncName.L {
650+
case ast.EQ: // (k=1 or k=2) --> [k=1, k=2]
651+
case ast.LogicAnd: // ((k1=1 and k2=1) or (k1=2 and k2=2)) --> [k1=1 and k2=1, k2=2 and k2=2]
652+
cnfExprs := expression.FlattenCNFConditions(f)
653+
if path.Ranges[0].Width() != len(cnfExprs) { // not all key columns are specified
654+
return false
655+
}
656+
for _, expr := range cnfExprs { // k1=1 and k2=1
657+
f, ok := expr.(*expression.ScalarFunction)
658+
if !ok || f.FuncName.L != ast.EQ {
659+
return false
660+
}
661+
}
662+
default:
663+
return false
664+
}
665+
}
666+
return true
667+
}

sessionctx/variable/session.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,9 @@ var (
14931493
TiDBOptFixControl44262 uint64 = 44262
14941494
// TiDBOptFixControl44389 controls whether to consider non-point ranges of some CNF item when building ranges.
14951495
TiDBOptFixControl44389 uint64 = 44389
1496+
// TiDBOptFixControl44830 controls whether to allow to cache Batch/PointGet from some complex scenarios.
1497+
// See #44830 for more details.
1498+
TiDBOptFixControl44830 uint64 = 44830
14961499
// TiDBOptFixControl44823 controls the maximum number of parameters for a query that can be cached in the Plan Cache.
14971500
TiDBOptFixControl44823 uint64 = 44823
14981501
// TiDBOptFixControl44855 controls whether to use a more accurate upper bound when estimating row count of index

0 commit comments

Comments
 (0)