Skip to content

Commit 79e237d

Browse files
authored
planner: generate PointGet plans for PlanCache when all conditions are EQ (#29859) (#29872)
1 parent c83f032 commit 79e237d

File tree

3 files changed

+121
-15
lines changed

3 files changed

+121
-15
lines changed

executor/explainfor_test.go

+16-7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"fmt"
2121
"math"
2222
"strconv"
23-
"strings"
2423
"sync"
2524

2625
. "github.com/pingcap/check"
@@ -514,9 +513,14 @@ func (s *testPrepareSerialSuite) TestPointGetUserVarPlanCache(c *C) {
514513
tkProcess := tk.Se.ShowProcess()
515514
ps := []*util.ProcessInfo{tkProcess}
516515
tk.Se.SetSessionManager(&mockSessionManager1{PS: ps})
517-
rows := tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Rows()
518-
c.Assert(strings.Contains(fmt.Sprintf("%v", rows[5][0]), "IndexRangeScan"), IsTrue)
519-
c.Assert(strings.Contains(fmt.Sprintf("%v", rows[5][3]), "table:t2"), IsTrue)
516+
tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Check(testkit.Rows( // can use idx_a
517+
`Projection_9 1.00 root test.t1.a, test.t1.b, test.t2.a, test.t2.b`,
518+
`└─IndexJoin_17 1.00 root inner join, inner:TableReader_13, outer key:test.t2.a, inner key:test.t1.a, equal cond:eq(test.t2.a, test.t1.a)`,
519+
` ├─Selection_44(Build) 0.80 root not(isnull(test.t2.a))`,
520+
` │ └─Point_Get_43 1.00 root table:t2, index:idx_a(a) `,
521+
` └─TableReader_13(Probe) 0.00 root data:Selection_12`,
522+
` └─Selection_12 0.00 cop[tikv] eq(test.t1.a, 1)`,
523+
` └─TableRangeScan_11 1.00 cop[tikv] table:t1 range: decided by [test.t2.a], keep order:false, stats:pseudo`))
520524

521525
tk.MustExec("set @a=2")
522526
tk.MustQuery("execute stmt using @a").Check(testkit.Rows(
@@ -525,9 +529,14 @@ func (s *testPrepareSerialSuite) TestPointGetUserVarPlanCache(c *C) {
525529
tkProcess = tk.Se.ShowProcess()
526530
ps = []*util.ProcessInfo{tkProcess}
527531
tk.Se.SetSessionManager(&mockSessionManager1{PS: ps})
528-
rows = tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Rows()
529-
c.Assert(strings.Contains(fmt.Sprintf("%v", rows[5][0]), "IndexRangeScan"), IsTrue)
530-
c.Assert(strings.Contains(fmt.Sprintf("%v", rows[5][3]), "table:t2"), IsTrue)
532+
tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Check(testkit.Rows( // can use idx_a
533+
`Projection_9 1.00 root test.t1.a, test.t1.b, test.t2.a, test.t2.b`,
534+
`└─IndexJoin_17 1.00 root inner join, inner:TableReader_13, outer key:test.t2.a, inner key:test.t1.a, equal cond:eq(test.t2.a, test.t1.a)`,
535+
` ├─Selection_44(Build) 0.80 root not(isnull(test.t2.a))`,
536+
` │ └─Point_Get_43 1.00 root table:t2, index:idx_a(a) `,
537+
` └─TableReader_13(Probe) 0.00 root data:Selection_12`,
538+
` └─Selection_12 0.00 cop[tikv] eq(test.t1.a, 2)`,
539+
` └─TableRangeScan_11 1.00 cop[tikv] table:t1 range: decided by [test.t2.a], keep order:false, stats:pseudo`))
531540
tk.MustQuery("execute stmt using @a").Check(testkit.Rows(
532541
"2 4 2 2",
533542
))

executor/prepared_test.go

+78-4
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,83 @@ func (s *testSerialSuite) TestIssue28782(c *C) {
780780
tk.MustQuery("select @@last_plan_from_cache;").Check(testkit.Rows("1"))
781781
}
782782

783+
func (s *testSerialSuite) TestIssue29850(c *C) {
784+
store, dom, err := newStoreWithBootstrap()
785+
c.Assert(err, IsNil)
786+
tk := testkit.NewTestKit(c, store)
787+
defer func() {
788+
dom.Close()
789+
store.Close()
790+
}()
791+
orgEnable := plannercore.PreparedPlanCacheEnabled()
792+
defer func() {
793+
plannercore.SetPreparedPlanCache(orgEnable)
794+
}()
795+
plannercore.SetPreparedPlanCache(true)
796+
797+
tk.MustExec(`set tidb_enable_clustered_index=on`)
798+
tk.MustExec("set @@tidb_enable_collect_execution_info=0")
799+
tk.MustExec(`use test`)
800+
tk.MustExec(`CREATE TABLE customer (
801+
c_id int(11) NOT NULL,
802+
c_d_id int(11) NOT NULL,
803+
c_first varchar(16) DEFAULT NULL,
804+
c_w_id int(11) NOT NULL,
805+
c_last varchar(16) DEFAULT NULL,
806+
c_credit char(2) DEFAULT NULL,
807+
c_discount decimal(4,4) DEFAULT NULL,
808+
PRIMARY KEY (c_w_id,c_d_id,c_id),
809+
KEY idx_customer (c_w_id,c_d_id,c_last,c_first))`)
810+
tk.MustExec(`CREATE TABLE warehouse (
811+
w_id int(11) NOT NULL,
812+
w_tax decimal(4,4) DEFAULT NULL,
813+
PRIMARY KEY (w_id))`)
814+
tk.MustExec(`prepare stmt from 'SELECT c_discount, c_last, c_credit, w_tax
815+
FROM customer, warehouse
816+
WHERE w_id = ? AND c_w_id = w_id AND c_d_id = ? AND c_id = ?'`)
817+
tk.MustExec(`set @w_id=1262`)
818+
tk.MustExec(`set @c_d_id=7`)
819+
tk.MustExec(`set @c_id=1549`)
820+
tk.MustQuery(`execute stmt using @w_id, @c_d_id, @c_id`).Check(testkit.Rows())
821+
tkProcess := tk.Se.ShowProcess()
822+
ps := []*util.ProcessInfo{tkProcess}
823+
tk.Se.SetSessionManager(&mockSessionManager1{PS: ps})
824+
tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Check(testkit.Rows( // can use PointGet
825+
`Projection_7 0.00 root test.customer.c_discount, test.customer.c_last, test.customer.c_credit, test.warehouse.w_tax`,
826+
`└─MergeJoin_8 0.00 root inner join, left key:test.customer.c_w_id, right key:test.warehouse.w_id`,
827+
` ├─Point_Get_34(Build) 1.00 root table:warehouse handle:1262`,
828+
` └─Point_Get_33(Probe) 1.00 root table:customer, clustered index:PRIMARY(c_w_id, c_d_id, c_id) `))
829+
tk.MustQuery(`execute stmt using @w_id, @c_d_id, @c_id`).Check(testkit.Rows())
830+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("1")) // can use the cached plan
831+
832+
tk.MustExec(`create table t (a int primary key)`)
833+
tk.MustExec(`insert into t values (1), (2)`)
834+
tk.MustExec(`prepare stmt from 'select * from t where a>=? and a<=?'`)
835+
tk.MustExec(`set @a1=1, @a2=2`)
836+
tk.MustQuery(`execute stmt using @a1, @a1`).Check(testkit.Rows("1"))
837+
tkProcess = tk.Se.ShowProcess()
838+
ps = []*util.ProcessInfo{tkProcess}
839+
tk.Se.SetSessionManager(&mockSessionManager1{PS: ps})
840+
tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Check(testkit.Rows( // cannot use PointGet since it contains a range condition
841+
`Selection_7 1.00 root ge(test.t.a, 1), le(test.t.a, 1)`,
842+
`└─TableReader_6 1.00 root data:TableRangeScan_5`,
843+
` └─TableRangeScan_5 1.00 cop[tikv] table:t range:[1,1], keep order:false, stats:pseudo`))
844+
tk.MustQuery(`execute stmt using @a1, @a2`).Check(testkit.Rows("1", "2"))
845+
tk.MustQuery(`select @@last_plan_from_cache`).Check(testkit.Rows("1"))
846+
847+
tk.MustExec(`prepare stmt from 'select * from t where a=? or a=?'`)
848+
tk.MustQuery(`execute stmt using @a1, @a1`).Check(testkit.Rows("1"))
849+
tkProcess = tk.Se.ShowProcess()
850+
ps = []*util.ProcessInfo{tkProcess}
851+
tk.Se.SetSessionManager(&mockSessionManager1{PS: ps})
852+
tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Check(testkit.Rows( // cannot use PointGet since it contains a or condition
853+
`Selection_7 1.00 root or(eq(test.t.a, 1), eq(test.t.a, 1))`,
854+
`└─TableReader_6 1.00 root data:TableRangeScan_5`,
855+
` └─TableRangeScan_5 1.00 cop[tikv] table:t range:[1,1], keep order:false, stats:pseudo`))
856+
tk.MustQuery(`execute stmt using @a1, @a2`).Check(testkit.Rows("1", "2"))
857+
858+
}
859+
783860
func (s *testSerialSuite) TestIssue29101(c *C) {
784861
store, dom, err := newStoreWithBootstrap()
785862
c.Assert(err, IsNil)
@@ -821,10 +898,7 @@ func (s *testSerialSuite) TestIssue29101(c *C) {
821898
tk.MustQuery(fmt.Sprintf("explain for connection %d", tkProcess.ID)).Check(testkit.Rows( // can use IndexJoin
822899
`Projection_6 1.00 root test.customer.c_discount, test.customer.c_last, test.customer.c_credit, test.warehouse.w_tax`,
823900
`└─IndexJoin_14 1.00 root inner join, inner:TableReader_10, outer key:test.customer.c_w_id, inner key:test.warehouse.w_id, equal cond:eq(test.customer.c_w_id, test.warehouse.w_id)`,
824-
` ├─Selection_36(Build) 1.00 root eq(test.customer.c_d_id, 7), eq(test.customer.c_id, 158), eq(test.customer.c_w_id, 936)`,
825-
` │ └─IndexLookUp_35 1.00 root `,
826-
` │ ├─IndexRangeScan_33(Build) 1.00 cop[tikv] table:customer, index:PRIMARY(c_w_id, c_d_id, c_id) range:[936 7 158,936 7 158], keep order:false, stats:pseudo`,
827-
` │ └─TableRowIDScan_34(Probe) 1.00 cop[tikv] table:customer keep order:false, stats:pseudo`,
901+
` ├─Point_Get_33(Build) 1.00 root table:customer, index:PRIMARY(c_w_id, c_d_id, c_id) `,
828902
` └─TableReader_10(Probe) 0.00 root data:Selection_9`,
829903
` └─Selection_9 0.00 cop[tikv] eq(test.warehouse.w_id, 936)`,
830904
` └─TableRangeScan_8 1.00 cop[tikv] table:warehouse range: decided by [test.customer.c_w_id], keep order:false, stats:pseudo`))

planner/core/find_best_task.go

+27-4
Original file line numberDiff line numberDiff line change
@@ -812,10 +812,12 @@ func (ds *DataSource) findBestTask(prop *property.PhysicalProperty, planCounter
812812
p: dual,
813813
}, cntPlan, nil
814814
}
815-
canConvertPointGet := len(path.Ranges) > 0 && path.StoreType == kv.TiKV && ds.isPointGetConvertableSchema() &&
816-
// to avoid the over-optimized risk, do not generate PointGet for plan cache, for example,
817-
// `pk>=$a and pk<=$b` can be optimized to a PointGet when `$a==$b`, but it can cause wrong results when `$a!=$b`.
818-
!ds.ctx.GetSessionVars().StmtCtx.UseCache
815+
canConvertPointGet := len(path.Ranges) > 0 && path.StoreType == kv.TiKV && ds.isPointGetConvertableSchema()
816+
817+
if canConvertPointGet && expression.MaybeOverOptimized4PlanCache(ds.ctx, path.AccessConds) {
818+
canConvertPointGet = ds.canConvertToPointGetForPlanCache(path)
819+
}
820+
819821
if canConvertPointGet && !path.IsIntHandlePath {
820822
// We simply do not build [batch] point get for prefix indexes. This can be optimized.
821823
canConvertPointGet = path.Index.Unique && !path.Index.HasPrefixIndex()
@@ -934,6 +936,27 @@ func (ds *DataSource) findBestTask(prop *property.PhysicalProperty, planCounter
934936
return
935937
}
936938

939+
func (ds *DataSource) canConvertToPointGetForPlanCache(path *util.AccessPath) bool {
940+
// PointGet might contain some over-optimized assumptions, like `a>=1 and a<=1` --> `a=1`, but
941+
// these assumptions may be broken after parameters change.
942+
// So for safety, we narrow down the scope and just generate PointGet in some particular and simple scenarios.
943+
944+
// scenario 1: each column corresponds to a single EQ, `a=1 and b=2 and c=3` --> `[1, 2, 3]`
945+
if len(path.Ranges) > 0 && path.Ranges[0].Width() == len(path.AccessConds) {
946+
for _, accessCond := range path.AccessConds {
947+
f, ok := accessCond.(*expression.ScalarFunction)
948+
if !ok {
949+
return false
950+
}
951+
if f.FuncName.L != ast.EQ {
952+
return false
953+
}
954+
}
955+
return true
956+
}
957+
return false
958+
}
959+
937960
func (ds *DataSource) convertToIndexMergeScan(prop *property.PhysicalProperty, candidate *candidatePath) (task task, err error) {
938961
if prop.TaskTp != property.RootTaskType || !prop.IsEmpty() {
939962
return invalidTask, nil

0 commit comments

Comments
 (0)