Merge pull request #856 from pingcap/coocood/fiilter-rate
optimizer: refactor optimization flow, push filter condition to table.
This commit is contained in:
@ -53,8 +53,6 @@ func (b *executorBuilder) build(p plan.Plan) Executor {
|
||||
return b.buildDeallocate(v)
|
||||
case *plan.Execute:
|
||||
return b.buildExecute(v)
|
||||
case *plan.Filter:
|
||||
return b.buildFilter(v)
|
||||
case *plan.IndexScan:
|
||||
return b.buildIndexScan(v)
|
||||
case *plan.Limit:
|
||||
@ -77,15 +75,27 @@ func (b *executorBuilder) build(p plan.Plan) Executor {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *executorBuilder) buildFilter(src Executor, conditions []ast.ExprNode) Executor {
|
||||
if len(conditions) == 0 {
|
||||
return src
|
||||
}
|
||||
return &FilterExec{
|
||||
Src: src,
|
||||
Condition: b.joinConditions(conditions),
|
||||
ctx: b.ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *executorBuilder) buildTableScan(v *plan.TableScan) Executor {
|
||||
table, _ := b.is.TableByID(v.Table.ID)
|
||||
return &TableScanExec{
|
||||
e := &TableScanExec{
|
||||
t: table,
|
||||
fields: v.Fields(),
|
||||
ctx: b.ctx,
|
||||
ranges: v.Ranges,
|
||||
seekHandle: math.MinInt64,
|
||||
}
|
||||
return b.buildFilter(e, v.FilterConditions)
|
||||
}
|
||||
|
||||
func (b *executorBuilder) buildShowDDL(v *plan.ShowDDL) Executor {
|
||||
@ -136,7 +146,7 @@ func (b *executorBuilder) buildIndexScan(v *plan.IndexScan) Executor {
|
||||
for i, val := range v.Ranges {
|
||||
e.Ranges[i] = b.buildIndexRange(e, val)
|
||||
}
|
||||
return e
|
||||
return b.buildFilter(e, v.FilterConditions)
|
||||
}
|
||||
|
||||
func (b *executorBuilder) buildIndexRange(scan *IndexScanExec, v *plan.IndexRange) *IndexRangeExec {
|
||||
@ -162,16 +172,6 @@ func (b *executorBuilder) joinConditions(conditions []ast.ExprNode) ast.ExprNode
|
||||
return condition
|
||||
}
|
||||
|
||||
func (b *executorBuilder) buildFilter(v *plan.Filter) Executor {
|
||||
src := b.build(v.Src())
|
||||
e := &FilterExec{
|
||||
Src: src,
|
||||
Condition: b.joinConditions(v.Conditions),
|
||||
ctx: b.ctx,
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (b *executorBuilder) buildSelectLock(v *plan.SelectLock) Executor {
|
||||
src := b.build(v.Src())
|
||||
if autocommit.ShouldAutocommit(b.ctx) {
|
||||
@ -213,9 +213,6 @@ func (b *executorBuilder) buildAggregate(v *plan.Aggregate) Executor {
|
||||
|
||||
func (b *executorBuilder) buildSort(v *plan.Sort) Executor {
|
||||
src := b.build(v.Src())
|
||||
if v.Bypass && !v.ByItems[0].Desc {
|
||||
return src
|
||||
}
|
||||
e := &SortExec{
|
||||
Src: src,
|
||||
ByItems: v.ByItems,
|
||||
|
||||
@ -39,28 +39,11 @@ func Optimize(ctx context.Context, node ast.Node) (plan.Plan, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
alts, err := plan.Alternatives(p)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
err = plan.Refine(p)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
bestCost := plan.EstimateCost(p)
|
||||
bestPlan := p
|
||||
for _, alt := range alts {
|
||||
err = plan.Refine(alt)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
cost := plan.EstimateCost(alt)
|
||||
if cost < bestCost {
|
||||
bestCost = cost
|
||||
bestPlan = alt
|
||||
}
|
||||
}
|
||||
return bestPlan, nil
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Prepare prepares a raw statement parsed from parser.
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
// Copyright 2015 PingCAP, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plan
|
||||
|
||||
import "github.com/juju/errors"
|
||||
|
||||
// Alternatives returns multiple alternative plans that
|
||||
// can be picked based on their cost.
|
||||
func Alternatives(p Plan) ([]Plan, error) {
|
||||
var plans []Plan
|
||||
switch x := p.(type) {
|
||||
case nil:
|
||||
case *TableScan:
|
||||
plans = tableScanAlternatives(x)
|
||||
case WithSrcPlan:
|
||||
var err error
|
||||
plans, err = planWithSrcAlternatives(x)
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
case *ShowDDL:
|
||||
case *CheckTable:
|
||||
case *Prepare:
|
||||
case *Execute:
|
||||
case *Deallocate:
|
||||
default:
|
||||
return nil, ErrUnsupportedType.Gen("Unknown plan %T", p)
|
||||
}
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
// tableScanAlternatives returns all index plans from the same table.
|
||||
func tableScanAlternatives(p *TableScan) []Plan {
|
||||
var alts []Plan
|
||||
for _, v := range p.Table.Indices {
|
||||
fullRange := &IndexRange{
|
||||
LowVal: []interface{}{nil},
|
||||
HighVal: []interface{}{MaxVal},
|
||||
}
|
||||
is := &IndexScan{
|
||||
Index: v,
|
||||
Table: p.Table,
|
||||
Ranges: []*IndexRange{fullRange},
|
||||
}
|
||||
is.SetFields(p.Fields())
|
||||
alts = append(alts, is)
|
||||
}
|
||||
return alts
|
||||
}
|
||||
|
||||
// planWithSrcAlternatives shallow copies the WithSrcPlan,
|
||||
// and sets its src to src alternatives.
|
||||
func planWithSrcAlternatives(p WithSrcPlan) ([]Plan, error) {
|
||||
srcs, err := Alternatives(p.Src())
|
||||
if err != nil {
|
||||
return nil, errors.Trace(err)
|
||||
}
|
||||
for i, val := range srcs {
|
||||
alt := shallowCopy(p)
|
||||
alt.SetSrc(val)
|
||||
srcs[i] = alt
|
||||
}
|
||||
return srcs, nil
|
||||
}
|
||||
|
||||
func shallowCopy(p WithSrcPlan) WithSrcPlan {
|
||||
var copied WithSrcPlan
|
||||
switch x := p.(type) {
|
||||
case *Filter:
|
||||
n := *x
|
||||
copied = &n
|
||||
case *SelectLock:
|
||||
n := *x
|
||||
copied = &n
|
||||
case *SelectFields:
|
||||
n := *x
|
||||
copied = &n
|
||||
case *Sort:
|
||||
n := *x
|
||||
copied = &n
|
||||
case *Limit:
|
||||
n := *x
|
||||
copied = &n
|
||||
case *Aggregate:
|
||||
n := *x
|
||||
copied = &n
|
||||
}
|
||||
return copied
|
||||
}
|
||||
@ -40,10 +40,6 @@ func (c *costEstimator) Enter(p Plan) (Plan, bool) {
|
||||
// Leave implements Visitor Leave interface.
|
||||
func (c *costEstimator) Leave(p Plan) (Plan, bool) {
|
||||
switch v := p.(type) {
|
||||
case *Filter:
|
||||
v.startupCost = v.Src().StartupCost()
|
||||
v.rowCount = v.Src().RowCount() * FilterRate
|
||||
v.totalCost = v.Src().TotalCost()
|
||||
case *IndexScan:
|
||||
c.indexScan(v)
|
||||
case *Limit:
|
||||
@ -61,21 +57,14 @@ func (c *costEstimator) Leave(p Plan) (Plan, bool) {
|
||||
v.rowCount = v.Src().RowCount()
|
||||
v.totalCost = v.Src().TotalCost()
|
||||
case *Sort:
|
||||
if v.Bypass {
|
||||
// Bypassed sort doesn't add extra cost.
|
||||
v.startupCost = v.Src().StartupCost()
|
||||
// Sort plan must retrieve all the rows before returns the first row.
|
||||
v.startupCost = v.Src().TotalCost() + v.Src().RowCount()*SortCost
|
||||
if v.limit == 0 {
|
||||
v.rowCount = v.Src().RowCount()
|
||||
v.totalCost = v.Src().TotalCost()
|
||||
} else {
|
||||
// Sort plan must retrieve all the rows before returns the first row.
|
||||
v.startupCost = v.Src().TotalCost() + v.Src().RowCount()*SortCost
|
||||
if v.limit == 0 {
|
||||
v.rowCount = v.Src().RowCount()
|
||||
} else {
|
||||
v.rowCount = math.Min(v.Src().RowCount(), v.limit)
|
||||
}
|
||||
v.totalCost = v.startupCost + v.rowCount*RowCost
|
||||
v.rowCount = math.Min(v.Src().RowCount(), v.limit)
|
||||
}
|
||||
v.totalCost = v.startupCost + v.rowCount*RowCost
|
||||
case *TableScan:
|
||||
c.tableScan(v)
|
||||
}
|
||||
@ -83,28 +72,9 @@ func (c *costEstimator) Leave(p Plan) (Plan, bool) {
|
||||
}
|
||||
|
||||
func (c *costEstimator) tableScan(v *TableScan) {
|
||||
var rowCount float64
|
||||
if len(v.Ranges) == 1 && v.Ranges[0].LowVal == math.MinInt64 && v.Ranges[0].HighVal == math.MaxInt64 {
|
||||
// full range use default row count.
|
||||
rowCount = FullRangeCount
|
||||
} else {
|
||||
for _, v := range v.Ranges {
|
||||
// for condition like 'a = 0'.
|
||||
if v.LowVal == v.HighVal {
|
||||
rowCount++
|
||||
continue
|
||||
}
|
||||
// For condition like 'a < 0'.
|
||||
if v.LowVal == math.MinInt64 {
|
||||
rowCount += HalfRangeCount
|
||||
}
|
||||
// For condition like 'a > 0'.
|
||||
if v.HighVal == math.MaxInt64 {
|
||||
rowCount += HalfRangeCount
|
||||
}
|
||||
// For condition like 'a > 0 and a < 1'.
|
||||
rowCount += MiddleRangeCount
|
||||
}
|
||||
var rowCount float64 = FullRangeCount
|
||||
for _, con := range v.AccessConditions {
|
||||
rowCount *= guesstimateFilterRate(con)
|
||||
}
|
||||
v.startupCost = 0
|
||||
if v.limit == 0 {
|
||||
@ -117,33 +87,9 @@ func (c *costEstimator) tableScan(v *TableScan) {
|
||||
}
|
||||
|
||||
func (c *costEstimator) indexScan(v *IndexScan) {
|
||||
var rowCount float64
|
||||
if len(v.Ranges) == 1 && v.Ranges[0].LowVal[0] == nil && v.Ranges[0].HighVal[0] == MaxVal {
|
||||
// full range use default row count.
|
||||
rowCount = FullRangeCount
|
||||
} else {
|
||||
for _, v := range v.Ranges {
|
||||
// for condition like 'a = 0'.
|
||||
if v.IsPoint() {
|
||||
rowCount++
|
||||
continue
|
||||
}
|
||||
// For condition like 'a < 0'.
|
||||
if v.LowVal[0] == nil || v.LowVal[0] == MinNotNullVal {
|
||||
rowCount += HalfRangeCount
|
||||
}
|
||||
// For condition like 'a > 0'.
|
||||
if v.HighVal[0] == MaxVal {
|
||||
rowCount += HalfRangeCount
|
||||
}
|
||||
// For condition like 'a > 0 and a < 1'.
|
||||
rowCount += MiddleRangeCount
|
||||
}
|
||||
// If the index has too many ranges, the row count may exceed the default row count.
|
||||
// Make sure the cost is lower than full range.
|
||||
if rowCount >= FullRangeCount {
|
||||
rowCount = FullRangeCount - 1
|
||||
}
|
||||
var rowCount float64 = FullRangeCount
|
||||
for _, con := range v.AccessConditions {
|
||||
rowCount *= guesstimateFilterRate(con)
|
||||
}
|
||||
v.startupCost = 0
|
||||
if v.limit == 0 {
|
||||
|
||||
@ -40,8 +40,6 @@ func (e *explainer) Leave(in Plan) (Plan, bool) {
|
||||
switch x := in.(type) {
|
||||
case *CheckTable:
|
||||
str = "CheckTable"
|
||||
case *Filter:
|
||||
str = "Filter"
|
||||
case *IndexScan:
|
||||
str = fmt.Sprintf("Index(%s.%s)", x.Table.Name.L, x.Index.Name.L)
|
||||
case *Limit:
|
||||
@ -53,9 +51,6 @@ func (e *explainer) Leave(in Plan) (Plan, bool) {
|
||||
case *ShowDDL:
|
||||
str = "ShowDDL"
|
||||
case *Sort:
|
||||
if x.Bypass {
|
||||
return in, true
|
||||
}
|
||||
str = "Sort"
|
||||
case *TableScan:
|
||||
if len(x.Ranges) > 0 {
|
||||
|
||||
115
optimizer/plan/filterrate.go
Normal file
115
optimizer/plan/filterrate.go
Normal file
@ -0,0 +1,115 @@
|
||||
// Copyright 2015 PingCAP, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plan
|
||||
|
||||
import (
|
||||
"github.com/pingcap/tidb/ast"
|
||||
"github.com/pingcap/tidb/parser/opcode"
|
||||
)
|
||||
|
||||
const (
|
||||
rateFull float64 = 1
|
||||
rateEqual float64 = 0.01
|
||||
rateNotEqual float64 = 0.99
|
||||
rateBetween float64 = 0.1
|
||||
rateGreaterOrLess float64 = 0.33
|
||||
rateIsFalse float64 = 0.1
|
||||
rateIsNull float64 = 0.1
|
||||
rateLike float64 = 0.1
|
||||
)
|
||||
|
||||
// guesstimateFilterRate guesstimates the filter rate for an expression.
|
||||
// For example: a table has 100 rows, after filter expression 'a between 0 and 9',
|
||||
// 10 rows returned, then the filter rate is '0.1'.
|
||||
// It only depends on the expression type, not the expression value.
|
||||
// The expr parameter should contain only one column name.
|
||||
func guesstimateFilterRate(expr ast.ExprNode) float64 {
|
||||
switch x := expr.(type) {
|
||||
case *ast.BetweenExpr:
|
||||
return rateBetween
|
||||
case *ast.BinaryOperationExpr:
|
||||
return guesstimateBinop(x)
|
||||
case *ast.ColumnNameExpr:
|
||||
return rateFull
|
||||
case *ast.IsNullExpr:
|
||||
return guesstimateIsNull(x)
|
||||
case *ast.IsTruthExpr:
|
||||
return guesstimateIsTrue(x)
|
||||
case *ast.ParenthesesExpr:
|
||||
return guesstimateFilterRate(x.Expr)
|
||||
case *ast.PatternInExpr:
|
||||
return guesstimatePatternIn(x)
|
||||
case *ast.PatternLikeExpr:
|
||||
return guesstimatePatternLike(x)
|
||||
}
|
||||
return rateFull
|
||||
}
|
||||
|
||||
func guesstimateBinop(expr *ast.BinaryOperationExpr) float64 {
|
||||
switch expr.Op {
|
||||
case opcode.AndAnd:
|
||||
// P(A and B) = P(A) * P(B)
|
||||
return guesstimateFilterRate(expr.L) * guesstimateFilterRate(expr.R)
|
||||
case opcode.OrOr:
|
||||
// P(A or B) = P(A) + P(B) – P(A and B)
|
||||
rateL := guesstimateFilterRate(expr.L)
|
||||
rateR := guesstimateFilterRate(expr.R)
|
||||
return rateL + rateR - rateL*rateR
|
||||
case opcode.EQ:
|
||||
return rateEqual
|
||||
case opcode.GT, opcode.GE, opcode.LT, opcode.LE:
|
||||
return rateGreaterOrLess
|
||||
case opcode.NE:
|
||||
return rateNotEqual
|
||||
}
|
||||
return rateFull
|
||||
}
|
||||
|
||||
func guesstimateIsNull(expr *ast.IsNullExpr) float64 {
|
||||
if expr.Not {
|
||||
return rateFull - rateIsNull
|
||||
}
|
||||
return rateIsNull
|
||||
}
|
||||
|
||||
func guesstimateIsTrue(expr *ast.IsTruthExpr) float64 {
|
||||
if expr.True == 0 {
|
||||
if expr.Not {
|
||||
return rateFull - rateIsFalse
|
||||
}
|
||||
return rateIsFalse
|
||||
}
|
||||
if expr.Not {
|
||||
return rateIsFalse + rateIsNull
|
||||
}
|
||||
return rateFull - rateIsFalse - rateIsNull
|
||||
}
|
||||
|
||||
func guesstimatePatternIn(expr *ast.PatternInExpr) float64 {
|
||||
if len(expr.List) > 0 {
|
||||
rate := rateEqual * float64(len(expr.List))
|
||||
if expr.Not {
|
||||
return rateFull - rate
|
||||
}
|
||||
return rate
|
||||
}
|
||||
return rateFull
|
||||
}
|
||||
|
||||
func guesstimatePatternLike(expr *ast.PatternLikeExpr) float64 {
|
||||
if expr.Not {
|
||||
return rateFull - rateLike
|
||||
}
|
||||
return rateLike
|
||||
}
|
||||
@ -15,7 +15,6 @@ package plan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/pingcap/check"
|
||||
@ -184,6 +183,10 @@ func (s *testPlanSuite) TestRangeBuilder(c *C) {
|
||||
exprStr: `(a < 0 OR a > 3) AND (a < 1 OR a > 4)`,
|
||||
resultStr: `[[-inf 0) (4 +inf]]`,
|
||||
},
|
||||
{
|
||||
exprStr: `a > NULL`,
|
||||
resultStr: `[]`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, ca := range cases {
|
||||
@ -198,63 +201,35 @@ func (s *testPlanSuite) TestRangeBuilder(c *C) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *testPlanSuite) TestBuilder(c *C) {
|
||||
func (s *testPlanSuite) TestFilterRate(c *C) {
|
||||
cases := []struct {
|
||||
sqlStr string
|
||||
planStr string
|
||||
expr string
|
||||
rate float64
|
||||
}{
|
||||
{
|
||||
sqlStr: "select 1",
|
||||
planStr: "Fields",
|
||||
},
|
||||
{
|
||||
sqlStr: "select a from t",
|
||||
planStr: "Table(t)->Fields",
|
||||
},
|
||||
{
|
||||
sqlStr: "select a from t where a = 1",
|
||||
planStr: "Table(t)->Filter->Fields",
|
||||
},
|
||||
{
|
||||
sqlStr: "select a from t where a = 1 order by a",
|
||||
planStr: "Table(t)->Filter->Fields->Sort",
|
||||
},
|
||||
{
|
||||
sqlStr: "select a from t where a = 1 order by a limit 1",
|
||||
planStr: "Table(t)->Filter->Fields->Sort->Limit",
|
||||
},
|
||||
{
|
||||
sqlStr: "select a from t where a = 1 limit 1",
|
||||
planStr: "Table(t)->Filter->Fields->Limit",
|
||||
},
|
||||
{
|
||||
sqlStr: "select a from t where a = 1 limit 1 for update",
|
||||
planStr: "Table(t)->Filter->Lock->Fields->Limit",
|
||||
},
|
||||
{
|
||||
sqlStr: "admin show ddl",
|
||||
planStr: "ShowDDL",
|
||||
},
|
||||
{
|
||||
sqlStr: "admin check table t",
|
||||
planStr: "CheckTable",
|
||||
},
|
||||
{expr: "a = 1", rate: rateEqual},
|
||||
{expr: "a > 1", rate: rateGreaterOrLess},
|
||||
{expr: "a between 1 and 100", rate: rateBetween},
|
||||
{expr: "a is null", rate: rateIsNull},
|
||||
{expr: "a is not null", rate: rateFull - rateIsNull},
|
||||
{expr: "a is true", rate: rateFull - rateIsNull - rateIsFalse},
|
||||
{expr: "a is not true", rate: rateIsNull + rateIsFalse},
|
||||
{expr: "a is false", rate: rateIsFalse},
|
||||
{expr: "a is not false", rate: rateFull - rateIsFalse},
|
||||
{expr: "a like 'a'", rate: rateLike},
|
||||
{expr: "a not like 'a'", rate: rateFull - rateLike},
|
||||
{expr: "a in (1, 2, 3)", rate: rateEqual * 3},
|
||||
{expr: "a not in (1, 2, 3)", rate: rateFull - rateEqual*3},
|
||||
{expr: "a > 1 and a < 9", rate: float64(rateGreaterOrLess) * float64(rateGreaterOrLess)},
|
||||
{expr: "a = 1 or a = 2", rate: rateEqual + rateEqual - rateEqual*rateEqual},
|
||||
{expr: "a != 1", rate: rateNotEqual},
|
||||
}
|
||||
var stmt ast.StmtNode
|
||||
for _, ca := range cases {
|
||||
s, err := parser.ParseOneStmt(ca.sqlStr, "", "")
|
||||
c.Assert(err, IsNil, Commentf("for expr %s", ca.sqlStr))
|
||||
if strings.HasPrefix(ca.sqlStr, "select") {
|
||||
stmt = s.(*ast.SelectStmt)
|
||||
} else if strings.HasPrefix(ca.sqlStr, "admin") {
|
||||
stmt = s.(*ast.AdminStmt)
|
||||
}
|
||||
mockResolve(stmt)
|
||||
p, err := BuildPlan(stmt)
|
||||
c.Assert(err, IsNil)
|
||||
explainStr, err := Explain(p)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(explainStr, Equals, ca.planStr, Commentf("for expr %s", ca.sqlStr))
|
||||
sql := "select 1 from dual where " + ca.expr
|
||||
s, err := parser.ParseOneStmt(sql, "", "")
|
||||
c.Assert(err, IsNil, Commentf("for expr %s", ca.expr))
|
||||
stmt := s.(*ast.SelectStmt)
|
||||
rate := guesstimateFilterRate(stmt.Where)
|
||||
c.Assert(rate, Equals, ca.rate, Commentf("for expr %s", ca.expr))
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,66 +248,63 @@ func (s *testPlanSuite) TestBestPlan(c *C) {
|
||||
},
|
||||
{
|
||||
sql: "select * from t where b = 1 order by a",
|
||||
best: "Index(t.b)->Filter->Fields->Sort",
|
||||
best: "Index(t.b)->Fields->Sort",
|
||||
},
|
||||
{
|
||||
sql: "select * from t where (a between 1 and 2) and (b = 3)",
|
||||
best: "Index(t.b)->Filter->Fields",
|
||||
best: "Index(t.b)->Fields",
|
||||
},
|
||||
{
|
||||
sql: "select * from t where a > 0 order by b limit 100",
|
||||
best: "Index(t.b)->Filter->Fields->Limit",
|
||||
best: "Index(t.b)->Fields->Limit",
|
||||
},
|
||||
{
|
||||
sql: "select * from t where d = 0",
|
||||
best: "Table(t)->Filter->Fields",
|
||||
best: "Table(t)->Fields",
|
||||
},
|
||||
{
|
||||
sql: "select * from t where c = 0 and d = 0",
|
||||
best: "Index(t.c_d)->Filter->Fields",
|
||||
best: "Index(t.c_d)->Fields",
|
||||
},
|
||||
{
|
||||
sql: "select * from t where b like 'abc%'",
|
||||
best: "Index(t.b)->Filter->Fields",
|
||||
best: "Index(t.b)->Fields",
|
||||
},
|
||||
{
|
||||
sql: "select * from t where d",
|
||||
best: "Table(t)->Filter->Fields",
|
||||
best: "Table(t)->Fields",
|
||||
},
|
||||
{
|
||||
sql: "select * from t where a is null",
|
||||
best: "Range(t)->Filter->Fields",
|
||||
best: "Range(t)->Fields",
|
||||
},
|
||||
{
|
||||
sql: "select a from t where a = 1 limit 1 for update",
|
||||
best: "Range(t)->Lock->Fields->Limit",
|
||||
},
|
||||
{
|
||||
sql: "admin show ddl",
|
||||
best: "ShowDDL",
|
||||
},
|
||||
{
|
||||
sql: "admin check table t",
|
||||
best: "CheckTable",
|
||||
},
|
||||
}
|
||||
for _, ca := range cases {
|
||||
comment := Commentf("for %s", ca.sql)
|
||||
s, err := parser.ParseOneStmt(ca.sql, "", "")
|
||||
stmt, err := parser.ParseOneStmt(ca.sql, "", "")
|
||||
c.Assert(err, IsNil, comment)
|
||||
stmt := s.(*ast.SelectStmt)
|
||||
ast.SetFlag(stmt)
|
||||
mockResolve(stmt)
|
||||
|
||||
p, err := BuildPlan(stmt)
|
||||
c.Assert(err, IsNil)
|
||||
alts, err := Alternatives(p)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = Refine(p)
|
||||
explainStr, err := Explain(p)
|
||||
c.Assert(err, IsNil)
|
||||
bestCost := EstimateCost(p)
|
||||
bestPlan := p
|
||||
|
||||
for _, alt := range alts {
|
||||
c.Assert(Refine(alt), IsNil)
|
||||
cost := EstimateCost(alt)
|
||||
if cost < bestCost {
|
||||
bestCost = cost
|
||||
bestPlan = alt
|
||||
}
|
||||
}
|
||||
explainStr, err := Explain(bestPlan)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(explainStr, Equals, ca.best, Commentf("for %s cost %v", ca.sql, bestCost))
|
||||
c.Assert(explainStr, Equals, ca.best, Commentf("for %s cost %v", ca.sql, EstimateCost(p)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -14,8 +14,6 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/juju/errors"
|
||||
"github.com/pingcap/tidb/ast"
|
||||
"github.com/pingcap/tidb/infoschema"
|
||||
@ -137,16 +135,10 @@ func (b *planBuilder) buildSelect(sel *ast.SelectStmt) Plan {
|
||||
}
|
||||
var p Plan
|
||||
if sel.From != nil {
|
||||
p = b.buildJoin(sel.From.TableRefs)
|
||||
p = b.buildJoin(sel)
|
||||
if b.err != nil {
|
||||
return nil
|
||||
}
|
||||
if sel.Where != nil {
|
||||
p = b.buildFilter(p, sel.Where)
|
||||
if b.err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if sel.LockTp != ast.SelectLockNone {
|
||||
p = b.buildSelectLock(p, sel.LockTp)
|
||||
if b.err != nil {
|
||||
@ -168,14 +160,8 @@ func (b *planBuilder) buildSelect(sel *ast.SelectStmt) Plan {
|
||||
if b.err != nil {
|
||||
return nil
|
||||
}
|
||||
if sel.Where != nil {
|
||||
p = b.buildFilter(p, sel.Where)
|
||||
if b.err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if sel.OrderBy != nil {
|
||||
if sel.OrderBy != nil && !matchOrder(p, sel.OrderBy.Items) {
|
||||
p = b.buildSort(p, sel.OrderBy.Items)
|
||||
if b.err != nil {
|
||||
return nil
|
||||
@ -190,8 +176,12 @@ func (b *planBuilder) buildSelect(sel *ast.SelectStmt) Plan {
|
||||
return p
|
||||
}
|
||||
|
||||
func (b *planBuilder) buildJoin(from *ast.Join) Plan {
|
||||
// Only support single table for now.
|
||||
func (b *planBuilder) buildJoin(sel *ast.SelectStmt) Plan {
|
||||
from := sel.From.TableRefs
|
||||
if from.Right != nil {
|
||||
b.err = ErrUnsupportedType.Gen("Only support single table for now.")
|
||||
return nil
|
||||
}
|
||||
ts, ok := from.Left.(*ast.TableSource)
|
||||
if !ok {
|
||||
b.err = ErrUnsupportedType.Gen("Unsupported type %T", from.Left)
|
||||
@ -202,38 +192,99 @@ func (b *planBuilder) buildJoin(from *ast.Join) Plan {
|
||||
b.err = ErrUnsupportedType.Gen("Unsupported type %T", ts.Source)
|
||||
return nil
|
||||
}
|
||||
conditions := splitWhere(sel.Where)
|
||||
candidates := b.buildAllAccessMethodsPlan(tn, conditions)
|
||||
var bestPlan Plan
|
||||
var lowestCost float64
|
||||
for _, v := range candidates {
|
||||
cost := EstimateCost(b.buildPseudoSelectPlan(v, sel))
|
||||
if bestPlan == nil {
|
||||
bestPlan = v
|
||||
lowestCost = cost
|
||||
}
|
||||
if cost < lowestCost {
|
||||
bestPlan = v
|
||||
lowestCost = cost
|
||||
}
|
||||
}
|
||||
return bestPlan
|
||||
}
|
||||
|
||||
func (b *planBuilder) buildAllAccessMethodsPlan(tn *ast.TableName, conditions []ast.ExprNode) []Plan {
|
||||
var candidates []Plan
|
||||
p := b.buildTableScanPlan(tn, conditions)
|
||||
candidates = append(candidates, p)
|
||||
for _, index := range tn.TableInfo.Indices {
|
||||
ip := b.buildIndexScanPlan(index, tn, conditions)
|
||||
candidates = append(candidates, ip)
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func (b *planBuilder) buildTableScanPlan(tn *ast.TableName, conditions []ast.ExprNode) Plan {
|
||||
p := &TableScan{
|
||||
Table: tn.TableInfo,
|
||||
Ranges: []TableRange{{math.MinInt64, math.MaxInt64}},
|
||||
Table: tn.TableInfo,
|
||||
}
|
||||
p.SetFields(tn.GetResultFields())
|
||||
var pkName model.CIStr
|
||||
if p.Table.PKIsHandle {
|
||||
for _, colInfo := range p.Table.Columns {
|
||||
if mysql.HasPriKeyFlag(colInfo.Flag) {
|
||||
pkName = colInfo.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, con := range conditions {
|
||||
if pkName.L != "" {
|
||||
checker := conditionChecker{tableName: tn.TableInfo.Name, pkName: pkName}
|
||||
if checker.check(con) {
|
||||
p.AccessConditions = append(p.AccessConditions, con)
|
||||
} else {
|
||||
p.FilterConditions = append(p.FilterConditions, con)
|
||||
}
|
||||
} else {
|
||||
p.FilterConditions = append(p.FilterConditions, con)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// splitWhere split a where expression to a list of AND conditions.
|
||||
func (b *planBuilder) splitWhere(where ast.ExprNode) []ast.ExprNode {
|
||||
var conditions []ast.ExprNode
|
||||
switch x := where.(type) {
|
||||
case *ast.BinaryOperationExpr:
|
||||
if x.Op == opcode.AndAnd {
|
||||
conditions = append(conditions, x.L)
|
||||
conditions = append(conditions, b.splitWhere(x.R)...)
|
||||
func (b *planBuilder) buildIndexScanPlan(index *model.IndexInfo, tn *ast.TableName, conditions []ast.ExprNode) Plan {
|
||||
ip := &IndexScan{Table: tn.TableInfo, Index: index}
|
||||
ip.SetFields(tn.GetResultFields())
|
||||
// Only use first column as access condition for cost estimation,
|
||||
// In executor, we can try to use second index column to build index range.
|
||||
checker := conditionChecker{tableName: tn.TableInfo.Name, idx: index, columnOffset: 0}
|
||||
for _, con := range conditions {
|
||||
if checker.check(con) {
|
||||
ip.AccessConditions = append(ip.AccessConditions, con)
|
||||
} else {
|
||||
conditions = append(conditions, x)
|
||||
ip.FilterConditions = append(ip.FilterConditions, con)
|
||||
}
|
||||
default:
|
||||
conditions = append(conditions, where)
|
||||
}
|
||||
return conditions
|
||||
return ip
|
||||
}
|
||||
|
||||
func (b *planBuilder) buildFilter(src Plan, where ast.ExprNode) *Filter {
|
||||
filter := &Filter{
|
||||
Conditions: b.splitWhere(where),
|
||||
// buildPseudoSelectPlan pre-builds more complete plans that may affect total cost.
|
||||
func (b *planBuilder) buildPseudoSelectPlan(p Plan, sel *ast.SelectStmt) Plan {
|
||||
if sel.OrderBy == nil {
|
||||
return p
|
||||
}
|
||||
filter.SetSrc(src)
|
||||
filter.SetFields(src.Fields())
|
||||
return filter
|
||||
if sel.GroupBy != nil {
|
||||
return p
|
||||
}
|
||||
if !matchOrder(p, sel.OrderBy.Items) {
|
||||
np := &Sort{ByItems: sel.OrderBy.Items}
|
||||
np.SetSrc(p)
|
||||
p = np
|
||||
}
|
||||
if sel.Limit != nil {
|
||||
np := &Limit{Offset: sel.Limit.Offset, Count: sel.Limit.Count}
|
||||
np.SetSrc(p)
|
||||
np.SetLimit(0)
|
||||
p = np
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (b *planBuilder) buildSelectLock(src Plan, lock ast.SelectLockType) *SelectLock {
|
||||
@ -355,3 +406,77 @@ func buildResultField(tableName, name string, tp byte, size int) *ast.ResultFiel
|
||||
Expr: expr,
|
||||
}
|
||||
}
|
||||
|
||||
// matchOrder checks if the plan has the same ordering as items.
|
||||
func matchOrder(p Plan, items []*ast.ByItem) bool {
|
||||
switch x := p.(type) {
|
||||
case *Aggregate:
|
||||
return false
|
||||
case *IndexScan:
|
||||
if len(items) > len(x.Index.Columns) {
|
||||
return false
|
||||
}
|
||||
for i, item := range items {
|
||||
if item.Desc {
|
||||
return false
|
||||
}
|
||||
var rf *ast.ResultField
|
||||
switch y := item.Expr.(type) {
|
||||
case *ast.ColumnNameExpr:
|
||||
rf = y.Refer
|
||||
case *ast.PositionExpr:
|
||||
rf = y.Refer
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if rf.Table.Name.L != x.Table.Name.L || rf.Column.Name.L != x.Index.Columns[i].Name.L {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case *TableScan:
|
||||
if len(items) != 1 || !x.Table.PKIsHandle {
|
||||
return false
|
||||
}
|
||||
if items[0].Desc {
|
||||
return false
|
||||
}
|
||||
var refer *ast.ResultField
|
||||
switch x := items[0].Expr.(type) {
|
||||
case *ast.ColumnNameExpr:
|
||||
refer = x.Refer
|
||||
case *ast.PositionExpr:
|
||||
refer = x.Refer
|
||||
default:
|
||||
return false
|
||||
}
|
||||
if mysql.HasPriKeyFlag(refer.Column.Flag) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case *Sort:
|
||||
// Sort plan should not be checked here as there should only be one sort plan in a plan tree.
|
||||
return false
|
||||
case WithSrcPlan:
|
||||
return matchOrder(x.Src(), items)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// splitWhere split a where expression to a list of AND conditions.
|
||||
func splitWhere(where ast.ExprNode) []ast.ExprNode {
|
||||
var conditions []ast.ExprNode
|
||||
switch x := where.(type) {
|
||||
case nil:
|
||||
case *ast.BinaryOperationExpr:
|
||||
if x.Op == opcode.AndAnd {
|
||||
conditions = append(conditions, x.L)
|
||||
conditions = append(conditions, splitWhere(x.R)...)
|
||||
} else {
|
||||
conditions = append(conditions, x)
|
||||
}
|
||||
default:
|
||||
conditions = append(conditions, where)
|
||||
}
|
||||
return conditions
|
||||
}
|
||||
|
||||
@ -31,6 +31,12 @@ type TableScan struct {
|
||||
Table *model.TableInfo
|
||||
Desc bool
|
||||
Ranges []TableRange
|
||||
|
||||
// AccessConditions can be used to build index range.
|
||||
AccessConditions []ast.ExprNode
|
||||
|
||||
// FilterConditions can be used to filter result.
|
||||
FilterConditions []ast.ExprNode
|
||||
}
|
||||
|
||||
// Accept implements Plan Accept interface.
|
||||
@ -118,6 +124,12 @@ type IndexScan struct {
|
||||
|
||||
// Desc indicates whether the index should be scanned in descending order.
|
||||
Desc bool
|
||||
|
||||
// AccessConditions can be used to build index range.
|
||||
AccessConditions []ast.ExprNode
|
||||
|
||||
// FilterConditions can be used to filter result.
|
||||
FilterConditions []ast.ExprNode
|
||||
}
|
||||
|
||||
// Accept implements Plan Accept interface.
|
||||
@ -126,38 +138,6 @@ func (p *IndexScan) Accept(v Visitor) (Plan, bool) {
|
||||
return v.Leave(np)
|
||||
}
|
||||
|
||||
// Filter represents a filter plan.
|
||||
type Filter struct {
|
||||
planWithSrc
|
||||
|
||||
// Originally the WHERE or ON condition is parsed into a single expression,
|
||||
// but after we converted to CNF(Conjunctive normal form), it can be
|
||||
// split into a list of AND conditions.
|
||||
Conditions []ast.ExprNode
|
||||
}
|
||||
|
||||
// Accept implements Plan Accept interface.
|
||||
func (p *Filter) Accept(v Visitor) (Plan, bool) {
|
||||
np, skip := v.Enter(p)
|
||||
if skip {
|
||||
v.Leave(np)
|
||||
}
|
||||
p = np.(*Filter)
|
||||
var ok bool
|
||||
p.src, ok = p.src.Accept(v)
|
||||
if !ok {
|
||||
return p, false
|
||||
}
|
||||
return v.Leave(p)
|
||||
}
|
||||
|
||||
// SetLimit implements Plan SetLimit interface.
|
||||
func (p *Filter) SetLimit(limit float64) {
|
||||
p.limit = limit
|
||||
// We assume 50% of the src row is filtered out.
|
||||
p.src.SetLimit(limit * 2)
|
||||
}
|
||||
|
||||
// SelectLock represents a select lock plan.
|
||||
type SelectLock struct {
|
||||
planWithSrc
|
||||
@ -221,10 +201,6 @@ type Sort struct {
|
||||
planWithSrc
|
||||
|
||||
ByItems []*ast.ByItem
|
||||
// If the source is already in the same order, the sort process can be by passed.
|
||||
// It depends on the Src plan, so if the Src plan has been modified, Bypass needs
|
||||
// to be recalculated.
|
||||
Bypass bool
|
||||
}
|
||||
|
||||
// Accept implements Plan Accept interface.
|
||||
@ -247,9 +223,6 @@ func (p *Sort) Accept(v Visitor) (Plan, bool) {
|
||||
// Bypass has to be determined before this get called.
|
||||
func (p *Sort) SetLimit(limit float64) {
|
||||
p.limit = limit
|
||||
if p.Bypass {
|
||||
p.src.SetLimit(limit)
|
||||
}
|
||||
}
|
||||
|
||||
// Limit represents offset and limit plan.
|
||||
|
||||
@ -163,6 +163,9 @@ func (r *rangeBuilder) buildFromBinop(x *ast.BinaryOperationExpr) []rangePoint {
|
||||
value = x.R.GetValue()
|
||||
op = x.Op
|
||||
}
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
switch op {
|
||||
case opcode.EQ:
|
||||
startPoint := rangePoint{value: value, start: true}
|
||||
|
||||
@ -14,15 +14,15 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/pingcap/tidb/ast"
|
||||
"github.com/pingcap/tidb/model"
|
||||
"github.com/pingcap/tidb/mysql"
|
||||
"github.com/pingcap/tidb/parser/opcode"
|
||||
"github.com/pingcap/tidb/util/types"
|
||||
)
|
||||
|
||||
// Refine tries to build index range, bypass sort, set limit for source plan.
|
||||
// It prepares the plan for cost estimation.
|
||||
// Refine tries to build index or table range.
|
||||
func Refine(p Plan) error {
|
||||
r := refiner{}
|
||||
p.Accept(&r)
|
||||
@ -30,22 +30,10 @@ func Refine(p Plan) error {
|
||||
}
|
||||
|
||||
type refiner struct {
|
||||
conditions []ast.ExprNode
|
||||
// store scan plan for sort to use.
|
||||
indexScan *IndexScan
|
||||
tableScan *TableScan
|
||||
err error
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *refiner) Enter(in Plan) (Plan, bool) {
|
||||
switch x := in.(type) {
|
||||
case *Filter:
|
||||
r.conditions = x.Conditions
|
||||
case *IndexScan:
|
||||
r.indexScan = x
|
||||
case *TableScan:
|
||||
r.tableScan = x
|
||||
}
|
||||
return in, false
|
||||
}
|
||||
|
||||
@ -55,66 +43,12 @@ func (r *refiner) Leave(in Plan) (Plan, bool) {
|
||||
r.buildIndexRange(x)
|
||||
case *Limit:
|
||||
x.SetLimit(0)
|
||||
case *Sort:
|
||||
r.sortBypass(x)
|
||||
case *TableScan:
|
||||
r.buildTableRange(x)
|
||||
}
|
||||
return in, r.err == nil
|
||||
}
|
||||
|
||||
func (r *refiner) sortBypass(p *Sort) {
|
||||
if r.indexScan != nil {
|
||||
idx := r.indexScan.Index
|
||||
if len(p.ByItems) > len(idx.Columns) {
|
||||
return
|
||||
}
|
||||
var desc bool
|
||||
for i, val := range p.ByItems {
|
||||
if val.Desc {
|
||||
desc = true
|
||||
}
|
||||
cn, ok := val.Expr.(*ast.ColumnNameExpr)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if r.indexScan.Table.Name.L != cn.Refer.Table.Name.L {
|
||||
return
|
||||
}
|
||||
indexColumn := idx.Columns[i]
|
||||
if indexColumn.Name.L != cn.Refer.Column.Name.L {
|
||||
return
|
||||
}
|
||||
}
|
||||
if desc {
|
||||
// TODO: support desc when index reverse iterator is supported.
|
||||
r.indexScan.Desc = true
|
||||
return
|
||||
}
|
||||
p.Bypass = true
|
||||
} else if r.tableScan != nil {
|
||||
if len(p.ByItems) != 1 {
|
||||
return
|
||||
}
|
||||
byItem := p.ByItems[0]
|
||||
if byItem.Desc {
|
||||
// TODO: support desc when table reverse iterator is supported.
|
||||
return
|
||||
}
|
||||
cn, ok := byItem.Expr.(*ast.ColumnNameExpr)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !mysql.HasPriKeyFlag(cn.Refer.Column.Flag) {
|
||||
return
|
||||
}
|
||||
if !cn.Refer.Table.PKIsHandle {
|
||||
return
|
||||
}
|
||||
p.Bypass = true
|
||||
}
|
||||
}
|
||||
|
||||
var fullRange = []rangePoint{
|
||||
{start: true},
|
||||
{value: MaxVal},
|
||||
@ -122,50 +56,25 @@ var fullRange = []rangePoint{
|
||||
|
||||
func (r *refiner) buildIndexRange(p *IndexScan) {
|
||||
rb := rangeBuilder{}
|
||||
for i := 0; i < len(p.Index.Columns); i++ {
|
||||
checker := conditionChecker{idx: p.Index, tableName: p.Table.Name, columnOffset: i}
|
||||
rangePoints := fullRange
|
||||
var columnUsed bool
|
||||
for _, cond := range r.conditions {
|
||||
if checker.check(cond) {
|
||||
rangePoints = rb.intersection(rangePoints, rb.build(cond))
|
||||
columnUsed = true
|
||||
}
|
||||
}
|
||||
if !columnUsed {
|
||||
// For multi-column index, if the prefix column is not used, following columns
|
||||
// can not be used.
|
||||
break
|
||||
}
|
||||
if i == 0 {
|
||||
// Build index range from the first column.
|
||||
p.Ranges = rb.buildIndexRanges(rangePoints)
|
||||
} else {
|
||||
// range built from following columns should be appended to previous ranges.
|
||||
p.Ranges = rb.appendIndexRanges(p.Ranges, rangePoints)
|
||||
}
|
||||
rangePoints := fullRange
|
||||
for _, cond := range p.AccessConditions {
|
||||
rangePoints = rb.intersection(rangePoints, rb.build(cond))
|
||||
}
|
||||
p.Ranges = rb.buildIndexRanges(rangePoints)
|
||||
// TODO: build index range for second column.
|
||||
r.err = rb.err
|
||||
return
|
||||
}
|
||||
|
||||
func (r *refiner) buildTableRange(p *TableScan) {
|
||||
var pkHandleColumn *model.ColumnInfo
|
||||
for _, colInfo := range p.Table.Columns {
|
||||
if mysql.HasPriKeyFlag(colInfo.Flag) && p.Table.PKIsHandle {
|
||||
pkHandleColumn = colInfo
|
||||
}
|
||||
}
|
||||
if pkHandleColumn == nil {
|
||||
if len(p.AccessConditions) == 0 {
|
||||
p.Ranges = []TableRange{{math.MinInt64, math.MaxInt64}}
|
||||
return
|
||||
}
|
||||
rb := rangeBuilder{}
|
||||
rangePoints := fullRange
|
||||
checker := conditionChecker{pkName: pkHandleColumn.Name, tableName: p.Table.Name}
|
||||
for _, cond := range r.conditions {
|
||||
if checker.check(cond) {
|
||||
rangePoints = rb.intersection(rangePoints, rb.build(cond))
|
||||
}
|
||||
for _, cond := range p.AccessConditions {
|
||||
rangePoints = rb.intersection(rangePoints, rb.build(cond))
|
||||
}
|
||||
p.Ranges = rb.buildTableRanges(rangePoints)
|
||||
r.err = rb.err
|
||||
@ -264,6 +173,8 @@ func (c *conditionChecker) checkColumnExpr(expr ast.ExprNode) bool {
|
||||
if c.pkName.L != "" {
|
||||
return c.pkName.L == cn.Refer.Column.Name.L
|
||||
}
|
||||
|
||||
return cn.Refer.Column.Name.L == c.idx.Columns[c.columnOffset].Name.L
|
||||
if c.idx != nil {
|
||||
return cn.Refer.Column.Name.L == c.idx.Columns[c.columnOffset].Name.L
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1366,7 +1366,7 @@ func (s *testSessionSuite) TestMultiColumnIndex(c *C) {
|
||||
mustExecSQL(c, se, "insert into t values (1, 5)")
|
||||
|
||||
sql := "select c1 from t where c1 in (1) and c2 < 10"
|
||||
expectedExplain := "Index(t.idx_c1_c2)->Filter->Fields"
|
||||
expectedExplain := "Index(t.idx_c1_c2)->Fields"
|
||||
checkPlan(c, se, sql, expectedExplain)
|
||||
mustExecMatch(c, se, sql, [][]interface{}{{1}})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user