Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

planner: label plans as over-optimized for plan cache after refining cmp-function arguments (#41136) #42685

Merged
merged 4 commits into from
Mar 29, 2023
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
250 changes: 249 additions & 1 deletion executor/prepared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,254 @@ func TestPreparedNullParam(t *testing.T) {
}
}

func TestIssue38710(t *testing.T) {
store, clean := testkit.CreateMockStore(t)
defer clean()

orgEnable := plannercore.PreparedPlanCacheEnabled()
defer func() {
plannercore.SetPreparedPlanCache(orgEnable)
}()
plannercore.SetPreparedPlanCache(true)
tk := testkit.NewTestKit(t, store)

tk.MustExec("use test")
tk.MustExec("drop table if exists UK_NO_PRECISION_19392;")
tk.MustExec("CREATE TABLE `UK_NO_PRECISION_19392` (\n `COL1` bit(1) DEFAULT NULL,\n `COL2` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,\n `COL4` datetime DEFAULT NULL,\n `COL3` bigint DEFAULT NULL,\n `COL5` float DEFAULT NULL,\n UNIQUE KEY `UK_COL1` (`COL1`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;")
tk.MustExec("INSERT INTO `UK_NO_PRECISION_19392` VALUES (0x00,'缎馗惫砲兣肬憵急鳸嫅稩邏蠧鄂艘腯灩專妴粈','9294-12-26 06:50:40',-3088380202191555887,-3.33294e38),(NULL,'仲膩蕦圓猴洠飌镂喵疎偌嫺荂踖Ƕ藨蜿諪軁笞','1746-08-30 18:04:04',-4016793239832666288,-2.52633e38),(0x01,'冑溜畁脊乤纊繳蟥哅稐奺躁悼貘飗昹槐速玃沮','1272-01-19 23:03:27',-8014797887128775012,1.48868e38);\n")
tk.MustExec(`prepare stmt from 'select * from UK_NO_PRECISION_19392 where col1 between ? and ? or col3 = ? or col2 in (?, ?, ?);';`)
tk.MustExec("set @a=0x01, @b=0x01, @c=-3088380202191555887, @d=\"缎馗惫砲兣肬憵急鳸嫅稩邏蠧鄂艘腯灩專妴粈\", @e=\"缎馗惫砲兣肬憵急鳸嫅稩邏蠧鄂艘腯灩專妴粈\", @f=\"缎馗惫砲兣肬憵急鳸嫅稩邏蠧鄂艘腯灩專妴粈\";")
rows := tk.MustQuery(`execute stmt using @a,@b,@c,@d,@e,@f;`) // can not be cached because @a = @b
require.Equal(t, 2, len(rows.Rows()))

tk.MustExec(`set @a=NULL, @b=NULL, @c=-4016793239832666288, @d="缎馗惫砲兣肬憵急鳸嫅稩邏蠧鄂艘腯灩專妴粈", @e="仲膩蕦圓猴洠飌镂喵疎偌嫺荂踖Ƕ藨蜿諪軁笞", @f="缎馗惫砲兣肬憵急鳸嫅稩邏蠧鄂艘腯灩專妴粈";`)
rows = tk.MustQuery(`execute stmt using @a,@b,@c,@d,@e,@f;`)
require.Equal(t, 2, len(rows.Rows()))

rows = tk.MustQuery(`execute stmt using @a,@b,@c,@d,@e,@f;`)
require.Equal(t, 2, len(rows.Rows()))

tk.MustExec(`set @a=0x01, @b=0x01, @c=-3088380202191555887, @d="缎馗惫砲兣肬憵急鳸嫅稩邏蠧鄂艘腯灩專妴粈", @e="缎馗惫砲兣肬憵急鳸嫅稩邏蠧鄂艘腯灩專妴粈", @f="缎馗惫砲兣肬憵急鳸嫅稩邏蠧鄂艘腯灩專妴粈";`)
rows = tk.MustQuery(`execute stmt using @a,@b,@c,@d,@e,@f;`)
require.Equal(t, 2, len(rows.Rows()))
}

