Skip to content

Commit 715b1cc

Browse files
authored
planner, core: implement partial order TopN attach2Task and partial order flow (#65799)
close #65813
1 parent 995d6f9 commit 715b1cc

File tree

16 files changed

+1425
-155
lines changed

16 files changed

+1425
-155
lines changed

pkg/planner/core/find_best_task.go

Lines changed: 98 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -753,28 +753,10 @@ type candidatePath struct {
753753
accessCondsColMap util.Col2Len // accessCondsColMap maps Column.UniqueID to column length for the columns in AccessConds.
754754
indexCondsColMap util.Col2Len // indexCondsColMap maps Column.UniqueID to column length for the columns in AccessConds and indexFilters.
755755
matchPropResult property.PhysicalPropMatchResult
756-
757-
indexJoinCols int // how many index columns are used in access conditions in this IndexJoin.
758-
759756
// partialOrderMatch records the partial order match result for TopN optimization.
760-
// When this field is not nil, it means this path can provide partial order using prefix index.
761-
partialOrderMatch *PartialOrderMatchResult
762-
}
763-
764-
// PartialOrderMatchResult records the result of matching partial order for TopN optimization.
765-
type PartialOrderMatchResult struct {
766-
// Matched indicates whether the index can provide partial order
767-
Matched bool
768-
// PrefixColID is the last and only one prefix column ID of index, only used for executor part
769-
// For example:
770-
// Query ORDER BY a,b,c
771-
// Index: a, b, c(10)
772-
// ColIds: a=0, b=1, c=2
773-
// PrefixColID: 2, the col id of c
774-
// PrefixLen: 10, the col length of c in index
775-
PrefixColID int64
776-
// PrefixLen is the prefix length in bytes for prefix index, only used for executor part
777-
PrefixLen int
757+
// When the matched is true, it means this path can provide partial order using prefix index.
758+
partialOrderMatchResult property.PartialOrderMatchResult // Result of matching partial order property
759+
indexJoinCols int // how many index columns are used in access conditions in this IndexJoin.
778760
}
779761

780762
func compareBool(l, r bool) int {
@@ -1170,73 +1152,86 @@ func matchProperty(ds *logicalop.DataSource, path *util.AccessPath, prop *proper
11701152
// the index can **completely match** the **prefix** of the order by column.
11711153
// Case 1: Prefix index INDEX idx(col(N)) matches ORDER BY col
11721154
// For example:
1173-
// query: order by a, b
1174-
// index: prefix(a)
1175-
// index: a, prefix(b)
1155+
//
1156+
// query: order by a, b
1157+
// index: a(10)
1158+
// index: a, b(10)
1159+
//
1160+
// On success, this function will return `PartialOrderMatchResult.Matched`= true, `PartialOrderMatchResult.PrefixCol` and `PartialOrderMatchResult.PrefixLen`.
1161+
// Otherwise it returns false and not initialize `PrefixCol and PrefixLen`.
11761162
// TODO
11771163
// Case 2: Composite index INDEX idx(a, b) matches ORDER BY a, b, c (index provides order for a, b)
11781164
// In fact, there is a Case3 that can also be supported, but it will not be explained in detail here.
11791165
// Please refer to the design document for details.
1180-
func matchPartialOrderProperty(path *util.AccessPath, partialOrderInfo *property.PartialOrderInfo) *PartialOrderMatchResult {
1166+
func matchPartialOrderProperty(path *util.AccessPath, partialOrderInfo *property.PartialOrderInfo) property.PartialOrderMatchResult {
1167+
emptyResult := property.PartialOrderMatchResult{Matched: false}
1168+
11811169
if partialOrderInfo == nil || path.Index == nil || len(path.IdxCols) == 0 {
1182-
return nil
1170+
return emptyResult
11831171
}
11841172

11851173
sortItems := partialOrderInfo.SortItems
11861174
if len(sortItems) == 0 {
1187-
return nil
1175+
return emptyResult
11881176
}
11891177

11901178
allSameOrder, _ := partialOrderInfo.AllSameOrder()
11911179
if !allSameOrder {
1192-
return nil
1180+
return emptyResult
11931181
}
11941182

11951183
// Case 1: Prefix index INDEX idx(col(N)) matches ORDER BY col
11961184
// Check if index columns can match ORDER BY columns (allowing prefix index)
1197-
// Constraint 1: The number of index columns must be <= the number of ORDER BY columns
1198-
if len(path.IdxCols) > len(sortItems) {
1199-
return nil
1200-
}
1201-
// Constraint 2: The last column of the index must be a prefix column
1202-
if path.IdxColLens[len(path.IdxCols)-1] == types.UnspecifiedLength {
1185+
//
1186+
// NOTE: We use path.Index.Columns to get the actual index definition columns count,
1187+
// because path.IdxCols may include additional handle columns (e.g., primary key `id`)
1188+
// appended for non-unique indexes on tables with PKIsHandle.
1189+
// For example, for `index idx_name_prefix (name(10))` on a table with `id int primary key`,
1190+
// path.IdxCols = [name, id] but path.Index.Columns only contains [name].
1191+
// We should only consider the actual index definition columns for partial order matching.
1192+
indexColCount := len(path.Index.Columns)
1193+
1194+
// Constraint 1: The number of index definition columns must be <= the number of ORDER BY columns
1195+
if indexColCount > len(sortItems) {
1196+
return emptyResult
1197+
}
1198+
// Constraint 2: The last column of the index definition must be a prefix column
1199+
lastIdxColLen := path.Index.Columns[indexColCount-1].Length
1200+
if lastIdxColLen == types.UnspecifiedLength {
12031201
// The last column is not a prefix column, skip this index
1204-
return nil
1202+
return emptyResult
12051203
}
12061204
// Extract ORDER BY columns
12071205
orderByCols := make([]*expression.Column, 0, len(sortItems))
12081206
for _, item := range sortItems {
12091207
orderByCols = append(orderByCols, item.Col)
12101208
}
12111209

1212-
var prefixColumnID int64
1213-
var prefixLen int
1214-
for i := range len(path.IdxCols) {
1210+
// Only iterate over the actual index definition columns, not the appended handle columns
1211+
for i := range indexColCount {
12151212
// check if the same column
12161213
if !orderByCols[i].EqualColumn(path.IdxCols[i]) {
1217-
return nil
1214+
return emptyResult
12181215
}
12191216

12201217
// meet prefix index column, match termination
12211218
if path.IdxColLens[i] != types.UnspecifiedLength {
1222-
// If we meet a prefix column but it's not the last index column, it's not supported.
1219+
// If we meet a prefix column but it's not the last index definition column, it's not supported.
12231220
// e.g. prefix(a), b cannot provide partial order for ORDER BY a, b.
1224-
if i != len(path.IdxCols)-1 {
1225-
return nil
1221+
if i != indexColCount-1 {
1222+
return emptyResult
12261223
}
12271224
// Encountered a prefix index column.
12281225
// This prefix index column can provide partial order, but subsequent columns cannot match.
1229-
prefixColumnID = path.IdxCols[i].UniqueID
1230-
prefixLen = path.IdxColLens[i]
1231-
return &PartialOrderMatchResult{
1232-
Matched: true,
1233-
PrefixColID: prefixColumnID,
1234-
PrefixLen: prefixLen,
1226+
return property.PartialOrderMatchResult{
1227+
Matched: true,
1228+
PrefixCol: path.IdxCols[i],
1229+
PrefixLen: path.IdxColLens[i],
12351230
}
12361231
}
12371232
}
12381233

1239-
return nil
1234+
return emptyResult
12401235
}
12411236

12421237
// GroupRangesByCols groups the ranges by the values of the columns specified by groupByColIdxs.
@@ -1462,6 +1457,13 @@ func getTableCandidate(ds *logicalop.DataSource, path *util.AccessPath, prop *pr
14621457
func getIndexCandidate(ds *logicalop.DataSource, path *util.AccessPath, prop *property.PhysicalProperty) *candidatePath {
14631458
candidate := &candidatePath{path: path}
14641459
candidate.matchPropResult = matchProperty(ds, path, prop)
1460+
// Because the skyline pruning already prune the indexes that cannot provide partial order
1461+
// when prop has PartialOrderInfo physical property,
1462+
// So here we just need to record the partial order match result(prefixCol, prefixLen).
1463+
// The partialOrderMatchResult.Matched() will be always true after skyline pruning.
1464+
if ds.SCtx().GetSessionVars().IsPartialOrderedIndexForTopNEnabled() && prop.PartialOrderInfo != nil {
1465+
candidate.partialOrderMatchResult = matchPartialOrderProperty(path, prop.PartialOrderInfo)
1466+
}
14651467
candidate.accessCondsColMap = util.ExtractCol2Len(ds.SCtx().GetExprCtx().GetEvalCtx(), path.AccessConds, path.IdxCols, path.IdxColLens)
14661468
candidate.indexCondsColMap = util.ExtractCol2Len(ds.SCtx().GetExprCtx().GetEvalCtx(), append(path.AccessConds, path.IndexFilters...), path.FullIdxCols, path.FullIdxColLens)
14671469
return candidate
@@ -1528,17 +1530,29 @@ func skylinePruning(ds *logicalop.DataSource, prop *property.PhysicalProperty) [
15281530
}
15291531
var currentCandidate *candidatePath
15301532
if path.IsTablePath() {
1533+
if prop.PartialOrderInfo != nil {
1534+
// skyline pruning table path with partial order property is not supported yet.
1535+
// TODO: support it in the future after we support prefix column as partial order.
1536+
continue
1537+
}
15311538
currentCandidate = getTableCandidate(ds, path, prop)
15321539
} else {
1533-
// Check if candidate path match the partial order property
1534-
// To consider an index for partial order optimization, the following conditions must be met together:
1535-
// 1. OptPartialOrderedIndexForTopN should be enabled.
1536-
// 2. Physical property should has PartialOrderInfo
1537-
// 3. The path should match PartialOrderInfo
1538-
// TODO: use the PartialOrderMatchResult in the further PR to construct the special TopN and Limit executor
1539-
matchPartialOrderIndex := ds.SCtx().GetSessionVars().OptPartialOrderedIndexForTopN &&
1540-
prop.PartialOrderInfo != nil &&
1541-
matchPartialOrderProperty(path, prop.PartialOrderInfo) != nil
1540+
// Check if this path can be used for partial order optimization
1541+
var matchPartialOrderIndex bool
1542+
if ds.SCtx().GetSessionVars().IsPartialOrderedIndexForTopNEnabled() &&
1543+
prop.PartialOrderInfo != nil {
1544+
if !matchPartialOrderProperty(path, prop.PartialOrderInfo).Matched {
1545+
// skyline pruning all indexes that cannot provide partial order when we are looking for
1546+
continue
1547+
}
1548+
matchPartialOrderIndex = true
1549+
// If the index can match partial order requirement and user use "use/force index" in hint.
1550+
// If the index can't match partial order requirement and use use "use/force index" and enable partial order optimization together,
1551+
// the behavior will degenerate into normal index use behavior without considering partial order optimization.
1552+
if path.Forced {
1553+
path.ForcePartialOrder = true
1554+
}
1555+
}
15421556

15431557
// We will use index to generate physical plan if any of the following conditions is satisfied:
15441558
// 1. This path's access cond is not nil.
@@ -1551,6 +1565,8 @@ func skylinePruning(ds *logicalop.DataSource, prop *property.PhysicalProperty) [
15511565
// If none of the above conditions are met, this index will be directly pruned here.
15521566
continue
15531567
}
1568+
1569+
// After passing the check, generate the candidate
15541570
currentCandidate = getIndexCandidate(ds, path, prop)
15551571
}
15561572
pruned := false
@@ -2245,17 +2261,29 @@ func convertToIndexScan(ds *logicalop.DataSource, prop *property.PhysicalPropert
22452261
// If it's parent requires double read task, return max cost.
22462262
return base.InvalidTask, nil
22472263
}
2264+
// Check if sort items can be matched. If not, return Invalid task
22482265
if !prop.IsSortItemEmpty() && !candidate.matchPropResult.Matched() {
22492266
return base.InvalidTask, nil
22502267
}
22512268
// If we need to keep order for the index scan, we should forbid the non-keep-order index scan when we try to generate the path.
2252-
if prop.IsSortItemEmpty() && candidate.path.ForceKeepOrder {
2269+
if !prop.NeedKeepOrder() && candidate.path.ForceKeepOrder {
22532270
return base.InvalidTask, nil
22542271
}
22552272
// If we don't need to keep order for the index scan, we should forbid the non-keep-order index scan when we try to generate the path.
2256-
if !prop.IsSortItemEmpty() && candidate.path.ForceNoKeepOrder {
2273+
if prop.NeedKeepOrder() && candidate.path.ForceNoKeepOrder {
22572274
return base.InvalidTask, nil
22582275
}
2276+
// If we want to force partial order, then we should remove all others property candidate path such as: full order and no order.
2277+
if candidate.path.ForcePartialOrder && prop.PartialOrderInfo == nil {
2278+
return base.InvalidTask, nil
2279+
}
2280+
// For partial order property
2281+
// We **don't need to check** the partial order property is matched in here.
2282+
// Because if the index scan cannot satisfy partial order, it will be pruned at the SkylinePruning phase
2283+
// (which is the previous phase before this function).
2284+
// So, if an index can enter this function and also contains the requirement of a partial order property,
2285+
// then it must meet the requirements.
2286+
22592287
path := candidate.path
22602288
is := physicalop.GetOriginalPhysicalIndexScan(ds, prop, path, candidate.matchPropResult.Matched(), candidate.path.IsSingleScan)
22612289
cop := &physicalop.CopTask{
@@ -2264,12 +2292,17 @@ func convertToIndexScan(ds *logicalop.DataSource, prop *property.PhysicalPropert
22642292
TblCols: ds.TblCols,
22652293
ExpectCnt: uint64(prop.ExpectedCnt),
22662294
}
2295+
// Store partial order match result in CopTask for use in attach2Task
2296+
if candidate.partialOrderMatchResult.Matched {
2297+
cop.PartialOrderMatchResult = &candidate.partialOrderMatchResult
2298+
}
22672299
cop.PhysPlanPartInfo = &physicalop.PhysPlanPartInfo{
22682300
PruningConds: ds.AllConds,
22692301
PartitionNames: ds.PartitionNames,
22702302
Columns: ds.TblCols,
22712303
ColumnNames: ds.OutputNames(),
22722304
}
2305+
22732306
if !candidate.path.IsSingleScan {
22742307
// On this way, it's double read case.
22752308
ts := physicalop.PhysicalTableScan{
@@ -2307,7 +2340,8 @@ func convertToIndexScan(ds *logicalop.DataSource, prop *property.PhysicalPropert
23072340
}
23082341
}
23092342
}
2310-
if candidate.matchPropResult.Matched() {
2343+
// handles both normal sort (SortItems) and partial order (PartialOrderInfo)
2344+
if prop.NeedKeepOrder() {
23112345
cop.KeepOrder = true
23122346
if cop.TablePlan != nil && !ds.TableInfo.IsCommonHandle {
23132347
col, isNew := cop.TablePlan.(*physicalop.PhysicalTableScan).AppendExtraHandleCol(ds)
@@ -2322,8 +2356,9 @@ func convertToIndexScan(ds *logicalop.DataSource, prop *property.PhysicalPropert
23222356
// Case 3: both
23232357
if (ds.TableInfo.GetPartitionInfo() != nil && !is.Index.Global) ||
23242358
candidate.matchPropResult == property.PropMatchedNeedMergeSort {
2325-
byItems := make([]*util.ByItems, 0, len(prop.SortItems))
2326-
for _, si := range prop.SortItems {
2359+
sortItems := prop.GetSortItemsForKeepOrder()
2360+
byItems := make([]*util.ByItems, 0, len(sortItems))
2361+
for _, si := range sortItems {
23272362
byItems = append(byItems, &util.ByItems{
23282363
Expr: si.Col,
23292364
Desc: si.Desc,

pkg/planner/core/hint_test.go

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -144,34 +144,29 @@ func TestSetVarPartialOrderedIndexForTopN(t *testing.T) {
144144
testKit.MustExec(`use test`)
145145

146146
// Test default value
147-
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("0"))
147+
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("DISABLE"))
148148

149149
// Test set_var hint changes the value during query execution
150-
testKit.MustExec(`set @@tidb_opt_partial_ordered_index_for_topn = 0`)
151-
testKit.MustQuery(`select /*+ set_var(tidb_opt_partial_ordered_index_for_topn=ON) */ @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("1"))
150+
testKit.MustExec(`set @@tidb_opt_partial_ordered_index_for_topn = DISABLE`)
151+
testKit.MustQuery(`select /*+ set_var(tidb_opt_partial_ordered_index_for_topn=COST) */ @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("COST"))
152152
// Value should be restored after query
153-
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("0"))
153+
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("DISABLE"))
154154

155155
// Test set_var hint with OFF
156-
testKit.MustExec(`set @@tidb_opt_partial_ordered_index_for_topn = 1`)
157-
testKit.MustQuery(`select /*+ set_var(tidb_opt_partial_ordered_index_for_topn=OFF) */ @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("0"))
156+
testKit.MustExec(`set @@tidb_opt_partial_ordered_index_for_topn = COST`)
157+
testKit.MustQuery(`select /*+ set_var(tidb_opt_partial_ordered_index_for_topn=DISABLE) */ @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("DISABLE"))
158158
// Value should be restored after query
159-
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("1"))
160-
161-
// Test set_var hint with numeric values
162-
testKit.MustExec(`set @@tidb_opt_partial_ordered_index_for_topn = 0`)
163-
testKit.MustQuery(`select /*+ set_var(tidb_opt_partial_ordered_index_for_topn=1) */ @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("1"))
164-
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("0"))
159+
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("COST"))
165160

166161
// Test set_var hint with multiple queries
167162
testKit.MustExec(`create table t(a int, b varchar(10), index idx_b(b(5)));`)
168-
testKit.MustExec(`set @@tidb_opt_partial_ordered_index_for_topn = 0`)
169-
testKit.MustExec(`select /*+ set_var(tidb_opt_partial_ordered_index_for_topn=ON) */ * from t order by b limit 10;`)
170-
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("0"))
163+
testKit.MustExec(`set @@tidb_opt_partial_ordered_index_for_topn = DISABLE`)
164+
testKit.MustExec(`select /*+ set_var(tidb_opt_partial_ordered_index_for_topn=COST) */ * from t order by b limit 10;`)
165+
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("DISABLE"))
171166

172167
// Test with EXPLAIN (should not change the value)
173-
testKit.MustExec(`set @@tidb_opt_partial_ordered_index_for_topn = 0`)
174-
testKit.MustExec(`explain select /*+ set_var(tidb_opt_partial_ordered_index_for_topn=ON) */ * from t order by b limit 10;`)
175-
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("0"))
168+
testKit.MustExec(`set @@tidb_opt_partial_ordered_index_for_topn = DISABLE`)
169+
testKit.MustExec(`explain select /*+ set_var(tidb_opt_partial_ordered_index_for_topn=COST) */ * from t order by b limit 10;`)
170+
testKit.MustQuery(`select @@tidb_opt_partial_ordered_index_for_topn`).Check(testkit.Rows("DISABLE"))
176171
})
177172
}

pkg/planner/core/operator/physicalop/physical_index_scan.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -688,8 +688,9 @@ func GetOriginalPhysicalIndexScan(ds *logicalop.DataSource, prop *property.Physi
688688
if usedStats != nil && usedStats.GetUsedInfo(is.PhysicalTableID) != nil {
689689
is.UsedStatsInfo = usedStats.GetUsedInfo(is.PhysicalTableID)
690690
}
691-
if isMatchProp {
692-
is.Desc = prop.SortItems[0].Desc
691+
// Index scan should maintain order (true for both normal sorting via SortItems and partial order via PartialOrderInfo)
692+
if prop.NeedKeepOrder() {
693+
is.Desc = prop.GetSortDescForKeepOrder()
693694
is.KeepOrder = true
694695
}
695696
return is

pkg/planner/core/operator/physicalop/physical_limit.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ type PhysicalLimit struct {
3737
PartitionBy []property.SortItem
3838
Offset uint64
3939
Count uint64
40+
41+
// PrefixCol is the prefix index column for partial order optimization.
42+
// Used for both execution (via UniqueID) and explain (via column name).
43+
// If prefix index optimization is not used, this field is nil.
44+
PrefixCol *expression.Column
45+
46+
// PrefixLen is the prefix index length (in bytes) for TiKV-side short-circuiting.
47+
// If prefix index optimization is not used, this field is 0.
48+
PrefixLen int
4049
}
4150

4251
// ExhaustPhysicalPlans4LogicalLimit will be called by LogicalLimit in logicalOp pkg.
@@ -101,7 +110,10 @@ func (p *PhysicalLimit) MemoryUsage() (sum int64) {
101110
return
102111
}
103112

104-
sum = p.PhysicalSchemaProducer.MemoryUsage() + size.SizeOfUint64*2
113+
sum = p.PhysicalSchemaProducer.MemoryUsage() +
114+
size.SizeOfUint64*2 + // Offset, Count
115+
size.SizeOfInt64 + // PrefixColID
116+
size.SizeOfInt // PrefixLen
105117
return
106118
}
107119

@@ -116,10 +128,23 @@ func (p *PhysicalLimit) ExplainInfo() string {
116128
}
117129
if redact == perrors.RedactLogDisable {
118130
fmt.Fprintf(buffer, "offset:%v, count:%v", p.Offset, p.Count)
131+
if p.PrefixCol != nil {
132+
prefixColName := p.PrefixCol.ColumnExplainInfo(ectx, false)
133+
fmt.Fprintf(buffer, ", prefix_col:%v, prefix_len:%v",
134+
prefixColName, p.PrefixLen)
135+
}
119136
} else if redact == perrors.RedactLogMarker {
120137
fmt.Fprintf(buffer, "offset:‹%v›, count:‹%v›", p.Offset, p.Count)
138+
if p.PrefixCol != nil {
139+
prefixColName := p.PrefixCol.ColumnExplainInfo(ectx, false)
140+
fmt.Fprintf(buffer, ", prefix_col:‹%v›, prefix_len:‹%v›",
141+
prefixColName, p.PrefixLen)
142+
}
121143
} else if redact == perrors.RedactLogEnable {
122144
fmt.Fprintf(buffer, "offset:?, count:?")
145+
if p.PrefixCol != nil {
146+
fmt.Fprintf(buffer, ", prefix_col:?, prefix_len:?")
147+
}
123148
}
124149
return buffer.String()
125150
}

0 commit comments

Comments
 (0)