Skip to content

Commit

Permalink
planner: non-prep plan cache to support limit clauses (#42879)
Browse files Browse the repository at this point in the history
ref #36598
  • Loading branch information
qw4990 authored Apr 10, 2023
1 parent 49d8b46 commit 03b8605
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 60 deletions.
12 changes: 6 additions & 6 deletions executor/explain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 11 additions & 30 deletions planner/core/plan_cache_param.go
Original file line number Diff line number Diff line change
Expand Up @@ -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<? and b<?`, instead of
// `select a+? from t where a<? and b<?`.
// This is to make the output field names be corresponding to these values.
// Use int instead of bool to support nested SelectField.
selFieldsCnt int

// Skip all values in GroupByClause since them can affect the full_group_by check, e.g.
// `select a*2 from t group by a*?` cannot pass the full_group_by check.
groupByCnt int
}

func (pr *paramReplacer) Enter(in ast.Node) (out ast.Node, skipChildren bool) {
switch n := in.(type) {
case *ast.SelectField:
pr.selFieldsCnt++
case *ast.GroupByClause:
pr.groupByCnt++
case *ast.SelectField, *ast.GroupByClause, *ast.Limit:
// Skip replacing values in these case:
// 1. SelectField: to keep the output field names be corresponding to these values.
// 2. GroupByClause: to avoid breaking the full_group_by check.
// 3. Limit: to generate different plans for queries with different limit values.
return in, true
case *driver.ValueExpr:
if pr.selFieldsCnt == 0 && // not in SelectField
pr.groupByCnt == 0 { // not in GroupBy
pr.params = append(pr.params, n)
param := ast.NewParamMarkerExpr(len(pr.params) - 1) // offset is used as order in non-prepared plan cache.
param.(*driver.ParamMarkerExpr).Datum = *n.Datum.Clone() // init the ParamMakerExpr's Datum
return param, true
}
pr.params = append(pr.params, n)
param := ast.NewParamMarkerExpr(len(pr.params) - 1) // offset is used as order in non-prepared plan cache.
param.(*driver.ParamMarkerExpr).Datum = *n.Datum.Clone() // init the ParamMakerExpr's Datum
return param, true
}
return in, false
}

func (pr *paramReplacer) Leave(in ast.Node) (out ast.Node, ok bool) {
switch in.(type) {
case *ast.SelectField:
pr.selFieldsCnt--
case *ast.GroupByClause:
pr.groupByCnt--
}
return in, true
}

func (pr *paramReplacer) Reset() { pr.params, pr.selFieldsCnt, pr.groupByCnt = nil, 0, 0 }
func (pr *paramReplacer) Reset() { pr.params = nil }

// GetParamSQLFromAST returns the parameterized SQL of this AST.
// NOTICE: this function does not modify the original AST.
Expand Down
12 changes: 12 additions & 0 deletions planner/core/plan_cache_param_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,18 @@ func TestParameterize(t *testing.T) {
`INSERT INTO t (a,B,c) VALUES (?,?,?),(?,?,?)`,
[]interface{}{int64(1), int64(2), int64(3), int64(4), int64(5), int64(6)},
},

// keep the original format for limit clauses
{
`select * from t limit 10`,
`SELECT * FROM t LIMIT 10`,
[]interface{}{},
},
{
`select * from t limit 10, 20`,
`SELECT * FROM t LIMIT 10,20`,
[]interface{}{},
},
// TODO: more test cases
}

Expand Down
14 changes: 6 additions & 8 deletions planner/core/plan_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1623,11 +1623,10 @@ func TestNonPreparedPlanExplainWarning(t *testing.T) {
unsupported := []string{
"select /*+ use_index(t1, idx_b) */ * from t1 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'",
Expand All @@ -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",
Expand Down
24 changes: 12 additions & 12 deletions planner/core/plan_cacheable_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,20 +202,14 @@ 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.
func NonPreparedPlanCacheableWithCtx(sctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (ok bool, reason string) {
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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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`
Expand Down
11 changes: 7 additions & 4 deletions planner/core/plan_cacheable_checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand All @@ -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)
}
}

Expand All @@ -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++ {
Expand Down

0 comments on commit 03b8605

Please sign in to comment.