From 8c49f07b01f229ca8f9dd9e7503e723f370285ff Mon Sep 17 00:00:00 2001 From: Yuanjia Zhang Date: Wed, 29 Mar 2023 19:18:54 +0800 Subject: [PATCH] planner: label plans as over-optimized for plan cache after refining cmp-function arguments (#41136) (#42685) close pingcap/tidb#38335 --- executor/prepared_test.go | 250 +++++++++++++++++++++++++++++++++- expression/builtin_compare.go | 57 ++++++-- planner/core/cache.go | 4 +- 3 files changed, 293 insertions(+), 18 deletions(-) diff --git a/executor/prepared_test.go b/executor/prepared_test.go index 86d331ceae282..d357ed15cdf58 100644 --- a/executor/prepared_test.go +++ b/executor/prepared_test.go @@ -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 ? 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 2", "0 0", "1 1", "2 2")) tk.MustQuery("execute stmt using @c, @d;").Check(testkit.Rows(" 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) { diff --git a/expression/builtin_compare.go b/expression/builtin_compare.go index 1c72eaa012149..12e8417e0d5b2 100644 --- a/expression/builtin_compare.go +++ b/expression/builtin_compare.go @@ -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 const + // 2. int-expr 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 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 non-int constant` or // `non-int constant 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. @@ -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 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) diff --git a/planner/core/cache.go b/planner/core/cache.go index d544f9ac7719a..8bcc99ac577bf 100644 --- a/planner/core/cache.go +++ b/planner/core/cache.go @@ -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