planner: non-prep plan cache supports 2-way joins (#42740)
ref pingcap/tidb#36598
This commit is contained in:
@ -43,6 +43,7 @@ var (
|
||||
buf := new(strings.Builder)
|
||||
buf.Reset()
|
||||
restoreCtx := format.NewRestoreCtx(format.DefaultRestoreFlags, buf)
|
||||
restoreCtx.Flags ^= format.RestoreKeyWordUppercase
|
||||
return restoreCtx
|
||||
}}
|
||||
)
|
||||
|
||||
@ -70,7 +70,7 @@ func TestParameterize(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"select a+1, sum(b) from t where a<10 group by a+1",
|
||||
"SELECT `a`+1,SUM(`b`) FROM `t` WHERE `a`<? GROUP BY `a`+1",
|
||||
"SELECT `a`+1,sum(`b`) FROM `t` WHERE `a`<? GROUP BY `a`+1",
|
||||
[]interface{}{int64(10)},
|
||||
"SELECT `a`+1,SUM(`b`) FROM `t` WHERE `a`<10 GROUP BY `a`+1",
|
||||
},
|
||||
|
||||
@ -344,9 +344,6 @@ func TestNonPreparedPlanCacheReason(t *testing.T) {
|
||||
tk.MustExec(`explain format = 'plan_cache' select * from t where a=1`)
|
||||
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("1"))
|
||||
|
||||
tk.MustExec(`explain format = 'plan_cache' select * from t t1, t t2`)
|
||||
tk.MustQuery(`show warnings`).Check(testkit.Rows(`Warning 1105 skip non-prepared plan-cache: queries that access multiple tables are not supported`))
|
||||
|
||||
tk.MustExec(`explain format = 'plan_cache' select * from (select * from t) tx`)
|
||||
tk.MustQuery(`show warnings`).Check(testkit.Rows(`Warning 1105 skip non-prepared plan-cache: queries that have sub-queries are not supported`))
|
||||
|
||||
@ -1622,7 +1619,6 @@ func TestNonPreparedPlanExplainWarning(t *testing.T) {
|
||||
"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 t1, t2", // join
|
||||
"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
|
||||
@ -1656,7 +1652,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 hints, aggregation, window-function, order-by, limit and lock are not supported",
|
||||
"skip non-prepared plan-cache: queries that access multiple tables 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",
|
||||
@ -1771,6 +1766,45 @@ func TestNonPreparedPlanCachePanic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonPreparedPlanCacheJoin(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 t1 (a int, b int, c int)")
|
||||
tk.MustExec("create table t2 (a int, b int, c int)")
|
||||
tk.MustExec("create table t3 (a int, b int, c int)")
|
||||
|
||||
supported := []string{
|
||||
"select * from t1, t2 where t1.a=t2.a and t1.b<10",
|
||||
"select * from t1, t2",
|
||||
"select * from t1, t2 where t1.a<t2.a and t2.c=10",
|
||||
"select * from t1 tx, t1 ty",
|
||||
"select * from t1 tx, t1 ty where tx.a=ty.a",
|
||||
"select * from t1 inner join t2",
|
||||
"select * from t1 inner join t2 on t1.a=t2.a",
|
||||
"select * from t1 inner join t2 on t1.a=t2.a and t2.c<10",
|
||||
"select * from t1 left join t2 on t1.a=t2.a",
|
||||
"select * from t1 left join t2 on t1.a=t2.a and t2.c<10",
|
||||
}
|
||||
unsupported := []string{
|
||||
"select * from t1, t2, t3", // 3-way join
|
||||
"select * from t1, t2, t1 tx", // 3-way join
|
||||
"select * from t1, (select * from t2) t2", // subquery
|
||||
}
|
||||
|
||||
for _, sql := range supported {
|
||||
tk.MustExec(sql)
|
||||
tk.MustExec(sql)
|
||||
tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("1"))
|
||||
}
|
||||
for _, sql := range unsupported {
|
||||
tk.MustExec(sql)
|
||||
tk.MustExec(sql)
|
||||
tk.MustQuery("select @@last_plan_from_cache").Check(testkit.Rows("0"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonPreparedPlanCacheAgg(t *testing.T) {
|
||||
store := testkit.CreateMockStore(t)
|
||||
tk := testkit.NewTestKit(t, store)
|
||||
|
||||
@ -234,24 +234,16 @@ func NonPreparedPlanCacheableWithCtx(sctx sessionctx.Context, node ast.Node, is
|
||||
return false, "queries that have sub-queries are not supported"
|
||||
}
|
||||
tableRefs := from.TableRefs
|
||||
if tableRefs.Right != nil {
|
||||
// We don't support the join for the non-prepared plan cache now.
|
||||
return false, "queries that access multiple tables are not supported"
|
||||
}
|
||||
|
||||
var tableNode *ast.TableName
|
||||
switch x := tableRefs.Left.(type) {
|
||||
case *ast.TableSource:
|
||||
tbl, isTableName := x.Source.(*ast.TableName)
|
||||
if !isTableName {
|
||||
return false, "queries that have sub-queries are not supported"
|
||||
}
|
||||
tableNode = tbl
|
||||
// match table names, currently only support 2 tables(2-way join) at most.
|
||||
tableNames, ok, reason := extractTableNames(tableRefs, nil)
|
||||
if !ok {
|
||||
return false, reason
|
||||
}
|
||||
|
||||
// allocate and init the checker
|
||||
checker := nonPrepCacheCheckerPool.Get().(*nonPreparedPlanCacheableChecker)
|
||||
checker.reset(sctx, is, tableNode)
|
||||
checker.reset(sctx, is, tableNames)
|
||||
|
||||
node.Accept(checker)
|
||||
cacheable, reason := checker.cacheable, checker.reason
|
||||
@ -262,6 +254,46 @@ func NonPreparedPlanCacheableWithCtx(sctx sessionctx.Context, node ast.Node, is
|
||||
return cacheable, reason
|
||||
}
|
||||
|
||||
// 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) {
|
||||
var ok bool
|
||||
var reason string
|
||||
switch x := node.(type) {
|
||||
case *ast.TableSource:
|
||||
name, isName := x.Source.(*ast.TableName)
|
||||
if isName {
|
||||
names = append(names, name)
|
||||
} else {
|
||||
if x.Source != nil {
|
||||
names, ok, reason = extractTableNames(x.Source, names)
|
||||
if !ok {
|
||||
return nil, ok, reason
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ast.Join:
|
||||
if x.Left != nil {
|
||||
names, ok, reason = extractTableNames(x.Left, names)
|
||||
if !ok {
|
||||
return nil, ok, reason
|
||||
}
|
||||
}
|
||||
if x.Right != nil {
|
||||
names, ok, reason = extractTableNames(x.Right, names)
|
||||
if !ok {
|
||||
return nil, ok, reason
|
||||
}
|
||||
}
|
||||
default:
|
||||
return names, false, "queries that have sub-queries are not supported"
|
||||
}
|
||||
if len(names) > 2 {
|
||||
return names, false, "queries that have more than 2 tables are not supported"
|
||||
}
|
||||
return names, true, ""
|
||||
}
|
||||
|
||||
// nonPreparedPlanCacheableChecker checks whether a query's plan can be cached for non-prepared plan cache.
|
||||
// NOTE: we can add more rules in the future.
|
||||
type nonPreparedPlanCacheableChecker struct {
|
||||
@ -270,17 +302,18 @@ type nonPreparedPlanCacheableChecker struct {
|
||||
reason string // reason why this statement cannot hit the cache
|
||||
schema infoschema.InfoSchema
|
||||
|
||||
tableNode *ast.TableName
|
||||
tableNodes []*ast.TableName // only support 2-way joins currently
|
||||
|
||||
constCnt int // the number of constants/parameters in this query
|
||||
filterCnt int // the number of filters in the current node
|
||||
}
|
||||
|
||||
func (checker *nonPreparedPlanCacheableChecker) reset(sctx sessionctx.Context, schema infoschema.InfoSchema, tableNode *ast.TableName) {
|
||||
func (checker *nonPreparedPlanCacheableChecker) reset(sctx sessionctx.Context, schema infoschema.InfoSchema, tableNodes []*ast.TableName) {
|
||||
checker.sctx = sctx
|
||||
checker.cacheable = true
|
||||
checker.schema = schema
|
||||
checker.reason = ""
|
||||
checker.tableNode = tableNode
|
||||
checker.tableNodes = tableNodes
|
||||
checker.constCnt = 0
|
||||
checker.filterCnt = 0
|
||||
}
|
||||
@ -292,19 +325,24 @@ 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,
|
||||
case *ast.SelectStmt, *ast.FieldList, *ast.SelectField, *ast.TableRefsClause, *ast.Join, *ast.BetweenExpr, *ast.OnCondition,
|
||||
*ast.TableSource, *ast.ColumnNameExpr, *ast.PatternInExpr, *ast.BinaryOperationExpr, *ast.ByItem, *ast.AggregateFuncExpr:
|
||||
return in, !checker.cacheable // skip child if un-cacheable
|
||||
case *ast.ColumnName:
|
||||
if checker.filterCnt > 0 {
|
||||
// this column is appearing some filters, e.g. `col = 1`
|
||||
colType, found := getColType(checker.schema, checker.tableNode, node)
|
||||
if !found {
|
||||
checker.cacheable = false
|
||||
checker.reason = "some column is not found in table schema"
|
||||
} else if colType == mysql.TypeJSON || colType == mysql.TypeEnum || colType == mysql.TypeSet || colType == mysql.TypeBit {
|
||||
checker.cacheable = false
|
||||
checker.reason = "query has some filters with JSON, Enum, Set or Bit columns"
|
||||
for _, tableNode := range checker.tableNodes {
|
||||
if tableNode == nil {
|
||||
continue
|
||||
}
|
||||
colType, found := getColType(checker.schema, tableNode, node)
|
||||
if !found {
|
||||
checker.cacheable = false
|
||||
checker.reason = "some column is not found in table schema"
|
||||
} else if colType == mysql.TypeJSON || colType == mysql.TypeEnum || colType == mysql.TypeSet || colType == mysql.TypeBit {
|
||||
checker.cacheable = false
|
||||
checker.reason = "query has some filters with JSON, Enum, Set or Bit columns"
|
||||
}
|
||||
}
|
||||
}
|
||||
return in, !checker.cacheable
|
||||
@ -394,6 +432,7 @@ func (checker *nonPreparedPlanCacheableChecker) Enter(in ast.Node) (out ast.Node
|
||||
}
|
||||
return in, !checker.cacheable
|
||||
}
|
||||
|
||||
checker.cacheable = false // unexpected cases
|
||||
checker.reason = "query has some unsupported Node"
|
||||
return in, !checker.cacheable
|
||||
|
||||
@ -303,6 +303,14 @@ 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",
|
||||
|
||||
// 2-way joins
|
||||
"select * from test.t inner join test.t3 on test.t.a=test.t3.a",
|
||||
"select * from test.t inner join test.t3 on test.t.a=test.t3.a where test.t.a<10",
|
||||
"select * from test.t, test.t3",
|
||||
"select * from test.t, test.t3 where test.t.a=test.t3.a",
|
||||
"select * from test.t, test.t3 where test.t.a=test.t3.a and test.t.b=t3.b",
|
||||
"select * from test.t, test.t3 where test.t.a=test.t3.a and test.t.a<10",
|
||||
}
|
||||
|
||||
unsupported := []string{
|
||||
@ -312,7 +320,6 @@ func TestNonPreparedPlanCacheable(t *testing.T) {
|
||||
"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 test.t1, test.t2", // join
|
||||
"select * from (select * from test.t1) t", // sub-query
|
||||
"insert into test.t1 values(1, 1)", // insert
|
||||
"insert into t1(a, b) select a, b from test.t1", // insert into select
|
||||
|
||||
Reference in New Issue
Block a user