Skip to content

Commit

Permalink
planner: label plans as over-optimized for plan cache after refining …
Browse files Browse the repository at this point in the history
…cmp-function arguments (#41136) (#42684)

close #41032
  • Loading branch information
qw4990 authored Mar 29, 2023
1 parent 6e854ff commit 2adad09
Show file tree
Hide file tree
Showing 2 changed files with 329 additions and 18 deletions.
280 changes: 280 additions & 0 deletions executor/prepared_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,286 @@ func TestPreparedStmtWithHint(t *testing.T) {
require.Equal(t, int32(1), atomic.LoadInt32(&sm.killed))
}

func TestIssue38710(t *testing.T) {
store, dom, err := newStoreWithBootstrap()
require.NoError(t, err)
defer func() {
require.NoError(t, store.Close())
dom.Close()
}()

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, dom, err := newStoreWithBootstrap()
require.NoError(t, err)
defer func() {
require.NoError(t, store.Close())
dom.Close()
}()

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, dom, err := newStoreWithBootstrap()
require.NoError(t, err)
defer func() {
require.NoError(t, store.Close())
dom.Close()
}()

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, dom, err := newStoreWithBootstrap()
require.NoError(t, err)
defer func() {
require.NoError(t, store.Close())
dom.Close()
}()

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(&mockSessionManager1{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, dom, err := newStoreWithBootstrap()
require.NoError(t, err)
defer func() {
require.NoError(t, store.Close())
dom.Close()
}()

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, dom, err := newStoreWithBootstrap()
require.NoError(t, err)
defer func() {
require.NoError(t, store.Close())
dom.Close()
}()

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(&mockSessionManager1{PS: ps})

// use TableDual directly instead of TableFullScan
var plan []interface{}
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, dom, err := newStoreWithBootstrap()
require.NoError(t, err)
defer func() {
require.NoError(t, store.Close())
dom.Close()
}()

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(&mockSessionManager1{PS: ps})

// RangeScan instead of FullScan
var plan []interface{}
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, dom, err := newStoreWithBootstrap()
require.NoError(t, err)
defer func() {
require.NoError(t, store.Close())
dom.Close()
}()

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(&mockSessionManager1{PS: ps})

// IndexJoin
var plan []interface{}
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 TestIssue38533(t *testing.T) {
store, dom, err := newStoreWithBootstrap()
require.NoError(t, err)
Expand Down
67 changes: 49 additions & 18 deletions expression/builtin_compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -1432,8 +1432,9 @@ func isTemporalColumn(expr Expression) bool {
// If isExceptional is true, ExecptionalVal is returned. Or, CorrectVal is returned.
// CorrectVal: The computed result. If the constant can be converted to int without exception, return the val. Else return 'con'(the input).
// ExceptionalVal : It is used to get more information to check whether 'int column [cmp] const' is true/false
// If the op == LT,LE,GT,GE and it gets an Overflow when converting, return inf/-inf.
// If the op == EQ,NullEQ and the constant can never be equal to the int column, return ‘con’(the input, a non-int constant).
//
// If the op == LT,LE,GT,GE and it gets an Overflow when converting, return inf/-inf.
// If the op == EQ,NullEQ and the constant can never be equal to the int column, return ‘con’(the input, a non-int constant).
func tryToConvertConstantInt(ctx sessionctx.Context, targetFieldType *types.FieldType, con *Constant) (_ *Constant, isExceptional bool) {
if con.GetType().EvalType() == types.ETInt {
return con, false
Expand Down Expand Up @@ -1469,8 +1470,9 @@ func tryToConvertConstantInt(ctx sessionctx.Context, targetFieldType *types.Fiel
// If isExceptional is true, ExecptionalVal is returned. Or, CorrectVal is returned.
// CorrectVal: The computed result. If the constant can be converted to int without exception, return the val. Else return 'con'(the input).
// ExceptionalVal : It is used to get more information to check whether 'int column [cmp] const' is true/false
// If the op == LT,LE,GT,GE and it gets an Overflow when converting, return inf/-inf.
// If the op == EQ,NullEQ and the constant can never be equal to the int column, return ‘con’(the input, a non-int constant).
//
// If the op == LT,LE,GT,GE and it gets an Overflow when converting, return inf/-inf.
// If the op == EQ,NullEQ and the constant can never be equal to the int column, return ‘con’(the input, a non-int constant).
func RefineComparedConstant(ctx sessionctx.Context, targetFieldType types.FieldType, con *Constant, op opcode.Op) (_ *Constant, isExceptional bool) {
dt, err := con.Eval(chunk.Row{})
if err != nil {
Expand Down Expand Up @@ -1557,6 +1559,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.Tp == 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 @@ -1566,25 +1605,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

0 comments on commit 2adad09

Please sign in to comment.