planner: support OR list nested in AND list for mv index (#51716)

close pingcap/tidb#51778
This commit is contained in:
Zhou Kunqin
2024-03-18 14:23:13 +08:00
committed by GitHub
parent 3f915c0a36
commit af76c2ff1e
7 changed files with 637 additions and 322 deletions

View File

@ -19,6 +19,7 @@ go_library(
"hashcode.go",
"hint_utils.go",
"indexmerge_path.go",
"indexmerge_unfinished_path.go",
"initialize.go",
"logical_plan_builder.go",
"logical_plans.go",

View File

@ -138,7 +138,7 @@ func (ds *DataSource) generateIndexMergePath() error {
func (ds *DataSource) generateNormalIndexPartialPaths4DNF(
dnfItems []expression.Expression,
candidatePaths []*util.AccessPath,
) (paths []*util.AccessPath, needSelection bool, usedMap []bool, err error) {
) (paths []*util.AccessPath, needSelection bool, usedMap []bool) {
paths = make([]*util.AccessPath, 0, len(dnfItems))
usedMap = make([]bool, len(dnfItems))
for offset, item := range dnfItems {
@ -160,7 +160,7 @@ func (ds *DataSource) generateNormalIndexPartialPaths4DNF(
// for this dnf item, we couldn't generate an index merge partial path.
// (1 member of (a)) or (3 member of (b)) or d=1; if one dnf item like d=1 here could walk index path,
// the entire index merge is not valid anymore.
return nil, false, usedMap, nil
return nil, false, usedMap
}
// prune out global indexes.
itemPaths = slices.DeleteFunc(itemPaths, func(path *util.AccessPath) bool {
@ -174,7 +174,7 @@ func (ds *DataSource) generateNormalIndexPartialPaths4DNF(
// for this dnf item, we couldn't generate an index merge partial path.
// (1 member of (a)) or (3 member of (b)) or d=1; if one dnf item like d=1 here could walk index path,
// the entire index merge is not valid anymore.
return nil, false, usedMap, nil
return nil, false, usedMap
}
// identify whether all pushedDownCNFItems are fully used.
@ -196,7 +196,7 @@ func (ds *DataSource) generateNormalIndexPartialPaths4DNF(
usedMap[offset] = true
paths = append(paths, partialPath)
}
return paths, needSelection, usedMap, nil
return paths, needSelection, usedMap
}
// getIndexMergeOrPath generates all possible IndexMergeOrPaths.
@ -485,44 +485,6 @@ func (ds *DataSource) buildIndexMergeOrPath(
return indexMergePath
}
func (ds *DataSource) generateNormalIndexPartialPath4Or(dnfItems []expression.Expression, usedAccessMap []bool, normalPathCnt int) ([]*util.AccessPath, []bool, bool, error) {
remainedDNFItems := make([]expression.Expression, 0, len(dnfItems))
for i, b := range usedAccessMap {
if !b {
remainedDNFItems = append(remainedDNFItems, dnfItems[i])
}
}
noMVIndexPartialPath := false
if len(dnfItems) == len(remainedDNFItems) {
// there is no mv index paths generated, so for: (a<1) OR (a>2), no need to generated index merge.
noMVIndexPartialPath = true
}
paths, needSelection, usedMap, err := ds.generateNormalIndexPartialPaths4DNF(remainedDNFItems, ds.possibleAccessPaths[:normalPathCnt])
if err != nil {
return nil, usedAccessMap, false, err
}
// If all the partialPaths use the same index, we will not use the indexMerge.
singlePath := true
for i := len(paths) - 1; i >= 1; i-- {
if paths[i].Index != paths[i-1].Index {
singlePath = false
break
}
}
if singlePath && noMVIndexPartialPath {
return nil, usedAccessMap, false, nil
}
// collect the remain filter's used map.
cnt := 0
for i, b := range usedAccessMap {
if !b {
usedAccessMap[i] = usedMap[cnt]
cnt++
}
}
return paths, usedAccessMap, needSelection, nil
}
func (ds *DataSource) generateNormalIndexPartialPath4And(normalPathCnt int, usedAccessMap map[string]expression.Expression) []*util.AccessPath {
if res := ds.generateIndexMergeAndPaths(normalPathCnt, usedAccessMap); res != nil {
return res.PartialIndexPaths
@ -666,115 +628,6 @@ func (ds *DataSource) generateIndexMergeAndPaths(normalPathCnt int, usedAccessMa
return indexMergePath
}
/*
select * from t where ((1 member of (a) and b=1) or (2 member of (a) and b=2)) and (c > 10)
IndexMerge(OR)
IndexRangeScan(a, b, [1 1, 1 1])
IndexRangeScan(a, b, [2 2, 2 2])
Selection(c > 10)
TableRowIdScan(t)
Two limitations now:
1). Not support the embedded index merge case, which (DNF_Item1 OR DNF_Item2), for every DNF item,
try to map it into a simple normal index path or mv index path, other than an internal index merge path.
2). Every dnf item should exactly be used as full length index range or prefix index range, other than being not used.
*/
func (ds *DataSource) generateMVIndexPartialPath4Or(normalPathCnt int, indexMergeDNFConds []expression.Expression) ([]*util.AccessPath, []bool, bool, error) {
// step1: collect all mv index paths
possibleMVIndexPaths := make([]*util.AccessPath, 0, len(ds.possibleAccessPaths))
for idx := 0; idx < normalPathCnt; idx++ {
if !isMVIndexPath(ds.possibleAccessPaths[idx]) {
continue // not a MVIndex path
}
if !ds.isInIndexMergeHints(ds.possibleAccessPaths[idx].Index.Name.L) {
continue
}
possibleMVIndexPaths = append(possibleMVIndexPaths, ds.possibleAccessPaths[idx])
}
// step2: mapping index merge conditions into possible mv index path
mvAndPartialPaths := make([]*util.AccessPath, 0, len(possibleMVIndexPaths))
usedMap := make([]bool, len(indexMergeDNFConds))
needSelection := false
for offset, dnfCond := range indexMergeDNFConds {
var cnfConds []expression.Expression
sf, ok := dnfCond.(*expression.ScalarFunction)
if !ok {
continue
}
cnfConds = expression.SplitCNFItems(sf)
// for every dnf condition, find the most suitable mv index path.
// otherwise, for table(a json, b json, c int, idx(c,a), idx2(b,c))
// condition: (1 member of (a) and c=1 and d=2) or (2 member of (b) and c=3 and d=2);
// will both pick(c,a) idx with range [1 1, 1 1] and [3,3], the latter can pick the most
// valuable index idx2 with range [2 3,2 3]
var (
bestPaths []*util.AccessPath
bestCountAfterAccess float64
bestNeedSelection bool
)
for _, onePossibleMVIndexPath := range possibleMVIndexPaths {
idxCols, ok := PrepareIdxColsAndUnwrapArrayType(
ds.table.Meta(),
onePossibleMVIndexPath.Index,
ds.TblCols,
true,
)
if !ok {
continue
}
// for every cnfCond, try to map it into possible mv index path.
// remainingFilters is not cared here, because it will be all suspended on the table side.
accessFilters, remainingFilters, _ := collectFilters4MVIndex(ds.SCtx(), cnfConds, idxCols)
if len(accessFilters) == 0 {
continue
}
paths, isIntersection, ok, err := buildPartialPaths4MVIndex(ds.SCtx(), accessFilters, idxCols, onePossibleMVIndexPath.Index, ds.tableStats.HistColl)
if err != nil {
logutil.BgLogger().Debug("build index merge partial mv index paths failed", zap.Error(err))
return nil, nil, false, err
}
if !ok || len(paths) == 0 {
continue
}
// only under 2 cases we can fallthrough it.
// 1: the index merge only has one partial path.
// 2: index merge is UNION type.
canFallThrough := len(paths) == 1 || !isIntersection
if !canFallThrough {
continue
}
// UNION case, use the max count after access for simplicity.
maxCountAfterAccess := -1.0
for _, p := range paths {
maxCountAfterAccess = math.Max(maxCountAfterAccess, p.CountAfterAccess)
}
// Note that: here every path is about mv index path.
// find the most valuable mv index path, which means it has the minimum countAfterAccess.
if len(bestPaths) == 0 {
bestPaths = paths
bestCountAfterAccess = maxCountAfterAccess
bestNeedSelection = len(remainingFilters) != 0
} else if bestCountAfterAccess > maxCountAfterAccess {
bestPaths = paths
bestCountAfterAccess = maxCountAfterAccess
bestNeedSelection = len(remainingFilters) != 0
}
}
if len(bestPaths) != 0 {
usedMap[offset] = true
// correctly find a dnf condition for this mv index path
mvAndPartialPaths = append(mvAndPartialPaths, bestPaths...)
if !needSelection && bestNeedSelection {
// collect one path's need selection flag.
needSelection = bestNeedSelection
}
}
}
return mvAndPartialPaths, usedMap, needSelection, nil
}
// generateMVIndexMergePartialPaths4And try to find mv index merge partial path from a collection of cnf conditions.
func (ds *DataSource) generateMVIndexMergePartialPaths4And(normalPathCnt int, indexMergeConds []expression.Expression, histColl *statistics.HistColl) ([]*util.AccessPath, map[string]expression.Expression, error) {
// step1: collect all mv index paths
@ -953,62 +806,28 @@ func (ds *DataSource) generateIndexMergeOnDNF4MVIndex(normalPathCnt int, filters
continue
}
idxCols, ok := PrepareIdxColsAndUnwrapArrayType(
ds.table.Meta(),
ds.possibleAccessPaths[idx].Index,
ds.TblCols,
true,
)
if !ok {
continue
}
for current, filter := range filters {
sf, ok := filter.(*expression.ScalarFunction)
if !ok || sf.FuncName.L != ast.LogicOr {
continue
}
dnfFilters := expression.FlattenDNFConditions(sf) // [(1 member of (a) and b=1), (2 member of (a) and b=2)]
dnfFilters := expression.SplitDNFItems(sf) // [(1 member of (a) and b=1), (2 member of (a) and b=2)]
// build partial paths for each dnf filter
cannotFit := false
var partialPaths []*util.AccessPath
for _, dnfFilter := range dnfFilters {
mvIndexFilters := []expression.Expression{dnfFilter}
if sf, ok := dnfFilter.(*expression.ScalarFunction); ok && sf.FuncName.L == ast.LogicAnd {
mvIndexFilters = expression.FlattenCNFConditions(sf) // (1 member of (a) and b=1) --> [(1 member of (a)), b=1]
}
accessFilters, remainingFilters, _ := collectFilters4MVIndex(ds.SCtx(), mvIndexFilters, idxCols)
if len(accessFilters) == 0 || len(remainingFilters) > 0 { // limitation 1
cannotFit = true
break
}
paths, isIntersection, ok, err := buildPartialPaths4MVIndex(ds.SCtx(), accessFilters, idxCols, ds.possibleAccessPaths[idx].Index, ds.tableStats.HistColl)
if err != nil {
return nil, err
}
if isIntersection || !ok { // limitation 2
cannotFit = true
break
}
partialPaths = append(partialPaths, paths...)
}
if cannotFit {
continue
}
var remainingFilters []expression.Expression
remainingFilters = append(remainingFilters, filters[:current]...)
remainingFilters = append(remainingFilters, filters[current+1:]...)
indexMergePath := ds.buildPartialPathUp4MVIndex(
partialPaths,
false,
remainingFilters,
ds.tableStats.HistColl,
unfinishedIndexMergePath := generateUnfinishedIndexMergePathFromORList(
ds,
dnfFilters,
[]*util.AccessPath{ds.possibleAccessPaths[idx]},
)
mvIndexPaths = append(mvIndexPaths, indexMergePath)
finishedIndexMergePath := handleTopLevelANDListAndGenFinishedPath(
ds,
filters,
current,
[]*util.AccessPath{ds.possibleAccessPaths[idx]},
unfinishedIndexMergePath,
)
if finishedIndexMergePath != nil {
mvIndexPaths = append(mvIndexPaths, finishedIndexMergePath)
}
}
}
return
@ -1086,57 +905,67 @@ func (ds *DataSource) generateIndexMerge4ComposedIndex(normalPathCnt int, indexM
return nil
}
if len(indexMergeConds) == 1 {
// Collect access paths that satisfy the hints, and make sure there is at least one MV index path.
var mvIndexPathCnt int
candidateAccessPaths := make([]*util.AccessPath, 0, len(ds.possibleAccessPaths))
for idx := 0; idx < normalPathCnt; idx++ {
if ds.possibleAccessPaths[idx].Index != nil && ds.possibleAccessPaths[idx].Index.Global {
continue
}
if (ds.possibleAccessPaths[idx].IsTablePath() &&
!ds.isInIndexMergeHints("primary")) ||
(!ds.possibleAccessPaths[idx].IsTablePath() &&
!ds.isInIndexMergeHints(ds.possibleAccessPaths[idx].Index.Name.L)) {
continue
}
if isMVIndexPath(ds.possibleAccessPaths[idx]) {
mvIndexPathCnt++
}
candidateAccessPaths = append(candidateAccessPaths, ds.possibleAccessPaths[idx])
}
if mvIndexPathCnt == 0 {
return nil
}
for current, filter := range indexMergeConds {
// DNF path.
sf, ok := indexMergeConds[0].(*expression.ScalarFunction)
sf, ok := filter.(*expression.ScalarFunction)
if !ok || sf.FuncName.L != ast.LogicOr {
// targeting: cond1 or cond2 or cond3
return nil
continue
}
dnfFilters := expression.FlattenDNFConditions(sf)
mvIndexPartialPaths, usedAccessMap, needSelection4MVIndex, err := ds.generateMVIndexPartialPath4Or(normalPathCnt, dnfFilters)
if err != nil {
return err
}
if len(mvIndexPartialPaths) == 0 {
// no mv index partial paths to be composed of.
return nil
}
normalIndexPartialPaths, usedAccessMap, needSelection4NormalIndex, err := ds.generateNormalIndexPartialPath4Or(dnfFilters, usedAccessMap, normalPathCnt)
if err != nil {
return err
}
// since multi normal index merge path is handled before, here focus on multi mv index merge, or mv and normal mixed index merge
composed := (len(mvIndexPartialPaths) > 1) || (len(mvIndexPartialPaths) == 1 && len(normalIndexPartialPaths) >= 1)
if !composed {
return nil
}
// if any cnf item is not used as index partial path, index merge is not valid anymore.
if slices.Contains(usedAccessMap, false) {
return nil
}
// todo: make this code as a portal of all index merge path.
// if we derive:
// 1: some mv index partial path, no normal index path, it means multi mv index merge.
// 2: some mv index partial path, some normal index path, it means hybrid index merge.
// 3: no mv index partial path, several normal index path, it means multi normal index merge.
combinedPartialPaths := append(normalIndexPartialPaths, mvIndexPartialPaths...)
if len(combinedPartialPaths) == 0 {
return nil
}
// here we directly use the all index merge conditions as the table filers for simplicity.
// todo: make estimation more correct rather than pruning other index merge path.
var indexMergeTableFilters []expression.Expression
if needSelection4MVIndex || needSelection4NormalIndex {
indexMergeTableFilters = indexMergeConds
}
mvp := ds.buildPartialPathUp4MVIndex(
combinedPartialPaths,
false,
indexMergeTableFilters,
ds.tableStats.HistColl,
dnfFilters := expression.SplitDNFItems(sf)
unfinishedIndexMergePath := generateUnfinishedIndexMergePathFromORList(
ds,
dnfFilters,
candidateAccessPaths,
)
ds.possibleAccessPaths = append(ds.possibleAccessPaths, mvp)
finishedIndexMergePath := handleTopLevelANDListAndGenFinishedPath(
ds,
indexMergeConds,
current,
candidateAccessPaths,
unfinishedIndexMergePath,
)
if finishedIndexMergePath == nil {
return nil
}
var mvIndexPartialPathCnt, normalIndexPartialPathCnt int
for _, path := range finishedIndexMergePath.PartialIndexPaths {
if isMVIndexPath(path) {
mvIndexPartialPathCnt++
} else {
normalIndexPartialPathCnt++
}
}
// Keep the same behavior with previous implementation, we only handle the "composed" case here.
if mvIndexPartialPathCnt == 0 || (mvIndexPartialPathCnt == 1 && normalIndexPartialPathCnt == 0) {
return nil
}
ds.possibleAccessPaths = append(ds.possibleAccessPaths, finishedIndexMergePath)
return nil
}
// CNF path.
@ -1437,13 +1266,13 @@ func buildPartialPath4MVIndex(
// This function is exported for test.
func PrepareIdxColsAndUnwrapArrayType(
tableInfo *model.TableInfo,
mvIndex *model.IndexInfo,
idxInfo *model.IndexInfo,
tblCols []*expression.Column,
checkOnly1ArrayTypeCol bool,
) (idxCols []*expression.Column, ok bool) {
var virColNum = 0
for i := range mvIndex.Columns {
colOffset := mvIndex.Columns[i].Offset
for i := range idxInfo.Columns {
colOffset := idxInfo.Columns[i].Offset
colMeta := tableInfo.Cols()[colOffset]
var col *expression.Column
for _, c := range tblCols {
@ -1486,7 +1315,7 @@ func collectFilters4MVIndex(
if usedAsAccess[i] {
continue
}
if ok, tp := checkFilter4MVIndexColumn(sctx, f, col); ok {
if ok, tp := checkAccessFilter4IdxCol(sctx, f, col); ok {
accessFilters = append(accessFilters, f)
usedAsAccess[i] = true
found = true
@ -1560,7 +1389,7 @@ func CollectFilters4MVIndexMutations(sctx PlanContext, filters []expression.Expr
if usedAsAccess[i] {
continue
}
if ok, _ := checkFilter4MVIndexColumn(sctx, f, col); ok {
if ok, _ := checkAccessFilter4IdxCol(sctx, f, col); ok {
if col.VirtualExpr != nil && col.VirtualExpr.GetType().IsArray() {
// assert jsonColOffset should always be the same.
// if the filter is from virtual expression, it means it is about the mv json col.
@ -1652,10 +1481,11 @@ const (
singleValueOnMVColTp
)
// checkFilter4MVIndexColumn checks whether this filter can be used as an accessFilter to access the MVIndex column, and
// which type the access filter is, as defined above.
// checkAccessFilter4IdxCol checks whether this filter can be used as an accessFilter to access the column of an index,
// and returns which type the access filter is, as defined above.
// Though this function is introduced for MV index, it can also be used for normal index
// If the return value ok is false, the type must be unspecifiedFilterTp.
func checkFilter4MVIndexColumn(
func checkAccessFilter4IdxCol(
sctx PlanContext,
filter expression.Expression,
idxCol *expression.Column,

View File

@ -0,0 +1,416 @@
// Copyright 2024 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,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package core
import (
"math"
"slices"
"github.com/pingcap/tidb/pkg/expression"
"github.com/pingcap/tidb/pkg/parser/model"
"github.com/pingcap/tidb/pkg/planner/util"
)
// Note that at this moment, the implementation related to unfinishedAccessPath only aims to handle OR list nested in
// AND list, which is like ... AND (... OR ... OR ...) AND ..., and build an MV IndexMerge access path. So some struct
// definition and code logic are specially designed for this case or only consider this case.
// This may be changed in the future.
// unfinishedAccessPath is for collecting access filters for an access path.
// It maintains the information during iterating all filters. Importantly, it maintains incomplete access filters, which
// means they may not be able to build a valid range, but could build a valid range after collecting more access filters.
// After iterating all filters, we can check and build it into a valid util.AccessPath.
type unfinishedAccessPath struct {
index *model.IndexInfo
accessFilters []expression.Expression
// To avoid regression and keep the same behavior as the previous implementation, we collect access filters in two
// methods:
//
// 1. Use the same functions as the previous implementation to collect access filters. They are able to handle some
// complicated expressions, but the expressions must be able to build into a valid range at once (that's also what
// "finished" implies).
// In this case, idxColHasAccessFilter will be nil and initedAsFinished will be true.
//
// 2. Use the new logic, which is to collect access filters for each column respectively, gradually collect more
// access filters during iterating all filters and try to form a valid range at last.
// In this case, initedAsFinished will be false, and idxColHasAccessFilter will record if we have already collected
// a valid access filter for each column of the index.
idxColHasAccessFilter []bool
initedAsFinished bool
// needKeepFilter means the OR list need to become a filter in the final Selection.
needKeepFilter bool
// Similar to AccessPath.PartialIndexPaths, each element in the slice is expected to build into a partial AccessPath.
// Currently, it can only mean an OR type IndexMerge.
indexMergeORPartialPaths []unfinishedAccessPathList
}
// unfinishedAccessPathList is for collecting access filters for a slice of candidate access paths.
// This type is useful because usually we have several candidate index/table paths. When we are iterating the
// expressions, we want to match them against all index/table paths and try to find access filter for every path. After
// iterating all expressions, we check if any of them can form a valid range so that we can build a valid AccessPath.
type unfinishedAccessPathList []*unfinishedAccessPath
// generateUnfinishedIndexMergePathFromORList handles a list of filters connected by OR, collects access filters for
// each candidate access path, and returns an unfinishedAccessPath, which must be an index merge OR unfinished path,
// each partial path of which corresponds to one filter in the input orList.
/*
Example:
Input:
orList: 1 member of j->'$.a' OR 2 member of j->'$.b'
candidateAccessPaths: [idx1(a, j->'$.a' unsigned array), idx2(j->'$.b' unsigned array, a)]
Output:
unfinishedAccessPath{
indexMergeORPartialPaths: [
// Collect access filters for (1 member of j->'$.a') using two candidates respectively.
[unfinishedAccessPath{idx1,1 member of j->'$.a'}, nil]
// Collect access filters for (2 member of j->'$.b') using two candidates respectively.
[nil, unfinishedAccessPath{idx2,2 member of j->'$.b'}]
]
}
*/
func generateUnfinishedIndexMergePathFromORList(
ds *DataSource,
orList []expression.Expression,
candidateAccessPaths []*util.AccessPath,
) *unfinishedAccessPath {
if len(orList) < 2 {
return nil
}
unfinishedPartialPaths := make([]unfinishedAccessPathList, 0, len(orList))
for _, singleFilter := range orList {
unfinishedPathList := initUnfinishedPathsFromExpr(ds, candidateAccessPaths, singleFilter)
if unfinishedPathList == nil {
return nil
}
unfinishedPartialPaths = append(unfinishedPartialPaths, unfinishedPathList)
}
return &unfinishedAccessPath{
indexMergeORPartialPaths: unfinishedPartialPaths,
}
}
// initUnfinishedPathsFromExpr tries to collect access filters from the input filter for each candidate access path,
// and returns them as a slice of unfinishedAccessPath, each of which corresponds to an input candidate access path.
// If we failed to collect access filters for one candidate access path, the corresponding element in the return slice
// will be nil.
// If we failed to collect access filters for all candidate access paths, this function will return nil.
/*
Example1 (consistent with the one in generateUnfinishedIndexMergePathFromORList()):
Input:
expr: 1 member of j->'$.a'
candidateAccessPaths: [idx1(a, j->'$.a' unsigned array), idx2(j->'$.b' unsigned array, a)]
Output:
[unfinishedAccessPath{idx1,1 member of j->'$.a'}, nil]
Example2:
Input:
expr: a = 3
candidateAccessPaths: [idx1(a, j->'$.a' unsigned array), idx2(j->'$.b' unsigned array, a)]
Output:
[unfinishedAccessPath{idx1,a=3}, unfinishedAccessPath{idx2,a=3}]
*/
func initUnfinishedPathsFromExpr(
ds *DataSource,
candidateAccessPaths []*util.AccessPath,
expr expression.Expression,
) unfinishedAccessPathList {
retValues := make([]unfinishedAccessPath, len(candidateAccessPaths))
ret := make([]*unfinishedAccessPath, 0, len(candidateAccessPaths))
for i := range candidateAccessPaths {
ret = append(ret, &retValues[i])
}
for i, path := range candidateAccessPaths {
ret[i].index = path.Index
// case 1: try to use the previous logic to handle non-mv index
if !isMVIndexPath(path) {
// generateNormalIndexPartialPaths4DNF is introduced for handle a slice of DNF items and a slice of
// candidate AccessPaths before, now we reuse it to handle single filter and single candidate AccessPath,
// so we need to wrap them in a slice here.
paths, needSelection, usedMap := ds.generateNormalIndexPartialPaths4DNF(
[]expression.Expression{expr},
[]*util.AccessPath{path},
)
if len(usedMap) == 1 && usedMap[0] && len(paths) == 1 {
ret[i].initedAsFinished = true
ret[i].accessFilters = paths[0].AccessConds
ret[i].needKeepFilter = needSelection
continue
}
}
if path.IsTablePath() {
continue
}
idxCols, ok := PrepareIdxColsAndUnwrapArrayType(ds.table.Meta(), path.Index, ds.TblCols, false)
if !ok {
continue
}
cnfItems := expression.SplitCNFItems(expr)
// case 2: try to use the previous logic to handle mv index
if isMVIndexPath(path) {
accessFilters, remainingFilters, tp := collectFilters4MVIndex(ds.SCtx(), cnfItems, idxCols)
if len(accessFilters) > 0 && (tp == multiValuesOROnMVColTp || tp == singleValueOnMVColTp) {
ret[i].initedAsFinished = true
ret[i].accessFilters = accessFilters
ret[i].needKeepFilter = len(remainingFilters) > 0
continue
}
}
// case 3: use the new logic if the previous logic didn't succeed to collect access filters that can build a
// valid range directly.
ret[i].idxColHasAccessFilter = make([]bool, len(idxCols))
for j, col := range idxCols {
for _, cnfItem := range cnfItems {
if ok, tp := checkAccessFilter4IdxCol(ds.SCtx(), cnfItem, col); ok &&
// Since we only handle the OR list nested in the AND list, and only generate IndexMerge OR path,
// we disable the multiValuesANDOnMVColTp case here.
(tp == eqOnNonMVColTp || tp == multiValuesOROnMVColTp || tp == singleValueOnMVColTp) {
ret[i].accessFilters = append(ret[i].accessFilters, cnfItem)
ret[i].idxColHasAccessFilter[j] = true
// Once we find one valid access filter for this column, we directly go to the next column without
// looking into other filters.
break
}
}
}
}
validCnt := 0
// remove useless paths
for i, path := range ret {
if !path.initedAsFinished &&
!slices.Contains(path.idxColHasAccessFilter, true) {
ret[i] = nil
} else {
validCnt++
}
}
if validCnt == 0 {
return nil
}
return ret
}
// handleTopLevelANDListAndGenFinishedPath is expected to be used together with
// generateUnfinishedIndexMergePathFromORList() to handle the expression like ... AND (... OR ... OR ...) AND ...
// for mv index.
// It will try to collect possible access filters from other items in the top level AND list and try to merge them into
// the unfinishedAccessPath from generateUnfinishedIndexMergePathFromORList(), and try to build it into a valid
// util.AccessPath.
// The input candidateAccessPaths argument should be the same with generateUnfinishedIndexMergePathFromORList().
func handleTopLevelANDListAndGenFinishedPath(
ds *DataSource,
allConds []expression.Expression,
orListIdxInAllConds int,
candidateAccessPaths []*util.AccessPath,
unfinishedIndexMergePath *unfinishedAccessPath,
) *util.AccessPath {
for i, cnfItem := range allConds {
// Skip the (... OR ... OR ...) in the list.
if i == orListIdxInAllConds {
continue
}
// Collect access filters from one AND item.
pathListFromANDItem := initUnfinishedPathsFromExpr(ds, candidateAccessPaths, cnfItem)
// Try to merge useful access filters in them into unfinishedIndexMergePath, which is from the nested OR list.
unfinishedIndexMergePath = mergeANDItemIntoUnfinishedIndexMergePath(unfinishedIndexMergePath, pathListFromANDItem)
}
if unfinishedIndexMergePath == nil {
return nil
}
return buildIntoAccessPath(
ds,
candidateAccessPaths,
unfinishedIndexMergePath,
allConds,
orListIdxInAllConds,
)
}
/*
Example (consistent with the one in generateUnfinishedIndexMergePathFromORList()):
idx1: (a, j->'$.a' unsigned array) idx2: (j->'$.b' unsigned array, a)
Input:
indexMergePath:
unfinishedAccessPath{ indexMergeORPartialPaths:[
[unfinishedAccessPath{idx1,1 member of j->'$.a'}, nil]
[nil, unfinishedAccessPath{idx2,2 member of j->'$.b'}]
]}
pathListFromANDItem:
[unfinishedAccessPath{idx1,a=3}, unfinishedAccessPath{idx2,a=3}]
Output:
unfinishedAccessPath{ indexMergeORPartialPaths:[
[unfinishedAccessPath{idx1,1 member of j->'$.a', a=3}, nil]
[nil, unfinishedAccessPath{idx2,2 member of j->'$.b', a=3}]
]}
*/
func mergeANDItemIntoUnfinishedIndexMergePath(
indexMergePath *unfinishedAccessPath,
pathListFromANDItem unfinishedAccessPathList,
) *unfinishedAccessPath {
// Currently, we only handle the case where indexMergePath is an index merge OR unfinished path and
// pathListFromANDItem is a normal unfinished path or nil
if indexMergePath == nil || len(indexMergePath.indexMergeORPartialPaths) == 0 {
return nil
}
// This means we failed to find any valid access filter from other expressions in the top level AND list.
// In this case, we ignore them and only rely on the nested OR list to try to build a IndexMerge OR path.
if pathListFromANDItem == nil {
return indexMergePath
}
for _, pathListForSinglePartialPath := range indexMergePath.indexMergeORPartialPaths {
if len(pathListForSinglePartialPath) != len(pathListFromANDItem) {
continue
}
for i, path := range pathListForSinglePartialPath {
if path == nil || pathListFromANDItem[i] == nil {
continue
}
// We don't do precise checks. As long as any columns have valid access filters, we collect the entire
// access filters from the AND item.
// We just collect as many possibly useful access filters as possible, buildIntoAccessPath() should handle
// them correctly.
if pathListFromANDItem[i].initedAsFinished ||
slices.Contains(pathListFromANDItem[i].idxColHasAccessFilter, true) {
path.accessFilters = append(path.accessFilters, pathListFromANDItem[i].accessFilters...)
}
}
}
return indexMergePath
}
func buildIntoAccessPath(
ds *DataSource,
originalPaths []*util.AccessPath,
indexMergePath *unfinishedAccessPath,
allConds []expression.Expression,
orListIdxInAllConds int,
) *util.AccessPath {
if indexMergePath == nil || len(indexMergePath.indexMergeORPartialPaths) == 0 {
return nil
}
var needSelectionGlobal bool
// 1. Generate one or more partial access path for each partial unfinished path (access filter on mv index may
// produce several partial paths).
partialPaths := make([]*util.AccessPath, 0, len(indexMergePath.indexMergeORPartialPaths))
// for each partial path
for _, unfinishedPathList := range indexMergePath.indexMergeORPartialPaths {
var (
bestPaths []*util.AccessPath
bestCountAfterAccess float64
bestNeedSelection bool
)
// for each possible access path of this partial path
for i, unfinishedPath := range unfinishedPathList {
if unfinishedPath == nil {
continue
}
var paths []*util.AccessPath
var needSelection bool
if unfinishedPath.index != nil && unfinishedPath.index.MVIndex {
// case 1: mv index
idxCols, ok := PrepareIdxColsAndUnwrapArrayType(
ds.table.Meta(),
unfinishedPath.index,
ds.TblCols,
true,
)
if !ok {
continue
}
accessFilters, remainingFilters, _ := collectFilters4MVIndex(
ds.SCtx(),
unfinishedPath.accessFilters,
idxCols,
)
if len(accessFilters) == 0 {
continue
}
var isIntersection bool
var err error
paths, isIntersection, ok, err = buildPartialPaths4MVIndex(
ds.SCtx(),
accessFilters,
idxCols,
unfinishedPath.index,
ds.tableStats.HistColl,
)
if err != nil || !ok || (isIntersection && len(paths) > 1) {
continue
}
needSelection = len(remainingFilters) > 0 || len(unfinishedPath.idxColHasAccessFilter) > 0
} else {
// case 2: non-mv index
var usedMap []bool
// Reuse the previous implementation. The same usage as in initUnfinishedPathsFromExpr().
paths, needSelection, usedMap = ds.generateNormalIndexPartialPaths4DNF(
[]expression.Expression{
expression.ComposeCNFCondition(
ds.SCtx().GetExprCtx(),
unfinishedPath.accessFilters...,
),
},
[]*util.AccessPath{originalPaths[i]},
)
if len(paths) != 1 || slices.Contains(usedMap, false) {
continue
}
}
needSelection = needSelection || unfinishedPath.needKeepFilter
// If there are several partial paths, we use the max CountAfterAccess for comparison.
maxCountAfterAccess := -1.0
for _, p := range paths {
maxCountAfterAccess = math.Max(maxCountAfterAccess, p.CountAfterAccess)
}
// Choose the best partial path for this partial path.
if len(bestPaths) == 0 {
bestPaths = paths
bestCountAfterAccess = maxCountAfterAccess
bestNeedSelection = needSelection
} else if bestCountAfterAccess > maxCountAfterAccess {
bestPaths = paths
bestCountAfterAccess = maxCountAfterAccess
bestNeedSelection = needSelection
}
}
if len(bestPaths) == 0 {
return nil
}
// Succeeded to get valid path(s) for this partial path.
partialPaths = append(partialPaths, bestPaths...)
needSelectionGlobal = needSelectionGlobal || bestNeedSelection
}
// 2. Collect the final table filter
// We always put all filters in the top level AND list except for the OR list into the final table filters.
// Whether to put the OR list into the table filters also depends on the needSelectionGlobal.
tableFilter := allConds[:]
if !needSelectionGlobal {
tableFilter = slices.Delete(tableFilter, orListIdxInAllConds, orListIdxInAllConds+1)
}
// 3. Build the final access path
ret := ds.buildPartialPathUp4MVIndex(partialPaths, false, tableFilter, ds.tableStats.HistColl)
return ret
}

View File

@ -1411,7 +1411,7 @@ func TestPlanCacheMVIndexRandomly(t *testing.T) {
verifyPlanCacheForMVIndex(t, tk, true, true,
`select /*+ use_index_merge(t2, idx2, idx) */ * from t2 where (? member of (a) and c=? and d=?) or (? member of (b) and c=? and d=?)`,
`int`, `int`, `int`, `int`, `int`, `int`)
verifyPlanCacheForMVIndex(t, tk, true, false,
verifyPlanCacheForMVIndex(t, tk, false, false,
`select /*+ use_index_merge(t2, idx2, idx) */ * from t2 where ( json_contains(a, ?) and c=? and d=?) or (? member of (b) and c=? and d=?)`,
`json-signed`, `int`, `int`, `int`, `int`, `int`)
verifyPlanCacheForMVIndex(t, tk, true, false,

View File

@ -54,11 +54,10 @@ IndexMerge_9 0.20 root type: union
└─TableRowIDScan_7 0.20 cop[tikv] table:t2 keep order:false, stats:pseudo
explain select /*+ use_index_merge(t2, idx2, idx) */ * from t2 where ( json_contains(a, '[1, 2, 3]') and c=1 and d=2) or (2 member of (b) and c=3 and d=2); -- 4: OR index merge from multi complicated mv index (memberof),make full use of DNF item's condition even if the predicate is intersection case (json_contains);
id estRows task access object operator info
IndexMerge_9 0.01 root type: union
├─IndexRangeScan_5(Build) 10.00 cop[tikv] table:t2, index:idx(c, cast(`a` as signed array)) range:[1,1], keep order:false, stats:pseudo
├─IndexRangeScan_6(Build) 0.10 cop[tikv] table:t2, index:idx2(cast(`b` as signed array), c) range:[2 3,2 3], keep order:false, stats:pseudo
└─Selection_8(Probe) 0.01 cop[tikv] or(and(json_contains(planner__core__casetest__index__index.t2.a, cast("[1, 2, 3]", json BINARY)), and(eq(planner__core__casetest__index__index.t2.c, 1), eq(planner__core__casetest__index__index.t2.d, 2))), and(json_memberof(cast(2, json BINARY), planner__core__casetest__index__index.t2.b), and(eq(planner__core__casetest__index__index.t2.c, 3), eq(planner__core__casetest__index__index.t2.d, 2))))
└─TableRowIDScan_7 10.10 cop[tikv] table:t2 keep order:false, stats:pseudo
IndexLookUp_11 8.00 root
├─IndexRangeScan_8(Build) 10.00 cop[tikv] table:t2, index:idx3(c, d) range:[1 2,1 2], [3 2,3 2], keep order:false, stats:pseudo
└─Selection_10(Probe) 8.00 cop[tikv] or(and(json_contains(planner__core__casetest__index__index.t2.a, cast("[1, 2, 3]", json BINARY)), and(eq(planner__core__casetest__index__index.t2.c, 1), eq(planner__core__casetest__index__index.t2.d, 2))), and(json_memberof(cast(2, json BINARY), planner__core__casetest__index__index.t2.b), and(eq(planner__core__casetest__index__index.t2.c, 3), eq(planner__core__casetest__index__index.t2.d, 2))))
└─TableRowIDScan_9 10.00 cop[tikv] table:t2 keep order:false, stats:pseudo
explain select /*+ use_index_merge(t2, idx2, idx) */ * from t2 where ( json_overlaps(a, '[1, 2, 3]') and c=1 and d=2) or (2 member of (b) and c=3 and d=2); -- 5: OR index merge from multi complicated mv index (memberof),make full use of DNF item's condition even if the predicate is intersection case (json_contains);
id estRows task access object operator info
Selection_5 0.32 root or(and(json_overlaps(planner__core__casetest__index__index.t2.a, cast("[1, 2, 3]", json BINARY)), and(eq(planner__core__casetest__index__index.t2.c, 1), eq(planner__core__casetest__index__index.t2.d, 2))), and(json_memberof(cast(2, json BINARY), planner__core__casetest__index__index.t2.b), and(eq(planner__core__casetest__index__index.t2.c, 3), eq(planner__core__casetest__index__index.t2.d, 2))))
@ -90,9 +89,9 @@ TableReader_7 10.18 root data:Selection_6
explain select /*+ use_index_merge(t2, idx2, idx, idx4) */ * from t2 where (1 member of (a) and 1 member of (b) and c=3) or (3 member of (b) and c=4) or d=1; -- 9: OR index merge from multi complicated mv index (memberof), each DNF item should be strict or lax used as index partial path, specify the index in index merge hint;
id estRows task access object operator info
IndexMerge_10 0.01 root type: union
├─IndexRangeScan_5(Build) 10.00 cop[tikv] table:t2, index:idx4(d) range:[1,1], keep order:false, stats:pseudo
├─IndexRangeScan_6(Build) 0.10 cop[tikv] table:t2, index:idx(c, cast(`a` as signed array)) range:[3 1,3 1], keep order:false, stats:pseudo
├─IndexRangeScan_7(Build) 0.10 cop[tikv] table:t2, index:idx2(cast(`b` as signed array), c) range:[3 4,3 4], keep order:false, stats:pseudo
├─IndexRangeScan_5(Build) 0.10 cop[tikv] table:t2, index:idx(c, cast(`a` as signed array)) range:[3 1,3 1], keep order:false, stats:pseudo
├─IndexRangeScan_6(Build) 0.10 cop[tikv] table:t2, index:idx2(cast(`b` as signed array), c) range:[3 4,3 4], keep order:false, stats:pseudo
├─IndexRangeScan_7(Build) 10.00 cop[tikv] table:t2, index:idx4(d) range:[1,1], keep order:false, stats:pseudo
└─Selection_9(Probe) 0.01 cop[tikv] or(and(json_memberof(cast(1, json BINARY), planner__core__casetest__index__index.t2.a), and(json_memberof(cast(1, json BINARY), planner__core__casetest__index__index.t2.b), eq(planner__core__casetest__index__index.t2.c, 3))), or(and(json_memberof(cast(3, json BINARY), planner__core__casetest__index__index.t2.b), eq(planner__core__casetest__index__index.t2.c, 4)), eq(planner__core__casetest__index__index.t2.d, 1)))
└─TableRowIDScan_8 10.20 cop[tikv] table:t2 keep order:false, stats:pseudo
drop table if exists t1, t2;

View File

@ -744,7 +744,7 @@ key mvi2(a, (cast(j->'$.c' as unsigned array))),
key mvi3((cast(j->'$.d' as unsigned array)), c),
key idx(b, c)
);
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
json_overlaps(j->'$.a', '[4,5,6]') or
(2 member of (j->'$.a'))
@ -753,10 +753,14 @@ c = 10 and
a = 20;
id estRows task access object operator info
Selection 0.01 root or(json_overlaps(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[4,5,6]", json BINARY)), json_memberof(cast(2, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")))
└─TableReader 0.01 root data:Selection
└─Selection 0.01 cop[tikv] eq(planner__core__indexmerge_path.t.a, 20), eq(planner__core__indexmerge_path.t.c, 10)
└─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
└─IndexMerge 0.40 root type: union
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 4,10 4], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 5,10 5], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 6,10 6], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 2,10 2], keep order:false, stats:pseudo
└─Selection(Probe) 0.40 cop[tikv] eq(planner__core__indexmerge_path.t.a, 20), eq(planner__core__indexmerge_path.t.c, 10)
└─TableRowIDScan 0.40 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
c = 1 or
c = 2 or
@ -764,11 +768,20 @@ c = 3
) and
json_overlaps(j->'$.a', '[4,5,6]');
id estRows task access object operator info
Selection 24.00 root json_overlaps(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[4,5,6]", json BINARY))
└─TableReader 30.00 root data:Selection
└─Selection 30.00 cop[tikv] or(eq(planner__core__indexmerge_path.t.c, 1), or(eq(planner__core__indexmerge_path.t.c, 2), eq(planner__core__indexmerge_path.t.c, 3)))
└─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
Selection 0.72 root json_overlaps(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[4,5,6]", json BINARY))
└─IndexMerge 0.90 root type: union
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[1 4,1 4], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[1 5,1 5], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[1 6,1 6], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[2 4,2 4], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[2 5,2 5], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[2 6,2 6], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[3 4,3 4], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[3 5,3 5], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[3 6,3 6], keep order:false, stats:pseudo
└─Selection(Probe) 0.90 cop[tikv] or(eq(planner__core__indexmerge_path.t.c, 1), or(eq(planner__core__indexmerge_path.t.c, 2), eq(planner__core__indexmerge_path.t.c, 3)))
└─TableRowIDScan 0.90 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
c = 1 or
c = 2 or
@ -779,7 +792,7 @@ id estRows task access object operator info
TableReader 24.00 root data:Selection
└─Selection 24.00 cop[tikv] json_contains(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[4,5,6]", json BINARY)), or(eq(planner__core__indexmerge_path.t.c, 1), or(eq(planner__core__indexmerge_path.t.c, 2), eq(planner__core__indexmerge_path.t.c, 3)))
└─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
c = 1 or
c = 2 or
@ -787,10 +800,13 @@ c = 3
) and
json_contains(j->'$.a', '[2]');
id estRows task access object operator info
TableReader 24.00 root data:Selection
└─Selection 24.00 cop[tikv] json_contains(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[2]", json BINARY)), or(eq(planner__core__indexmerge_path.t.c, 1), or(eq(planner__core__indexmerge_path.t.c, 2), eq(planner__core__indexmerge_path.t.c, 3)))
└─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
IndexMerge 0.30 root type: union
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[1 2,1 2], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[2 2,2 2], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[3 2,3 2], keep order:false, stats:pseudo
└─Selection(Probe) 0.30 cop[tikv] json_contains(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[2]", json BINARY)), or(eq(planner__core__indexmerge_path.t.c, 1), or(eq(planner__core__indexmerge_path.t.c, 2), eq(planner__core__indexmerge_path.t.c, 3)))
└─TableRowIDScan 0.30 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
1 member of (j->'$.a') or
2 member of (j->'$.a')
@ -798,10 +814,12 @@ explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
c = 10 and
a = 20;
id estRows task access object operator info
TableReader 0.01 root data:Selection
└─Selection 0.01 cop[tikv] eq(planner__core__indexmerge_path.t.a, 20), eq(planner__core__indexmerge_path.t.c, 10), or(json_memberof(cast(1, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")), json_memberof(cast(2, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")))
└─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi1) */ * from t where
IndexMerge 0.20 root type: union
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 1,10 1], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 2,10 2], keep order:false, stats:pseudo
└─Selection(Probe) 0.20 cop[tikv] eq(planner__core__indexmerge_path.t.a, 20), eq(planner__core__indexmerge_path.t.c, 10), or(json_memberof(cast(1, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")), json_memberof(cast(2, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")))
└─TableRowIDScan 0.20 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
1 member of (j->'$.a') or
2 member of (j->'$.d')
@ -811,7 +829,7 @@ id estRows task access object operator info
TableReader 8.00 root data:Selection
└─Selection 8.00 cop[tikv] eq(planner__core__indexmerge_path.t.a, 20), or(json_memberof(cast(1, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")), json_memberof(cast(2, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.d")))
└─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi3) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1, mvi3) */ * from t where
c = 5 and
(
1 member of (j->'$.a') or
@ -819,10 +837,12 @@ c = 5 and
) and
a = 20;
id estRows task access object operator info
TableReader 0.01 root data:Selection
└─Selection 0.01 cop[tikv] eq(planner__core__indexmerge_path.t.a, 20), eq(planner__core__indexmerge_path.t.c, 5), or(json_memberof(cast(1, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")), json_memberof(cast(2, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.d")))
└─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2) */ * from t where
IndexMerge 0.20 root type: union
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[5 1,5 1], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi3(cast(json_extract(`j`, _utf8mb4'$.d') as unsigned array), c) range:[2 5,2 5], keep order:false, stats:pseudo
└─Selection(Probe) 0.20 cop[tikv] eq(planner__core__indexmerge_path.t.a, 20), eq(planner__core__indexmerge_path.t.c, 5), or(json_memberof(cast(1, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")), json_memberof(cast(2, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.d")))
└─TableRowIDScan 0.20 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, primary) */ * from t where
(
pk = 2 or
json_overlaps(j->'$.a', '[4,5,6]') or
@ -833,11 +853,15 @@ b = '2' and
c = 3;
id estRows task access object operator info
Selection 0.00 root or(eq(planner__core__indexmerge_path.t.pk, 2), or(json_overlaps(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[4,5,6]", json BINARY)), json_contains(json_extract(planner__core__indexmerge_path.t.j, "$.c"), cast("[3]", json BINARY))))
└─IndexLookUp 0.00 root
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:idx(b, c) range:["2" 3,"2" 3], keep order:false, stats:pseudo
└─Selection(Probe) 0.00 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1)
└─TableRowIDScan 0.10 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2) */ * from t where
└─IndexMerge 0.00 root type: union
├─TableRangeScan(Build) 1.00 cop[tikv] table:t range:[2,2], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.00 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[3 4 "2",3 4 "2"], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.00 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[3 5 "2",3 5 "2"], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.00 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[3 6 "2",3 6 "2"], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi2(a, cast(json_extract(`j`, _utf8mb4'$.c') as unsigned array)) range:[1 3,1 3], keep order:false, stats:pseudo
└─Selection(Probe) 0.00 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1), eq(planner__core__indexmerge_path.t.b, "2"), eq(planner__core__indexmerge_path.t.c, 3)
└─TableRowIDScan 1.10 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, primary) */ * from t where
a = 1 and
b = '2' and
c = 3 and
@ -847,11 +871,13 @@ pk = 2 or
(3 member of (j->'$.c'))
);
id estRows task access object operator info
IndexLookUp 0.00 root
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:idx(b, c) range:["2" 3,"2" 3], keep order:false, stats:pseudo
└─Selection(Probe) 0.00 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1), or(eq(planner__core__indexmerge_path.t.pk, 2), or(json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.c"))))
└─TableRowIDScan 0.10 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2, idx) */ * from t where
IndexMerge 0.00 root type: union
├─TableRangeScan(Build) 1.00 cop[tikv] table:t range:[2,2], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.00 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[3 3 "2",3 3 "2"], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi2(a, cast(json_extract(`j`, _utf8mb4'$.c') as unsigned array)) range:[1 3,1 3], keep order:false, stats:pseudo
└─Selection(Probe) 0.00 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1), eq(planner__core__indexmerge_path.t.b, "2"), eq(planner__core__indexmerge_path.t.c, 3), or(eq(planner__core__indexmerge_path.t.pk, 2), or(json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a")), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.c"))))
└─TableRowIDScan 1.10 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, idx) */ * from t where
a = 1 and
b = '2' and
(
@ -860,11 +886,13 @@ c = 20 or
3 member of (j->'$.c')
);
id estRows task access object operator info
IndexLookUp 0.01 root
├─IndexRangeScan(Build) 10.00 cop[tikv] table:t, index:idx(b, c) range:["2","2"], keep order:false, stats:pseudo
└─Selection(Probe) 0.01 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1), or(eq(planner__core__indexmerge_path.t.c, 20), or(and(eq(planner__core__indexmerge_path.t.c, 10), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a"))), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.c"))))
└─TableRowIDScan 10.00 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2, idx) */ * from t where
IndexMerge 0.20 root type: union
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:idx(b, c) range:["2" 20,"2" 20], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.00 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 3 "2",10 3 "2"], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi2(a, cast(json_extract(`j`, _utf8mb4'$.c') as unsigned array)) range:[1 3,1 3], keep order:false, stats:pseudo
└─Selection(Probe) 0.20 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1), eq(planner__core__indexmerge_path.t.b, "2"), or(eq(planner__core__indexmerge_path.t.c, 20), or(and(eq(planner__core__indexmerge_path.t.c, 10), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.a"))), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.c"))))
└─TableRowIDScan 0.20 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, idx) */ * from t where
a = 1 and
(json_overlaps(j->'$.a', '[4,5,6]')) and
(
@ -874,10 +902,15 @@ c = 10 or
);
id estRows task access object operator info
Selection 6.41 root json_overlaps(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[4,5,6]", json BINARY))
└─TableReader 8.01 root data:Selection
└─Selection 8.01 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1), or(and(eq(planner__core__indexmerge_path.t.b, "2"), gt(planner__core__indexmerge_path.t.c, 20)), or(eq(planner__core__indexmerge_path.t.c, 10), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.c"))))
└─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2, idx) */ * from t where
└─IndexMerge 0.03 root type: union
├─IndexRangeScan(Build) 33.33 cop[tikv] table:t, index:idx(b, c) range:("2" 20,"2" +inf], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 4,10 4], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 5,10 5], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 6,10 6], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi2(a, cast(json_extract(`j`, _utf8mb4'$.c') as unsigned array)) range:[1 3,1 3], keep order:false, stats:pseudo
└─Selection(Probe) 0.03 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1), or(and(eq(planner__core__indexmerge_path.t.b, "2"), gt(planner__core__indexmerge_path.t.c, 20)), or(eq(planner__core__indexmerge_path.t.c, 10), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.c"))))
└─TableRowIDScan 33.73 cop[tikv] table:t keep order:false, stats:pseudo
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, idx) */ * from t where
a = 1 and
(json_overlaps(j->'$.a', '[4,5,6]')) and
(
@ -886,7 +919,27 @@ a = 1 and
(3 member of (j->'$.c'))
);
id estRows task access object operator info
Selection 6.40 root json_overlaps(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[4,5,6]", json BINARY))
└─TableReader 8.00 root data:Selection
└─Selection 8.00 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1), or(and(gt(planner__core__indexmerge_path.t.b, "2"), eq(planner__core__indexmerge_path.t.c, 20)), or(eq(planner__core__indexmerge_path.t.c, 10), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.c"))))
└─TableFullScan 10000.00 cop[tikv] table:t keep order:false, stats:pseudo
Selection 0.56 root json_overlaps(json_extract(planner__core__indexmerge_path.t.j, "$.a"), cast("[4,5,6]", json BINARY))
└─IndexMerge 0.70 root type: union
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[20 4,20 4], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[20 5,20 5], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[20 6,20 6], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 4,10 4], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 5,10 5], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi1(c, cast(json_extract(`j`, _utf8mb4'$.a') as unsigned array), b) range:[10 6,10 6], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t, index:mvi2(a, cast(json_extract(`j`, _utf8mb4'$.c') as unsigned array)) range:[1 3,1 3], keep order:false, stats:pseudo
└─Selection(Probe) 0.70 cop[tikv] eq(planner__core__indexmerge_path.t.a, 1), or(and(gt(planner__core__indexmerge_path.t.b, "2"), eq(planner__core__indexmerge_path.t.c, 20)), or(eq(planner__core__indexmerge_path.t.c, 10), json_memberof(cast(3, json BINARY), json_extract(planner__core__indexmerge_path.t.j, "$.c"))))
└─TableRowIDScan 0.70 cop[tikv] table:t keep order:false, stats:pseudo
create table t1 (a int, b int, c int, d int, j json, key kb(b, (cast(j as unsigned array))), key(d, c));
explain format=brief select * from t1 where (c=1 or b=1) and (1 member of (j));
id estRows task access object operator info
TableReader 15.99 root data:Selection
└─Selection 15.99 cop[tikv] json_memberof(cast(1, json BINARY), planner__core__indexmerge_path.t1.j), or(eq(planner__core__indexmerge_path.t1.c, 1), eq(planner__core__indexmerge_path.t1.b, 1))
└─TableFullScan 10000.00 cop[tikv] table:t1 keep order:false, stats:pseudo
explain format=brief select * from t1 where (c=1 or b=1) and (1 member of (j)) and d=1;
id estRows task access object operator info
IndexMerge 0.20 root type: union
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t1, index:d(d, c) range:[1 1,1 1], keep order:false, stats:pseudo
├─IndexRangeScan(Build) 0.10 cop[tikv] table:t1, index:kb(b, cast(`j` as unsigned array)) range:[1 1,1 1], keep order:false, stats:pseudo
└─Selection(Probe) 0.20 cop[tikv] eq(planner__core__indexmerge_path.t1.d, 1), json_memberof(cast(1, json BINARY), planner__core__indexmerge_path.t1.j), or(eq(planner__core__indexmerge_path.t1.c, 1), eq(planner__core__indexmerge_path.t1.b, 1))
└─TableRowIDScan 0.20 cop[tikv] table:t1 keep order:false, stats:pseudo

View File

@ -272,55 +272,63 @@ key mvi2(a, (cast(j->'$.c' as unsigned array))),
key mvi3((cast(j->'$.d' as unsigned array)), c),
key idx(b, c)
);
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
json_overlaps(j->'$.a', '[4,5,6]') or
(2 member of (j->'$.a'))
) and
c = 10 and
a = 20;
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
c = 1 or
c = 2 or
c = 3
) and
json_overlaps(j->'$.a', '[4,5,6]');
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
c = 1 or
c = 2 or
c = 3
) and
(json_contains(j->'$.a', '[4,5,6]'));
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
c = 1 or
c = 2 or
c = 3
) and
json_contains(j->'$.a', '[2]');
explain format=brief select /*+ use_index_merge(t1, mvi1) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
1 member of (j->'$.a') or
2 member of (j->'$.a')
) and
c = 10 and
a = 20;
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi1) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1) */ * from t where
(
1 member of (j->'$.a') or
2 member of (j->'$.d')
) and
a = 20;
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi3) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1, mvi3) */ * from t where
c = 5 and
(
1 member of (j->'$.a') or
2 member of (j->'$.d')
) and
a = 20;
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, primary) */ * from t where
(
pk = 2 or
json_overlaps(j->'$.a', '[4,5,6]') or
@ -329,7 +337,8 @@ explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2) */ * from t wher
a = 1 and
b = '2' and
c = 3;
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, primary) */ * from t where
a = 1 and
b = '2' and
c = 3 and
@ -338,7 +347,8 @@ c = 3 and
(3 member of (j->'$.a')) or
(3 member of (j->'$.c'))
);
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2, idx) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, idx) */ * from t where
a = 1 and
b = '2' and
(
@ -346,7 +356,8 @@ b = '2' and
(c = 10 and 3 member of (j->'$.a')) or
3 member of (j->'$.c')
);
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2, idx) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, idx) */ * from t where
a = 1 and
(json_overlaps(j->'$.a', '[4,5,6]')) and
(
@ -354,7 +365,8 @@ a = 1 and
c = 10 or
3 member of (j->'$.c')
);
explain format=brief select /*+ use_index_merge(t1, mvi1, mvi2, idx) */ * from t where
explain format=brief select /*+ use_index_merge(t, mvi1, mvi2, idx) */ * from t where
a = 1 and
(json_overlaps(j->'$.a', '[4,5,6]')) and
(
@ -362,3 +374,7 @@ a = 1 and
(c = 10) or
(3 member of (j->'$.c'))
);
create table t1 (a int, b int, c int, d int, j json, key kb(b, (cast(j as unsigned array))), key(d, c));
explain format=brief select * from t1 where (c=1 or b=1) and (1 member of (j));
explain format=brief select * from t1 where (c=1 or b=1) and (1 member of (j)) and d=1;