func TestIssue41626(t *testing.T) {
store, clean := testkit.CreateMockStore(t)
defer clean()

orgEnable := plannercore.PreparedPlanCacheEnabled()
defer func() {
plannercore.SetPreparedPlanCache(orgEnable)
}()
plannercore.SetPreparedPlanCache(true)
tk := testkit.NewTestKit(t, store)

tk.MustExec(`use test`)
tk.MustExec(`create table t (a year)`)
tk.MustExec(`insert into t values (2000)`)
tk.MustExec(`prepare st from 'select * from t where a<?'`)
tk.MustExec(`set @a=12`)
tk.MustQuery(`execute st using @a`).Check(testkit.Rows("2000"))
}

func TestIssue41828(t *testing.T) {
store, clean := testkit.CreateMockStore(t)
defer clean()

orgEnable := plannercore.PreparedPlanCacheEnabled()
defer func() {
plannercore.SetPreparedPlanCache(orgEnable)
}()
plannercore.SetPreparedPlanCache(true)
tk := testkit.NewTestKit(t, store)

tk.MustExec("use test")
tk.MustExec(`CREATE TABLE IDT_MULTI15840STROBJSTROBJ (
COL1 enum('aa', 'zzz') DEFAULT NULL,
COL2 smallint(6) DEFAULT NULL,
COL3 date DEFAULT NULL,
KEY U_M_COL4 (COL1,COL2),
KEY U_M_COL5 (COL3,COL2))`)

tk.MustExec(`INSERT INTO IDT_MULTI15840STROBJSTROBJ VALUES ('zzz',1047,'6115-06-05'),('zzz',-23221,'4250-09-03'),('zzz',27138,'1568-07-30'),('zzz',-30903,'6753-08-21'),('zzz',-26875,'6117-10-10')`)
tk.MustExec(`prepare stmt from 'select * from IDT_MULTI15840STROBJSTROBJ where col3 <=> ? or col1 in (?, ?, ?) and col2 not between ? and ?'`)
tk.MustExec(`set @a="0051-12-23", @b="none", @c="none", @d="none", @e=-32757, @f=-32757`)
tk.MustQuery(`execute stmt using @a,@b,@c,@d,@e,@f`).Check(testkit.Rows())
tk.MustExec(`set @a="9795-01-10", @b="aa", @c="aa", @d="aa", @e=31928, @f=31928`)
tk.MustQuery(`execute stmt using @a,@b,@c,@d,@e,@f`).Check(testkit.Rows())
}

func TestIssue40679(t *testing.T) {
store, clean := testkit.CreateMockStore(t)
defer clean()

orgEnable := plannercore.PreparedPlanCacheEnabled()
defer func() {
plannercore.SetPreparedPlanCache(orgEnable)
}()
plannercore.SetPreparedPlanCache(true)
tk := testkit.NewTestKit(t, store)

tk.MustExec("use test")
tk.MustExec("create table t (a int, key(a));")
tk.MustExec("prepare st from 'select * from t use index(a) where a < ?'")
tk.MustExec("set @a1=1.1")
tk.MustExec("execute st using @a1")

tkProcess := tk.Session().ShowProcess()
ps := []*util.ProcessInfo{tkProcess}
tk.Session().SetSessionManager(&testkit.MockSessionManager{PS: ps})
rows := tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Rows()
require.True(t, strings.Contains(rows[1][0].(string), "RangeScan")) // RangeScan not FullScan
}

