planner: non-prep plan cache supports DML (#42765)

ref pingcap/tidb#36598
This commit is contained in:
Yuanjia Zhang
2023-04-03 20:30:57 +08:00
committed by GitHub
parent 3ad4410b0b
commit 3eff64a5f1
2 changed files with 125 additions and 39 deletions

View File

@ -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")
}
}

View File

@ -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: