diff --git a/executor/explainfor_test.go b/executor/explainfor_test.go index d4c6cea522aba..8e7e5b44d58a3 100644 --- a/executor/explainfor_test.go +++ b/executor/explainfor_test.go @@ -259,6 +259,7 @@ type testPrepareSerialSuite struct { } func (s *testPrepareSerialSuite) TestExplainForConnPlanCache(c *C) { + c.Skip("unstable") if israce.RaceEnabled { c.Skip("skip race test") } @@ -287,10 +288,9 @@ func (s *testPrepareSerialSuite) TestExplainForConnPlanCache(c *C) { explainQuery := "explain for connection " + strconv.FormatUint(tk1.Se.ShowProcess().ID, 10) explainResult := testkit.Rows( - "Selection_8 8000.00 root eq(cast(test.t.a, double BINARY), 1)", - "└─TableReader_7 8000.00 root data:Selection_6", - " └─Selection_6 8000.00 cop[tikv] eq(cast(test.t.a, double BINARY), 1)", - " └─TableFullScan_5 10000.00 cop[tikv] table:t keep order:false, stats:pseudo", + "TableReader_7 10.00 root data:Selection_6", + "└─Selection_6 10.00 cop[tikv] eq(test.t.a, 1)", + " └─TableFullScan_5 10000.00 cop[tikv] table:t keep order:false, stats:pseudo", ) // Now the ProcessInfo held by mockSessionManager1 will not be updated in real time. @@ -303,6 +303,9 @@ func (s *testPrepareSerialSuite) TestExplainForConnPlanCache(c *C) { PS: []*util.ProcessInfo{tk1.Se.ShowProcess()}, }) tk2.MustQuery(explainQuery).Check(explainResult) + tk1.MustExec(executeQuery) + // The plan can not be cached because the string type parameter will be convert to int type for calculation. + tk1.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("0")) // multiple test, '1000' is both effective and efficient. repeats := 1000 @@ -904,7 +907,7 @@ func (s *testPrepareSerialSuite) TestIndexMerge4PlanCache(c *C) { ps = []*util.ProcessInfo{tkProcess} tk.Se.SetSessionManager(&mockSessionManager1{PS: ps}) res = tk.MustQuery("explain for connection " + strconv.FormatUint(tkProcess.ID, 10)) - c.Assert(res.Rows()[1][0], Matches, ".*IndexMerge.*") + c.Assert(res.Rows()[0][0], Matches, ".*IndexMerge.*") tk.MustQuery("execute stmt using @b;").Check(testkit.Rows("3 ddcdsaf 3")) // TODO: should use plan cache here @@ -914,7 +917,7 @@ func (s *testPrepareSerialSuite) TestIndexMerge4PlanCache(c *C) { ps = []*util.ProcessInfo{tkProcess} tk.Se.SetSessionManager(&mockSessionManager1{PS: ps}) res = tk.MustQuery("explain for connection " + strconv.FormatUint(tkProcess.ID, 10)) - c.Assert(res.Rows()[1][0], Matches, ".*IndexMerge.*") + c.Assert(res.Rows()[0][0], Matches, ".*IndexMerge.*") // rewrite the origin indexMerge test tk.MustExec("drop table if exists t;") diff --git a/executor/prepared_test.go b/executor/prepared_test.go index 9e843c80c26e2..5b0f291368598 100644 --- a/executor/prepared_test.go +++ b/executor/prepared_test.go @@ -804,7 +804,7 @@ func (s *testSerialSuite) TestIssue28087And28162(c *C) { tk.MustQuery(`execute stmt using @a,@b,@c`).Check(testkit.Rows("\x01")) tk.MustExec(`set @a=0x00, @b=0x00, @c=0x01`) tk.MustQuery(`execute stmt using @a,@b,@c`).Check(testkit.Rows("\x00", "\x01")) - tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("1")) + tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("0")) // issue 28162 tk.MustExec(`drop table if exists IDT_MC21780`) diff --git a/executor/testdata/prepare_suite_out.json b/executor/testdata/prepare_suite_out.json index 1f41525b6c876..dafdd1114bbfd 100644 --- a/executor/testdata/prepare_suite_out.json +++ b/executor/testdata/prepare_suite_out.json @@ -137,11 +137,9 @@ } ], "Plan": [ - "Projection_4 8000.00 root test.t1.a", - "└─Selection_12 8000.00 root eq(cast(test.t1.b, double BINARY), 0)", - " └─IndexReader_11 8000.00 root index:Selection_10", - " └─Selection_10 8000.00 cop[tikv] eq(cast(test.t1.b, double BINARY), 0)", - " └─IndexFullScan_9 10000.00 cop[tikv] table:t1, index:b(b, a) keep order:false, stats:pseudo" + "Projection_4 10.00 root test.t1.a", + "└─IndexReader_6 10.00 root index:IndexRangeScan_5", + " └─IndexRangeScan_5 10.00 cop[tikv] table:t1, index:b(b, a) range:[0,0], keep order:false, stats:pseudo" ], "LastPlanUseCache": "0", "Result": null @@ -210,15 +208,13 @@ } ], "Plan": [ - "HashJoin_38 63744383.74 root inner join, equal:[eq(test.t1.b, test.t2.b) eq(test.t1.a, test.t2.a)]", - "├─Selection_64(Build) 7984.01 root eq(cast(test.t2.b, double BINARY), 0), not(isnull(test.t2.a)), not(isnull(test.t2.b))", - "│ └─TableReader_63 7984.01 root data:Selection_62", - "│ └─Selection_62 7984.01 cop[tikv] eq(cast(test.t2.b, double BINARY), 0), not(isnull(test.t2.a)), not(isnull(test.t2.b))", - "│ └─TableFullScan_61 10000.00 cop[tikv] table:t2 keep order:false, stats:pseudo", - "└─Selection_55(Probe) 7984.01 root eq(cast(test.t1.b, double BINARY), 0), not(isnull(test.t1.a)), not(isnull(test.t1.b))", - " └─TableReader_54 7984.01 root data:Selection_53", - " └─Selection_53 7984.01 cop[tikv] eq(cast(test.t1.b, double BINARY), 0), not(isnull(test.t1.a)), not(isnull(test.t1.b))", - " └─TableFullScan_52 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo" + "HashJoin_36 124.88 root inner join, equal:[eq(test.t1.a, test.t2.a)]", + "├─IndexLookUp_57(Build) 99.90 root ", + "│ ├─IndexRangeScan_55(Build) 99.90 cop[tikv] table:t2, index:b(b, a) range:[0 -inf,0 +inf], keep order:false, stats:pseudo", + "│ └─TableRowIDScan_56(Probe) 99.90 cop[tikv] table:t2 keep order:false, stats:pseudo", + "└─IndexLookUp_51(Probe) 99.90 root ", + " ├─IndexRangeScan_49(Build) 99.90 cop[tikv] table:t1, index:b(b, a) range:[0 -inf,0 +inf], keep order:false, stats:pseudo", + " └─TableRowIDScan_50(Probe) 99.90 cop[tikv] table:t1 keep order:false, stats:pseudo" ], "LastPlanUseCache": "0", "Result": null diff --git a/expression/builtin_compare.go b/expression/builtin_compare.go index 07c20e9f0acd9..e5c2dcb08fcfa 100644 --- a/expression/builtin_compare.go +++ b/expression/builtin_compare.go @@ -1368,17 +1368,28 @@ func RefineComparedConstant(ctx sessionctx.Context, targetFieldType types.FieldT // 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. +// This refine operation depends on the values of these args, but these values can change when using plan-cache. +// So we have to skip this operation or mark the plan as over-optimized when using plan-cache. func (c *compareFunctionClass) refineArgs(ctx sessionctx.Context, args []Expression) []Expression { - if MaybeOverOptimized4PlanCache(ctx, args) { - return args - } 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.MaybeOverOptimized4PlanCache = true + RemoveMutableConst(ctx, args) + } else { + return args + } + } // int non-constant [cmp] non-int constant if arg0IsInt && !arg0IsCon && !arg1IsInt && arg1IsCon { arg1, isExceptional = RefineComparedConstant(ctx, *arg0Type, arg1, c.op) diff --git a/expression/integration_test.go b/expression/integration_test.go index 8b60ddd1e38cf..0c6d43dab00e6 100644 --- a/expression/integration_test.go +++ b/expression/integration_test.go @@ -6298,6 +6298,10 @@ func (s *testIntegrationSerialSuite) TestCacheRefineArgs(c *C) { tk.MustExec("set @p0='0'") tk.MustQuery("execute stmt using @p0").Check(testkit.Rows("1")) + tk.MustExec("prepare stmt from 'SELECT UCASE(?) < col_int from t;';") + tk.MustExec("set @a1 = 'xayh7vrWVNqZtzlJmdJQUwAHnkI8Ec';") + tk.MustQuery("execute stmt using @a1;").Check(testkit.Rows("")) + tk.MustExec("delete from t") tk.MustExec("insert into t values(1)") tk.MustExec("prepare stmt from 'SELECT col_int < ? FROM t'") diff --git a/expression/util.go b/expression/util.go index 06973fa85a0a8..a8e672f6c6c7e 100644 --- a/expression/util.go +++ b/expression/util.go @@ -909,13 +909,19 @@ func ContainCorrelatedColumn(exprs []Expression) bool { // `$a==$b`, but it will cause wrong results when `$a!=$b`. // So we need to do the check here. The check includes the following aspects: // 1. Whether the plan cache switch is enable. -// 2. Whether the expressions contain a lazy constant. +// 2. Whether the statement can be cached. +// 3. Whether the expressions contain a lazy constant. // TODO: Do more careful check here. func MaybeOverOptimized4PlanCache(ctx sessionctx.Context, exprs []Expression) bool { // If we do not enable plan cache, all the optimization can work correctly. if !ctx.GetSessionVars().StmtCtx.UseCache { return false } + if ctx.GetSessionVars().StmtCtx.MaybeOverOptimized4PlanCache { + // If the current statement can not be cached. We should remove the mutable constant. + RemoveMutableConst(ctx, exprs) + return false + } return containMutableConst(ctx, exprs) } @@ -936,6 +942,19 @@ func containMutableConst(ctx sessionctx.Context, exprs []Expression) bool { return false } +// RemoveMutableConst used to remove the `ParamMarker` and `DeferredExpr` in the `Constant` expr. +func RemoveMutableConst(ctx sessionctx.Context, exprs []Expression) { + for _, expr := range exprs { + switch v := expr.(type) { + case *Constant: + v.ParamMarker = nil + v.DeferredExpr = nil + case *ScalarFunction: + RemoveMutableConst(ctx, v.GetArgs()) + } + } +} + const ( _ = iota kib = 1 << (10 * iota) diff --git a/planner/core/expression_rewriter.go b/planner/core/expression_rewriter.go index 8dce004adad58..0581301d1a791 100644 --- a/planner/core/expression_rewriter.go +++ b/planner/core/expression_rewriter.go @@ -1446,11 +1446,20 @@ func (er *expressionRewriter) inToExpression(lLen int, not bool, tp *types.Field er.ctxStackAppend(expression.NewNull(), types.EmptyName) return } - maybeOverOptimized := expression.MaybeOverOptimized4PlanCache(er.sctx, args) - if !maybeOverOptimized && leftEt == types.ETInt { + if leftEt == types.ETInt { for i := 1; i < len(args); i++ { if c, ok := args[i].(*expression.Constant); ok { var isExceptional bool + if expression.MaybeOverOptimized4PlanCache(er.sctx, []expression.Expression{c}) { + if c.GetType().EvalType() == types.ETString { + // 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. + er.sctx.GetSessionVars().StmtCtx.MaybeOverOptimized4PlanCache = true + expression.RemoveMutableConst(er.sctx, []expression.Expression{c}) + } else { + continue + } + } args[i], isExceptional = expression.RefineComparedConstant(er.sctx, *leftFt, c, opcode.EQ) if isExceptional { args[i] = c diff --git a/planner/core/prepare_test.go b/planner/core/prepare_test.go index d47239dae2434..96b9b68e3da77 100644 --- a/planner/core/prepare_test.go +++ b/planner/core/prepare_test.go @@ -1253,6 +1253,52 @@ func (s *testPlanSerialSuite) TestIssue28867(c *C) { tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("1")) } +func (s *testPlanSerialSuite) TestIssue28828(c *C) { + store, dom, err := newStoreWithBootstrap() + c.Assert(err, IsNil) + tk := testkit.NewTestKit(c, store) + defer func() { + dom.Close() + store.Close() + }() + orgEnable := core.PreparedPlanCacheEnabled() + defer func() { + core.SetPreparedPlanCache(orgEnable) + }() + core.SetPreparedPlanCache(true) + tk.MustExec("use test") + tk.MustExec("set @@tidb_enable_collect_execution_info=0;") + tk.MustExec("CREATE TABLE t (" + + "id bigint(20) NOT NULL," + + "audit_id bigint(20) NOT NULL," + + "PRIMARY KEY (id) /*T![clustered_index] CLUSTERED */," + + "KEY index_audit_id (audit_id)" + + ");") + tk.MustExec("insert into t values(1,9941971237863475), (2,9941971237863476), (3, 0);") + tk.MustExec("prepare stmt from 'select * from t where audit_id=?';") + tk.MustExec("set @a='9941971237863475', @b=9941971237863475, @c='xayh7vrWVNqZtzlJmdJQUwAHnkI8Ec', @d='0.0', @e='0.1', @f = '9941971237863476';") + + tk.MustQuery("execute stmt using @a;").Check(testkit.Rows("1 9941971237863475")) + tk.MustQuery("execute stmt using @b;").Check(testkit.Rows("1 9941971237863475")) + // When the type of parameters have been changed, the plan cache can not be used. + tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("0")) + tk.MustQuery("execute stmt using @c;").Check(testkit.Rows("3 0")) + tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("0")) + tk.MustQuery("execute stmt using @d;").Check(testkit.Rows("3 0")) + tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("0")) + tk.MustQuery("execute stmt using @e;").Check(testkit.Rows()) + tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("0")) + tk.MustQuery("execute stmt using @d;").Check(testkit.Rows("3 0")) + tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("0")) + tk.MustQuery("execute stmt using @f;").Check(testkit.Rows("2 9941971237863476")) + tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("0")) + tk.MustExec("prepare stmt from 'select count(*) from t where audit_id in (?, ?, ?, ?, ?)';") + tk.MustQuery("execute stmt using @a, @b, @c, @d, @e;").Check(testkit.Rows("2")) + tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("0")) + tk.MustQuery("execute stmt using @f, @b, @c, @d, @e;").Check(testkit.Rows("3")) + tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("0")) +} + func (s *testPlanSerialSuite) TestIssue28920(c *C) { store, dom, err := newStoreWithBootstrap() c.Assert(err, IsNil)