func TestIssue41032(t *testing.T) {
store, clean := testkit.CreateMockStore(t)
defer clean()

orgEnable := plannercore.PreparedPlanCacheEnabled()
defer func() {
plannercore.SetPreparedPlanCache(orgEnable)
}()
plannercore.SetPreparedPlanCache(true)
tk := testkit.NewTestKit(t, store)

tk.MustExec("use test")
tk.MustExec(`CREATE TABLE PK_SIGNED_10087 (
COL1 mediumint(8) unsigned NOT NULL,
COL2 varchar(20) DEFAULT NULL,
COL4 datetime DEFAULT NULL,
COL3 bigint(20) DEFAULT NULL,
COL5 float DEFAULT NULL,
PRIMARY KEY (COL1) )`)
tk.MustExec(`insert into PK_SIGNED_10087 values(0, "痥腜蟿鮤枓欜喧檕澙姭袐裄钭僇剕焍哓閲疁櫘", "0017-11-14 05:40:55", -4504684261333179273, 7.97449e37)`)
tk.MustExec(`prepare stmt from 'SELECT/*+ HASH_JOIN(t1, t2) */ t2.* FROM PK_SIGNED_10087 t1 JOIN PK_SIGNED_10087 t2 ON t1.col1 = t2.col1 WHERE t2.col1 >= ? AND t1.col1 >= ?;'`)
tk.MustExec(`set @a=0, @b=0`)
tk.MustQuery(`execute stmt using @a,@b`).Check(testkit.Rows("0 痥腜蟿鮤枓欜喧檕澙姭袐裄钭僇剕焍哓閲疁櫘 0017-11-14 05:40:55 -4504684261333179273 79744900000000000000000000000000000000"))
tk.MustExec(`set @a=8950167, @b=16305982`)
tk.MustQuery(`execute stmt using @a,@b`).Check(testkit.Rows())
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("1"))
}

func TestInvalidRange(t *testing.T) {
store, clean := testkit.CreateMockStore(t)
defer clean()

orgEnable := plannercore.PreparedPlanCacheEnabled()
defer func() {
plannercore.SetPreparedPlanCache(orgEnable)
}()
plannercore.SetPreparedPlanCache(true)
tk := testkit.NewTestKit(t, store)

tk.MustExec("use test")
tk.MustExec("create table t (a int, key(a))")
tk.MustExec("prepare st from 'select * from t where a>? and a<?'")
tk.MustExec("set @l=100, @r=10")
tk.MustExec("execute st using @l, @r")

tkProcess := tk.Session().ShowProcess()
ps := []*util.ProcessInfo{tkProcess}
tk.Session().SetSessionManager(&testkit.MockSessionManager{PS: ps})

// use TableDual directly instead of TableFullScan
plan := make([]interface{}, 0, 1)
for _, r := range tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Rows() {
plan = append(plan, r[0])
}
require.Equal(t, plan, []interface{}{"TableDual_5"})

tk.MustExec("execute st using @l, @r")
tk.MustExec("execute st using @l, @r")
tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("0"))
}

func TestIssue40093(t *testing.T) {
store, clean := testkit.CreateMockStore(t)
defer clean()

orgEnable := plannercore.PreparedPlanCacheEnabled()
defer func() {
plannercore.SetPreparedPlanCache(orgEnable)
}()
plannercore.SetPreparedPlanCache(true)
tk := testkit.NewTestKit(t, store)

tk.MustExec("use test")
tk.MustExec("create table t1 (a int, b int)")
tk.MustExec("create table t2 (a int, b int, key(b, a))")
tk.MustExec("prepare st from 'select * from t1 left join t2 on t1.a=t2.a where t2.b in (?)'")
tk.MustExec("set @b=1")
tk.MustExec("execute st using @b")

tkProcess := tk.Session().ShowProcess()
ps := []*util.ProcessInfo{tkProcess}
tk.Session().SetSessionManager(&testkit.MockSessionManager{PS: ps})

// RangeScan instead of FullScan
plan := make([]interface{}, 0, 1)
for _, r := range tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Rows() {
plan = append(plan, r[0])
}
require.Equal(t, plan, []interface{}{
"Projection_9",
"└─HashJoin_21",
" ├─IndexReader_26(Build)",
" │ └─IndexRangeScan_25", // range scan
" └─TableReader_24(Probe)",
" └─Selection_23",
" └─TableFullScan_22",
})

tk.MustExec("execute st using @b")
tk.MustExec("execute st using @b")
tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("0"))
}

