Skip to content

Commit d43231c

Browse files
apurtellclaude
andauthored
PHOENIX-7891 Explain the query optimizer's index selection rationale (#2513)
Co-authored-by: Claude Opus 4.8[1m] <noreply@anthropic.com>
1 parent e177c4c commit d43231c

35 files changed

Lines changed: 1179 additions & 274 deletions

phoenix-core-client/src/main/java/org/apache/phoenix/compile/ExplainPlanAttributes.java

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Set;
2626
import org.apache.hadoop.hbase.HRegionLocation;
2727
import org.apache.hadoop.hbase.client.Consistency;
28+
import org.apache.phoenix.optimize.RejectedIndexEntry;
2829
import org.apache.phoenix.parse.HintNode;
2930
import org.apache.phoenix.parse.HintNode.Hint;
3031
import org.apache.phoenix.schema.PColumn;
@@ -35,16 +36,16 @@
3536
* Strings containing entire plan.
3637
*/
3738
@JsonPropertyOrder({ "abstractExplainPlan", "hint", "explainScanType", "consistency", "tableName",
38-
"keyRanges", "indexName", "indexKind", "saltBuckets", "regionsPlanned", "scanTimeRangeMin",
39-
"scanTimeRangeMax", "splitsChunk", "useRoundRobinIterator", "samplingRate", "hexStringRVCOffset",
40-
"iteratorTypeAndScanSize", "estimatedRows", "estimatedSizeInBytes", "serverWhereFilter",
41-
"serverDistinctFilter", "serverMergeColumns", "serverArrayElementProjection",
42-
"serverFirstKeyOnlyProjection", "serverEmptyColumnOnlyProjection", "serverAggregate",
43-
"serverGroupByLimit", "serverSortedBy", "serverOffset", "serverRowLimit", "clientFilterBy",
44-
"clientAggregate", "clientDistinctFilter", "clientAfterAggregate", "clientSortAlgo",
45-
"clientSortedBy", "clientOffset", "clientRowLimit", "clientSequenceCount", "clientCursorName",
46-
"clientSteps", "lhsJoinQueryExplainPlan", "rhsJoinQueryExplainPlan", "subPlans",
47-
"dynamicServerFilter", "afterJoinFilter", "joinScannerLimit", "sortMergeSkipMerge",
39+
"keyRanges", "indexName", "indexKind", "indexRule", "indexRejected", "saltBuckets",
40+
"regionsPlanned", "scanTimeRangeMin", "scanTimeRangeMax", "splitsChunk", "useRoundRobinIterator",
41+
"samplingRate", "hexStringRVCOffset", "iteratorTypeAndScanSize", "estimatedRows",
42+
"estimatedSizeInBytes", "serverWhereFilter", "serverDistinctFilter", "serverMergeColumns",
43+
"serverArrayElementProjection", "serverFirstKeyOnlyProjection", "serverEmptyColumnOnlyProjection",
44+
"serverAggregate", "serverGroupByLimit", "serverSortedBy", "serverOffset", "serverRowLimit",
45+
"clientFilterBy", "clientAggregate", "clientDistinctFilter", "clientAfterAggregate",
46+
"clientSortAlgo", "clientSortedBy", "clientOffset", "clientRowLimit", "clientSequenceCount",
47+
"clientCursorName", "clientSteps", "lhsJoinQueryExplainPlan", "rhsJoinQueryExplainPlan",
48+
"subPlans", "dynamicServerFilter", "afterJoinFilter", "joinScannerLimit", "sortMergeSkipMerge",
4849
"regionLocations", "regionLocationsTotalSize", "numRegionLocationLookups" })
4950
public class ExplainPlanAttributes {
5051

@@ -57,6 +58,8 @@ public class ExplainPlanAttributes {
5758
private final String keyRanges;
5859
private final String indexName;
5960
private final String indexKind;
61+
private final String indexRule;
62+
private final List<RejectedIndexEntry> indexRejected;
6063
private final Integer saltBuckets;
6164
private final Integer regionsPlanned;
6265
private final Long scanTimeRangeMin;
@@ -122,6 +125,8 @@ private ExplainPlanAttributes() {
122125
this.keyRanges = null;
123126
this.indexName = null;
124127
this.indexKind = null;
128+
this.indexRule = null;
129+
this.indexRejected = null;
125130
this.saltBuckets = null;
126131
this.regionsPlanned = null;
127132
this.scanTimeRangeMin = null;
@@ -169,20 +174,20 @@ private ExplainPlanAttributes() {
169174

170175
public ExplainPlanAttributes(String abstractExplainPlan, Hint hint, String explainScanType,
171176
Consistency consistency, String tableName, String keyRanges, String indexName, String indexKind,
172-
Integer saltBuckets, Integer regionsPlanned, Long scanTimeRangeMin, Long scanTimeRangeMax,
173-
Integer splitsChunk, boolean useRoundRobinIterator, Double samplingRate,
174-
String hexStringRVCOffset, String iteratorTypeAndScanSize, Long estimatedRows,
175-
Long estimatedSizeInBytes, String serverWhereFilter, String serverDistinctFilter,
176-
Set<PColumn> serverMergeColumns, boolean serverArrayElementProjection,
177-
boolean serverFirstKeyOnlyProjection, boolean serverEmptyColumnOnlyProjection,
178-
String serverAggregate, Integer serverGroupByLimit, String serverSortedBy, Integer serverOffset,
179-
Long serverRowLimit, String clientFilterBy, String clientAggregate, String clientDistinctFilter,
180-
String clientAfterAggregate, String clientSortAlgo, String clientSortedBy, Integer clientOffset,
181-
Integer clientRowLimit, Integer clientSequenceCount, String clientCursorName,
182-
List<String> clientSteps, ExplainPlanAttributes lhsJoinQueryExplainPlan,
183-
ExplainPlanAttributes rhsJoinQueryExplainPlan, List<ExplainPlanAttributes> subPlans,
184-
String dynamicServerFilter, String afterJoinFilter, Long joinScannerLimit,
185-
boolean sortMergeSkipMerge, List<HRegionLocation> regionLocations,
177+
String indexRule, List<RejectedIndexEntry> indexRejected, Integer saltBuckets,
178+
Integer regionsPlanned, Long scanTimeRangeMin, Long scanTimeRangeMax, Integer splitsChunk,
179+
boolean useRoundRobinIterator, Double samplingRate, String hexStringRVCOffset,
180+
String iteratorTypeAndScanSize, Long estimatedRows, Long estimatedSizeInBytes,
181+
String serverWhereFilter, String serverDistinctFilter, Set<PColumn> serverMergeColumns,
182+
boolean serverArrayElementProjection, boolean serverFirstKeyOnlyProjection,
183+
boolean serverEmptyColumnOnlyProjection, String serverAggregate, Integer serverGroupByLimit,
184+
String serverSortedBy, Integer serverOffset, Long serverRowLimit, String clientFilterBy,
185+
String clientAggregate, String clientDistinctFilter, String clientAfterAggregate,
186+
String clientSortAlgo, String clientSortedBy, Integer clientOffset, Integer clientRowLimit,
187+
Integer clientSequenceCount, String clientCursorName, List<String> clientSteps,
188+
ExplainPlanAttributes lhsJoinQueryExplainPlan, ExplainPlanAttributes rhsJoinQueryExplainPlan,
189+
List<ExplainPlanAttributes> subPlans, String dynamicServerFilter, String afterJoinFilter,
190+
Long joinScannerLimit, boolean sortMergeSkipMerge, List<HRegionLocation> regionLocations,
186191
Integer regionLocationsTotalSize, int numRegionLocationLookups) {
187192
this.abstractExplainPlan = abstractExplainPlan;
188193
this.hint = hint;
@@ -192,6 +197,10 @@ public ExplainPlanAttributes(String abstractExplainPlan, Hint hint, String expla
192197
this.keyRanges = keyRanges;
193198
this.indexName = indexName;
194199
this.indexKind = indexKind;
200+
this.indexRule = indexRule;
201+
this.indexRejected = (indexRejected == null || indexRejected.isEmpty())
202+
? null
203+
: Collections.unmodifiableList(new ArrayList<>(indexRejected));
195204
this.saltBuckets = saltBuckets;
196205
this.regionsPlanned = regionsPlanned;
197206
this.scanTimeRangeMin = scanTimeRangeMin;
@@ -271,6 +280,14 @@ public String getIndexKind() {
271280
return indexKind;
272281
}
273282

283+
public String getIndexRule() {
284+
return indexRule;
285+
}
286+
287+
public List<RejectedIndexEntry> getIndexRejected() {
288+
return indexRejected;
289+
}
290+
274291
public Integer getSaltBuckets() {
275292
return saltBuckets;
276293
}
@@ -458,6 +475,8 @@ public static class ExplainPlanAttributesBuilder {
458475
private String keyRanges;
459476
private String indexName;
460477
private String indexKind;
478+
private String indexRule;
479+
private List<RejectedIndexEntry> indexRejected;
461480
private Integer saltBuckets;
462481
private Integer regionsPlanned;
463482
private Long scanTimeRangeMin;
@@ -515,6 +534,9 @@ public ExplainPlanAttributesBuilder(ExplainPlanAttributes explainPlanAttributes)
515534
this.keyRanges = explainPlanAttributes.getKeyRanges();
516535
this.indexName = explainPlanAttributes.getIndexName();
517536
this.indexKind = explainPlanAttributes.getIndexKind();
537+
this.indexRule = explainPlanAttributes.getIndexRule();
538+
List<RejectedIndexEntry> srcIndexRejected = explainPlanAttributes.getIndexRejected();
539+
this.indexRejected = srcIndexRejected == null ? null : new ArrayList<>(srcIndexRejected);
518540
this.saltBuckets = explainPlanAttributes.getSaltBuckets();
519541
this.regionsPlanned = explainPlanAttributes.getRegionsPlanned();
520542
this.scanTimeRangeMin = explainPlanAttributes.getScanTimeRangeMin();
@@ -602,6 +624,16 @@ public ExplainPlanAttributesBuilder setIndexKind(String indexKind) {
602624
return this;
603625
}
604626

627+
public ExplainPlanAttributesBuilder setIndexRule(String indexRule) {
628+
this.indexRule = indexRule;
629+
return this;
630+
}
631+
632+
public ExplainPlanAttributesBuilder setIndexRejected(List<RejectedIndexEntry> indexRejected) {
633+
this.indexRejected = indexRejected == null ? null : new ArrayList<>(indexRejected);
634+
return this;
635+
}
636+
605637
public ExplainPlanAttributesBuilder setSaltBuckets(Integer saltBuckets) {
606638
this.saltBuckets = saltBuckets;
607639
return this;
@@ -833,16 +865,16 @@ public ExplainPlanAttributesBuilder setNumRegionLocationLookups(int numRegionLoc
833865

834866
public ExplainPlanAttributes build() {
835867
return new ExplainPlanAttributes(abstractExplainPlan, hint, explainScanType, consistency,
836-
tableName, keyRanges, indexName, indexKind, saltBuckets, regionsPlanned, scanTimeRangeMin,
837-
scanTimeRangeMax, splitsChunk, useRoundRobinIterator, samplingRate, hexStringRVCOffset,
838-
iteratorTypeAndScanSize, estimatedRows, estimatedSizeInBytes, serverWhereFilter,
839-
serverDistinctFilter, serverMergeColumns, serverArrayElementProjection,
840-
serverFirstKeyOnlyProjection, serverEmptyColumnOnlyProjection, serverAggregate,
841-
serverGroupByLimit, serverSortedBy, serverOffset, serverRowLimit, clientFilterBy,
842-
clientAggregate, clientDistinctFilter, clientAfterAggregate, clientSortAlgo, clientSortedBy,
843-
clientOffset, clientRowLimit, clientSequenceCount, clientCursorName, clientSteps,
844-
lhsJoinQueryExplainPlan, rhsJoinQueryExplainPlan, subPlans, dynamicServerFilter,
845-
afterJoinFilter, joinScannerLimit, sortMergeSkipMerge, regionLocations,
868+
tableName, keyRanges, indexName, indexKind, indexRule, indexRejected, saltBuckets,
869+
regionsPlanned, scanTimeRangeMin, scanTimeRangeMax, splitsChunk, useRoundRobinIterator,
870+
samplingRate, hexStringRVCOffset, iteratorTypeAndScanSize, estimatedRows,
871+
estimatedSizeInBytes, serverWhereFilter, serverDistinctFilter, serverMergeColumns,
872+
serverArrayElementProjection, serverFirstKeyOnlyProjection, serverEmptyColumnOnlyProjection,
873+
serverAggregate, serverGroupByLimit, serverSortedBy, serverOffset, serverRowLimit,
874+
clientFilterBy, clientAggregate, clientDistinctFilter, clientAfterAggregate, clientSortAlgo,
875+
clientSortedBy, clientOffset, clientRowLimit, clientSequenceCount, clientCursorName,
876+
clientSteps, lhsJoinQueryExplainPlan, rhsJoinQueryExplainPlan, subPlans,
877+
dynamicServerFilter, afterJoinFilter, joinScannerLimit, sortMergeSkipMerge, regionLocations,
846878
regionLocationsTotalSize, numRegionLocationLookups);
847879
}
848880
}

phoenix-core-client/src/main/java/org/apache/phoenix/compile/QueryPlan.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.phoenix.iterate.ParallelScanGrouper;
2929
import org.apache.phoenix.iterate.ResultIterator;
3030
import org.apache.phoenix.optimize.Cost;
31+
import org.apache.phoenix.optimize.OptimizerDecision;
3132
import org.apache.phoenix.parse.FilterableStatement;
3233
import org.apache.phoenix.parse.SelectStatement;
3334
import org.apache.phoenix.query.KeyRange;
@@ -103,4 +104,17 @@ public interface QueryPlan extends StatementPlan {
103104
* </pre>
104105
*/
105106
public List<OrderBy> getOutputOrderBys();
107+
108+
/**
109+
* The optimizer's index selection rationale for this plan, or {@code null} if this plan did not
110+
* participate in optimizer index selection.
111+
*/
112+
default OptimizerDecision getOptimizerDecision() {
113+
return null;
114+
}
115+
116+
/** Records the optimizer's index selection rationale on this plan. */
117+
default void setOptimizerDecision(OptimizerDecision decision) {
118+
// no-op
119+
}
106120
}

phoenix-core-client/src/main/java/org/apache/phoenix/execute/BaseQueryPlan.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.apache.phoenix.iterate.ResultIterator;
5858
import org.apache.phoenix.jdbc.PhoenixConnection;
5959
import org.apache.phoenix.jdbc.PhoenixStatement.Operation;
60+
import org.apache.phoenix.optimize.OptimizerDecision;
6061
import org.apache.phoenix.parse.FilterableStatement;
6162
import org.apache.phoenix.parse.HintNode.Hint;
6263
import org.apache.phoenix.parse.ParseNodeFactory;
@@ -110,6 +111,7 @@ public abstract class BaseQueryPlan implements QueryPlan {
110111
protected Long estimateInfoTimestamp;
111112
private boolean getEstimatesCalled;
112113
protected boolean isApplicable = true;
114+
private OptimizerDecision optimizerDecision;
113115

114116
protected BaseQueryPlan(StatementContext context, FilterableStatement statement, TableRef table,
115117
RowProjector projection, ParameterMetaData paramMetaData, Integer limit, Integer offset,
@@ -543,6 +545,11 @@ private Pair<List<String>, ExplainPlanAttributes> getPlanStepsV2(ResultIterator
543545
List<String> planSteps = Lists.newArrayListWithExpectedSize(5);
544546
ExplainPlanAttributesBuilder builder = new ExplainPlanAttributesBuilder();
545547
iterator.explain(planSteps, builder);
548+
OptimizerDecision decision = getOptimizerDecision();
549+
if (decision != null) {
550+
builder.setIndexRule(decision.getRule());
551+
builder.setIndexRejected(decision.getRejectedIndexes());
552+
}
546553
return Pair.of(planSteps, builder.build());
547554
}
548555

@@ -585,6 +592,16 @@ public void setApplicable(boolean isApplicable) {
585592
this.isApplicable = isApplicable;
586593
}
587594

595+
@Override
596+
public OptimizerDecision getOptimizerDecision() {
597+
return optimizerDecision;
598+
}
599+
600+
@Override
601+
public void setOptimizerDecision(OptimizerDecision decision) {
602+
this.optimizerDecision = decision;
603+
}
604+
588605
private void getEstimates() throws SQLException {
589606
getEstimatesCalled = true;
590607
// Initialize a dummy iterator to get the estimates based on stats.

phoenix-core-client/src/main/java/org/apache/phoenix/execute/DelegateQueryPlan.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.phoenix.iterate.ResultIterator;
3333
import org.apache.phoenix.jdbc.PhoenixStatement.Operation;
3434
import org.apache.phoenix.optimize.Cost;
35+
import org.apache.phoenix.optimize.OptimizerDecision;
3536
import org.apache.phoenix.parse.FilterableStatement;
3637
import org.apache.phoenix.query.KeyRange;
3738
import org.apache.phoenix.schema.TableRef;
@@ -171,4 +172,14 @@ public List<OrderBy> getOutputOrderBys() {
171172
public boolean isApplicable() {
172173
return delegate.isApplicable();
173174
}
175+
176+
@Override
177+
public OptimizerDecision getOptimizerDecision() {
178+
return delegate.getOptimizerDecision();
179+
}
180+
181+
@Override
182+
public void setOptimizerDecision(OptimizerDecision decision) {
183+
delegate.setOptimizerDecision(decision);
184+
}
174185
}

phoenix-core-client/src/main/java/org/apache/phoenix/iterate/BaseResultIterators.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
import org.apache.phoenix.hbase.index.util.VersionUtil;
8787
import org.apache.phoenix.join.HashCacheClient;
8888
import org.apache.phoenix.monitoring.OverAllQueryMetrics;
89+
import org.apache.phoenix.optimize.OptimizerDecision;
8990
import org.apache.phoenix.parse.FilterableStatement;
9091
import org.apache.phoenix.parse.HintNode;
9192
import org.apache.phoenix.parse.HintNode.Hint;
@@ -645,6 +646,11 @@ protected int getSplitCount() {
645646
return splits == null ? 0 : splits.size();
646647
}
647648

649+
@Override
650+
protected OptimizerDecision getOptimizerDecision() {
651+
return plan.getOptimizerDecision();
652+
}
653+
648654
@Override
649655
public List<List<Scan>> getScans() {
650656
if (scans == null) return Collections.emptyList();

phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
import org.apache.phoenix.filter.BooleanExpressionFilter;
4444
import org.apache.phoenix.filter.DistinctPrefixFilter;
4545
import org.apache.phoenix.filter.EmptyColumnOnlyFilter;
46+
import org.apache.phoenix.optimize.OptimizerDecision;
47+
import org.apache.phoenix.optimize.OptimizerReasons;
48+
import org.apache.phoenix.optimize.RejectedIndexEntry;
4649
import org.apache.phoenix.parse.HintNode;
4750
import org.apache.phoenix.parse.HintNode.Hint;
4851
import org.apache.phoenix.query.KeyRange;
@@ -131,6 +134,27 @@ protected int getSplitCount() {
131134
return 0;
132135
}
133136

137+
/**
138+
* The optimizer's index selection rationale for the plan this scan belongs to, used to render the
139+
* per-scan {@code INDEX} rule comment and the {@code !INDEX} rejection comments. Returns
140+
* {@code null} when the plan did not participate in optimizer index selection (DML, DDL, and
141+
* non-optimizer plans).
142+
* @return the decision, or {@code null} when unavailable
143+
*/
144+
protected OptimizerDecision getOptimizerDecision() {
145+
return null;
146+
}
147+
148+
/**
149+
* Whether {@code rule} is a default rule whose {@code INDEX} comment is suppressed. The default
150+
* rules are {@link OptimizerReasons#RULE_DATA_TABLE} (no candidate indexes considered) and
151+
* {@link OptimizerReasons#RULE_ONLY_CANDIDATE} (a single viable candidate).
152+
*/
153+
private static boolean isDefaultRule(String rule) {
154+
return OptimizerReasons.RULE_DATA_TABLE.equals(rule)
155+
|| OptimizerReasons.RULE_ONLY_CANDIDATE.equals(rule);
156+
}
157+
134158
/**
135159
* Logical name used to render a table or index in EXPLAIN output. Shared by both the scan
136160
* {@code OVER} line's local index decoration and the per scan {@code INDEX} line.
@@ -216,7 +240,21 @@ protected void explain(String prefix, List<String> planSteps,
216240
indexKind = null;
217241
}
218242
}
219-
planSteps.add(" INDEX " + explainIndexName + (indexKind == null ? "" : " " + indexKind));
243+
OptimizerDecision decision = getOptimizerDecision();
244+
StringBuilder indexLine = new StringBuilder(" INDEX ").append(explainIndexName);
245+
if (indexKind != null) {
246+
indexLine.append(" ").append(indexKind);
247+
}
248+
if (decision != null && !isDefaultRule(decision.getRule())) {
249+
indexLine.append(" /* ").append(decision.getRule()).append(" */");
250+
}
251+
planSteps.add(indexLine.toString());
252+
if (decision != null) {
253+
for (RejectedIndexEntry rejected : decision.getRejectedIndexes()) {
254+
planSteps
255+
.add(" /* !INDEX " + rejected.getName() + " -- " + rejected.getReason() + " */");
256+
}
257+
}
220258
Integer bucketNum = tableRef.getTable().getBucketNum();
221259
if (bucketNum != null) {
222260
planSteps.add(" SALT BUCKETS " + bucketNum);

0 commit comments

Comments
 (0)