Skip to content

Commit d32c96c

Browse files
[ASTERIXDB-3688][COMP] Improving Index Selection for Index-Only Query Plans
- user model changes: no - storage format changes: no - interface changes: no Details: This change ensures that a secondary index capable of supporting an index-only query plan is selected when available. Ext-ref: MB-69514 Change-Id: I4f996ee7b6eb03067fb384161fb4eb60f94e8353 Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/20774 Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu> Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu> Reviewed-by: Ali Alsuliman <ali.al.solaiman@gmail.com>
1 parent 242011f commit d32c96c

21 files changed

Lines changed: 691 additions & 18 deletions

asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/AbstractIntroduceAccessMethodRule.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,15 @@ protected void chooseAllIndexes(Map<IAccessMethod, AccessMethodAnalysisContext>
287287
// 2) functions that take keyword as an argument, e.g. edit_distance_check() when the threshold is 1
288288
|| (chosenAccessMethod == InvertedIndexAccessMethod.INSTANCE && isKeywordIndexChosen
289289
&& isSameFullTextConfigInIndexAndQuery(analysisCtx, chosenIndex.getIndexDetails()))) {
290-
291-
if (resultVarsToIndexTypesMap.containsKey(indexEntry.getValue())) {
290+
// For Btrees and Rtrees we need to keep all the indexes to be able to find the covering ones.
291+
// therefore we add them without needing to check their index type.
292+
if ((chosenAccessMethod == RTreeAccessMethod.INSTANCE && indexType == IndexType.RTREE)
293+
|| (chosenAccessMethod == BTreeAccessMethod.INSTANCE && indexType == IndexType.BTREE)) {
294+
result.add(
295+
new IntroduceSelectAccessMethodRule.IndexAccessInfo(chosenAccessMethod, chosenIndex));
296+
}
297+
// this if seems to be needed for ngram indexes. todo: can we remove this for other types as well?
298+
else if (resultVarsToIndexTypesMap.containsKey(indexEntry.getValue())) {
292299
List<IndexType> appliedIndexTypes = resultVarsToIndexTypesMap.get(indexEntry.getValue());
293300
if (!appliedIndexTypes.contains(indexType)) {
294301
appliedIndexTypes.add(indexType);

asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
package org.apache.asterix.optimizer.rules.am;
2020

2121
import java.util.ArrayList;
22+
import java.util.Collections;
2223
import java.util.HashMap;
2324
import java.util.List;
2425
import java.util.Map;
2526
import java.util.Optional;
2627
import java.util.TreeMap;
28+
import java.util.stream.IntStream;
2729

2830
import org.apache.asterix.algebra.operators.CommitOperator;
2931
import org.apache.asterix.common.cluster.PartitioningProperties;
@@ -50,6 +52,7 @@
5052
import org.apache.hyracks.algebricks.core.algebra.expressions.AbstractFunctionCallExpression;
5153
import org.apache.hyracks.algebricks.core.algebra.expressions.IVariableTypeEnvironment;
5254
import org.apache.hyracks.algebricks.core.algebra.expressions.VariableReferenceExpression;
55+
import org.apache.hyracks.algebricks.core.algebra.functions.AlgebricksBuiltinFunctions;
5356
import org.apache.hyracks.algebricks.core.algebra.functions.FunctionIdentifier;
5457
import org.apache.hyracks.algebricks.core.algebra.operators.logical.AbstractLogicalOperator;
5558
import org.apache.hyracks.algebricks.core.algebra.operators.logical.AbstractLogicalOperator.ExecutionMode;
@@ -383,7 +386,8 @@ protected boolean prefix(List<List<String>> list1, List<List<String>> list2) {
383386
return true;
384387
}
385388

386-
protected void removeSmallerPrefixIndexes(List<IndexAccessInfo> indexes) throws CompilationException {
389+
protected void keepBestPrefixIndexes(List<IndexAccessInfo> indexes, List<Integer> numberOfMatchedKeys)
390+
throws CompilationException {
387391
int len = indexes.size();
388392
int i, j;
389393
Index indexI, indexJ;
@@ -412,10 +416,19 @@ protected void removeSmallerPrefixIndexes(List<IndexAccessInfo> indexes) throws
412416
fieldNamesJ = findKeyFieldNames(indexJ);
413417
if (fieldNamesI.size() <= fieldNamesJ.size()) {
414418
if (prefix(fieldNamesI, fieldNamesJ)) {
415-
include[i] = false;
419+
if (numberOfMatchedKeys.get(j) > numberOfMatchedKeys.get(i)) {
420+
include[i] = false;
421+
} else {
422+
include[j] = false;
423+
424+
}
416425
}
417426
} else if (prefix(fieldNamesJ, fieldNamesI)) {
418-
include[j] = false;
427+
if (numberOfMatchedKeys.get(i) > numberOfMatchedKeys.get(j)) {
428+
include[j] = false;
429+
} else {
430+
include[i] = false;
431+
}
419432
}
420433
}
421434
}
@@ -548,13 +561,16 @@ protected boolean checkAndApplyTheSelectTransformation(Mutable<ILogicalOperator>
548561

549562
// Choose all indexes that will be applied.
550563
chooseAllIndexes(analyzedAMs, chosenIndexes);
551-
removeSmallerPrefixIndexes(chosenIndexes);
552-
553564
if (chosenIndexes == null || chosenIndexes.isEmpty()) {
554565
// We can't apply any index for this SELECT operator
555566
context.addToDontApplySet(this, selectRef.getValue());
556567
return false;
557568
}
569+
List<Integer> matchedKeyCountsNonCovering = new ArrayList<>();
570+
List<Integer> matchedKeyCountsCovering = new ArrayList<>();
571+
List<IndexAccessInfo> nonCoverigIndexes = new ArrayList<>();
572+
List<IndexAccessInfo> coverigIndexes = new ArrayList<>();
573+
fillFieldNamesInTheSubTree(subTree, context);
558574

559575
for (IndexAccessInfo indexAccessInfo : chosenIndexes) {
560576
AccessMethodAnalysisContext analysisCtx = analyzedAMs.get(indexAccessInfo.getAccessMethod());
@@ -565,7 +581,15 @@ protected boolean checkAndApplyTheSelectTransformation(Mutable<ILogicalOperator>
565581
AccessMethodUtils.indexOnlyPlanCheck(afterSelectRefs, selectRef, subTree, null,
566582
indexAccessInfo.getIndex(), analysisCtx, context, indexOnlyPlanInfo, false);
567583
indexAccessInfo.setIsIndexOnlyPlan(indexOnlyPlanInfo.getFirst());
584+
if (indexAccessInfo.isIndexOnlyPlan) {
585+
matchedKeyCountsCovering.add(analysisCtx.getNumberOfMatchedKeys(indexAccessInfo.getIndex()));
586+
coverigIndexes.add(indexAccessInfo);
587+
} else {
588+
matchedKeyCountsNonCovering.add(analysisCtx.getNumberOfMatchedKeys(indexAccessInfo.getIndex()));
589+
nonCoverigIndexes.add(indexAccessInfo);
590+
}
568591
}
592+
keepBestPrefixIndexes(nonCoverigIndexes, matchedKeyCountsNonCovering);
569593

570594
if (checkApplicableOnly) {
571595
return true;
@@ -574,28 +598,36 @@ protected boolean checkAndApplyTheSelectTransformation(Mutable<ILogicalOperator>
574598
// Apply plan transformation using chosen index.
575599
boolean res;
576600
// Primary index applicable?
601+
// We should look in the list including both covering and non covering.
577602
IndexAccessInfo chosenPrimaryIndex = fetchPrimaryIndexAmongChosenIndexes(chosenIndexes);
578603
if (chosenPrimaryIndex != null) {
604+
subTree.getVarsToFieldNameMap().clear();
579605
AccessMethodAnalysisContext analysisCtx = analyzedAMs.get(chosenPrimaryIndex.getAccessMethod());
580606
res = chosenPrimaryIndex.getAccessMethod().applySelectPlanTransformation(afterSelectRefs, selectRef,
581607
subTree, chosenPrimaryIndex.getIndex(), analysisCtx, context);
582608
context.addToDontApplySet(this, selectRef.getValue());
583-
} else if (chosenIndexes.size() == 1) {
584-
// Index-only plan possible?
609+
} else if (coverigIndexes.isEmpty() && nonCoverigIndexes.size() == 1) {
585610
// Gets the analysis context for the given index.
586-
AccessMethodAnalysisContext analysisCtx = analyzedAMs.get(chosenIndexes.get(0).getAccessMethod());
587-
588-
// Finds the field name of each variable in the sub-tree.
589-
fillFieldNamesInTheSubTree(subTree, context);
611+
AccessMethodAnalysisContext analysisCtx =
612+
analyzedAMs.get(nonCoverigIndexes.get(0).getAccessMethod());
590613

591614
// Finally, try to apply plan transformation using chosen index.
592-
res = chosenIndexes.get(0).getAccessMethod().applySelectPlanTransformation(afterSelectRefs,
593-
selectRef, subTree, chosenIndexes.get(0).getIndex(), analysisCtx, context);
615+
res = nonCoverigIndexes.get(0).getAccessMethod().applySelectPlanTransformation(afterSelectRefs,
616+
selectRef, subTree, nonCoverigIndexes.get(0).getIndex(), analysisCtx, context);
594617
context.addToDontApplySet(this, selectRef.getValue());
595618
} else {
596-
// Multiple secondary indexes applicable?
597-
res = intersectAllSecondaryIndexes(chosenIndexes, analyzedAMs, context);
598-
context.addToDontApplySet(this, selectRef.getValue());
619+
if (!coverigIndexes.isEmpty()) {
620+
IndexAccessInfo bestCoveringIndex =
621+
chooseBestCoveringIndex(coverigIndexes, matchedKeyCountsCovering, analyzedAMs);
622+
AccessMethodAnalysisContext analysisCtx = analyzedAMs.get(bestCoveringIndex.getAccessMethod());
623+
res = bestCoveringIndex.getAccessMethod().applySelectPlanTransformation(afterSelectRefs,
624+
selectRef, subTree, bestCoveringIndex.getIndex(), analysisCtx, context);
625+
context.addToDontApplySet(this, selectRef.getValue());
626+
} else {
627+
subTree.getVarsToFieldNameMap().clear();
628+
res = intersectAllSecondaryIndexes(nonCoverigIndexes, analyzedAMs, context);
629+
context.addToDontApplySet(this, selectRef.getValue());
630+
}
599631
}
600632

601633
// If the plan transformation is successful, we don't need to traverse
@@ -619,6 +651,68 @@ protected boolean checkAndApplyTheSelectTransformation(Mutable<ILogicalOperator>
619651

620652
}
621653

654+
private IndexAccessInfo chooseBestCoveringIndex(List<IndexAccessInfo> indexes,
655+
List<Integer> numberOfMatchedKeysForCoveringindexes,
656+
Map<IAccessMethod, AccessMethodAnalysisContext> analyzedAMs) throws CompilationException {
657+
658+
if (indexes.size() == 1) {
659+
return indexes.get(0);
660+
}
661+
662+
// 1) Keep only indexes with max matched keys
663+
int max = numberOfMatchedKeysForCoveringindexes.stream().mapToInt(Integer::intValue).max().orElse(0);
664+
List<IndexAccessInfo> indexesWithMaxMatchedKeys = IntStream.range(0, indexes.size())
665+
.filter(i -> numberOfMatchedKeysForCoveringindexes.get(i) == max).mapToObj(indexes::get).toList();
666+
if (indexesWithMaxMatchedKeys.size() == 1) {
667+
return indexesWithMaxMatchedKeys.get(0);
668+
}
669+
670+
// 2) Find earliest equality positions
671+
List<IndexInfo> indexesWithMaxMatchedKeysInfo = new ArrayList<>(indexesWithMaxMatchedKeys.size());
672+
for (IndexAccessInfo indexAccessInfo : indexesWithMaxMatchedKeys) {
673+
List<List<String>> indexFields = findKeyFieldNames(indexAccessInfo.getIndex());
674+
List<IOptimizableFuncExpr> funcExprs =
675+
analyzedAMs.get(indexAccessInfo.getAccessMethod()).getMatchedFuncExprs();
676+
IndexInfo indexInfo = new IndexInfo();
677+
indexInfo.indexExprs = new ArrayList<>(Collections.nCopies(funcExprs.size(), null));
678+
indexInfo.numKeys = indexFields.size();
679+
indexInfo.indexaccessinfo = indexAccessInfo;
680+
for (IOptimizableFuncExpr expr : funcExprs) {
681+
for (int i = 0; i < expr.getNumLogicalVars(); i++) {
682+
int i1 = indexFields.indexOf(expr.getFieldName(i));
683+
indexInfo.indexExprs.set(i1, expr.getFuncExpr().getFunctionIdentifier());
684+
}
685+
}
686+
indexesWithMaxMatchedKeysInfo.add(indexInfo);
687+
}
688+
return indexesWithMaxMatchedKeysInfo.stream().sorted().toList().getFirst().indexaccessinfo;
689+
}
690+
691+
class IndexInfo implements Comparable<IndexInfo> {
692+
List<FunctionIdentifier> indexExprs = new ArrayList<>();
693+
int numKeys;
694+
IndexAccessInfo indexaccessinfo;
695+
696+
@Override
697+
public int compareTo(IndexInfo other) {
698+
for (int i = 0; i < this.indexExprs.size() && i < other.indexExprs.size(); i++) {
699+
if (this.indexExprs.get(i) == AlgebricksBuiltinFunctions.EQ
700+
&& other.indexExprs.get(i) != AlgebricksBuiltinFunctions.EQ) {
701+
return -1;
702+
} else if (this.indexExprs.get(i) != AlgebricksBuiltinFunctions.EQ
703+
&& other.indexExprs.get(i) == AlgebricksBuiltinFunctions.EQ) {
704+
return 1;
705+
}
706+
}
707+
708+
if (this.indexExprs.size() != other.indexExprs.size()) {
709+
return 0;
710+
}
711+
712+
return Integer.compare(this.numKeys, other.numKeys);
713+
}
714+
}
715+
622716
@Override
623717
public Map<FunctionIdentifier, List<IAccessMethod>> getAccessMethods() {
624718
return accessMethods;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
21+
use test;
22+
23+
drop index Orders.idx9_early_eq if exists;
24+
drop index Orders.idx9_late_eq if exists;
25+
26+
create index idx10_thin on Orders(city,country,storeName);
27+
create index idx10_thick on Orders(city,country,storeName,extra1,extra2);
28+
29+
30+
explain select o.city, o.country, o.storeName
31+
from Orders o
32+
where o.city = 'NYC' and o.country > 'USA';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
21+
use test;
22+
23+
drop index Orders.idx10_thin if exists;
24+
drop index Orders.idx10_thick if exists;
25+
26+
create index idx11_stops on Orders(city,pp,country,storeName);
27+
create index idx11_all_searchable on Orders(city,country,storeName);
28+
29+
explain select o.city, o.country, o.storeName
30+
from Orders o
31+
where o.city = 'NYC' and o.country > 'USA';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
use test;
21+
22+
23+
24+
create index storeName_idx on Orders(storeName);
25+
create index country_idx2 on Orders(storeName,city,country);
26+
create index storeName_city_idx2 on Orders(storeName,city);
27+
create index storeName_city_idx1 on Orders(city,storeName);
28+
create index country_idx1 on Orders(storeName,city,country);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
use test;
21+
22+
23+
explain select o.city
24+
from Orders o
25+
where o.storeName="big store";
26+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
use test;
21+
22+
23+
explain select o.storeName
24+
from Orders o
25+
where o.storeName="big store";
26+

0 commit comments

Comments
 (0)