func TestIssue38205(t *testing.T) {
store, clean := testkit.CreateMockStore(t)
defer clean()

orgEnable := plannercore.PreparedPlanCacheEnabled()
defer func() {
plannercore.SetPreparedPlanCache(orgEnable)
}()
plannercore.SetPreparedPlanCache(true)
tk := testkit.NewTestKit(t, store)

tk.MustExec("use test")
tk.MustExec("CREATE TABLE `item` (`id` int, `vid` varbinary(16), `sid` int)")
tk.MustExec("CREATE TABLE `lv` (`item_id` int, `sid` int, KEY (`sid`,`item_id`))")

tk.MustExec("prepare stmt from 'SELECT /*+ TIDB_INLJ(lv, item) */ * FROM lv LEFT JOIN item ON lv.sid = item.sid AND lv.item_id = item.id WHERE item.sid = ? AND item.vid IN (?, ?)'")
tk.MustExec("set @a=1, @b='1', @c='3'")
tk.MustExec("execute stmt using @a, @b, @c")

tkProcess := tk.Session().ShowProcess()
ps := []*util.ProcessInfo{tkProcess}
tk.Session().SetSessionManager(&testkit.MockSessionManager{PS: ps})

// IndexJoin
plan := make([]interface{}, 0, 1)
for _, r := range tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Rows() {
plan = append(plan, r[0])
}
require.Equal(t, plan, []interface{}{
"IndexJoin_10",
"├─TableReader_19(Build)",
"│ └─Selection_18",
"│ └─TableFullScan_17",
"└─IndexReader_9(Probe)",
" └─Selection_8",
" └─IndexRangeScan_7",
})

tk.MustExec("execute stmt using @a, @b, @c")
tk.MustExec("execute stmt using @a, @b, @c")
tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("0"))
}

func TestIssue40224(t *testing.T) {
store, clean := testkit.CreateMockStore(t)
defer clean()
Expand Down Expand Up @@ -1182,7 +1430,7 @@ func TestPreparePlanCache4Function(t *testing.T) {
tk.MustExec("set @a = 0, @b = 1, @c = 2, @d = null;")
tk.MustQuery("execute stmt using @a, @b;").Check(testkit.Rows("<nil> 2", "0 0", "1 1", "2 2"))
tk.MustQuery("execute stmt using @c, @d;").Check(testkit.Rows("<nil> 1", "0 2", "1 2", "2 0"))
tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("1"))
tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("0"))
}

