@@ -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
780762func 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
14621457func 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 ,
0 commit comments