From af76c2ff1e7cfadbccb74a3a0ffd761cfecbffef Mon Sep 17 00:00:00 2001 From: Zhou Kunqin <25057648+time-and-fate@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:23:13 +0800 Subject: [PATCH] planner: support OR list nested in AND list for mv index (#51716) close pingcap/tidb#51778 --- pkg/planner/core/BUILD.bazel | 1 + pkg/planner/core/indexmerge_path.go | 330 ++++---------- .../core/indexmerge_unfinished_path.go | 416 ++++++++++++++++++ pkg/planner/core/plan_cache_test.go | 2 +- .../planner/core/casetest/index/index.result | 15 +- .../r/planner/core/indexmerge_path.result | 147 +++++-- .../t/planner/core/indexmerge_path.test | 40 +- 7 files changed, 633 insertions(+), 318 deletions(-) create mode 100644 pkg/planner/core/indexmerge_unfinished_path.go diff --git a/pkg/planner/core/BUILD.bazel b/pkg/planner/core/BUILD.bazel index facbe600ff4c5..e366f4b03360d 100644 --- a/pkg/planner/core/BUILD.bazel +++ b/pkg/planner/core/BUILD.bazel @@ -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", diff --git a/pkg/planner/core/indexmerge_path.go b/pkg/planner/core/indexmerge_path.go index 9819126f23a1f..ea17974e063be 100644 --- a/pkg/planner/core/indexmerge_path.go +++ b/pkg/planner/core/indexmerge_path.go @@ -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)] - - // 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:]...) + dnfFilters := expression.SplitDNFItems(sf) // [(1 member of (a) and b=1), (2 member of (a) and b=2)] - indexMergePath := ds.buildPartialPathUp4MVIndex( - partialPaths, - false, - remainingFilters, - ds.tableStats.HistColl, + unfinishedIndexMergePath := generateUnfinishedIndexMergePathFromORList( + ds, + dnfFilters, + []*util.AccessPath{ds.possibleAccessPaths[idx]}, + ) + finishedIndexMergePath := handleTopLevelANDListAndGenFinishedPath( + ds, + filters, + current, + []*util.AccessPath{ds.possibleAccessPaths[idx]}, + unfinishedIndexMergePath, ) - mvIndexPaths = append(mvIndexPaths, indexMergePath) + 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 { - // DNF path. - sf, ok := indexMergeConds[0].(*expression.ScalarFunction) - if !ok || sf.FuncName.L != ast.LogicOr { - // targeting: cond1 or cond2 or cond3 - return nil + // 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 } - dnfFilters := expression.FlattenDNFConditions(sf) - mvIndexPartialPaths, usedAccessMap, needSelection4MVIndex, err := ds.generateMVIndexPartialPath4Or(normalPathCnt, dnfFilters) - if err != nil { - return err + if (ds.possibleAccessPaths[idx].IsTablePath() && + !ds.isInIndexMergeHints("primary")) || + (!ds.possibleAccessPaths[idx].IsTablePath() && + !ds.isInIndexMergeHints(ds.possibleAccessPaths[idx].Index.Name.L)) { + continue } - if len(mvIndexPartialPaths) == 0 { - // no mv index partial paths to be composed of. - return nil + if isMVIndexPath(ds.possibleAccessPaths[idx]) { + mvIndexPathCnt++ } - normalIndexPartialPaths, usedAccessMap, needSelection4NormalIndex, err := ds.generateNormalIndexPartialPath4Or(dnfFilters, usedAccessMap, normalPathCnt) - if err != nil { - return err + candidateAccessPaths = append(candidateAccessPaths, ds.possibleAccessPaths[idx]) + } + if mvIndexPathCnt == 0 { + return nil + } + + for current, filter := range indexMergeConds { + // DNF path. + sf, ok := filter.(*expression.ScalarFunction) + if !ok || sf.FuncName.L != ast.LogicOr { + // targeting: cond1 or cond2 or cond3 + continue } - // 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 { + dnfFilters := expression.SplitDNFItems(sf) + + unfinishedIndexMergePath := generateUnfinishedIndexMergePathFromORList( + ds, + dnfFilters, + candidateAccessPaths, + ) + finishedIndexMergePath := handleTopLevelANDListAndGenFinishedPath( + ds, + indexMergeConds, + current, + candidateAccessPaths, + unfinishedIndexMergePath, + ) + if finishedIndexMergePath == nil { 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 + + var mvIndexPartialPathCnt, normalIndexPartialPathCnt int + for _, path := range finishedIndexMergePath.PartialIndexPaths { + if isMVIndexPath(path) { + mvIndexPartialPathCnt++ + } else { + normalIndexPartialPathCnt++ + } } - // 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 { + + // Keep the same behavior with previous implementation, we only handle the "composed" case here. + if mvIndexPartialPathCnt == 0 || (mvIndexPartialPathCnt == 1 && normalIndexPartialPathCnt == 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, - ) - ds.possibleAccessPaths = append(ds.possibleAccessPaths, mvp) + 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, diff --git a/pkg/planner/core/indexmerge_unfinished_path.go b/pkg/planner/core/indexmerge_unfinished_path.go new file mode 100644 index 0000000000000..e9ad3eb878ac9 --- /dev/null +++ b/pkg/planner/core/indexmerge_unfinished_path.go @@ -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 +} diff --git a/pkg/planner/core/plan_cache_test.go b/pkg/planner/core/plan_cache_test.go index f5b8670be73ed..543cc30198d54 100644 --- a/pkg/planner/core/plan_cache_test.go +++ b/pkg/planner/core/plan_cache_test.go @@ -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, diff --git a/tests/integrationtest/r/planner/core/casetest/index/index.result b/tests/integrationtest/r/planner/core/casetest/index/index.result index 9fa10d38fdc10..42fa7873de187 100644 --- a/tests/integrationtest/r/planner/core/casetest/index/index.result +++ b/tests/integrationtest/r/planner/core/casetest/index/index.result @@ -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; diff --git a/tests/integrationtest/r/planner/core/indexmerge_path.result b/tests/integrationtest/r/planner/core/indexmerge_path.result index 51b768bacb3ea..77f45330efa03 100644 --- a/tests/integrationtest/r/planner/core/indexmerge_path.result +++ b/tests/integrationtest/r/planner/core/indexmerge_path.result @@ -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 diff --git a/tests/integrationtest/t/planner/core/indexmerge_path.test b/tests/integrationtest/t/planner/core/indexmerge_path.test index 57a8b2ffa3e31..32b51d4958a94 100644 --- a/tests/integrationtest/t/planner/core/indexmerge_path.test +++ b/tests/integrationtest/t/planner/core/indexmerge_path.test @@ -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;