func TestPreparePlanCache4DifferentSystemVars(t *testing.T) {
Expand Down
57 changes: 43 additions & 14 deletions expression/builtin_compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -1556,6 +1556,43 @@ func RefineComparedConstant(ctx sessionctx.Context, targetFieldType types.FieldT
return con, false
}

// Since the argument refining of cmp functions can bring some risks to the plan-cache, the optimizer
// needs to decide to whether to skip the refining or skip plan-cache for safety.
// For example, `unsigned_int_col > ?(-1)` can be refined to `True`, but the validation of this result
// can be broken if the parameter changes to 1 after.
func allowCmpArgsRefining4PlanCache(ctx sessionctx.Context, args []Expression) (allowRefining bool) {
if !MaybeOverOptimized4PlanCache(ctx, args) {
return true // plan-cache disabled or no parameter in these args
}

// For these 2 cases below, we skip the refining:
// 1. year-expr <cmp> const
// 2. int-expr <cmp> string/float/double/decimal-const
for conIdx := 0; conIdx < 2; conIdx++ {
if _, isCon := args[conIdx].(*Constant); !isCon {
continue // not a constant
}

// case 1: year-expr <cmp> const
// refine `year < 12` to `year < 2012` to guarantee the correctness.
// see https://github.com/pingcap/tidb/issues/41626 for more details.
exprType := args[1-conIdx].GetType()
if exprType.GetType() == mysql.TypeYear {
ctx.GetSessionVars().StmtCtx.SkipPlanCache = true
return true
}

conType := args[conIdx].GetType().EvalType()
if exprType.EvalType() == types.ETInt &&
(conType == types.ETString || conType == types.ETReal || conType == types.ETDecimal) {
ctx.GetSessionVars().StmtCtx.SkipPlanCache = true
return true
}
}

return false
}

// refineArgs will rewrite the arguments if the compare expression is `int column <cmp> non-int constant` or
// `non-int constant <cmp> int column`. E.g., `a < 1.1` will be rewritten to `a < 2`. It also handles comparing year type
// with int constant if the int constant falls into a sensible year representation.
Expand All @@ -1565,25 +1602,17 @@ func (c *compareFunctionClass) refineArgs(ctx sessionctx.Context, args []Express
arg0Type, arg1Type := args[0].GetType(), args[1].GetType()
arg0IsInt := arg0Type.EvalType() == types.ETInt
arg1IsInt := arg1Type.EvalType() == types.ETInt
arg0IsString := arg0Type.EvalType() == types.ETString
arg1IsString := arg1Type.EvalType() == types.ETString
arg0, arg0IsCon := args[0].(*Constant)
arg1, arg1IsCon := args[1].(*Constant)
isExceptional, finalArg0, finalArg1 := false, args[0], args[1]
isPositiveInfinite, isNegativeInfinite := false, false
if MaybeOverOptimized4PlanCache(ctx, args) {
// To keep the result be compatible with MySQL, refine `int non-constant <cmp> str constant`
// here and skip this refine operation in all other cases for safety.
if (arg0IsInt && !arg0IsCon && arg1IsString && arg1IsCon) || (arg1IsInt && !arg1IsCon && arg0IsString && arg0IsCon) {
ctx.GetSessionVars().StmtCtx.SkipPlanCache = true
RemoveMutableConst(ctx, args)
} else {
return args
}
} else if ctx.GetSessionVars().StmtCtx.SkipPlanCache {
// We should remove the mutable constant for correctness, because its value may be changed.
RemoveMutableConst(ctx, args)

if !allowCmpArgsRefining4PlanCache(ctx, args) {
return args
}
// We should remove the mutable constant for correctness, because its value may be changed.
RemoveMutableConst(ctx, args)

// int non-constant [cmp] non-int constant
if arg0IsInt && !arg0IsCon && !arg1IsInt && arg1IsCon {
arg1, isExceptional = RefineComparedConstant(ctx, *arg0Type, arg1, c.op)
Expand Down
4 changes: 1 addition & 3 deletions planner/core/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,7 @@ func (s FieldSlice) CheckTypesCompatibility4PC(tps []*types.FieldType) bool {
// string types will show up here, and (2) we don't need flen and decimal to be matched exactly to use plan cache
tpEqual := (s[i].GetType() == tps[i].GetType()) ||
(s[i].GetType() == mysql.TypeVarchar && tps[i].GetType() == mysql.TypeVarString) ||
(s[i].GetType() == mysql.TypeVarString && tps[i].GetType() == mysql.TypeVarchar) ||
// TypeNull should be considered the same as other types.
(s[i].GetType() == mysql.TypeNull || tps[i].GetType() == mysql.TypeNull)
(s[i].GetType() == mysql.TypeVarString && tps[i].GetType() == mysql.TypeVarchar)
if !tpEqual || s[i].GetCharset() != tps[i].GetCharset() || s[i].GetCollate() != tps[i].GetCollate() ||
(s[i].EvalType() == types.ETInt && mysql.HasUnsignedFlag(s[i].GetFlag()) != mysql.HasUnsignedFlag(tps[i].GetFlag())) {
return false
Expand Down