From 03b8605d9acfc28636ace51eed6d6a33cf6ed291 Mon Sep 17 00:00:00 2001 From: Yuanjia Zhang Date: Mon, 10 Apr 2023 13:44:59 +0800 Subject: [PATCH] planner: non-prep plan cache to support limit clauses (#42879) ref pingcap/tidb#36598 --- executor/explain_test.go | 12 +++--- planner/core/plan_cache_param.go | 41 ++++++--------------- planner/core/plan_cache_param_test.go | 12 ++++++ planner/core/plan_cache_test.go | 14 +++---- planner/core/plan_cacheable_checker.go | 24 ++++++------ planner/core/plan_cacheable_checker_test.go | 11 ++++-- 6 files changed, 54 insertions(+), 60 deletions(-) diff --git a/executor/explain_test.go b/executor/explain_test.go index 49a795a308238..89a6a28bad5bb 100644 --- a/executor/explain_test.go +++ b/executor/explain_test.go @@ -661,14 +661,14 @@ func TestExplainFormatPlanCache(t *testing.T) { tk.MustExec("select * from t limit 1") // miss - tk.MustExec("explain format = 'plan_cache' select * from t limit 1") - tk.MustQuery("show warnings").Check(testkit.Rows("Warning 1105 skip non-prepared plan-cache: queries that have hints, aggregation, window-function, order-by, limit and lock are not supported")) - tk.MustExec("explain format = 'plan_cache' select * from t limit 1") + tk.MustExec("explain format = 'plan_cache' select * from (select * from t) t1 limit 1") + tk.MustQuery("show warnings").Check(testkit.Rows("Warning 1105 skip non-prepared plan-cache: queries that have sub-queries are not supported")) + tk.MustExec("explain format = 'plan_cache' select * from (select * from t) t1 limit 1") tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("0")) - tk.MustExec("explain analyze format = 'plan_cache' select * from t limit 1") - tk.MustQuery("show warnings").Check(testkit.Rows("Warning 1105 skip non-prepared plan-cache: queries that have hints, aggregation, window-function, order-by, limit and lock are not supported")) - tk.MustExec("explain analyze format = 'plan_cache' select * from t limit 1") + tk.MustExec("explain analyze format = 'plan_cache' select * from (select * from t) t1 limit 1") + tk.MustQuery("show warnings").Check(testkit.Rows("Warning 1105 skip non-prepared plan-cache: queries that have sub-queries are not supported")) + tk.MustExec("explain analyze format = 'plan_cache' select * from (select * from t) t1 limit 1") tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("0")) // hit diff --git a/planner/core/plan_cache_param.go b/planner/core/plan_cache_param.go index e068cf57e828e..a997bb8f7b83d 100644 --- a/planner/core/plan_cache_param.go +++ b/planner/core/plan_cache_param.go @@ -51,49 +51,30 @@ var ( // paramReplacer is an ast.Visitor that replaces all values with `?` and collects them. type paramReplacer struct { params []*driver.ValueExpr - - // Skip all values in SelectField, e.g. - // `select a+1 from t where a<10 and b<23` should be parameterized to - // `select a+1 from t where a 1 and b < 2", // hint "select a, sum(b) as c from t1 where a > 1 and b < 2 group by a having sum(b) > 1", // having - "select * from t1 limit 1", // limit - "select * from (select * from t1) t", // sub-query - "select * from t1 where a in (select a from t)", // uncorrelated sub-query - "select * from t1 where a in (select a from t where a > t1.a)", // correlated sub-query - "select * from t where j < 1", // json + "select * from (select * from t1) t", // sub-query + "select * from t1 where a in (select a from t)", // uncorrelated sub-query + "select * from t1 where a in (select a from t where a > t1.a)", // correlated sub-query + "select * from t where j < 1", // json "select * from t where a > 1 and j < 1", "select * from t where e < '1'", // enum "select * from t where a > 1 and e < '1'", @@ -1649,9 +1648,8 @@ func TestNonPreparedPlanExplainWarning(t *testing.T) { } reasons := []string{ - "skip non-prepared plan-cache: queries that have hints, aggregation, window-function, order-by, limit and lock are not supported", - "skip non-prepared plan-cache: queries that have hints, aggregation, window-function, order-by, limit and lock are not supported", - "skip non-prepared plan-cache: queries that have hints, aggregation, window-function, order-by, limit and lock are not supported", + "skip non-prepared plan-cache: queries that have hints, having-clause, window-function are not supported", + "skip non-prepared plan-cache: queries that have hints, having-clause, window-function are not supported", "skip non-prepared plan-cache: queries that have sub-queries are not supported", "skip non-prepared plan-cache: queries that access partitioning table are not supported", "skip non-prepared plan-cache: queries that access partitioning table are not supported", diff --git a/planner/core/plan_cacheable_checker.go b/planner/core/plan_cacheable_checker.go index 697dfed2cf90f..ef7d7604e1a2f 100644 --- a/planner/core/plan_cacheable_checker.go +++ b/planner/core/plan_cacheable_checker.go @@ -202,12 +202,6 @@ func (checker *cacheableChecker) Leave(in ast.Node) (out ast.Node, ok bool) { return in, checker.cacheable } -// NonPreparedPlanCacheable checks whether the input ast is cacheable for non-prepared plan cache with empty session context, which is mainly for testing. -func NonPreparedPlanCacheable(node ast.Node, is infoschema.InfoSchema) bool { - ok, _ := NonPreparedPlanCacheableWithCtx(nil, node, is) - return ok -} - var nonPrepCacheCheckerPool = &sync.Pool{New: func() any { return &nonPreparedPlanCacheableChecker{} }} // NonPreparedPlanCacheableWithCtx checks whether this SQL is cacheable for non-prepared plan cache. @@ -215,7 +209,7 @@ func NonPreparedPlanCacheableWithCtx(sctx sessionctx.Context, node ast.Node, is var tableNames []*ast.TableName switch x := node.(type) { case *ast.SelectStmt: - tableNames, ok, reason = isSelectStmtNonPrepCacheableFastCheck(x) + tableNames, ok, reason = isSelectStmtNonPrepCacheableFastCheck(sctx, x) if !ok { return ok, reason } @@ -246,7 +240,7 @@ func NonPreparedPlanCacheableWithCtx(sctx sessionctx.Context, node ast.Node, is if !ok { return false, "not a select statement" } - tableNames, ok, reason = isSelectStmtNonPrepCacheableFastCheck(selectStmt) + tableNames, ok, reason = isSelectStmtNonPrepCacheableFastCheck(sctx, selectStmt) if !ok { return ok, reason } @@ -280,16 +274,16 @@ func NonPreparedPlanCacheableWithCtx(sctx sessionctx.Context, node ast.Node, is } // isSelectStmtNonPrepCacheableFastCheck checks whether the input select statement is cacheable for non-prepared plan cache. -func isSelectStmtNonPrepCacheableFastCheck(selectStmt *ast.SelectStmt) (names []*ast.TableName, ok bool, reason string) { +func isSelectStmtNonPrepCacheableFastCheck(sctx sessionctx.Context, selectStmt *ast.SelectStmt) (names []*ast.TableName, ok bool, reason string) { if selectStmt.Kind != ast.SelectStmtKindSelect { return nil, false, "not a select statement" } if len(selectStmt.TableHints) > 0 || // hints selectStmt.Having != nil || // having selectStmt.WindowSpecs != nil || // window function - selectStmt.Limit != nil || // limit + (selectStmt.Limit != nil && !sctx.GetSessionVars().EnablePlanCacheForParamLimit) || // limit selectStmt.SelectIntoOpt != nil { // select-into statement - return nil, false, "queries that have hints, aggregation, window-function, order-by, limit and lock are not supported" + return nil, false, "queries that have hints, having-clause, window-function are not supported" } from := selectStmt.From if from == nil || selectStmt.From.TableRefs == nil { @@ -377,9 +371,15 @@ func (checker *nonPreparedPlanCacheableChecker) Enter(in ast.Node) (out ast.Node switch node := in.(type) { case *ast.SelectStmt, *ast.FieldList, *ast.SelectField, *ast.TableRefsClause, *ast.Join, *ast.BetweenExpr, *ast.OnCondition, - *ast.InsertStmt, *ast.DeleteStmt, *ast.UpdateStmt, *ast.Assignment, + *ast.InsertStmt, *ast.DeleteStmt, *ast.UpdateStmt, *ast.Assignment, *ast.ParenthesesExpr, *ast.RowExpr, *ast.TableSource, *ast.ColumnNameExpr, *ast.PatternInExpr, *ast.BinaryOperationExpr, *ast.ByItem, *ast.AggregateFuncExpr: return in, !checker.cacheable // skip child if un-cacheable + case *ast.Limit: + if !checker.sctx.GetSessionVars().EnablePlanCacheForParamLimit { + checker.cacheable = false + checker.reason = "query has 'limit ?' is un-cacheable" + } + return in, !checker.cacheable case *ast.ColumnName: if checker.filterCnt > 0 { // this column is appearing some filters, e.g. `col = 1` diff --git a/planner/core/plan_cacheable_checker_test.go b/planner/core/plan_cacheable_checker_test.go index 1d70e244feb25..719e6ddfd15db 100644 --- a/planner/core/plan_cacheable_checker_test.go +++ b/planner/core/plan_cacheable_checker_test.go @@ -303,6 +303,7 @@ func TestNonPreparedPlanCacheable(t *testing.T) { "select * from test.t where d>now()", // now "select a+1 from test.t where a<13", "select mod(a, 10) from test.t where a<13", + "select * from test.t limit 1", // limit // 2-way joins "select * from test.t inner join test.t3 on test.t.a=test.t3.a", @@ -318,7 +319,6 @@ func TestNonPreparedPlanCacheable(t *testing.T) { "select distinct a from test.t1 where a > 1 and b < 2", // distinct "select count(*) from test.t1 where a > 1 and b < 2 group by a", // group by "select a, sum(b) as c from test.t1 where a > 1 and b < 2 group by a having sum(b) > 1", // having - "select * from test.t1 limit 1", // limit "select * from test.t1 order by a", // order by "select * from (select * from test.t1) t", // sub-query "insert into test.t1 values(1, 1)", // insert @@ -330,16 +330,19 @@ func TestNonPreparedPlanCacheable(t *testing.T) { "select * from test.t1 where a in (select a from test.t where a > t1.a)", // correlated sub-query } + sctx := tk.Session() for _, q := range unsupported { stmt, err := p.ParseOneStmt(q, charset, collation) require.NoError(t, err) - require.False(t, core.NonPreparedPlanCacheable(stmt, is)) + ok, _ := core.NonPreparedPlanCacheableWithCtx(sctx, stmt, is) + require.False(t, ok) } for _, q := range supported { stmt, err := p.ParseOneStmt(q, charset, collation) require.NoError(t, err) - require.True(t, core.NonPreparedPlanCacheable(stmt, is)) + ok, _ := core.NonPreparedPlanCacheableWithCtx(sctx, stmt, is) + require.True(t, ok) } } @@ -359,7 +362,7 @@ func BenchmarkNonPreparedPlanCacheableChecker(b *testing.B) { sctx := tk.Session() is := sessiontxn.GetTxnManager(sctx).GetTxnInfoSchema() - core.NonPreparedPlanCacheable(stmt, is) + core.NonPreparedPlanCacheableWithCtx(sctx, stmt, is) b.ResetTimer() for i := 0; i < b.N; i++ {