This is an automated email from the ASF dual-hosted git repository.
apurtell pushed a commit to branch PHOENIX-7876-feature
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/PHOENIX-7876-feature by this
push:
new 6aa792b5b5 PHOENIX-7967 Contain explain diagnostic recording (#2582)
6aa792b5b5 is described below
commit 6aa792b5b57a7f677876fa7fc4e26046b9a51478
Author: Andrew Purtell <[email protected]>
AuthorDate: Mon Jul 27 14:29:31 2026 -0700
PHOENIX-7967 Contain explain diagnostic recording (#2582)
---
.../org/apache/phoenix/compile/HavingCompiler.java | 12 +-
.../org/apache/phoenix/compile/JoinCompiler.java | 12 +-
.../apache/phoenix/compile/RVCOffsetCompiler.java | 4 +-
.../apache/phoenix/compile/StatementContext.java | 153 ++++++++++++++++-----
.../org/apache/phoenix/compile/WhereCompiler.java | 5 +-
.../org/apache/phoenix/jdbc/PhoenixStatement.java | 68 ++++++---
.../apache/phoenix/optimize/QueryOptimizer.java | 100 +++++++++-----
.../java/org/apache/phoenix/schema/PTableImpl.java | 4 +-
.../java/org/apache/phoenix/end2end/InListIT.java | 51 +++----
.../apache/phoenix/compile/QueryOptimizerTest.java | 2 +
.../phoenix/query/explain/ExplainPlanTest.java | 74 +++++++++-
.../phoenix/query/explain/ExplainPlanTestUtil.java | 114 ++++++++++++---
12 files changed, 443 insertions(+), 156 deletions(-)
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
index 01b3905820..164817311b 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
@@ -65,12 +65,14 @@ public class HavingCompiler {
.buildException();
}
// Tag the residual HAVING predicate(s) with their origin for VERBOSE
attribution.
- if (expression instanceof org.apache.phoenix.expression.AndExpression) {
- for (Expression child : expression.getChildren()) {
- context.tagPredicate(child, "HAVING");
+ if (context.isCollectDiagnostics()) {
+ if (expression instanceof org.apache.phoenix.expression.AndExpression) {
+ for (Expression child : expression.getChildren()) {
+ context.tagPredicate(child, "HAVING");
+ }
+ } else {
+ context.tagPredicate(expression, "HAVING");
}
- } else {
- context.tagPredicate(expression, "HAVING");
}
return expression;
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
index 5beab671c8..9c6f71f0fd 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
@@ -676,11 +676,13 @@ public class JoinCompiler {
right = CoerceExpression.create(right, toType, toSortOrder,
right.getMaxLength());
}
// Tag the compiled ON predicates with their origin for VERBOSE
attribution.
- String decorrelatedAlias =
lhsCtx.getDecorrelatedSubqueryAlias(condition);
- String onOrigin =
- decorrelatedAlias == null ? "JOIN ON" : "decorrelated from " +
decorrelatedAlias;
- lhsCtx.tagPredicate(left, onOrigin);
- rhsCtx.tagPredicate(right, onOrigin);
+ if (lhsCtx.isCollectDiagnostics()) {
+ String decorrelatedAlias =
lhsCtx.getDecorrelatedSubqueryAlias(condition);
+ String onOrigin =
+ decorrelatedAlias == null ? "JOIN ON" : "decorrelated from " +
decorrelatedAlias;
+ lhsCtx.tagPredicate(left, onOrigin);
+ rhsCtx.tagPredicate(right, onOrigin);
+ }
compiled.add(new Pair<Expression, Expression>(left, right));
}
// TODO PHOENIX-4618:
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
index abb3270017..4771dceb9f 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
@@ -181,7 +181,9 @@ public class RVCOffsetCompiler {
}
// Tag the RVC offset predicate with its origin for VERBOSE attribution.
- context.tagPredicate(whereExpression, "RVC OFFSET");
+ if (context.isCollectDiagnostics()) {
+ context.tagPredicate(whereExpression, "RVC OFFSET");
+ }
Expression expression;
try {
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
index 6767035b3c..ba19803a51 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
@@ -119,6 +119,9 @@ public class StatementContext {
private Map<String, List<Expression>> serverParsedProjections;
private StatementContext parentContext;
private ExplainOptions explainOptions = ExplainOptions.DEFAULT;
+ // True when compile-time diagnostic recording is enabled for this context
+ private final boolean collectDiagnostics;
+ // Diagnostic-only maps, lazily allocated
private Map<Expression, Set<String>> predicateOrigins;
private Map<ParseNode, String> decorrelatedSubqueryAlias;
private Map<Hint, String> ignoredHints;
@@ -177,6 +180,7 @@ public class StatementContext {
this.hasRawRowSizeFunction = context.hasRawRowSizeFunction;
this.parentContext = context.parentContext;
this.explainOptions = context.explainOptions;
+ this.collectDiagnostics = context.collectDiagnostics;
copyRewriteStateFrom(context);
}
@@ -242,15 +246,18 @@ public class StatementContext {
this.subStatementContexts = Sets.newLinkedHashSet();
this.appliedRewrites = new ArrayList<>();
this.derivedTableFlattenCount = 0;
- this.appliedIndexExpressionMatches = Maps.newLinkedHashMap();
- this.appliedIndexExpressionPairs = Maps.newLinkedHashMap();
- this.functionalIndexNames = Sets.newLinkedHashSet();
this.partialIndexCheckedSet = Sets.newLinkedHashSet();
this.serverParsedProjections = null;
this.parentContext = null;
- this.predicateOrigins = new IdentityHashMap<>();
- this.decorrelatedSubqueryAlias = new IdentityHashMap<>();
- this.ignoredHints = new EnumMap<>(Hint.class);
+ this.collectDiagnostics = statement.isCollectDiagnostics();
+ // Diagnostic-only collections are lazily allocated on first record under
+ // collectDiagnostics. Normal queries skip these allocations entirely.
+ this.predicateOrigins = null;
+ this.decorrelatedSubqueryAlias = null;
+ this.ignoredHints = null;
+ this.appliedIndexExpressionMatches = null;
+ this.appliedIndexExpressionPairs = null;
+ this.functionalIndexNames = null;
}
/**
@@ -521,9 +528,13 @@ public class StatementContext {
/**
* Records a top-of-plan rewrite breadcrumb (e.g. "STAR JOIN ON 2 RIGHT
LEGS"). Breadcrumbs are
- * diagnostic only and never affect the compiled plan.
+ * diagnostic only and never affect the compiled plan, so this is a no-op
when diagnostic
+ * recording is disabled on this context. Normal queries pay no cost for
breadcrumb bookkeeping.
*/
public void addAppliedRewrite(String rewrite) {
+ if (!collectDiagnostics) {
+ return;
+ }
appliedRewrites.add(rewrite);
}
@@ -553,26 +564,56 @@ public class StatementContext {
this.appliedRewrites =
source.appliedRewrites == null ? new ArrayList<>() : new
ArrayList<>(source.appliedRewrites);
this.derivedTableFlattenCount = source.derivedTableFlattenCount;
- this.appliedIndexExpressionMatches = Maps.newLinkedHashMap();
- for (Map.Entry<String, List<String>> entry :
source.appliedIndexExpressionMatches.entrySet()) {
- this.appliedIndexExpressionMatches.put(entry.getKey(), new
ArrayList<>(entry.getValue()));
- }
- this.appliedIndexExpressionPairs = Maps.newLinkedHashMap();
- for (Map.Entry<String, Map<String, String>> entry :
source.appliedIndexExpressionPairs
- .entrySet()) {
- this.appliedIndexExpressionPairs.put(entry.getKey(), new
LinkedHashMap<>(entry.getValue()));
- }
- this.functionalIndexNames =
Sets.newLinkedHashSet(source.functionalIndexNames);
this.partialIndexCheckedSet =
Sets.newLinkedHashSet(source.partialIndexCheckedSet);
// serverParsedProjections is nullable and only ever replaced wholesale
via its setter (never
// mutated in place), so the reference can be carried over directly.
this.serverParsedProjections = source.serverParsedProjections;
- this.predicateOrigins = new IdentityHashMap<>();
- for (Map.Entry<Expression, Set<String>> entry :
source.predicateOrigins.entrySet()) {
- this.predicateOrigins.put(entry.getKey(), new
LinkedHashSet<>(entry.getValue()));
+ // Only copy if the source actually recorded anything. Keep null otherwise.
+ if (source.predicateOrigins == null || source.predicateOrigins.isEmpty()) {
+ this.predicateOrigins = null;
+ } else {
+ this.predicateOrigins = new IdentityHashMap<>();
+ for (Map.Entry<Expression, Set<String>> entry :
source.predicateOrigins.entrySet()) {
+ this.predicateOrigins.put(entry.getKey(), new
LinkedHashSet<>(entry.getValue()));
+ }
+ }
+ if (source.decorrelatedSubqueryAlias == null ||
source.decorrelatedSubqueryAlias.isEmpty()) {
+ this.decorrelatedSubqueryAlias = null;
+ } else {
+ this.decorrelatedSubqueryAlias = new
IdentityHashMap<>(source.decorrelatedSubqueryAlias);
+ }
+ if (source.ignoredHints == null || source.ignoredHints.isEmpty()) {
+ this.ignoredHints = null;
+ } else {
+ this.ignoredHints = new EnumMap<>(source.ignoredHints);
+ }
+ if (
+ source.appliedIndexExpressionMatches == null ||
source.appliedIndexExpressionMatches.isEmpty()
+ ) {
+ this.appliedIndexExpressionMatches = null;
+ } else {
+ this.appliedIndexExpressionMatches = Maps.newLinkedHashMap();
+ for (Map.Entry<String, List<String>> entry :
source.appliedIndexExpressionMatches
+ .entrySet()) {
+ this.appliedIndexExpressionMatches.put(entry.getKey(), new
ArrayList<>(entry.getValue()));
+ }
+ }
+ if (
+ source.appliedIndexExpressionPairs == null ||
source.appliedIndexExpressionPairs.isEmpty()
+ ) {
+ this.appliedIndexExpressionPairs = null;
+ } else {
+ this.appliedIndexExpressionPairs = Maps.newLinkedHashMap();
+ for (Map.Entry<String, Map<String, String>> entry :
source.appliedIndexExpressionPairs
+ .entrySet()) {
+ this.appliedIndexExpressionPairs.put(entry.getKey(), new
LinkedHashMap<>(entry.getValue()));
+ }
+ }
+ if (source.functionalIndexNames == null ||
source.functionalIndexNames.isEmpty()) {
+ this.functionalIndexNames = null;
+ } else {
+ this.functionalIndexNames =
Sets.newLinkedHashSet(source.functionalIndexNames);
}
- this.decorrelatedSubqueryAlias = new
IdentityHashMap<>(source.decorrelatedSubqueryAlias);
- this.ignoredHints = new EnumMap<>(source.ignoredHints);
}
public void incrementDerivedTableFlattenCount() {
@@ -587,13 +628,17 @@ public class StatementContext {
* Records the path expressions that actually substituted against this query
for the given
* functional index. Used by the optimizer to label the chosen index's rule
as
* {@code matches <expr>} and to distinguish functional index rejections
that matched no query
- * expression. Deduplicated per index, preserving first seen order.
+ * expression. Deduplicated per index, preserving first seen order. No-op
when the context has
+ * diagnostic recording disabled.
*/
public void recordAppliedIndexExpressionMatches(String indexName,
Collection<String> expressions) {
- if (expressions == null || expressions.isEmpty()) {
+ if (!collectDiagnostics || expressions == null || expressions.isEmpty()) {
return;
}
+ if (appliedIndexExpressionMatches == null) {
+ appliedIndexExpressionMatches = Maps.newLinkedHashMap();
+ }
List<String> matches =
appliedIndexExpressionMatches.computeIfAbsent(indexName, k -> new
ArrayList<>());
for (String expression : expressions) {
@@ -608,6 +653,9 @@ public class StatementContext {
* functional index, or an empty list if none were recorded.
*/
public List<String> getAppliedIndexExpressionMatches(String indexName) {
+ if (appliedIndexExpressionMatches == null) {
+ return Collections.emptyList();
+ }
List<String> matches = appliedIndexExpressionMatches.get(indexName);
return matches == null ? Collections.emptyList() : matches;
}
@@ -617,12 +665,15 @@ public class StatementContext {
* actually fired against this query for the given functional index. Carries
enough information
* for the optimizer to emit one {@code REWRITE INDEX EXPRESSION <expr> AS
<col>} breadcrumb per
* applied substitution after the chosen plan is selected. Deduplicated per
index, preserving
- * first-seen insertion order.
+ * first-seen insertion order. No-op when the context has diagnostic
recording disabled.
*/
public void recordAppliedIndexExpressionPairs(String indexName, Map<String,
String> pairs) {
- if (pairs == null || pairs.isEmpty()) {
+ if (!collectDiagnostics || pairs == null || pairs.isEmpty()) {
return;
}
+ if (appliedIndexExpressionPairs == null) {
+ appliedIndexExpressionPairs = Maps.newLinkedHashMap();
+ }
Map<String, String> existing =
appliedIndexExpressionPairs.computeIfAbsent(indexName, k -> new
LinkedHashMap<>());
for (Map.Entry<String, String> entry : pairs.entrySet()) {
@@ -635,18 +686,30 @@ public class StatementContext {
* substituted against this query for the given functional index, or an
empty map if none.
*/
public Map<String, String> getAppliedIndexExpressionPairs(String indexName) {
+ if (appliedIndexExpressionPairs == null) {
+ return Collections.emptyMap();
+ }
Map<String, String> pairs = appliedIndexExpressionPairs.get(indexName);
return pairs == null ? Collections.emptyMap() : pairs;
}
- /** Marks the given index as a functional index. */
+ /**
+ * Marks the given index as a functional index. No-op when the context has
diagnostic recording
+ * disabled.
+ */
public void recordFunctionalIndex(String indexName) {
+ if (!collectDiagnostics) {
+ return;
+ }
+ if (functionalIndexNames == null) {
+ functionalIndexNames = Sets.newLinkedHashSet();
+ }
functionalIndexNames.add(indexName);
}
/** Returns whether the given index was recorded as a functional index
during rewriting. */
public boolean isFunctionalIndex(String indexName) {
- return functionalIndexNames.contains(indexName);
+ return functionalIndexNames != null &&
functionalIndexNames.contains(indexName);
}
/**
@@ -695,6 +758,11 @@ public class StatementContext {
return explainOptions != null && explainOptions.isVerbose();
}
+ /** True when compile-time diagnostic recording is enabled for this context.
*/
+ public boolean isCollectDiagnostics() {
+ return collectDiagnostics;
+ }
+
/**
* Tag a predicate {@link Expression} with the given origin (e.g. {@code
"WHERE"},
* {@code "JOIN ON"}). Tags accumulate as a set so a predicate fused from
multiple sources carries
@@ -702,19 +770,25 @@ public class StatementContext {
* expressions during compilation. Diagnostic only; never affects the
compiled plan.
*/
public void tagPredicate(Expression expression, String origin) {
- if (expression == null || origin == null) {
+ if (!collectDiagnostics || expression == null || origin == null) {
return;
}
+ if (predicateOrigins == null) {
+ predicateOrigins = new IdentityHashMap<>();
+ }
predicateOrigins.computeIfAbsent(expression, k -> new
LinkedHashSet<>()).add(origin);
}
/** Returns the predicate origin tags accumulated during compilation.
Identity keyed. */
public Map<Expression, Set<String>> getPredicateOrigins() {
- return predicateOrigins;
+ return predicateOrigins == null ? Collections.emptyMap() :
predicateOrigins;
}
/** Returns the origin tags recorded for {@code expression}, or an empty set
if none. */
public Set<String> getPredicateOrigins(Expression expression) {
+ if (predicateOrigins == null) {
+ return Collections.emptySet();
+ }
Set<String> tags = predicateOrigins.get(expression);
return tags == null ? Collections.emptySet() : tags;
}
@@ -724,9 +798,13 @@ public class StatementContext {
* the given temp alias.
*/
public void putDecorrelatedSubqueryAlias(ParseNode joinConditionNode, String
subqueryAlias) {
- if (joinConditionNode != null && subqueryAlias != null) {
- decorrelatedSubqueryAlias.put(joinConditionNode, subqueryAlias);
+ if (!collectDiagnostics || joinConditionNode == null || subqueryAlias ==
null) {
+ return;
}
+ if (decorrelatedSubqueryAlias == null) {
+ decorrelatedSubqueryAlias = new IdentityHashMap<>();
+ }
+ decorrelatedSubqueryAlias.put(joinConditionNode, subqueryAlias);
}
/**
@@ -734,7 +812,9 @@ public class StatementContext {
* {@code null} if the node is not a decorrelated predicate.
*/
public String getDecorrelatedSubqueryAlias(ParseNode joinConditionNode) {
- return decorrelatedSubqueryAlias.get(joinConditionNode);
+ return decorrelatedSubqueryAlias == null
+ ? null
+ : decorrelatedSubqueryAlias.get(joinConditionNode);
}
/**
@@ -743,15 +823,18 @@ public class StatementContext {
* comment. The first reason recorded for a hint wins. Diagnostic only.
*/
public void recordIgnoredHint(Hint hint, String reason) {
- if (hint == null || reason == null) {
+ if (!collectDiagnostics || hint == null || reason == null) {
return;
}
+ if (ignoredHints == null) {
+ ignoredHints = new EnumMap<>(Hint.class);
+ }
ignoredHints.putIfAbsent(hint, reason);
}
/** Returns the hints the planner intentionally ignored, mapped to the
reason. */
public Map<Hint, String> getIgnoredHints() {
- return ignoredHints;
+ return ignoredHints == null ? Collections.emptyMap() : ignoredHints;
}
/** Returns true if this is the top-level (root) statement context, i.e. it
has no parent. */
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/WhereCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/WhereCompiler.java
index 4d4ac7717f..be788ce8b8 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/WhereCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/WhereCompiler.java
@@ -265,10 +265,11 @@ public class WhereCompiler {
/**
* Tag the top-level conjuncts of the residual WHERE expression with their
origin for VERBOSE
- * predicate source attribution.
+ * predicate source attribution. No-op when the context has diagnostic
recording disabled so
+ * normal queries skip the child iteration and per-conjunct tag calls
entirely.
*/
private static void tagWhereOrigins(StatementContext context, Expression
expression) {
- if (expression == null) {
+ if (expression == null || !context.isCollectDiagnostics()) {
return;
}
if (expression instanceof AndExpression) {
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
index ba808630fd..f89bdb2bf1 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
@@ -307,6 +307,10 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
protected final Calendar localCalendar = Calendar.getInstance();
private boolean validateLastDdlTimestamp;
private long sqlQueryParsingTime = 0;
+ // Flipped on by ExecutableExplainStatement.compilePlan (and read by
StatementContext) so that
+ // compile-time diagnostic recording only runs when we are actually
producing an EXPLAIN.
+ // Normal queries pay no cost for these recording sites.
+ private boolean collectDiagnostics = false;
public PhoenixStatement(PhoenixConnection connection) {
this.connection = connection;
@@ -335,6 +339,18 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
return this.sqlQueryParsingTime;
}
+ /**
+ * True when compile time diagnostic recording is enabled for this
statement. Off by default.
+ * Normal queries pay no recording cost.
+ */
+ public boolean isCollectDiagnostics() {
+ return collectDiagnostics;
+ }
+
+ public void setCollectDiagnostics(boolean value) {
+ this.collectDiagnostics = value;
+ }
+
public PhoenixResultSet newResultSet(ResultIterator iterator, RowProjector
projector,
StatementContext context) throws SQLException {
return new PhoenixResultSet(iterator, projector, context);
@@ -974,28 +990,38 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
@Override
public QueryPlan compilePlan(PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
- CompilableStatement compilableStmt = getStatement();
- StatementPlan compilePlan =
- compilableStmt.compilePlan(stmt, Sequence.ValueOp.VALIDATE_SEQUENCE);
- // if client is validating timestamps, ensure its metadata cache is up
to date.
- if
(ValidateLastDDLTimestampUtil.getValidateLastDdlTimestampEnabled(stmt.getConnection()))
{
- Set<TableRef> tableRefs = compilePlan.getSourceRefs();
- for (TableRef tableRef : tableRefs) {
- new
MetaDataClient(stmt.getConnection()).updateCache(stmt.getConnection().getTenantId(),
- tableRef.getTable().getSchemaName().getString(),
- tableRef.getTable().getTableName().getString(), true);
- }
- compilePlan = compilableStmt.compilePlan(stmt,
Sequence.ValueOp.VALIDATE_SEQUENCE);
- }
- // For a QueryPlan, we need to get its optimized plan; for a
MutationPlan, its enclosed
- // QueryPlan
- // has already been optimized during compilation.
- if (compilePlan instanceof QueryPlan) {
- QueryPlan dataPlan = (QueryPlan) compilePlan;
- compilePlan =
-
stmt.getConnection().getQueryServices().getOptimizer().optimize(stmt, dataPlan);
+ // Enable diagnostic recording only around the inner compile/optimize so
that
+ // EXPLAIN sees full origin tags, optimizer decisions, and
rejected-index reasons.
+ // try/finally scopes the flag to this compile so nested compiles (e.g.
a partial-index
+ // predicate check) do not leak the flag beyond the EXPLAIN.
+ final boolean prevCollectDiagnostics = stmt.isCollectDiagnostics();
+ stmt.setCollectDiagnostics(true);
+ final StatementPlan plan;
+ try {
+ CompilableStatement compilableStmt = getStatement();
+ StatementPlan compilePlan =
+ compilableStmt.compilePlan(stmt, Sequence.ValueOp.VALIDATE_SEQUENCE);
+ // if client is validating timestamps, ensure its metadata cache is up
to date.
+ if
(ValidateLastDDLTimestampUtil.getValidateLastDdlTimestampEnabled(stmt.getConnection()))
{
+ Set<TableRef> tableRefs = compilePlan.getSourceRefs();
+ for (TableRef tableRef : tableRefs) {
+ new
MetaDataClient(stmt.getConnection()).updateCache(stmt.getConnection().getTenantId(),
+ tableRef.getTable().getSchemaName().getString(),
+ tableRef.getTable().getTableName().getString(), true);
+ }
+ compilePlan = compilableStmt.compilePlan(stmt,
Sequence.ValueOp.VALIDATE_SEQUENCE);
+ }
+ // For a QueryPlan we need to get its optimized plan. For a
MutationPlan its enclosed
+ // QueryPlan has already been optimized during compilation.
+ if (compilePlan instanceof QueryPlan) {
+ QueryPlan dataPlan = (QueryPlan) compilePlan;
+ compilePlan =
+
stmt.getConnection().getQueryServices().getOptimizer().optimize(stmt, dataPlan);
+ }
+ plan = compilePlan;
+ } finally {
+ stmt.setCollectDiagnostics(prevCollectDiagnostics);
}
- final StatementPlan plan = compilePlan;
// Propagate the parsed EXPLAIN options onto the resolved plan's context
so the renderer has
// access to the requested options.
if (plan.getContext() != null) {
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
index 04d25dfd13..0300227a7d 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
@@ -157,7 +157,8 @@ public class QueryOptimizer {
&& (select.getWhere() == null || !select.getWhere().hasSubquery())
) {
return getApplicablePlansForSingleFlatQuery(dataPlan, statement,
targetColumns,
- parallelIteratorFactory, stopAtBestPlan, new DecisionState());
+ parallelIteratorFactory, stopAtBestPlan,
+ new DecisionState(dataPlan.getContext().isCollectDiagnostics()));
}
Map<TableRef, QueryPlan> dataPlans = null;
@@ -223,17 +224,19 @@ public class QueryOptimizer {
WhereCompiler.contains(index.getIndexWhereExpression(dataPlan.getContext().getConnection()),
WhereCompiler.transformDNF(select.getWhere(), context));
StatementContext rootContext = dataPlan.getContext();
- String tableName = dataPlan.getTableRef().getTable().getName().getString();
- String indexName = index.getName().getString();
- // Dedupe so the breadcrumb is recorded once per table and index pair even
when the optimizer
- // scores the same index across multiple candidate paths.
- if (rootContext.markPartialIndexChecked(tableName, indexName)) {
- // Name the index in the breadcrumb so multiple partial indexes stay
distinct after the
- // rewrite list is deduped by string. Otherwise they collapse into a
single ambiguous line.
- rootContext.addAppliedRewrite(usable
- ? "PARTIAL INDEX " + indexName + " APPLICABLE"
- : "PARTIAL INDEX " + indexName
- + " NOT APPLICABLE -- index WHERE not implied by query WHERE");
+ if (rootContext.isCollectDiagnostics()) {
+ String tableName =
dataPlan.getTableRef().getTable().getName().getString();
+ String indexName = index.getName().getString();
+ // Dedupe so the breadcrumb is recorded once per table and index pair
even when the optimizer
+ // scores the same index across multiple candidate paths.
+ if (rootContext.markPartialIndexChecked(tableName, indexName)) {
+ // Name the index in the breadcrumb so multiple partial indexes stay
distinct after the
+ // rewrite list is deduped by string. Otherwise they collapse into a
single ambiguous line.
+ rootContext.addAppliedRewrite(usable
+ ? "PARTIAL INDEX " + indexName + " APPLICABLE"
+ : "PARTIAL INDEX " + indexName
+ + " NOT APPLICABLE -- index WHERE not implied by query WHERE");
+ }
}
return usable;
}
@@ -404,8 +407,13 @@ public class QueryOptimizer {
* Carry the rewrite breadcrumbs recorded on the data plan's context onto
the candidate plans'
* contexts. The partial index applicability decisions are recorded once per
index on the shared
* root context while scoring plans, so that all of them stay visible even
though only one wins.
+ * No-op when diagnostic recording is disabled on the source context, so
normal queries pay no
+ * cost.
*/
private static void carryForwardRewrites(StatementContext from,
List<QueryPlan> plans) {
+ if (!from.isCollectDiagnostics()) {
+ return;
+ }
List<String> rewrites = from.getAppliedRewrites();
Map<Hint, String> ignoredHints = from.getIgnoredHints();
if (rewrites.isEmpty() && (ignoredHints == null ||
ignoredHints.isEmpty())) {
@@ -557,14 +565,16 @@ public class QueryOptimizer {
ParseNodeRewriter.rewrite(indexSelect, indexExpressionRewriter);
// Record which functional index expressions actually matched this
query, so the
// optimizer can label the chosen index's rule and distinguish
functional index rejections
- // that matched nothing.
- if (indexExpressionRewriter.hasFunctionalColumns()) {
-
dataPlan.getContext().recordFunctionalIndex(index.getTableName().getString());
+ if (dataPlan.getContext().isCollectDiagnostics()) {
+ if (indexExpressionRewriter.hasFunctionalColumns()) {
+
dataPlan.getContext().recordFunctionalIndex(index.getTableName().getString());
+ }
+ dataPlan.getContext().recordAppliedIndexExpressionMatches(
+ index.getTableName().getString(),
+
indexExpressionRewriter.getAppliedFunctionalSubstitutions().values());
+
dataPlan.getContext().recordAppliedIndexExpressionPairs(index.getTableName().getString(),
+ indexExpressionRewriter.getAppliedFunctionalSubstitutions());
}
-
dataPlan.getContext().recordAppliedIndexExpressionMatches(index.getTableName().getString(),
-
indexExpressionRewriter.getAppliedFunctionalSubstitutions().values());
-
dataPlan.getContext().recordAppliedIndexExpressionPairs(index.getTableName().getString(),
- indexExpressionRewriter.getAppliedFunctionalSubstitutions());
QueryCompiler compiler = new QueryCompiler(statement,
rewrittenIndexSelect, resolver,
targetColumns, parallelIteratorFactory,
dataPlan.getContext().getSequenceManager(),
isProjected, true,
dataPlans).withRewriteContext(dataPlan.getContext());
@@ -924,15 +934,28 @@ public class QueryOptimizer {
/**
* Accumulates the rejected index entries gathered while selecting an index
for a single flat
- * query. Rejections are deduplicated by index name.
+ * query. Rejections are deduplicated by index name. When diagnostic
recording is disabled every
+ * {@code reject(...)} call is a no-op and the backing list is never
allocated.
*/
private static final class DecisionState {
- private final List<RejectedIndexEntry> rejections = new ArrayList<>();
+ private final boolean collectDiagnostics;
+ private List<RejectedIndexEntry> rejections;
+
+ DecisionState(boolean collectDiagnostics) {
+ this.collectDiagnostics = collectDiagnostics;
+ }
void reject(String name, String reason) {
- for (RejectedIndexEntry existing : rejections) {
- if (existing.getName().equals(name)) {
- return;
+ if (!collectDiagnostics) {
+ return;
+ }
+ if (rejections == null) {
+ rejections = new ArrayList<>();
+ } else {
+ for (RejectedIndexEntry existing : rejections) {
+ if (existing.getName().equals(name)) {
+ return;
+ }
}
}
rejections.add(new RejectedIndexEntry(name, reason));
@@ -949,6 +972,9 @@ public class QueryOptimizer {
}
void rejectAll(List<PTable> indexes, String reason) {
+ if (!collectDiagnostics) {
+ return;
+ }
for (PTable index : indexes) {
reject(index, reason);
}
@@ -961,9 +987,13 @@ public class QueryOptimizer {
/**
* Records the optimizer's index selection rationale on the chosen plan and
returns it so callers
- * can use this inline at a {@code return} site.
+ * can use this inline at a {@code return} site. No-op (returns the winner
unchanged) when the
+ * winner's context has diagnostic recording disabled.
*/
private static QueryPlan recordDecision(QueryPlan winner, String rule,
DecisionState state) {
+ if (!winner.getContext().isCollectDiagnostics()) {
+ return winner;
+ }
// The rule names the selection reason (e.g. hint, more bound PK columns).
When the winner is a
// functional index that matched a query expression, the "matches <expr>"
disclosure is
// recorded separately so both the selection reason and the functional
match are surfaced.
@@ -1163,16 +1193,18 @@ public class QueryOptimizer {
indexSelect = ParseNodeRewriter.rewrite(indexSelect,
indexExpressionRewriter);
// Surface which functional index expressions actually matched so the
optimizer can label
// the chosen index's rule and distinguish functional index rejections
that matched nothing.
- if (indexExpressionRewriter.hasFunctionalColumns()) {
- breadcrumbContext
-
.recordFunctionalIndex(indexTableRef.getTable().getTableName().getString());
+ if (breadcrumbContext.isCollectDiagnostics()) {
+ if (indexExpressionRewriter.hasFunctionalColumns()) {
+ breadcrumbContext
+
.recordFunctionalIndex(indexTableRef.getTable().getTableName().getString());
+ }
+ breadcrumbContext.recordAppliedIndexExpressionMatches(
+ indexTableRef.getTable().getTableName().getString(),
+
indexExpressionRewriter.getAppliedFunctionalSubstitutions().values());
+ breadcrumbContext.recordAppliedIndexExpressionPairs(
+ indexTableRef.getTable().getTableName().getString(),
+ indexExpressionRewriter.getAppliedFunctionalSubstitutions());
}
- breadcrumbContext.recordAppliedIndexExpressionMatches(
- indexTableRef.getTable().getTableName().getString(),
- indexExpressionRewriter.getAppliedFunctionalSubstitutions().values());
- breadcrumbContext.recordAppliedIndexExpressionPairs(
- indexTableRef.getTable().getTableName().getString(),
- indexExpressionRewriter.getAppliedFunctionalSubstitutions());
}
return indexSelect;
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/PTableImpl.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/PTableImpl.java
index 89f6fc7ad7..c1c1f13c7d 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/PTableImpl.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/PTableImpl.java
@@ -2538,7 +2538,9 @@ public class PTableImpl implements PTable {
plan.getContext().setResolver(FromCompiler.getResolver(plan.getTableRef()));
indexWhereExpression = transformDNF(where, plan.getContext());
// Tag the partial-index WHERE predicate with its origin for VERBOSE
attribution.
- plan.getContext().tagPredicate(indexWhereExpression, "INDEX WHERE");
+ if (plan.getContext().isCollectDiagnostics()) {
+ plan.getContext().tagPredicate(indexWhereExpression, "INDEX WHERE");
+ }
indexWhereColumns =
Sets.newHashSetWithExpectedSize(plan.getContext().getWhereConditionColumns().size());
for (Pair<byte[], byte[]> column :
plan.getContext().getWhereConditionColumns()) {
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/InListIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/InListIT.java
index 8b3541e73c..9d8aaf0520 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/InListIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/InListIT.java
@@ -20,6 +20,7 @@ package org.apache.phoenix.end2end;
import static java.util.Collections.singletonList;
import static
org.apache.phoenix.query.QueryServices.USE_BLOOMFILTER_FOR_MULTIKEY_POINTLOOKUP;
import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan;
+import static
org.apache.phoenix.query.explain.ExplainPlanTestUtil.getOptimizedQueryPlan;
import static org.apache.phoenix.util.PhoenixRuntime.TENANT_ID_ATTRIB;
import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
import static org.junit.Assert.assertEquals;
@@ -1024,14 +1025,14 @@ public class InListIT extends ParallelStatsDisabledIT {
try (PreparedStatement preparedStmt =
viewConn.prepareStatement("SELECT * FROM "
+ tenantView + " WHERE (ID1, ID2) " + "IN (('005xx000001Sv6o',
'000000000000500'))")) {
- assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+ assertPlan(getOptimizedQueryPlan(preparedStmt))
.scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
}
try (PreparedStatement preparedStmt =
viewConn.prepareStatement("SELECT * FROM "
+ tenantView + " WHERE (ID2, ID1) " + "IN (('000000000000500',
'005xx000001Sv6o'))")) {
- assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+ assertPlan(getOptimizedQueryPlan(preparedStmt))
.scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
}
@@ -1067,7 +1068,7 @@ public class InListIT extends ParallelStatsDisabledIT {
try (PreparedStatement preparedStmt =
viewConn.prepareStatement("SELECT * FROM "
+ tenantView + " WHERE (ID1, ID2) " + "IN (('005xx000001Sv6o',
'000000000000500'),"
+ "('bar', '000000000000400')," + "('foo', '000000000000300'))")) {
- assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+ assertPlan(getOptimizedQueryPlan(preparedStmt))
.scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
}
@@ -1075,7 +1076,7 @@ public class InListIT extends ParallelStatsDisabledIT {
try (PreparedStatement preparedStmt =
viewConn.prepareStatement("SELECT * FROM " + tenantView + " WHERE
(ID2, ID1) IN "
+ "(('bar', '005xx000001Sv6o')," + "('foo', '005xx000001Sv6o'))"))
{
- assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+ assertPlan(getOptimizedQueryPlan(preparedStmt))
.scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
}
@@ -1115,7 +1116,7 @@ public class InListIT extends ParallelStatsDisabledIT {
viewConn.prepareStatement("SELECT * FROM " + tenantView + " WHERE
(ID1, ID2) IN "
+ "(('005xx000001Sv6o', '000000000000500')," +
"('005xx000001Sv6o', '000000000000400'),"
+ "('005xx000001Sv6o', '000000000000300'))")) {
- assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+ assertPlan(getOptimizedQueryPlan(preparedStmt))
.scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
}
@@ -1123,7 +1124,7 @@ public class InListIT extends ParallelStatsDisabledIT {
try (PreparedStatement preparedStmt =
viewConn.prepareStatement("SELECT * FROM "
+ tenantView + " WHERE (ID2, ID1) IN " + "(('000000000000400',
'005xx000001Sv6o'),"
+ "('000000000000300', '005xx000001Sv6o'))")) {
- assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+ assertPlan(getOptimizedQueryPlan(preparedStmt))
.scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
}
@@ -1164,14 +1165,14 @@ public class InListIT extends ParallelStatsDisabledIT {
PreparedStatement preparedStmt =
viewConn.prepareStatement("SELECT * FROM " + tenantView + " WHERE
(ID1, ID2) IN "
+ "(('005xx000001Sv6o', '000000000000500')," + "('005xx000001Sv6o',
'000000000000400'))");
- QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
assertEquals(numberOfRowsToScan, queryPlan.getEstimatedRowsToScan());
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID1,
ID2) IN "
+ "(('005xx000001Sv6o', '000000000000500')," + "('005xx000001Sv6o',
'000000000000400'))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertEquals(numberOfRowsToScan, queryPlan.getEstimatedRowsToScan());
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
@@ -1182,25 +1183,25 @@ public class InListIT extends ParallelStatsDisabledIT {
try (Connection viewConn =
DriverManager.getConnection(TENANT_SPECIFIC_URL1)) {
PreparedStatement preparedStmt = viewConn.prepareStatement("SELECT *
FROM " + tenantView
+ " WHERE (ID1) IN " + "(('005xx000001Sv6o')," +
"('005xx000001Sv6o'))");
- QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID1) IN
"
+ "(('005xx000001Sv6o')," + "('005xx000001Sv6o'))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
preparedStmt = viewConn.prepareStatement("SELECT * FROM " + tenantView +
" WHERE (ID2) IN "
+ "(('000000000000500')," + "('000000000000400'))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID2) IN
"
+ "(('000000000000500')," + "('000000000000400'))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
}
@@ -1210,25 +1211,25 @@ public class InListIT extends ParallelStatsDisabledIT {
try (Connection viewConn =
DriverManager.getConnection(TENANT_SPECIFIC_URL1)) {
PreparedStatement preparedStmt = viewConn.prepareStatement("SELECT *
FROM " + tenantView
+ " WHERE (ID1, ID3) IN " + "(('005xx000001Sv6o', 1)," +
"('005xx000001Sv6o', 2))");
- QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID1,
ID3) IN "
+ "(('005xx000001Sv6o', 1)," + "('005xx000001Sv6o', 2))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
preparedStmt = viewConn.prepareStatement("SELECT * FROM " + tenantView
+ " WHERE (ID2, ID3) IN " + "(('000000000000500', 1)," +
"('000000000000400', 2))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID2,
ID3) IN "
+ "(('000000000000500', 1)," + "('000000000000400', 2))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
}
@@ -1239,13 +1240,13 @@ public class InListIT extends ParallelStatsDisabledIT {
try (Connection viewConn =
DriverManager.getConnection(TENANT_SPECIFIC_URL1)) {
PreparedStatement preparedStmt = viewConn.prepareStatement(
"SELECT * FROM " + tenantView + " WHERE (ID3, ID4) IN " + "((1, 1)," +
"(2, 2))");
- QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
viewConn.prepareStatement(
"DELETE FROM " + tenantView + " WHERE (ID3, ID4) IN " + "((1, 1)," +
"(2, 2))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE
SCAN")
.indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
}
@@ -1747,21 +1748,21 @@ public class InListIT extends ParallelStatsDisabledIT {
// TESTING for optimized scan
PreparedStatement preparedStmt = conn.prepareStatement(
"SELECT * FROM " + view + " WHERE (ID1, ID2) IN " + "((1, 1)," +
"(2, 2))");
- QueryPlan queryPlan =
PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
preparedStmt =
conn.prepareStatement("SELECT * FROM " + view + " WHERE (ID2, ID1)
IN ((1, 1),(2, 2))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
preparedStmt =
conn.prepareStatement("SELECT * FROM " + view + " WHERE (ID2, ID1)
IN ((1, 1),(2, 2))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
@@ -1809,20 +1810,20 @@ public class InListIT extends ParallelStatsDisabledIT {
// TESTING for optimized scan
PreparedStatement preparedStmt = conn.prepareStatement(
"SELECT * FROM " + view + " WHERE (ID4, ID2) IN " + "((1, 1)," +
"(2, 2))");
- QueryPlan queryPlan =
PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
assertPlan(queryPlan).scanType("RANGE
SCAN").indexRule(OptimizerReasons.RULE_ONLY_CANDIDATE)
.indexRejectedCount(1);
preparedStmt = conn.prepareStatement(
"SELECT ID1,ID5 FROM " + view + " WHERE (ID1, ID2) IN " + "((1,
1),(2, 2))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
preparedStmt =
conn.prepareStatement("SELECT * FROM " + view + " WHERE (ID2, ID1)
IN ((1, 1),(2, 2))");
- queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ queryPlan = getOptimizedQueryPlan(preparedStmt);
assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
@@ -1862,7 +1863,7 @@ public class InListIT extends ParallelStatsDisabledIT {
PreparedStatement preparedStmt = conn.prepareStatement(
"SELECT VAL2 FROM " + fullTableName + " WHERE (ID2, ID1) IN ((1,
1),(2, 2))");
- QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+ QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
queryPlan.getTableRef().getTable().getType();
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
.indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryOptimizerTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryOptimizerTest.java
index d0e362ae9a..6b882820fb 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryOptimizerTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryOptimizerTest.java
@@ -969,6 +969,8 @@ public class QueryOptimizerTest extends
BaseConnectionlessQueryTest {
public void testAdoptRewriteStateDoesNotBleedSourceMutations() throws
Exception {
Connection conn = DriverManager.getConnection(getUrl());
PhoenixStatement stmt =
conn.createStatement().unwrap(PhoenixStatement.class);
+ // Diagnostic recording is off by default. We need it for this test so
enable it explicitly.
+ stmt.setCollectDiagnostics(true);
StatementContext source = new StatementContext(stmt);
source.addAppliedRewrite("X");
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTest.java
index fef325834d..62034de010 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTest.java
@@ -1204,16 +1204,19 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
/**
* When a top-level AND is only partially tagged, renderVerboseFilters
collapses to a single
- * combined line. That line must still union the origins recorded on the
tagged conjunct(s) rather
+ * combined line. That line must still union the origins recorded on the
tagged conjuncts rather
* than reading only the parent expression. Normal WHERE/HAVING compilation
always tags every
- * conjunct, so this partial-tag state is reachable only when identity is
lost during expression
- * rewriting.
+ * conjunct, so this state is reachable only when identity is lost during
expression rewriting.
*/
@Test
public void testVerboseCombinedFilterUnionsConjunctOrigins() throws
Exception {
try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
- QueryPlan plan = conn.prepareStatement("SELECT * FROM atable")
- .unwrap(PhoenixPreparedStatement.class).optimizeQuery();
+ PhoenixPreparedStatement pstmt =
+ conn.prepareStatement("SELECT * FROM
atable").unwrap(PhoenixPreparedStatement.class);
+ // Compile time diagnostic recording is normally only enabled for
EXPLAIN, so enable it
+ // explicitly on this statement.
+
pstmt.unwrap(org.apache.phoenix.jdbc.PhoenixStatement.class).setCollectDiagnostics(true);
+ QueryPlan plan = pstmt.optimizeQuery();
StatementContext context = plan.getContext();
Expression tagged = LiteralExpression.newConstant(true);
Expression untagged = LiteralExpression.newConstant(false);
@@ -1955,4 +1958,65 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
return new PColumnImpl(cName, fName, PInteger.INSTANCE, null, null, false,
0, SortOrder.ASC, 0,
null, false, "expression", false, false, name.getBytes(), 0L);
}
+
+ /**
+ * A plain {@code SELECT} compile (not routed through {@code EXPLAIN}) must
record no compile time
+ * diagnostic state.
+ */
+ @Test
+ public void testPlainSelectRecordsNoDiagnostics() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps());
+ java.sql.Statement stmt = conn.createStatement()) {
+ String base = generateUniqueName();
+ String idx = base + "_IDX";
+ stmt
+ .execute("CREATE TABLE " + base + " (rowkey VARCHAR PRIMARY KEY, c1
VARCHAR, c2 VARCHAR)");
+ stmt.execute("CREATE LOCAL INDEX " + idx + " ON " + base + " (c1)");
+ String query =
+ "SELECT c1, max(rowkey), max(c2) FROM " + base + " WHERE rowkey <= 'z'
GROUP BY c1";
+ // Plain compile (not through EXPLAIN) must skip all diagnostic
recording.
+ QueryPlan plan =
+
conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery();
+ assertTrue("plain compile must not record predicate origins",
+ plan.getContext().getPredicateOrigins().isEmpty());
+ assertTrue("plain compile must not record ignored hints",
+ plan.getContext().getIgnoredHints().isEmpty());
+ assertTrue("plain compile must not accumulate rewrite breadcrumbs",
+ plan.getContext().getAppliedRewrites().isEmpty());
+ assertEquals("plain compile must not record an optimizer decision", null,
+ plan.getOptimizerDecision());
+ assertFalse("plain compile must have collectDiagnostics=false",
+ plan.getContext().isCollectDiagnostics());
+ }
+ }
+
+ /**
+ * The same query compiled with {@code collectDiagnostics=true} (matching
what the EXPLAIN entry
+ * point does in production) must record full diagnostics. Paired with
+ * {@link #testPlainSelectRecordsNoDiagnostics()}.
+ */
+ @Test
+ public void testExplainSelectRecordsDiagnostics() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps());
+ java.sql.Statement stmt = conn.createStatement()) {
+ String base = generateUniqueName();
+ String idx = base + "_IDX";
+ stmt
+ .execute("CREATE TABLE " + base + " (rowkey VARCHAR PRIMARY KEY, c1
VARCHAR, c2 VARCHAR)");
+ stmt.execute("CREATE LOCAL INDEX " + idx + " ON " + base + " (c1)");
+ String query =
+ "SELECT c1, max(rowkey), max(c2) FROM " + base + " WHERE rowkey <= 'z'
GROUP BY c1";
+ // ExplainPlanTestUtil enables collectDiagnostics for the duration of
the compile, so the
+ // optimizer records the winning index rule and every rejected candidate.
+ ExplainPlanTestUtil.assertPlan(conn, query).indexName(base)
+
.indexRule(OptimizerReasons.RULE_MORE_BOUND_PK_COLUMNS).indexRejectedCount(1)
+ .indexRejected(0, idx, OptimizerReasons.REASON_NO_PK_PREFIX_BOUND);
+
+ // And a query with a non-PK residual predicate surfaces the WHERE
origin on the server
+ // filter under VERBOSE.
+ String filterQuery = "SELECT c1 FROM " + base + " WHERE c2 = 'x'";
+ ExplainPlanTestUtil.assertPlanWithVerbose(conn,
filterQuery).serverFilterCount(1)
+ .serverFilterOrigin(0, "WHERE");
+ }
+ }
}
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java
index 135180db83..f8f9aae5c8 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.sql.Connection;
+import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
@@ -32,6 +33,7 @@ import org.apache.phoenix.compile.ExplainPlan;
import org.apache.phoenix.compile.ExplainPlanAttributes;
import org.apache.phoenix.compile.QueryPlan;
import org.apache.phoenix.jdbc.PhoenixPreparedStatement;
+import org.apache.phoenix.jdbc.PhoenixStatement;
import org.apache.phoenix.optimize.OptimizerReasons;
import org.apache.phoenix.optimize.RejectedIndexEntry;
import org.apache.phoenix.parse.ExplainOptions;
@@ -48,14 +50,40 @@ public final class ExplainPlanTestUtil {
private ExplainPlanTestUtil() {
}
- /** Optimize {@code query} and return its {@link ExplainPlan}. */
- public static ExplainPlan getExplainPlan(Connection conn, String query)
throws SQLException {
+ /**
+ * Compile {@code query} with compile-time diagnostic recording temporarily
enabled on the
+ * underlying {@link PhoenixStatement} and return the resolved inner {@link
QueryPlan}. This is
+ * what {@code ExecutableExplainStatement.compilePlan} does for a real
{@code EXPLAIN} (flip
+ * {@code collectDiagnostics} on before compile, run compile+optimize,
restore), without the
+ * EXPLAIN-result wrapping so tests can assert directly on the inner plan's
attributes. The
+ * optional {@link ExplainOptions} are applied to the resolved context so
{@code VERBOSE}- and
+ * {@code REGIONS}-gated rendering behave as they would under a real
+ * {@code EXPLAIN [(...)] query}.
+ */
+ private static QueryPlan compileWithDiagnostics(Connection conn, String
query,
+ ExplainOptions options) throws SQLException {
try (PhoenixPreparedStatement statement =
conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)) {
- return statement.optimizeQuery().getExplainPlan();
+ PhoenixStatement underlying = statement.unwrap(PhoenixStatement.class);
+ boolean prev = underlying.isCollectDiagnostics();
+ underlying.setCollectDiagnostics(true);
+ try {
+ QueryPlan plan = statement.optimizeQuery();
+ if (options != null && plan.getContext() != null) {
+ plan.getContext().setExplainOptions(options);
+ }
+ return plan;
+ } finally {
+ underlying.setCollectDiagnostics(prev);
+ }
}
}
+ /** Optimize {@code query} and return its {@link ExplainPlan}. */
+ public static ExplainPlan getExplainPlan(Connection conn, String query)
throws SQLException {
+ return compileWithDiagnostics(conn, query,
ExplainOptions.DEFAULT).getExplainPlan();
+ }
+
/** Optimize {@code query} and return its structured {@link
ExplainPlanAttributes}. */
public static ExplainPlanAttributes getExplainAttributes(Connection conn,
String query)
throws SQLException {
@@ -69,12 +97,7 @@ public final class ExplainPlanTestUtil {
*/
public static ExplainPlanAttributes getExplainAttributes(Connection conn,
String query,
ExplainOptions options) throws SQLException {
- try (PhoenixPreparedStatement statement =
- conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)) {
- QueryPlan plan = statement.optimizeQuery();
- plan.getContext().setExplainOptions(options);
- return plan.getExplainPlan().getPlanStepsAsAttributes();
- }
+ return compileWithDiagnostics(conn, query,
options).getExplainPlan().getPlanStepsAsAttributes();
}
/** Optimize {@code query} and return its plan-steps text. */
@@ -88,20 +111,26 @@ public final class ExplainPlanTestUtil {
*/
public static List<String> getPlanSteps(Connection conn, String query,
ExplainOptions options)
throws SQLException {
- try (PhoenixPreparedStatement statement =
- conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)) {
- QueryPlan plan = statement.optimizeQuery();
- plan.getContext().setExplainOptions(options);
- return plan.getExplainPlan().getPlanSteps();
- }
+ return compileWithDiagnostics(conn, query,
options).getExplainPlan().getPlanSteps();
}
- /** Compile a mutation (UPSERT/DELETE) and return its {@link ExplainPlan}. */
+ /**
+ * Compile a mutation (UPSERT/DELETE) and return its {@link ExplainPlan}.
Mutations do not go
+ * through the {@code EXPLAIN} entry point, so {@code collectDiagnostics} is
toggled on for the
+ * duration of the compile so the underlying scan compile records the same
diagnostics.
+ */
public static ExplainPlan getMutationExplainPlan(Connection conn, String
query)
throws SQLException {
try (PhoenixPreparedStatement statement =
conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)) {
- return statement.compileMutation().getExplainPlan();
+ PhoenixStatement underlying = statement.unwrap(PhoenixStatement.class);
+ boolean prev = underlying.isCollectDiagnostics();
+ underlying.setCollectDiagnostics(true);
+ try {
+ return statement.compileMutation().getExplainPlan();
+ } finally {
+ underlying.setCollectDiagnostics(prev);
+ }
}
}
@@ -145,18 +174,51 @@ public final class ExplainPlanTestUtil {
/**
* Optimize an already-prepared and, if needed, parameter-bound {@link
PhoenixPreparedStatement}
- * and begin assertions on its plan attributes.
+ * and begin assertions on its plan attributes. The prepared statement is
compiled directly
+ * (rather than routed through {@code EXPLAIN}) because callers may have
bound parameters that
+ * cannot be replayed against a re-prepared EXPLAIN statement. {@code
collectDiagnostics} is
+ * toggled on for the duration of the compile so diagnostics are recorded.
*/
public static ExplainPlanAssert assertPlan(PhoenixPreparedStatement
statement)
throws SQLException {
- return
assertPlan(statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes());
+ PhoenixStatement underlying = statement.unwrap(PhoenixStatement.class);
+ boolean prev = underlying.isCollectDiagnostics();
+ underlying.setCollectDiagnostics(true);
+ try {
+ return
assertPlan(statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes());
+ } finally {
+ underlying.setCollectDiagnostics(prev);
+ }
}
- /** Begin assertions on the plan attributes of a resolved {@link QueryPlan}.
*/
+ /**
+ * Begin assertions on the plan attributes of a resolved {@link QueryPlan}.
The plan is expected
+ * to have been produced by a compile that already had diagnostic recording
enabled (e.g. via
+ * {@link #assertPlan(PhoenixPreparedStatement)},
+ * {@link #getOptimizedQueryPlan(PreparedStatement)} or via a direct {@code
EXPLAIN} compile).
+ */
public static ExplainPlanAssert assertPlan(QueryPlan queryPlan) throws
SQLException {
return assertPlan(queryPlan.getExplainPlan().getPlanStepsAsAttributes());
}
+ /**
+ * Diagnostic drop in replacement for
+ * {@link
org.apache.phoenix.util.PhoenixRuntime#getOptimizedQueryPlan(PreparedStatement)}.
+ * Toggles {@code collectDiagnostics=true} on the underlying {@link
PhoenixStatement}. Use this
+ * for tests whenever the plan is fed to {@code assertPlan(...)}.
+ */
+ public static QueryPlan getOptimizedQueryPlan(PreparedStatement statement)
throws SQLException {
+ PhoenixPreparedStatement pstmt =
statement.unwrap(PhoenixPreparedStatement.class);
+ PhoenixStatement underlying = pstmt.unwrap(PhoenixStatement.class);
+ boolean prev = underlying.isCollectDiagnostics();
+ underlying.setCollectDiagnostics(true);
+ try {
+ return pstmt.optimizeQuery();
+ } finally {
+ underlying.setCollectDiagnostics(prev);
+ }
+ }
+
/** Compile the mutation {@code query} on {@code conn} and begin assertions
on its attributes. */
public static ExplainPlanAssert assertMutationPlan(Connection conn, String
query)
throws SQLException {
@@ -164,12 +226,20 @@ public final class ExplainPlanTestUtil {
}
/**
- * Compile an already-prepared and, if needed, parameter-bound mutation
+ * Compile an already prepared and, if needed, parameter bound mutation
* {@link PhoenixPreparedStatement} and begin assertions on its attributes.
+ * {@code collectDiagnostics} is toggled on for the duration of the compile.
*/
public static ExplainPlanAssert assertMutationPlan(PhoenixPreparedStatement
statement)
throws SQLException {
- return
assertPlan(statement.compileMutation().getExplainPlan().getPlanStepsAsAttributes());
+ PhoenixStatement underlying = statement.unwrap(PhoenixStatement.class);
+ boolean prev = underlying.isCollectDiagnostics();
+ underlying.setCollectDiagnostics(true);
+ try {
+ return
assertPlan(statement.compileMutation().getExplainPlan().getPlanStepsAsAttributes());
+ } finally {
+ underlying.setCollectDiagnostics(prev);
+ }
}
/** Fluent assertions over {@link ExplainPlanAttributes}. */