From 3eff64a5f1801bfa05c1bdefbb3eededef9583ac Mon Sep 17 00:00:00 2001 From: Yuanjia Zhang Date: Mon, 3 Apr 2023 20:30:57 +0800 Subject: [PATCH] planner: non-prep plan cache supports DML (#42765) ref pingcap/tidb#36598 --- planner/core/plan_cache_test.go | 54 +++++++++--- planner/core/plan_cacheable_checker.go | 110 ++++++++++++++++++------- 2 files changed, 125 insertions(+), 39 deletions(-) diff --git a/planner/core/plan_cache_test.go b/planner/core/plan_cache_test.go index ec94b18a6c..28f0e34d4c 100644 --- a/planner/core/plan_cache_test.go +++ b/planner/core/plan_cache_test.go @@ -1620,11 +1620,6 @@ func TestNonPreparedPlanExplainWarning(t *testing.T) { "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 - "insert into t1 values(1, 1)", // insert - "insert into t1(a, b) select a, b from t1", // insert into select - "update t1 set a = 1 where b = 2", // update - "delete from t1 where b = 1", // delete - "select * from t1 for update", // lock "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 @@ -1653,11 +1648,6 @@ func TestNonPreparedPlanExplainWarning(t *testing.T) { "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 sub-queries are not supported", - "skip non-prepared plan-cache: not a select statement", - "skip non-prepared plan-cache: not a select statement", - "skip non-prepared plan-cache: not a select statement", - "skip non-prepared plan-cache: not a select statement", - "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 access partitioning table are not supported", "skip non-prepared plan-cache: queries that access partitioning table are not supported", "skip non-prepared plan-cache: query has some filters with JSON, Enum, Set or Bit columns", @@ -1740,6 +1730,35 @@ func TestIssue42150(t *testing.T) { tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("1")) } +func TestNonPreparedPlanCacheDML(t *testing.T) { + store := testkit.CreateMockStore(t) + tk := testkit.NewTestKit(t, store) + tk.MustExec("use test") + tk.MustExec(`set tidb_enable_non_prepared_plan_cache=1`) + tk.MustExec("create table t (a int default 0, b int default 0)") + + for _, sql := range []string{ + `select a from t for update`, + `select a from t where a<10 for update`, + `insert into t values (1, 1)`, + `insert into t (a, b) values (1, 1)`, + `insert into t (a) values (1)`, + `insert into t (b) values (1)`, + `insert into t select * from t`, + `insert into t select * from t where a>10`, + `update t set a=1`, + `update t set a=1 where a>10`, + `update t set a=1, b=1`, + `update t set a=a+1 where a>10`, + `delete from t`, + `delete from t where a>10`, + } { + tk.MustExec(sql) + tk.MustExec(sql) + tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("1")) + } +} + func TestNonPreparedPlanCachePanic(t *testing.T) { store := testkit.CreateMockStore(t) tk := testkit.NewTestKit(t, store) @@ -1930,3 +1949,18 @@ func BenchmarkPlanCacheInsert(b *testing.B) { tk.MustExec("execute st") } } + +func BenchmarkNonPreparedPlanCacheDML(b *testing.B) { + store := testkit.CreateMockStore(b) + tk := testkit.NewTestKit(b, store) + tk.MustExec("use test") + tk.MustExec("create table t (a int)") + tk.MustExec("set tidb_enable_non_prepared_plan_cache=1") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tk.MustExec("insert into t values (1)") + tk.MustExec("update t set a = 2 where a = 1") + tk.MustExec("delete from t where a = 2") + } +} diff --git a/planner/core/plan_cacheable_checker.go b/planner/core/plan_cacheable_checker.go index c2298a05cf..32b4a09983 100644 --- a/planner/core/plan_cacheable_checker.go +++ b/planner/core/plan_cacheable_checker.go @@ -210,35 +210,61 @@ func NonPreparedPlanCacheable(node ast.Node, is infoschema.InfoSchema) bool { var nonPrepCacheCheckerPool = &sync.Pool{New: func() any { return &nonPreparedPlanCacheableChecker{} }} -// NonPreparedPlanCacheableWithCtx checks whether the input ast is cacheable for non-prepared plan cache. -// Only support: select {field} from {single-table} where {cond} and {cond} ... -// {cond}: {col} {op} {val} -// {op}: >, <, = +// NonPreparedPlanCacheableWithCtx checks whether this SQL is cacheable for non-prepared plan cache. func NonPreparedPlanCacheableWithCtx(sctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (ok bool, reason string) { - selectStmt, isSelect := node.(*ast.SelectStmt) - if !isSelect { // only support select statement now - return false, "not a select statement" - } - if selectStmt.Kind != ast.SelectStmtKindSelect { - return 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.LockInfo != nil || selectStmt.SelectIntoOpt != nil { // lock info - return false, "queries that have hints, aggregation, window-function, order-by, limit and lock are not supported" - } - from := selectStmt.From - if from == nil || selectStmt.From.TableRefs == nil { - return false, "queries that have sub-queries are not supported" - } - tableRefs := from.TableRefs - - // match table names, currently only support 2 tables(2-way join) at most. - tableNames, ok, reason := extractTableNames(tableRefs, nil) - if !ok { - return false, reason + var tableNames []*ast.TableName + switch x := node.(type) { + case *ast.SelectStmt: + tableNames, ok, reason = isSelectStmtNonPrepCacheableFastCheck(x) + if !ok { + return ok, reason + } + case *ast.UpdateStmt: + if x.MultipleTable { + return false, "not support multiple tables update statements" + } + tableNames, ok, reason = extractTableNames(x.TableRefs.TableRefs, tableNames) + if !ok { + return ok, reason + } + case *ast.InsertStmt: + if x.Select == nil { // `insert into t values (...)` + nRows := len(x.Lists) + nCols := 0 + if len(x.Lists) > 0 { // avoid index-out-of-range + nCols = len(x.Lists[0]) + } + if nRows*nCols > 200 { // to save memory + return false, "too many values (more than 200) in the insert statement" + } + tableNames, ok, reason = extractTableNames(x.Table.TableRefs, tableNames) + if !ok { + return ok, reason + } + } else { // `insert into t select ...` + selectStmt, ok := x.Select.(*ast.SelectStmt) + if !ok { + return false, "not a select statement" + } + tableNames, ok, reason = isSelectStmtNonPrepCacheableFastCheck(selectStmt) + if !ok { + return ok, reason + } + tableNames, ok, reason = extractTableNames(x.Table.TableRefs, tableNames) + if !ok { + return ok, reason + } + } + case *ast.DeleteStmt: + if x.IsMultiTable { + return false, "not support multiple tables delete statements" + } + tableNames, ok, reason = extractTableNames(x.TableRefs.TableRefs, tableNames) + if !ok { + return ok, reason + } + default: + return false, "not a SELECT/UPDATE/INSERT/DELETE statement" } // allocate and init the checker @@ -250,10 +276,35 @@ func NonPreparedPlanCacheableWithCtx(sctx sessionctx.Context, node ast.Node, is // put the checker back nonPrepCacheCheckerPool.Put(checker) - return cacheable, reason } +// 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) { + 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.SelectIntoOpt != nil { // select-into statement + return nil, false, "queries that have hints, aggregation, window-function, order-by, limit and lock are not supported" + } + from := selectStmt.From + if from == nil || selectStmt.From.TableRefs == nil { + return nil, false, "queries that have sub-queries are not supported" + } + tableRefs := from.TableRefs + + // match table names, currently only support 2 tables(2-way join) at most. + tableNames, ok, reason := extractTableNames(tableRefs, nil) + if !ok { + return nil, false, reason + } + return tableNames, true, "" +} + // extractTableNames extracts table names from the input node. // Currently support 2 tables(2-way join) at most. func extractTableNames(node ast.ResultSetNode, names []*ast.TableName) ([]*ast.TableName, bool, string) { @@ -326,6 +377,7 @@ 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.TableSource, *ast.ColumnNameExpr, *ast.PatternInExpr, *ast.BinaryOperationExpr, *ast.ByItem, *ast.AggregateFuncExpr: return in, !checker.cacheable // skip child if un-cacheable case *ast.ColumnName: