This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch PHOENIX-7876-feature in repository https://gitbox.apache.org/repos/asf/phoenix.git
commit 07a8afcb3304de501818866f07e0926260b72a61 Author: Andrew Purtell <[email protected]> AuthorDate: Sat Jun 6 00:24:41 2026 -0700 [WIP] Convert full-text EXPLAIN ITs to fluent API; reshape SMJ + add clientSteps Convert the four remaining full-text EXPLAIN ITs to pure fluent assertPlan assertions. In ExplainPlanAttributs sort-merge-join is reshaped into a synthetic root carrying the join operator with separate lhs and rhs children. Every node carries an ordered clientSteps list. Production builders populate the new fields. The emitted text is unchanged. --- .../phoenix/compile/ExplainPlanAttributes.java | 61 +++- .../phoenix/execute/ClientAggregatePlan.java | 77 +++-- .../org/apache/phoenix/execute/ClientScanPlan.java | 30 +- .../apache/phoenix/execute/SortMergeJoinPlan.java | 20 +- .../phoenix/execute/TupleProjectionPlan.java | 4 +- .../phoenix/iterate/CursorResultIterator.java | 4 +- .../iterate/DistinctAggregatingResultIterator.java | 4 +- .../iterate/FilterAggregatingResultIterator.java | 4 +- .../phoenix/iterate/FilterResultIterator.java | 4 +- .../phoenix/iterate/LimitingResultIterator.java | 4 +- .../iterate/MergeSortRowKeyResultIterator.java | 1 + .../iterate/MergeSortTopNResultIterator.java | 9 +- .../phoenix/iterate/OffsetResultIterator.java | 4 +- .../phoenix/iterate/OrderedResultIterator.java | 6 +- .../phoenix/iterate/SegmentResultIterator.java | 1 + .../phoenix/iterate/SequenceResultIterator.java | 6 +- .../phoenix/end2end/CostBasedDecisionIT.java | 379 +++++++++------------ .../org/apache/phoenix/end2end/DerivedTableIT.java | 108 +++--- .../phoenix/end2end/SortMergeJoinMoreIT.java | 6 +- .../phoenix/end2end/TenantSpecificViewIndexIT.java | 41 +-- .../org/apache/phoenix/end2end/UnionAllIT.java | 107 ++---- .../end2end/join/SortMergeJoinGlobalIndexIT.java | 35 +- .../end2end/join/SortMergeJoinLocalIndexIT.java | 30 +- .../end2end/join/SortMergeJoinNoIndexIT.java | 20 +- .../end2end/join/SubqueryUsingSortMergeJoinIT.java | 6 +- .../query/explain/ExplainJsonNormalizer.java | 5 + .../phoenix/query/explain/ExplainPlanTest.java | 81 ++++- .../phoenix/query/explain/ExplainPlanTestUtil.java | 34 ++ 28 files changed, 578 insertions(+), 513 deletions(-) diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ExplainPlanAttributes.java b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ExplainPlanAttributes.java index 9f69b2bc73..5bc1fe53b7 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ExplainPlanAttributes.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ExplainPlanAttributes.java @@ -19,6 +19,8 @@ package org.apache.phoenix.compile; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; import org.apache.hadoop.hbase.HRegionLocation; @@ -27,11 +29,6 @@ import org.apache.phoenix.parse.HintNode; import org.apache.phoenix.parse.HintNode.Hint; import org.apache.phoenix.schema.PColumn; -/** - * ExplainPlan attributes that contain individual attributes of ExplainPlan that we can assert - * against. This also makes attribute retrieval easier as an API rather than retrieving list of - * Strings containing entire plan. - */ @JsonPropertyOrder({ "abstractExplainPlan", "hint", "explainScanType", "consistency", "tableName", "keyRanges", "scanTimeRangeMin", "scanTimeRangeMax", "splitsChunk", "useRoundRobinIterator", "samplingRate", "hexStringRVCOffset", "iteratorTypeAndScanSize", "estimatedRows", @@ -41,8 +38,9 @@ import org.apache.phoenix.schema.PColumn; "serverGroupByLimit", "serverSortedBy", "serverOffset", "serverRowLimit", "clientFilterBy", "clientAggregate", "clientDistinctFilter", "clientAfterAggregate", "clientSortAlgo", "clientSortedBy", "clientOffset", "clientRowLimit", "clientSequenceCount", "clientCursorName", - "rhsJoinQueryExplainPlan", "subPlans", "dynamicServerFilter", "afterJoinFilter", - "joinScannerLimit", "sortMergeSkipMerge", "regionLocations", "numRegionLocationLookups" }) + "clientSteps", "lhsJoinQueryExplainPlan", "rhsJoinQueryExplainPlan", "subPlans", + "dynamicServerFilter", "afterJoinFilter", "joinScannerLimit", "sortMergeSkipMerge", + "regionLocations", "numRegionLocationLookups" }) public class ExplainPlanAttributes { // Plan identity and scan-level metadata @@ -92,8 +90,11 @@ public class ExplainPlanAttributes { private final Integer clientRowLimit; private final Integer clientSequenceCount; private final String clientCursorName; + // Ordered client-side pipeline (trimmed CLIENT* lines in emission order). + private final List<String> clientSteps; // Join / sub-plan + private final ExplainPlanAttributes lhsJoinQueryExplainPlan; private final ExplainPlanAttributes rhsJoinQueryExplainPlan; private final List<ExplainPlanAttributes> subPlans; private final String dynamicServerFilter; @@ -148,6 +149,8 @@ public class ExplainPlanAttributes { this.clientRowLimit = null; this.clientSequenceCount = null; this.clientCursorName = null; + this.clientSteps = null; + this.lhsJoinQueryExplainPlan = null; this.rhsJoinQueryExplainPlan = null; this.subPlans = null; this.dynamicServerFilter = null; @@ -170,6 +173,7 @@ public class ExplainPlanAttributes { String clientFilterBy, String clientAggregate, String clientDistinctFilter, String clientAfterAggregate, String clientSortAlgo, String clientSortedBy, Integer clientOffset, Integer clientRowLimit, Integer clientSequenceCount, String clientCursorName, + List<String> clientSteps, ExplainPlanAttributes lhsJoinQueryExplainPlan, ExplainPlanAttributes rhsJoinQueryExplainPlan, List<ExplainPlanAttributes> subPlans, String dynamicServerFilter, String afterJoinFilter, Long joinScannerLimit, boolean sortMergeSkipMerge, List<HRegionLocation> regionLocations, @@ -214,6 +218,10 @@ public class ExplainPlanAttributes { this.clientRowLimit = clientRowLimit; this.clientSequenceCount = clientSequenceCount; this.clientCursorName = clientCursorName; + this.clientSteps = (clientSteps == null || clientSteps.isEmpty()) + ? null + : Collections.unmodifiableList(new ArrayList<>(clientSteps)); + this.lhsJoinQueryExplainPlan = lhsJoinQueryExplainPlan; this.rhsJoinQueryExplainPlan = rhsJoinQueryExplainPlan; this.subPlans = subPlans; this.dynamicServerFilter = dynamicServerFilter; @@ -385,6 +393,14 @@ public class ExplainPlanAttributes { return clientCursorName; } + public List<String> getClientSteps() { + return clientSteps; + } + + public ExplainPlanAttributes getLhsJoinQueryExplainPlan() { + return lhsJoinQueryExplainPlan; + } + public ExplainPlanAttributes getRhsJoinQueryExplainPlan() { return rhsJoinQueryExplainPlan; } @@ -463,6 +479,8 @@ public class ExplainPlanAttributes { private Integer clientRowLimit; private Integer clientSequenceCount; private String clientCursorName; + private List<String> clientSteps; + private ExplainPlanAttributes lhsJoinQueryExplainPlan; private ExplainPlanAttributes rhsJoinQueryExplainPlan; private List<ExplainPlanAttributes> subPlans; private String dynamicServerFilter; @@ -518,6 +536,9 @@ public class ExplainPlanAttributes { this.clientRowLimit = explainPlanAttributes.getClientRowLimit(); this.clientSequenceCount = explainPlanAttributes.getClientSequenceCount(); this.clientCursorName = explainPlanAttributes.getClientCursorName(); + List<String> srcClientSteps = explainPlanAttributes.getClientSteps(); + this.clientSteps = srcClientSteps == null ? null : new ArrayList<>(srcClientSteps); + this.lhsJoinQueryExplainPlan = explainPlanAttributes.getLhsJoinQueryExplainPlan(); this.rhsJoinQueryExplainPlan = explainPlanAttributes.getRhsJoinQueryExplainPlan(); this.subPlans = explainPlanAttributes.getSubPlans(); this.dynamicServerFilter = explainPlanAttributes.getDynamicServerFilter(); @@ -731,6 +752,25 @@ public class ExplainPlanAttributes { return this; } + public ExplainPlanAttributesBuilder setClientSteps(List<String> clientSteps) { + this.clientSteps = clientSteps == null ? null : new ArrayList<>(clientSteps); + return this; + } + + public ExplainPlanAttributesBuilder addClientStep(String step) { + if (this.clientSteps == null) { + this.clientSteps = new ArrayList<>(); + } + this.clientSteps.add(step); + return this; + } + + public ExplainPlanAttributesBuilder + setLhsJoinQueryExplainPlan(ExplainPlanAttributes lhsJoinQueryExplainPlan) { + this.lhsJoinQueryExplainPlan = lhsJoinQueryExplainPlan; + return this; + } + public ExplainPlanAttributesBuilder setRhsJoinQueryExplainPlan(ExplainPlanAttributes rhsJoinQueryExplainPlan) { this.rhsJoinQueryExplainPlan = rhsJoinQueryExplainPlan; @@ -781,9 +821,10 @@ public class ExplainPlanAttributes { serverDistinctFilter, serverMergeColumns, serverArrayElementProjection, serverAggregate, serverGroupByLimit, serverSortedBy, serverOffset, serverRowLimit, clientFilterBy, clientAggregate, clientDistinctFilter, clientAfterAggregate, clientSortAlgo, clientSortedBy, - clientOffset, clientRowLimit, clientSequenceCount, clientCursorName, - rhsJoinQueryExplainPlan, subPlans, dynamicServerFilter, afterJoinFilter, joinScannerLimit, - sortMergeSkipMerge, regionLocations, numRegionLocationLookups); + clientOffset, clientRowLimit, clientSequenceCount, clientCursorName, clientSteps, + lhsJoinQueryExplainPlan, rhsJoinQueryExplainPlan, subPlans, dynamicServerFilter, + afterJoinFilter, joinScannerLimit, sortMergeSkipMerge, regionLocations, + numRegionLocationLookups); } } } diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientAggregatePlan.java b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientAggregatePlan.java index e89bf515cc..4a3d1b5d28 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientAggregatePlan.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientAggregatePlan.java @@ -226,63 +226,86 @@ public class ClientAggregatePlan extends ClientProcessingPlan { ExplainPlanAttributesBuilder newBuilder = new ExplainPlanAttributesBuilder(explainPlanAttributes); if (where != null) { - planSteps.add("CLIENT FILTER BY " + where.toString()); + String step = "CLIENT FILTER BY " + where.toString(); + planSteps.add(step); newBuilder.setClientFilterBy(where.toString()); + newBuilder.addClientStep(step); } if (groupBy.isEmpty()) { - planSteps.add("CLIENT AGGREGATE INTO SINGLE ROW"); - newBuilder.setClientAggregate("CLIENT AGGREGATE INTO SINGLE ROW"); + String step = "CLIENT AGGREGATE INTO SINGLE ROW"; + planSteps.add(step); + newBuilder.setClientAggregate(step); + newBuilder.addClientStep(step); } else if (groupBy.isOrderPreserving()) { - planSteps.add( - "CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY " + groupBy.getExpressions().toString()); - newBuilder.setClientAggregate( - "CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY " + groupBy.getExpressions().toString()); + String step = + "CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY " + groupBy.getExpressions().toString(); + planSteps.add(step); + newBuilder.setClientAggregate(step); + newBuilder.addClientStep(step); } else if (useHashAgg) { - planSteps - .add("CLIENT HASH AGGREGATE INTO DISTINCT ROWS BY " + groupBy.getExpressions().toString()); - newBuilder.setClientAggregate( - "CLIENT HASH AGGREGATE INTO DISTINCT ROWS BY " + groupBy.getExpressions().toString()); + String step = + "CLIENT HASH AGGREGATE INTO DISTINCT ROWS BY " + groupBy.getExpressions().toString(); + planSteps.add(step); + newBuilder.setClientAggregate(step); + newBuilder.addClientStep(step); if (orderBy == OrderBy.FWD_ROW_KEY_ORDER_BY || orderBy == OrderBy.REV_ROW_KEY_ORDER_BY) { - planSteps.add("CLIENT SORTED BY " + groupBy.getKeyExpressions().toString()); + String sortStep = "CLIENT SORTED BY " + groupBy.getKeyExpressions().toString(); + planSteps.add(sortStep); newBuilder.setClientSortedBy(groupBy.getKeyExpressions().toString()); + newBuilder.addClientStep(sortStep); } } else { - planSteps.add("CLIENT SORTED BY " + groupBy.getKeyExpressions().toString()); - planSteps - .add("CLIENT AGGREGATE INTO DISTINCT ROWS BY " + groupBy.getExpressions().toString()); + String sortStep = "CLIENT SORTED BY " + groupBy.getKeyExpressions().toString(); + String aggStep = + "CLIENT AGGREGATE INTO DISTINCT ROWS BY " + groupBy.getExpressions().toString(); + planSteps.add(sortStep); + planSteps.add(aggStep); newBuilder.setClientSortedBy(groupBy.getKeyExpressions().toString()); - newBuilder.setClientAggregate( - "CLIENT AGGREGATE INTO DISTINCT ROWS BY " + groupBy.getExpressions().toString()); + newBuilder.setClientAggregate(aggStep); + newBuilder.addClientStep(sortStep); + newBuilder.addClientStep(aggStep); } if (having != null) { - planSteps.add("CLIENT AFTER-AGGREGATION FILTER BY " + having.toString()); - newBuilder.setClientAfterAggregate("CLIENT AFTER-AGGREGATION FILTER BY " + having.toString()); + String step = "CLIENT AFTER-AGGREGATION FILTER BY " + having.toString(); + planSteps.add(step); + newBuilder.setClientAfterAggregate(step); + newBuilder.addClientStep(step); } if (statement.isDistinct() && statement.isAggregate()) { - planSteps.add("CLIENT DISTINCT ON " + projector.toString()); + String step = "CLIENT DISTINCT ON " + projector.toString(); + planSteps.add(step); newBuilder.setClientDistinctFilter(projector.toString()); + newBuilder.addClientStep(step); } if (offset != null) { - planSteps.add("CLIENT OFFSET " + offset); + String step = "CLIENT OFFSET " + offset; + planSteps.add(step); newBuilder.setClientOffset(offset); + newBuilder.addClientStep(step); } if (orderBy.getOrderByExpressions().isEmpty()) { if (limit != null) { - planSteps.add("CLIENT " + limit + " ROW LIMIT"); + String step = "CLIENT " + limit + " ROW LIMIT"; + planSteps.add(step); newBuilder.setClientRowLimit(limit); + newBuilder.addClientStep(step); } } else { - planSteps - .add("CLIENT" + (limit == null ? "" : " TOP " + limit + " ROW" + (limit == 1 ? "" : "S")) - + " SORTED BY " + orderBy.getOrderByExpressions().toString()); + String step = + "CLIENT" + (limit == null ? "" : " TOP " + limit + " ROW" + (limit == 1 ? "" : "S")) + + " SORTED BY " + orderBy.getOrderByExpressions().toString(); + planSteps.add(step); newBuilder.setClientRowLimit(limit); newBuilder.setClientSortedBy(orderBy.getOrderByExpressions().toString()); + newBuilder.addClientStep(step); } if (context.getSequenceManager().getSequenceCount() > 0) { int nSequences = context.getSequenceManager().getSequenceCount(); - planSteps.add( - "CLIENT RESERVE VALUES FROM " + nSequences + " SEQUENCE" + (nSequences == 1 ? "" : "S")); + String step = + "CLIENT RESERVE VALUES FROM " + nSequences + " SEQUENCE" + (nSequences == 1 ? "" : "S"); + planSteps.add(step); newBuilder.setClientSequenceCount(nSequences); + newBuilder.addClientStep(step); } return new ExplainPlan(planSteps, newBuilder.build()); diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientScanPlan.java b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientScanPlan.java index 8bc7198960..9807ba8ef9 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientScanPlan.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientScanPlan.java @@ -123,34 +123,46 @@ public class ClientScanPlan extends ClientProcessingPlan { ExplainPlanAttributesBuilder newBuilder = new ExplainPlanAttributesBuilder(explainPlanAttributes); if (where != null) { - planSteps.add("CLIENT FILTER BY " + where.toString()); + String step = "CLIENT FILTER BY " + where.toString(); + planSteps.add(step); newBuilder.setClientFilterBy(where.toString()); + newBuilder.addClientStep(step); } if (!orderBy.getOrderByExpressions().isEmpty()) { if (offset != null) { - planSteps.add("CLIENT OFFSET " + offset); + String step = "CLIENT OFFSET " + offset; + planSteps.add(step); newBuilder.setClientOffset(offset); + newBuilder.addClientStep(step); } - planSteps - .add("CLIENT" + (limit == null ? "" : " TOP " + limit + " ROW" + (limit == 1 ? "" : "S")) - + " SORTED BY " + orderBy.getOrderByExpressions().toString()); + String step = + "CLIENT" + (limit == null ? "" : " TOP " + limit + " ROW" + (limit == 1 ? "" : "S")) + + " SORTED BY " + orderBy.getOrderByExpressions().toString(); + planSteps.add(step); newBuilder.setClientRowLimit(limit); newBuilder.setClientSortedBy(orderBy.getOrderByExpressions().toString()); + newBuilder.addClientStep(step); } else { if (offset != null) { - planSteps.add("CLIENT OFFSET " + offset); + String step = "CLIENT OFFSET " + offset; + planSteps.add(step); newBuilder.setClientOffset(offset); + newBuilder.addClientStep(step); } if (limit != null) { - planSteps.add("CLIENT " + limit + " ROW LIMIT"); + String step = "CLIENT " + limit + " ROW LIMIT"; + planSteps.add(step); newBuilder.setClientRowLimit(limit); + newBuilder.addClientStep(step); } } if (context.getSequenceManager().getSequenceCount() > 0) { int nSequences = context.getSequenceManager().getSequenceCount(); - planSteps.add( - "CLIENT RESERVE VALUES FROM " + nSequences + " SEQUENCE" + (nSequences == 1 ? "" : "S")); + String step = + "CLIENT RESERVE VALUES FROM " + nSequences + " SEQUENCE" + (nSequences == 1 ? "" : "S"); + planSteps.add(step); newBuilder.setClientSequenceCount(nSequences); + newBuilder.addClientStep(step); } return new ExplainPlan(planSteps, newBuilder.build()); diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/SortMergeJoinPlan.java b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/SortMergeJoinPlan.java index ea210c09a4..9e1cd2290b 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/SortMergeJoinPlan.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/SortMergeJoinPlan.java @@ -192,11 +192,6 @@ public class SortMergeJoinPlan implements QueryPlan { ExplainPlan lhsExplainPlan = lhsPlan.getExplainPlan(); List<String> lhsPlanSteps = lhsExplainPlan.getPlanSteps(); ExplainPlanAttributes lhsPlanAttributes = lhsExplainPlan.getPlanStepsAsAttributes(); - ExplainPlanAttributesBuilder lhsPlanBuilder = - new ExplainPlanAttributesBuilder(lhsPlanAttributes); - lhsPlanBuilder - .setAbstractExplainPlan("SORT-MERGE-JOIN (" + joinType.toString().toUpperCase() + ")"); - lhsPlanBuilder.setSortMergeSkipMerge(rhsSchema.getFieldCount() == 0); for (String step : lhsPlanSteps) { steps.add(" " + step); @@ -206,15 +201,20 @@ public class SortMergeJoinPlan implements QueryPlan { ExplainPlan rhsExplainPlan = rhsPlan.getExplainPlan(); List<String> rhsPlanSteps = rhsExplainPlan.getPlanSteps(); ExplainPlanAttributes rhsPlanAttributes = rhsExplainPlan.getPlanStepsAsAttributes(); - ExplainPlanAttributesBuilder rhsPlanBuilder = - new ExplainPlanAttributesBuilder(rhsPlanAttributes); - - lhsPlanBuilder.setRhsJoinQueryExplainPlan(rhsPlanBuilder.build()); for (String step : rhsPlanSteps) { steps.add(" " + step); } - return new ExplainPlan(steps, lhsPlanBuilder.build()); + + // Build a synthetic root that holds the join operator and its two operands as separate + // child plans so nested sort-merge-joins can be represented. + ExplainPlanAttributesBuilder rootBuilder = new ExplainPlanAttributesBuilder(); + rootBuilder + .setAbstractExplainPlan("SORT-MERGE-JOIN (" + joinType.toString().toUpperCase() + ")"); + rootBuilder.setSortMergeSkipMerge(rhsSchema.getFieldCount() == 0); + rootBuilder.setLhsJoinQueryExplainPlan(lhsPlanAttributes); + rootBuilder.setRhsJoinQueryExplainPlan(rhsPlanAttributes); + return new ExplainPlan(steps, rootBuilder.build()); } @Override diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/TupleProjectionPlan.java b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/TupleProjectionPlan.java index ac38657f18..413f046d22 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/TupleProjectionPlan.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/TupleProjectionPlan.java @@ -147,10 +147,12 @@ public class TupleProjectionPlan extends DelegateQueryPlan { List<String> planSteps = Lists.newArrayList(explainPlan.getPlanSteps()); ExplainPlanAttributes explainPlanAttributes = explainPlan.getPlanStepsAsAttributes(); if (postFilter != null) { - planSteps.add("CLIENT FILTER BY " + postFilter.toString()); + String step = "CLIENT FILTER BY " + postFilter.toString(); + planSteps.add(step); ExplainPlanAttributesBuilder newBuilder = new ExplainPlanAttributesBuilder(explainPlanAttributes); newBuilder.setClientFilterBy(postFilter.toString()); + newBuilder.addClientStep(step); explainPlanAttributes = newBuilder.build(); } diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/CursorResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/CursorResultIterator.java index 9c44579366..113c6fad45 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/CursorResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/CursorResultIterator.java @@ -60,7 +60,9 @@ public class CursorResultIterator implements ResultIterator { ExplainPlanAttributesBuilder explainPlanAttributesBuilder) { delegate.explain(planSteps, explainPlanAttributesBuilder); explainPlanAttributesBuilder.setClientCursorName(cursorName); - planSteps.add("CLIENT CURSOR " + cursorName); + String step = "CLIENT CURSOR " + cursorName; + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } @Override diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/DistinctAggregatingResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/DistinctAggregatingResultIterator.java index 10f081c8a4..6dfdfb8857 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/DistinctAggregatingResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/DistinctAggregatingResultIterator.java @@ -134,7 +134,9 @@ public class DistinctAggregatingResultIterator implements AggregatingResultItera ExplainPlanAttributesBuilder explainPlanAttributesBuilder) { targetAggregatingResultIterator.explain(planSteps, explainPlanAttributesBuilder); explainPlanAttributesBuilder.setClientDistinctFilter(rowProjector.toString()); - planSteps.add("CLIENT DISTINCT ON " + rowProjector.toString()); + String step = "CLIENT DISTINCT ON " + rowProjector.toString(); + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } @Override diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/FilterAggregatingResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/FilterAggregatingResultIterator.java index fb3609066c..82d6ba6824 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/FilterAggregatingResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/FilterAggregatingResultIterator.java @@ -82,7 +82,9 @@ public class FilterAggregatingResultIterator implements AggregatingResultIterato ExplainPlanAttributesBuilder explainPlanAttributesBuilder) { delegate.explain(planSteps, explainPlanAttributesBuilder); explainPlanAttributesBuilder.setClientFilterBy(expression.toString()); - planSteps.add("CLIENT FILTER BY " + expression.toString()); + String step = "CLIENT FILTER BY " + expression.toString(); + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } @Override diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/FilterResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/FilterResultIterator.java index f6631dee43..9435f20464 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/FilterResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/FilterResultIterator.java @@ -80,7 +80,9 @@ public class FilterResultIterator extends LookAheadResultIterator { ExplainPlanAttributesBuilder explainPlanAttributesBuilder) { delegate.explain(planSteps, explainPlanAttributesBuilder); explainPlanAttributesBuilder.setClientFilterBy(expression.toString()); - planSteps.add("CLIENT FILTER BY " + expression.toString()); + String step = "CLIENT FILTER BY " + expression.toString(); + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } @Override diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/LimitingResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/LimitingResultIterator.java index 0d60c3b04d..1a90c87d3b 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/LimitingResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/LimitingResultIterator.java @@ -55,7 +55,9 @@ public class LimitingResultIterator extends DelegateResultIterator { ExplainPlanAttributesBuilder explainPlanAttributesBuilder) { super.explain(planSteps, explainPlanAttributesBuilder); explainPlanAttributesBuilder.setClientRowLimit(limit); - planSteps.add("CLIENT " + limit + " ROW LIMIT"); + String step = "CLIENT " + limit + " ROW LIMIT"; + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } @Override diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/MergeSortRowKeyResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/MergeSortRowKeyResultIterator.java index 2412c2a092..7691504d93 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/MergeSortRowKeyResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/MergeSortRowKeyResultIterator.java @@ -59,6 +59,7 @@ public class MergeSortRowKeyResultIterator extends MergeSortResultIterator { resultIterators.explain(planSteps, explainPlanAttributesBuilder); explainPlanAttributesBuilder.setClientSortAlgo("CLIENT MERGE SORT"); planSteps.add("CLIENT MERGE SORT"); + explainPlanAttributesBuilder.addClientStep("CLIENT MERGE SORT"); } @Override diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/MergeSortTopNResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/MergeSortTopNResultIterator.java index 2c72682e19..767656a950 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/MergeSortTopNResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/MergeSortTopNResultIterator.java @@ -116,13 +116,18 @@ public class MergeSortTopNResultIterator extends MergeSortResultIterator { resultIterators.explain(planSteps, explainPlanAttributesBuilder); explainPlanAttributesBuilder.setClientSortAlgo("CLIENT MERGE SORT"); planSteps.add("CLIENT MERGE SORT"); + explainPlanAttributesBuilder.addClientStep("CLIENT MERGE SORT"); if (offset > 0) { explainPlanAttributesBuilder.setClientOffset(offset); - planSteps.add("CLIENT OFFSET " + offset); + String step = "CLIENT OFFSET " + offset; + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } if (limit > 0) { explainPlanAttributesBuilder.setClientRowLimit(limit); - planSteps.add("CLIENT LIMIT " + limit); + String step = "CLIENT LIMIT " + limit; + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } } diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/OffsetResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/OffsetResultIterator.java index bc092a0a2e..fc369eed25 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/OffsetResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/OffsetResultIterator.java @@ -92,7 +92,9 @@ public class OffsetResultIterator extends DelegateResultIterator { ExplainPlanAttributesBuilder explainPlanAttributesBuilder) { super.explain(planSteps, explainPlanAttributesBuilder); explainPlanAttributesBuilder.setClientOffset(offset); - planSteps.add("CLIENT OFFSET " + offset); + String step = "CLIENT OFFSET " + offset; + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } @Override diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/OrderedResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/OrderedResultIterator.java index 2cc3ccb3f6..1662b5b465 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/OrderedResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/OrderedResultIterator.java @@ -514,9 +514,11 @@ public class OrderedResultIterator implements PeekingResultIterator { explainPlanAttributesBuilder.setClientOffset(offset); explainPlanAttributesBuilder.setClientRowLimit(limit); explainPlanAttributesBuilder.setClientSortedBy(orderByExpressions.toString()); - planSteps.add("CLIENT" + (offset == null || offset == 0 ? "" : " OFFSET " + offset) + String step = "CLIENT" + (offset == null || offset == 0 ? "" : " OFFSET " + offset) + (limit == null ? "" : " TOP " + limit + " ROW" + (limit == 1 ? "" : "S")) + " SORTED BY " - + orderByExpressions.toString()); + + orderByExpressions.toString(); + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } @Override diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/SegmentResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/SegmentResultIterator.java index 1bed34afbd..752600a296 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/SegmentResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/SegmentResultIterator.java @@ -120,5 +120,6 @@ public class SegmentResultIterator extends BaseResultIterator { public void explain(List<String> planSteps, ExplainPlanAttributesBuilder explainPlanAttributesBuilder) { planSteps.add("CLIENT SEGMENT SCAN"); + explainPlanAttributesBuilder.addClientStep("CLIENT SEGMENT SCAN"); } } diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/SequenceResultIterator.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/SequenceResultIterator.java index b58ba58610..ae98654cfa 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/SequenceResultIterator.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/SequenceResultIterator.java @@ -60,8 +60,10 @@ public class SequenceResultIterator extends DelegateResultIterator { super.explain(planSteps, explainPlanAttributesBuilder); int nSequences = sequenceManager.getSequenceCount(); explainPlanAttributesBuilder.setClientSequenceCount(nSequences); - planSteps - .add("CLIENT RESERVE VALUES FROM " + nSequences + " SEQUENCE" + (nSequences == 1 ? "" : "S")); + String step = + "CLIENT RESERVE VALUES FROM " + nSequences + " SEQUENCE" + (nSequences == 1 ? "" : "S"); + planSteps.add(step); + explainPlanAttributesBuilder.addClientStep(step); } @Override diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java index fa08004c32..02d85a4f2e 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java @@ -17,23 +17,18 @@ */ package org.apache.phoenix.end2end; +import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertMutationPlan; +import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan; import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.util.Map; import java.util.Properties; -import org.apache.phoenix.compile.ExplainPlan; -import org.apache.phoenix.compile.ExplainPlanAttributes; -import org.apache.phoenix.jdbc.PhoenixPreparedStatement; import org.apache.phoenix.query.BaseTest; import org.apache.phoenix.query.QueryServices; import org.apache.phoenix.util.PropertiesUtil; -import org.apache.phoenix.util.QueryUtil; import org.apache.phoenix.util.ReadOnlyProps; import org.junit.BeforeClass; import org.junit.Test; @@ -79,7 +74,7 @@ public class CostBasedDecisionIT extends BaseTest { String query = "SELECT rowkey, c1, c2 FROM " + tableName + " where c1 LIKE 'X0%' ORDER BY rowkey"; // Use the data table plan that opts out order-by when stats are not available. - verifyQueryPlan(query, "FULL SCAN"); + assertPlan(conn, query).scanType("FULL SCAN"); PreparedStatement stmt = conn.prepareStatement("UPSERT INTO " + tableName + " (rowkey, c1, c2) VALUES (?, ?, ?)"); @@ -94,7 +89,7 @@ public class CostBasedDecisionIT extends BaseTest { conn.createStatement().execute("UPDATE STATISTICS " + tableName); // Use the index table plan that has a lower cost when stats become available. - verifyQueryPlan(query, "RANGE SCAN"); + assertPlan(conn, query).scanType("RANGE SCAN"); } finally { conn.close(); } @@ -116,16 +111,9 @@ public class CostBasedDecisionIT extends BaseTest { String query = "SELECT c1, max(rowkey), max(c2) FROM " + tableName + " where rowkey <= 'z' GROUP BY c1"; // Use the index table plan that opts out order-by when stats are not available. - ExplainPlan plan = conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class) - .optimizeQuery().getExplainPlan(); - ExplainPlanAttributes explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("RANGE SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals(tableName, explainPlanAttributes.getTableName()); - assertEquals(" [*] - ['z']", explainPlanAttributes.getKeyRanges()); - assertEquals("SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]", - explainPlanAttributes.getServerAggregate()); - assertEquals("CLIENT MERGE SORT", explainPlanAttributes.getClientSortAlgo()); + assertPlan(conn, query).iteratorType("PARALLEL").scanType("RANGE SCAN").table(tableName) + .keyRanges(" [*] - ['z']").serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]") + .clientSortAlgo("CLIENT MERGE SORT"); PreparedStatement stmt = conn.prepareStatement("UPSERT INTO " + tableName + " (rowkey, c1, c2) VALUES (?, ?, ?)"); @@ -142,18 +130,11 @@ public class CostBasedDecisionIT extends BaseTest { // Given that the range on C1 is meaningless and group-by becomes // order-preserving if using the data table, the data table plan should // come out as the best plan based on the costs. - plan = conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery() - .getExplainPlan(); - explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("RANGE SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals(indexName + "(" + tableName + ")", explainPlanAttributes.getTableName()); - assertEquals(" [1]", explainPlanAttributes.getKeyRanges()); - assertTrue(explainPlanAttributes.isServerFirstKeyOnlyProjection()); - assertEquals("\"ROWKEY\" <= 'z'", explainPlanAttributes.getServerWhereFilter()); - assertEquals("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]", - explainPlanAttributes.getServerAggregate()); - assertEquals("CLIENT MERGE SORT", explainPlanAttributes.getClientSortAlgo()); + assertPlan(conn, query).iteratorType("PARALLEL").scanType("RANGE SCAN") + .table(indexName + "(" + tableName + ")").keyRanges(" [1]") + .serverFirstKeyOnlyProjection(true).serverWhereFilter("\"ROWKEY\" <= 'z'") + .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]") + .clientSortAlgo("CLIENT MERGE SORT"); } finally { conn.close(); } @@ -178,16 +159,10 @@ public class CostBasedDecisionIT extends BaseTest { String query = "SELECT * FROM " + tableName + " where c1 BETWEEN 10 AND 20 AND c2 < 9000 AND C3 < 5000"; // Use the idx2 plan with a wider PK slot span when stats are not available. - ExplainPlan plan = conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class) - .optimizeQuery().getExplainPlan(); - ExplainPlanAttributes explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("RANGE SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals(indexName2 + "(" + tableName + ")", explainPlanAttributes.getTableName()); - assertEquals(" [2,*] - [2,9,000]", explainPlanAttributes.getKeyRanges()); - assertEquals("((\"C1\" >= 10 AND \"C1\" <= 20) AND TO_INTEGER(\"C3\") < 5000)", - explainPlanAttributes.getServerWhereFilter()); - assertEquals("CLIENT MERGE SORT", explainPlanAttributes.getClientSortAlgo()); + assertPlan(conn, query).iteratorType("PARALLEL").scanType("RANGE SCAN") + .table(indexName2 + "(" + tableName + ")").keyRanges(" [2,*] - [2,9,000]") + .serverWhereFilter("((\"C1\" >= 10 AND \"C1\" <= 20) AND TO_INTEGER(\"C3\") < 5000)") + .clientSortAlgo("CLIENT MERGE SORT"); PreparedStatement stmt = conn .prepareStatement("UPSERT INTO " + tableName + " (rowkey, c1, c2, c3) VALUES (?, ?, ?, ?)"); @@ -202,16 +177,9 @@ public class CostBasedDecisionIT extends BaseTest { conn.createStatement().execute("UPDATE STATISTICS " + tableName); // Use the idx2 plan that scans less data when stats become available. - plan = conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery() - .getExplainPlan(); - explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("RANGE SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals(indexName1 + "(" + tableName + ")", explainPlanAttributes.getTableName()); - assertEquals(" [1,10] - [1,20]", explainPlanAttributes.getKeyRanges()); - assertEquals("(\"C2\" < 9000 AND \"C3\" < 5000)", - explainPlanAttributes.getServerWhereFilter()); - assertEquals("CLIENT MERGE SORT", explainPlanAttributes.getClientSortAlgo()); + assertPlan(conn, query).iteratorType("PARALLEL").scanType("RANGE SCAN") + .table(indexName1 + "(" + tableName + ")").keyRanges(" [1,10] - [1,20]") + .serverWhereFilter("(\"C2\" < 9000 AND \"C3\" < 5000)").clientSortAlgo("CLIENT MERGE SORT"); } finally { conn.close(); } @@ -236,11 +204,11 @@ public class CostBasedDecisionIT extends BaseTest { String query = "UPSERT INTO " + tableName + " SELECT * FROM " + tableName + " where c1 BETWEEN 10 AND 20 AND c2 < 9000 AND C3 < 5000"; // Use the idx2 plan with a wider PK slot span when stats are not available. - verifyQueryPlan(query, - "UPSERT SELECT\n" + "CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + indexName2 + "(" + tableName - + ")" + " [2,*] - [2,9,000]\n" - + " SERVER FILTER BY ((\"C1\" >= 10 AND \"C1\" <= 20) AND TO_INTEGER(\"C3\") < 5000)\n" - + "CLIENT MERGE SORT"); + assertMutationPlan(conn, query).abstractExplainPlan("UPSERT SELECT").iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(indexName2 + "(" + tableName + ")") + .keyRanges(" [2,*] - [2,9,000]") + .serverWhereFilter("((\"C1\" >= 10 AND \"C1\" <= 20) AND TO_INTEGER(\"C3\") < 5000)") + .clientSortAlgo("CLIENT MERGE SORT"); PreparedStatement stmt = conn .prepareStatement("UPSERT INTO " + tableName + " (rowkey, c1, c2, c3) VALUES (?, ?, ?, ?)"); @@ -255,10 +223,10 @@ public class CostBasedDecisionIT extends BaseTest { conn.createStatement().execute("UPDATE STATISTICS " + tableName); // Use the idx2 plan that scans less data when stats become available. - verifyQueryPlan(query, - "UPSERT SELECT\n" + "CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + indexName1 + "(" + tableName - + ")" + " [1,10] - [1,20]\n" + " SERVER FILTER BY (\"C2\" < 9000 AND \"C3\" < 5000)\n" - + "CLIENT MERGE SORT"); + assertMutationPlan(conn, query).abstractExplainPlan("UPSERT SELECT").iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(indexName1 + "(" + tableName + ")") + .keyRanges(" [1,10] - [1,20]").serverWhereFilter("(\"C2\" < 9000 AND \"C3\" < 5000)") + .clientSortAlgo("CLIENT MERGE SORT"); } finally { conn.close(); } @@ -283,11 +251,11 @@ public class CostBasedDecisionIT extends BaseTest { String query = "DELETE FROM " + tableName + " where c1 BETWEEN 10 AND 20 AND c2 < 9000 AND C3 < 5000"; // Use the idx2 plan with a wider PK slot span when stats are not available. - verifyQueryPlan(query, - "DELETE ROWS CLIENT SELECT\n" + "CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + indexName2 + "(" - + tableName + ")" + " [2,*] - [2,9,000]\n" - + " SERVER FILTER BY ((\"C1\" >= 10 AND \"C1\" <= 20) AND TO_INTEGER(\"C3\") < 5000)\n" - + "CLIENT MERGE SORT"); + assertMutationPlan(conn, query).abstractExplainPlan("DELETE ROWS CLIENT SELECT") + .iteratorType("PARALLEL").scanType("RANGE SCAN").table(indexName2 + "(" + tableName + ")") + .keyRanges(" [2,*] - [2,9,000]") + .serverWhereFilter("((\"C1\" >= 10 AND \"C1\" <= 20) AND TO_INTEGER(\"C3\") < 5000)") + .clientSortAlgo("CLIENT MERGE SORT"); PreparedStatement stmt = conn .prepareStatement("UPSERT INTO " + tableName + " (rowkey, c1, c2, c3) VALUES (?, ?, ?, ?)"); @@ -302,10 +270,10 @@ public class CostBasedDecisionIT extends BaseTest { conn.createStatement().execute("UPDATE STATISTICS " + tableName); // Use the idx2 plan that scans less data when stats become available. - verifyQueryPlan(query, - "DELETE ROWS CLIENT SELECT\n" + "CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + indexName1 + "(" - + tableName + ")" + " [1,10] - [1,20]\n" - + " SERVER FILTER BY (\"C2\" < 9000 AND \"C3\" < 5000)\n" + "CLIENT MERGE SORT"); + assertMutationPlan(conn, query).abstractExplainPlan("DELETE ROWS CLIENT SELECT") + .iteratorType("PARALLEL").scanType("RANGE SCAN").table(indexName1 + "(" + tableName + ")") + .keyRanges(" [1,10] - [1,20]").serverWhereFilter("(\"C2\" < 9000 AND \"C3\" < 5000)") + .clientSortAlgo("CLIENT MERGE SORT"); } finally { conn.close(); } @@ -328,12 +296,13 @@ public class CostBasedDecisionIT extends BaseTest { + " where rowkey <= 'z' GROUP BY c1 " + "UNION ALL SELECT c1, max(rowkey), max(c2) FROM " + tableName + " where rowkey >= 'a' GROUP BY c1"; // Use the default plan when stats are not available. - verifyQueryPlan(query, - "UNION ALL OVER 2 QUERIES\n" + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + tableName - + " [*] - ['z']\n" + " SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]\n" - + " CLIENT MERGE SORT\n" + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + tableName - + " ['a'] - [*]\n" + " SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]\n" - + " CLIENT MERGE SORT"); + assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES") + .iteratorType("PARALLEL").scanType("RANGE SCAN").table(tableName).keyRanges(" [*] - ['z']") + .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]") + .clientSortAlgo("CLIENT MERGE SORT").rhs().iteratorType("PARALLEL").scanType("RANGE SCAN") + .table(tableName).keyRanges(" ['a'] - [*]") + .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]") + .clientSortAlgo("CLIENT MERGE SORT"); PreparedStatement stmt = conn.prepareStatement("UPSERT INTO " + tableName + " (rowkey, c1, c2) VALUES (?, ?, ?)"); @@ -348,18 +317,16 @@ public class CostBasedDecisionIT extends BaseTest { conn.createStatement().execute("UPDATE STATISTICS " + tableName); // Use the optimal plan based on cost when stats become available. - verifyQueryPlan(query, - "UNION ALL OVER 2 QUERIES\n" + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + indexName - + "(" + tableName + ") [1]\n" + " SERVER MERGE [0.C2]\n" - + " SERVER PROJECTION FILTER BY FIRST KEY ONLY\n" - + " SERVER FILTER BY \"ROWKEY\" <= 'z'\n" - + " SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]\n" - + " CLIENT MERGE SORT\n" + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + indexName - + "(" + tableName + ") [1]\n" + " SERVER MERGE [0.C2]\n" - + " SERVER PROJECTION FILTER BY FIRST KEY ONLY\n" - + " SERVER FILTER BY \"ROWKEY\" >= 'a'\n" - + " SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]\n" - + " CLIENT MERGE SORT"); + assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES") + .iteratorType("PARALLEL").scanType("RANGE SCAN").table(indexName + "(" + tableName + ")") + .keyRanges(" [1]").serverMergeColumns("[0.C2]").serverFirstKeyOnlyProjection(true) + .serverWhereFilter("\"ROWKEY\" <= 'z'") + .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]") + .clientSortAlgo("CLIENT MERGE SORT").rhs().iteratorType("PARALLEL").scanType("RANGE SCAN") + .table(indexName + "(" + tableName + ")").keyRanges(" [1]").serverMergeColumns("[0.C2]") + .serverFirstKeyOnlyProjection(true).serverWhereFilter("\"ROWKEY\" >= 'a'") + .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]") + .clientSortAlgo("CLIENT MERGE SORT"); } finally { conn.close(); } @@ -383,12 +350,13 @@ public class CostBasedDecisionIT extends BaseTest { + " where rowkey <= 'z' GROUP BY c1) t2 " + "ON t1.rowkey = t2.mrk WHERE t1.c1 LIKE 'X0%' ORDER BY t1.rowkey"; // Use the default plan when stats are not available. - verifyQueryPlan(query, - "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + tableName + "\n" - + " SERVER FILTER BY C1 LIKE 'X0%'\n" + " PARALLEL INNER-JOIN TABLE 0\n" - + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + tableName + " [*] - ['z']\n" - + " SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]\n" - + " CLIENT MERGE SORT\n" + " DYNAMIC SERVER FILTER BY T1.ROWKEY IN (T2.MRK)"); + assertPlan(conn, query).iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName) + .serverWhereFilter("C1 LIKE 'X0%'") + .dynamicServerFilter("DYNAMIC SERVER FILTER BY T1.ROWKEY IN (T2.MRK)").subPlanCount(1) + .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(tableName).keyRanges(" [*] - ['z']") + .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]") + .clientSortAlgo("CLIENT MERGE SORT"); PreparedStatement stmt = conn.prepareStatement("UPSERT INTO " + tableName + " (rowkey, c1, c2) VALUES (?, ?, ?)"); @@ -403,18 +371,17 @@ public class CostBasedDecisionIT extends BaseTest { conn.createStatement().execute("UPDATE STATISTICS " + tableName); // Use the optimal plan based on cost when stats become available. - verifyQueryPlan(query, - "CLIENT PARALLEL 626-WAY RANGE SCAN OVER " + indexName + "(" + tableName - + ") [1,'X0'] - [1,'X1']\n" + " SERVER MERGE [0.C2]\n" - + " SERVER PROJECTION FILTER BY FIRST KEY ONLY\n" - + " SERVER SORTED BY [\"T1.:ROWKEY\"]\n" + "CLIENT MERGE SORT\n" - + " PARALLEL INNER-JOIN TABLE 0\n" + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " - + indexName + "(" + tableName + ") [1]\n" + " SERVER MERGE [0.C2]\n" - + " SERVER PROJECTION FILTER BY FIRST KEY ONLY\n" - + " SERVER FILTER BY \"ROWKEY\" <= 'z'\n" - + " SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]\n" - + " CLIENT MERGE SORT\n" - + " DYNAMIC SERVER FILTER BY \"T1.:ROWKEY\" IN (T2.MRK)"); + assertPlan(conn, query).iteratorType("PARALLEL").scanType("RANGE SCAN") + .table(indexName + "(" + tableName + ")").keyRanges(" [1,'X0'] - [1,'X1']") + .serverMergeColumns("[0.C2]").serverFirstKeyOnlyProjection(true) + .serverSortedBy("[\"T1.:ROWKEY\"]").clientSortAlgo("CLIENT MERGE SORT") + .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"T1.:ROWKEY\" IN (T2.MRK)").subPlanCount(1) + .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(indexName + "(" + tableName + ")").keyRanges(" [1]") + .serverMergeColumns("[0.C2]").serverFirstKeyOnlyProjection(true) + .serverWhereFilter("\"ROWKEY\" <= 'z'") + .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]") + .clientSortAlgo("CLIENT MERGE SORT"); } finally { conn.close(); } @@ -440,11 +407,7 @@ public class CostBasedDecisionIT extends BaseTest { String indexPlan = "(\"ROWKEY\" >= 1 AND \"ROWKEY\" <= 10)"; // Use the index table plan that opts out order-by when stats are not available. - ExplainPlan plan = conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class) - .optimizeQuery().getExplainPlan(); - ExplainPlanAttributes explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertTrue(explainPlanAttributes.isServerFirstKeyOnlyProjection()); - assertEquals(indexPlan, explainPlanAttributes.getServerWhereFilter()); + assertPlan(conn, query).serverFirstKeyOnlyProjection(true).serverWhereFilter(indexPlan); PreparedStatement stmt = conn.prepareStatement("UPSERT INTO " + tableName + " (rowkey, c1, c2) VALUES (?, ?, ?)"); @@ -459,17 +422,10 @@ public class CostBasedDecisionIT extends BaseTest { conn.createStatement().execute("UPDATE STATISTICS " + tableName); // Use the data table plan that has a lower cost when stats are available. - plan = conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery() - .getExplainPlan(); - explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals(dataPlan, explainPlanAttributes.getServerSortedBy()); + assertPlan(conn, query).serverSortedBy(dataPlan); // Use the index table plan as has been hinted. - plan = conn.prepareStatement(hintedQuery).unwrap(PhoenixPreparedStatement.class) - .optimizeQuery().getExplainPlan(); - explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertTrue(explainPlanAttributes.isServerFirstKeyOnlyProjection()); - assertEquals(indexPlan, explainPlanAttributes.getServerWhereFilter()); + assertPlan(conn, hintedQuery).serverFirstKeyOnlyProjection(true).serverWhereFilter(indexPlan); } finally { conn.close(); } @@ -482,17 +438,9 @@ public class CostBasedDecisionIT extends BaseTest { + "ON t1.ID = t2.ID"; Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); try (Connection conn = DriverManager.getConnection(getUrl(), props)) { - ExplainPlan plan = conn.prepareStatement(q).unwrap(PhoenixPreparedStatement.class) - .optimizeQuery().getExplainPlan(); - ExplainPlanAttributes explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("SORT-MERGE-JOIN (INNER)", explainPlanAttributes.getAbstractExplainPlan()); - assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals(testTable500, explainPlanAttributes.getTableName()); - ExplainPlanAttributes rhsTable = explainPlanAttributes.getRhsJoinQueryExplainPlan(); - assertEquals("PARALLEL 1-WAY", rhsTable.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", rhsTable.getExplainScanType()); - assertEquals(testTable1000, rhsTable.getTableName()); + assertPlan(conn, q).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").lhs() + .iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable500).end().rhs() + .iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable1000); } } @@ -505,20 +453,11 @@ public class CostBasedDecisionIT extends BaseTest { + "ON t1.ID = t2.ID\n" + "WHERE t1.COL1 < 200"; Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); try (Connection conn = DriverManager.getConnection(getUrl(), props)) { - ExplainPlan plan = conn.prepareStatement(q).unwrap(PhoenixPreparedStatement.class) - .optimizeQuery().getExplainPlan(); - ExplainPlanAttributes explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("SORT-MERGE-JOIN (INNER)", explainPlanAttributes.getAbstractExplainPlan()); - assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals("COL1 < 200", explainPlanAttributes.getServerWhereFilter()); - assertEquals(testTable500, explainPlanAttributes.getTableName()); - assertEquals("CLIENT AGGREGATE INTO SINGLE ROW", explainPlanAttributes.getClientAggregate()); - ExplainPlanAttributes rhsTable = explainPlanAttributes.getRhsJoinQueryExplainPlan(); - assertEquals("PARALLEL 1-WAY", rhsTable.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", rhsTable.getExplainScanType()); - assertTrue(rhsTable.isServerFirstKeyOnlyProjection()); - assertEquals(testTable1000, rhsTable.getTableName()); + assertPlan(conn, q).abstractExplainPlan("SORT-MERGE-JOIN (INNER)") + .clientAggregate("CLIENT AGGREGATE INTO SINGLE ROW").lhs().iteratorType("PARALLEL") + .scanType("FULL SCAN").table(testTable500).serverWhereFilter("COL1 < 200").end().rhs() + .iteratorType("PARALLEL").scanType("FULL SCAN").serverFirstKeyOnlyProjection(true) + .table(testTable1000); } } @@ -527,10 +466,13 @@ public class CostBasedDecisionIT extends BaseTest { public void testJoinStrategy3() throws Exception { String q = "SELECT *\n" + "FROM " + testTable500 + " t1 JOIN " + testTable1000 + " t2\n" + "ON t1.COL1 = t2.ID\n" + "WHERE t1.ID > 200"; - String expected = "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + testTable1000 + "\n" - + " PARALLEL INNER-JOIN TABLE 0\n" + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " - + testTable500 + " [201] - [*]\n" + " DYNAMIC SERVER FILTER BY T2.ID IN (T1.COL1)"; - verifyQueryPlan(q, expected); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + assertPlan(conn, q).iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable1000) + .dynamicServerFilter("DYNAMIC SERVER FILTER BY T2.ID IN (T1.COL1)").subPlanCount(1) + .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(testTable500).keyRanges(" [201] - [*]"); + } } /** @@ -541,10 +483,13 @@ public class CostBasedDecisionIT extends BaseTest { public void testJoinStrategy4() throws Exception { String q = "SELECT *\n" + "FROM " + testTable990 + " t1 JOIN " + testTable1000 + " t2\n" + "ON t1.ID = t2.COL1"; - String expected = "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + testTable990 + "\n" - + " PARALLEL INNER-JOIN TABLE 0\n" + " CLIENT PARALLEL 1-WAY FULL SCAN OVER " - + testTable1000 + "\n" + " DYNAMIC SERVER FILTER BY T1.ID IN (T2.COL1)"; - verifyQueryPlan(q, expected); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + assertPlan(conn, q).iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable990) + .dynamicServerFilter("DYNAMIC SERVER FILTER BY T1.ID IN (T2.COL1)").subPlanCount(1) + .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").iteratorType("PARALLEL") + .scanType("FULL SCAN").table(testTable1000); + } } /** Hash-join wins over sort-merge-join w/ smaller side ordered. */ @@ -552,10 +497,13 @@ public class CostBasedDecisionIT extends BaseTest { public void testJoinStrategy5() throws Exception { String q = "SELECT *\n" + "FROM " + testTable500 + " t1 JOIN " + testTable1000 + " t2\n" + "ON t1.ID = t2.COL1\n" + "WHERE t1.ID > 200"; - String expected = "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + testTable1000 + "\n" - + " PARALLEL INNER-JOIN TABLE 0\n" + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " - + testTable500 + " [201] - [*]"; - verifyQueryPlan(q, expected); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + assertPlan(conn, q).iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable1000) + .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0") + .iteratorType("PARALLEL").scanType("RANGE SCAN").table(testTable500) + .keyRanges(" [201] - [*]"); + } } /** Hash-join wins over sort-merge-join w/o any side ordered. */ @@ -563,10 +511,13 @@ public class CostBasedDecisionIT extends BaseTest { public void testJoinStrategy6() throws Exception { String q = "SELECT *\n" + "FROM " + testTable500 + " t1 JOIN " + testTable1000 + " t2\n" + "ON t1.COL1 = t2.COL1\n" + "WHERE t1.ID > 200"; - String expected = "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + testTable1000 + "\n" - + " PARALLEL INNER-JOIN TABLE 0\n" + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " - + testTable500 + " [201] - [*]"; - verifyQueryPlan(q, expected); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + assertPlan(conn, q).iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable1000) + .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0") + .iteratorType("PARALLEL").scanType("RANGE SCAN").table(testTable500) + .keyRanges(" [201] - [*]"); + } } /** @@ -578,11 +529,14 @@ public class CostBasedDecisionIT extends BaseTest { public void testJoinStrategy7() throws Exception { String q = "SELECT *\n" + "FROM " + testTable500 + " t1 JOIN " + testTable1000 + " t2\n" + "ON t1.ID = t2.ID\n" + "ORDER BY t1.COL1"; - String expected = "CLIENT PARALLEL 1001-WAY FULL SCAN OVER " + testTable1000 + "\n" - + " SERVER SORTED BY [T1.COL1]\n" + "CLIENT MERGE SORT\n" - + " PARALLEL INNER-JOIN TABLE 0\n" + " CLIENT PARALLEL 1-WAY FULL SCAN OVER " - + testTable500 + "\n" + " DYNAMIC SERVER FILTER BY T2.ID IN (T1.ID)"; - verifyQueryPlan(q, expected); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + assertPlan(conn, q).iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable1000) + .serverSortedBy("[T1.COL1]").clientSortAlgo("CLIENT MERGE SORT") + .dynamicServerFilter("DYNAMIC SERVER FILTER BY T2.ID IN (T1.ID)").subPlanCount(1).subPlan(0) + .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").iteratorType("PARALLEL") + .scanType("FULL SCAN").table(testTable500); + } } /** @@ -596,19 +550,9 @@ public class CostBasedDecisionIT extends BaseTest { + "ON t1.ID = t2.ID\n" + "ORDER BY t1.COL1 LIMIT 5"; Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); try (Connection conn = DriverManager.getConnection(getUrl(), props)) { - ExplainPlan plan = conn.prepareStatement(q).unwrap(PhoenixPreparedStatement.class) - .optimizeQuery().getExplainPlan(); - ExplainPlanAttributes explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("SORT-MERGE-JOIN (INNER)", explainPlanAttributes.getAbstractExplainPlan()); - assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals(testTable500, explainPlanAttributes.getTableName()); - assertEquals("[T1.COL1]", explainPlanAttributes.getClientSortedBy()); - assertEquals(new Integer(5), explainPlanAttributes.getClientRowLimit()); - ExplainPlanAttributes rhsTable = explainPlanAttributes.getRhsJoinQueryExplainPlan(); - assertEquals("PARALLEL 1-WAY", rhsTable.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", rhsTable.getExplainScanType()); - assertEquals(testTable1000, rhsTable.getTableName()); + assertPlan(conn, q).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").clientSortedBy("[T1.COL1]") + .clientRowLimit(5).lhs().iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable500) + .end().rhs().iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable1000); } } @@ -620,28 +564,36 @@ public class CostBasedDecisionIT extends BaseTest { String q = "SELECT *\n" + "FROM " + testTable1000 + " t1 LEFT JOIN " + testTable500 + " t2\n" + "ON t1.ID = t2.ID AND t2.ID > 200\n" + "LEFT JOIN " + testTable990 + " t3\n" + "ON t1.ID = t3.ID AND t3.ID < 100"; - String expected = "SORT-MERGE-JOIN (LEFT) TABLES\n" + " SORT-MERGE-JOIN (LEFT) TABLES\n" - + " CLIENT PARALLEL 1-WAY FULL SCAN OVER " + testTable1000 + "\n" + " AND\n" - + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + testTable500 + " [201] - [*]\n" + "AND\n" - + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + testTable990 + " [*] - [100]"; - verifyQueryPlan(q, expected); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + assertPlan(conn, q).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").lhs() + .abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").lhs().iteratorType("PARALLEL") + .scanType("FULL SCAN").table(testTable1000).end().rhs().iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(testTable500).keyRanges(" [201] - [*]").end().end().rhs() + .iteratorType("PARALLEL").scanType("RANGE SCAN").table(testTable990) + .keyRanges(" [*] - [100]"); + } } /** - * Multi-table join: a mix of join strategies chosen based on cost. + * Multi-table join: a mix of join strategies chosen based on cost. SMJ root, lhs is hash-join, + * rhs is range scan. */ @Test public void testJoinStrategy10() throws Exception { String q = "SELECT *\n" + "FROM " + testTable1000 + " t1 JOIN " + testTable500 + " t2\n" + "ON t1.ID = t2.COL1 AND t2.ID > 200\n" + "JOIN " + testTable990 + " t3\n" + "ON t1.ID = t3.ID AND t3.ID < 100"; - String expected = - "SORT-MERGE-JOIN (INNER) TABLES\n" + " CLIENT PARALLEL 1-WAY FULL SCAN OVER " - + testTable1000 + "\n" + " PARALLEL INNER-JOIN TABLE 0\n" - + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + testTable500 + " [201] - [*]\n" - + " DYNAMIC SERVER FILTER BY T1.ID IN (T2.COL1)\n" + "AND\n" - + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + testTable990 + " [*] - [100]"; - verifyQueryPlan(q, expected); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + assertPlan(conn, q).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").lhs() + .iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable1000) + .dynamicServerFilter("DYNAMIC SERVER FILTER BY T1.ID IN (T2.COL1)").subPlanCount(1) + .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(testTable500).keyRanges(" [201] - [*]").end().end().rhs() + .iteratorType("PARALLEL").scanType("RANGE SCAN").table(testTable990) + .keyRanges(" [*] - [100]"); + } } /** @@ -653,11 +605,15 @@ public class CostBasedDecisionIT extends BaseTest { String q = "SELECT *\n" + "FROM " + testTable1000 + " t1 JOIN " + testTable500 + " t2\n" + "ON t1.COL2 = t2.COL1 AND t2.ID > 200\n" + "JOIN " + testTable990 + " t3\n" + "ON t1.COL1 = t3.COL2 AND t3.ID < 100"; - String expected = "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + testTable1000 + "\n" - + " PARALLEL INNER-JOIN TABLE 0\n" + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " - + testTable500 + " [201] - [*]\n" + " PARALLEL INNER-JOIN TABLE 1\n" - + " CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + testTable990 + " [*] - [100]"; - verifyQueryPlan(q, expected); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + assertPlan(conn, q).iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable1000) + .subPlanCount(2).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0") + .iteratorType("PARALLEL").scanType("RANGE SCAN").table(testTable500) + .keyRanges(" [201] - [*]").end().subPlan(1) + .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 1").iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(testTable990).keyRanges(" [*] - [100]"); + } } /** @@ -668,29 +624,16 @@ public class CostBasedDecisionIT extends BaseTest { public void testJoinStrategy12() throws Exception { String q = "SELECT *\n" + "FROM " + testTable1000 + " t1 JOIN " + testTable990 + " t2\n" + "ON t1.COL2 = t2.COL1\n" + "JOIN " + testTable990 + " t3\n" + "ON t1.COL1 = t3.COL2"; - String expected = - "SORT-MERGE-JOIN (INNER) TABLES\n" + " CLIENT PARALLEL 1001-WAY FULL SCAN OVER " - + testTable1000 + "\n" + " SERVER SORTED BY [T1.COL1]\n" + " CLIENT MERGE SORT\n" - + " PARALLEL INNER-JOIN TABLE 0\n" - + " CLIENT PARALLEL 1-WAY FULL SCAN OVER " + testTable990 + "\n" + "AND\n" - + " CLIENT PARALLEL 991-WAY FULL SCAN OVER " + testTable990 + "\n" - + " SERVER SORTED BY [T3.COL2]\n" + " CLIENT MERGE SORT"; - verifyQueryPlan(q, expected); - } - - private static void verifyQueryPlan(String query, String expected) throws Exception { Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); - Connection conn = DriverManager.getConnection(getUrl(), props); - ResultSet rs = conn.createStatement().executeQuery("explain " + query); - String plan = QueryUtil.getExplainPlan(rs); - // These assertions verify cost-based plan structure (scan type, join strategy, ordering). The - // per-scan INDEX and REGIONS PLANNED annotations carry guidepost-derived region counts that are - // covered directly by the attribute-based tests, so strip them here to keep these structural - // substring checks stable. - String normalizedPlan = plan.replaceAll("(?m)^ *INDEX [^\n]*\n", "") - .replaceAll("(?m)^ *REGIONS PLANNED [^\n]*\n", ""); - assertTrue("Expected '" + expected + "' in the plan:\n" + plan + ".", - normalizedPlan.contains(expected)); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + assertPlan(conn, q).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").lhs() + .iteratorType("PARALLEL").scanType("FULL SCAN").table(testTable1000) + .serverSortedBy("[T1.COL1]").clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0) + .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").iteratorType("PARALLEL") + .scanType("FULL SCAN").table(testTable990).end().end().rhs().iteratorType("PARALLEL") + .scanType("FULL SCAN").table(testTable990).serverSortedBy("[T3.COL2]") + .clientSortAlgo("CLIENT MERGE SORT"); + } } private static String initTestTableValues(int rows) throws Exception { diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/DerivedTableIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/DerivedTableIT.java index d35960fae9..2e809ae609 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/DerivedTableIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/DerivedTableIT.java @@ -17,6 +17,7 @@ */ package org.apache.phoenix.end2end; +import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan; import static org.apache.phoenix.util.TestUtil.A_VALUE; import static org.apache.phoenix.util.TestUtil.B_VALUE; import static org.apache.phoenix.util.TestUtil.C_VALUE; @@ -43,8 +44,8 @@ import java.sql.ResultSet; import java.util.Collection; import java.util.List; import java.util.Properties; +import org.apache.phoenix.query.explain.ExplainPlanTestUtil.ExplainPlanAssert; import org.apache.phoenix.util.PropertiesUtil; -import org.apache.phoenix.util.QueryUtil; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -54,8 +55,6 @@ import org.junit.rules.TestName; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.phoenix.thirdparty.com.google.common.collect.Lists; @@ -68,12 +67,31 @@ public class DerivedTableIT extends ParallelStatsDisabledIT { public TestName name = new TestName(); private String[] indexDDL; - private String[] plans; + private PlanSpec[] plans; private String tableName; - private static final Logger LOGGER = LoggerFactory.getLogger(DerivedTableIT.class); + /** + * Structured expected plan for one query under {@link #testNestedAggregationsWithGroupBy}. + * Captures the table the server-side scan targets, the {@code SERVER AGGREGATE} string, and the + * ordered list of client-side pipeline steps. + */ + private static final class PlanSpec { + final String tableSuffix; + final String serverAggregate; + final String[] clientSteps; + + PlanSpec(String tableSuffix, String serverAggregate, String... clientSteps) { + this.tableSuffix = tableSuffix; + this.serverAggregate = serverAggregate; + this.clientSteps = clientSteps; + } + + String resolvedTable(String concreteTableName) { + return tableSuffix.replace(dynamicTableName, concreteTableName); + } + } - public DerivedTableIT(String[] indexDDL, String[] plans) { + public DerivedTableIT(String[] indexDDL, PlanSpec[] plans) { this.indexDDL = indexDDL; this.plans = plans; } @@ -92,13 +110,6 @@ public class DerivedTableIT extends ParallelStatsDisabledIT { conn.createStatement().execute(ddl); } } - String[] newplan = new String[plans.length]; - if (plans != null && plans.length > 0) { - for (int i = 0; i < plans.length; i++) { - newplan[i] = plans[i].replace(dynamicTableName, tableName); - } - plans = newplan; - } } @After @@ -109,41 +120,36 @@ public class DerivedTableIT extends ParallelStatsDisabledIT { } @Parameters(name = "DerivedTableIT_{index}") // name is used by failsafe as file name in reports - public static synchronized Collection<Object> data() { - List<Object> testCases = Lists.newArrayList(); - testCases.add(new String[][] { - { "CREATE INDEX " + dynamicTableName + "_DERIVED_IDX ON " + dynamicTableName + public static synchronized Collection<Object[]> data() { + List<Object[]> testCases = Lists.newArrayList(); + // Indexed case: server-side aggregate runs over the covered index (PARALLEL 1-WAY). + testCases.add(new Object[] { + new String[] { "CREATE INDEX " + dynamicTableName + "_DERIVED_IDX ON " + dynamicTableName + " (a_byte) INCLUDE (A_STRING, B_STRING)" }, - { "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + dynamicTableName + "_DERIVED_IDX\n" - + " SERVER AGGREGATE INTO DISTINCT ROWS BY [\"A_STRING\", \"B_STRING\"]\n" - + "CLIENT MERGE SORT\n" + "CLIENT SORTED BY [\"B_STRING\"]\n" + "CLIENT SORTED BY [A]\n" - + "CLIENT AGGREGATE INTO DISTINCT ROWS BY [A]\n" + "CLIENT SORTED BY [A DESC]", - - "CLIENT PARALLEL 1-WAY FULL SCAN OVER " + dynamicTableName + "_DERIVED_IDX\n" - + " SERVER AGGREGATE INTO DISTINCT ROWS BY [\"A_STRING\", \"B_STRING\"]\n" - + "CLIENT MERGE SORT\n" + "CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY [A]\n" - + "CLIENT DISTINCT ON [COLLECTDISTINCT(B)]\n" + "CLIENT SORTED BY [A DESC]" } }); - testCases.add(new String[][] { {}, - { "CLIENT PARALLEL 4-WAY FULL SCAN OVER " + dynamicTableName + "\n" - + " SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING, B_STRING]\n" - + "CLIENT MERGE SORT\n" + "CLIENT SORTED BY [B_STRING]\n" + "CLIENT SORTED BY [A]\n" - + "CLIENT AGGREGATE INTO DISTINCT ROWS BY [A]\n" + "CLIENT SORTED BY [A DESC]", - - "CLIENT PARALLEL 4-WAY FULL SCAN OVER " + dynamicTableName + "\n" - + " SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING, B_STRING]\n" - + "CLIENT MERGE SORT\n" + "CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY [A]\n" - + "CLIENT DISTINCT ON [COLLECTDISTINCT(B)]\n" + "CLIENT SORTED BY [A DESC]" } }); + new PlanSpec[] { + new PlanSpec(dynamicTableName + "_DERIVED_IDX", + "SERVER AGGREGATE INTO DISTINCT ROWS BY [\"A_STRING\", \"B_STRING\"]", + "CLIENT MERGE SORT", "CLIENT SORTED BY [\"B_STRING\"]", "CLIENT SORTED BY [A]", + "CLIENT AGGREGATE INTO DISTINCT ROWS BY [A]", "CLIENT SORTED BY [A DESC]"), + new PlanSpec(dynamicTableName + "_DERIVED_IDX", + "SERVER AGGREGATE INTO DISTINCT ROWS BY [\"A_STRING\", \"B_STRING\"]", + "CLIENT MERGE SORT", "CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY [A]", + "CLIENT DISTINCT ON [COLLECTDISTINCT(B)]", "CLIENT SORTED BY [A DESC]") } }); + // Non-indexed case: server-side aggregate runs over the data table (PARALLEL 4-WAY). + testCases.add(new Object[] { new String[] {}, new PlanSpec[] { + new PlanSpec(dynamicTableName, "SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING, B_STRING]", + "CLIENT MERGE SORT", "CLIENT SORTED BY [B_STRING]", "CLIENT SORTED BY [A]", + "CLIENT AGGREGATE INTO DISTINCT ROWS BY [A]", "CLIENT SORTED BY [A DESC]"), + new PlanSpec(dynamicTableName, "SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING, B_STRING]", + "CLIENT MERGE SORT", "CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY [A]", + "CLIENT DISTINCT ON [COLLECTDISTINCT(B)]", "CLIENT SORTED BY [A DESC]") } }); return testCases; } - /** - * Removes the per-scan {@code INDEX} and {@code REGIONS PLANNED} annotation lines so these - * structural plan assertions stay stable; those attributes are covered by the attribute-based - * tests and {@code REGIONS PLANNED} carries an environment-dependent region count. - */ - private static String stripScanAnnotations(String plan) { - return plan.replaceAll("(?m)^ *INDEX [^\n]*\n", "") - .replaceAll("(?m)^ *REGIONS PLANNED [^\n]*\n", ""); + private void verifyPlan(Connection conn, String query, PlanSpec spec) throws Exception { + ExplainPlanAssert assertion = assertPlan(conn, query); + assertion.iteratorType("PARALLEL").scanType("FULL SCAN").table(spec.resolvedTable(tableName)) + .serverAggregate(spec.serverAggregate).clientSteps(spec.clientSteps); } @Test @@ -394,12 +400,7 @@ public class DerivedTableIT extends ParallelStatsDisabledIT { assertFalse(rs.next()); - rs = conn.createStatement().executeQuery("EXPLAIN WITH REGIONS " + query); - String explainPlanOutput = QueryUtil.getExplainPlan(rs); - LOGGER.info("Explain plan output: {}", explainPlanOutput); - String[] splitExplainPlan = explainPlanOutput.split("\\n \\(region locations = \\[region="); - String[] secondSplitExplainPlan = splitExplainPlan[1].split("]\\)"); - assertEquals(plans[0], stripScanAnnotations(splitExplainPlan[0] + secondSplitExplainPlan[1])); + verifyPlan(conn, query, plans[0]); // distinct b (groupby a, b) groupby a orderby a query = "SELECT DISTINCT COLLECTDISTINCT(t.b) FROM (SELECT b_string b, a_string a FROM " @@ -421,12 +422,7 @@ public class DerivedTableIT extends ParallelStatsDisabledIT { assertFalse(rs.next()); - rs = conn.createStatement().executeQuery("EXPLAIN WITH REGIONS " + query); - explainPlanOutput = QueryUtil.getExplainPlan(rs); - LOGGER.info("Explain plan output: {}", explainPlanOutput); - splitExplainPlan = explainPlanOutput.split("\\n \\(region locations = \\[region="); - secondSplitExplainPlan = splitExplainPlan[1].split("]\\)"); - assertEquals(plans[1], stripScanAnnotations(splitExplainPlan[0] + secondSplitExplainPlan[1])); + verifyPlan(conn, query, plans[1]); // (orderby) groupby query = "SELECT t.a_string, count(*) FROM (SELECT * FROM " + tableName diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SortMergeJoinMoreIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SortMergeJoinMoreIT.java index 0116d31e1a..4a626c01a7 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SortMergeJoinMoreIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SortMergeJoinMoreIT.java @@ -427,14 +427,14 @@ public class SortMergeJoinMoreIT extends ParallelStatsDisabledIT { String rhsClientSortedBy = i == 0 ? "[BUCKET, \"TIMESTAMP\"]" : null; assertPlan(conn, q).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(true) - .iteratorType("PARALLEL").scanType("SKIP SCAN ON 2 RANGES").table(eventCountTableName) - .keyRanges(lhsKeyRanges).serverFirstKeyOnlyProjection(true) + .lhs().iteratorType("PARALLEL").scanType("SKIP SCAN ON 2 RANGES") + .table(eventCountTableName).keyRanges(lhsKeyRanges).serverFirstKeyOnlyProjection(true) .serverDistinctFilter("SERVER DISTINCT PREFIX FILTER OVER [BUCKET, TIMESTAMP, LOCATION]") .serverAggregate( "SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [BUCKET, TIMESTAMP, LOCATION]") .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[BUCKET, TIMESTAMP]") .clientAggregate("CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY [E.BUCKET, E.TIMESTAMP]") - .rhs().iteratorType("PARALLEL").scanType("SKIP SCAN ON 2 RANGES").table(t[i]) + .end().rhs().iteratorType("PARALLEL").scanType("SKIP SCAN ON 2 RANGES").table(t[i]) .keyRanges(rhsKeyRanges).serverFirstKeyOnlyProjection(true) .serverWhereFilter("SRC_LOCATION = DST_LOCATION") .serverDistinctFilter( diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/TenantSpecificViewIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/TenantSpecificViewIndexIT.java index 0a8ef6a425..dfc9c7a7c9 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/TenantSpecificViewIndexIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/TenantSpecificViewIndexIT.java @@ -17,6 +17,7 @@ */ package org.apache.phoenix.end2end; +import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan; import static org.apache.phoenix.util.MetaDataUtil.getViewIndexSequenceName; import static org.apache.phoenix.util.MetaDataUtil.getViewIndexSequenceSchemaName; import static org.apache.phoenix.util.PhoenixRuntime.TENANT_ID_ATTRIB; @@ -41,7 +42,6 @@ import org.apache.phoenix.schema.ColumnNotFoundException; import org.apache.phoenix.schema.PNameFactory; import org.apache.phoenix.util.MetaDataUtil; import org.apache.phoenix.util.PhoenixRuntime; -import org.apache.phoenix.util.QueryUtil; import org.apache.phoenix.util.SchemaUtil; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -358,40 +358,33 @@ public class TenantSpecificViewIndexIT extends BaseTenantSpecificViewIndexIT { viewConn.createStatement() .execute("CREATE VIEW IF NOT EXISTS " + viewName + " AS SELECT * FROM " + tableName); - String query = "EXPLAIN SELECT PARENT_ID FROM " + viewName + " WHERE PARENT_TYPE='001' " + String query1 = "SELECT PARENT_ID FROM " + viewName + " WHERE PARENT_TYPE='001' " + "AND (CREATED_DATE > to_date('2011-01-01') AND CREATED_DATE < to_date('2016-10-31'))" + "ORDER BY PARENT_TYPE,CREATED_DATE LIMIT 501"; - ResultSet rs = viewConn.createStatement().executeQuery(query); - String exptectedIndexName = SchemaUtil.getTableName(SCHEMA1, "IDX"); - String expectedPlanFormat = "CLIENT SERIAL 1-WAY RANGE SCAN OVER " + exptectedIndexName - + " ['tenant1 ','001','%s 00:00:00.001'] - ['tenant1 ','001','%s 00:00:00.000']" - + "\n" + " SERVER PROJECTION FILTER BY FIRST KEY ONLY" + "\n" - + " SERVER 501 ROW LIMIT" + "\n" + "CLIENT 501 ROW LIMIT"; - assertEquals(String.format(expectedPlanFormat, "2011-01-01", "2016-10-31"), - stripScanAnnotations(QueryUtil.getExplainPlan(rs))); + String expectedIndexName = SchemaUtil.getTableName(SCHEMA1, "IDX"); + String expectedKeyRangesFormat = + " ['tenant1 ','001','%s 00:00:00.001'] - ['tenant1 ','001','%s 00:00:00.000']"; - query = "EXPLAIN SELECT PARENT_ID FROM " + viewName + " WHERE PARENT_TYPE='001' " + assertPlan(viewConn, query1).iteratorType("SERIAL").scanType("RANGE SCAN") + .indexName(expectedIndexName) + .keyRanges(String.format(expectedKeyRangesFormat, "2011-01-01", "2016-10-31")) + .serverFirstKeyOnlyProjection(true).serverRowLimit(501L).clientRowLimit(501) + .clientSteps("CLIENT 501 ROW LIMIT"); + + String query2 = "SELECT PARENT_ID FROM " + viewName + " WHERE PARENT_TYPE='001' " + " AND (CREATED_DATE >= to_date('2011-01-01') AND CREATED_DATE <= to_date('2016-01-01'))" + " AND (CREATED_DATE > to_date('2012-10-21') AND CREATED_DATE < to_date('2016-10-31')) " + "ORDER BY PARENT_TYPE,CREATED_DATE LIMIT 501"; - rs = viewConn.createStatement().executeQuery(query); - assertEquals(String.format(expectedPlanFormat, "2012-10-21", "2016-01-01"), - stripScanAnnotations(QueryUtil.getExplainPlan(rs))); + assertPlan(viewConn, query2).iteratorType("SERIAL").scanType("RANGE SCAN") + .indexName(expectedIndexName) + .keyRanges(String.format(expectedKeyRangesFormat, "2012-10-21", "2016-01-01")) + .serverFirstKeyOnlyProjection(true).serverRowLimit(501L).clientRowLimit(501) + .clientSteps("CLIENT 501 ROW LIMIT"); } } - /** - * Removes the per-scan {@code INDEX} and {@code REGIONS PLANNED} annotation lines so this plan - * assertion stays stable; those attributes are covered by the attribute-based tests and - * {@code REGIONS PLANNED} carries an environment-dependent region count. - */ - private static String stripScanAnnotations(String plan) { - return plan.replaceAll("(?m)^ *INDEX [^\n]*\n", "") - .replaceAll("(?m)^ *REGIONS PLANNED [^\n]*\n", ""); - } - @Test public void testCurrentTimeWithViewIndexes() throws Exception { diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UnionAllIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UnionAllIT.java index e468e6a7ba..7144653792 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UnionAllIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UnionAllIT.java @@ -17,10 +17,10 @@ */ package org.apache.phoenix.end2end; +import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan; import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -30,15 +30,10 @@ import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; import java.util.Properties; -import org.apache.phoenix.compile.ExplainPlan; -import org.apache.phoenix.compile.ExplainPlanAttributes; import org.apache.phoenix.exception.SQLExceptionCode; -import org.apache.phoenix.jdbc.PhoenixPreparedStatement; import org.apache.phoenix.query.QueryServices; import org.apache.phoenix.util.PropertiesUtil; -import org.apache.phoenix.util.QueryUtil; import org.apache.phoenix.util.TestUtil; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -621,84 +616,34 @@ public class UnionAllIT extends ParallelStatsDisabledIT { + " CONSTRAINT pk PRIMARY KEY (a_string))\n"; createTestTable(getUrl(), ddl); - ddl = "select a_string, col1 from " + tableName1 + " union all select a_string, col1 from " - + tableName2 + " order by col1 limit 1"; - ExplainPlan plan = conn.prepareStatement(ddl).unwrap(PhoenixPreparedStatement.class) - .optimizeQuery().getExplainPlan(); - ExplainPlanAttributes explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("UNION ALL OVER 2 QUERIES", explainPlanAttributes.getAbstractExplainPlan()); - assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals(tableName1, explainPlanAttributes.getTableName()); - assertEquals("[COL1]", explainPlanAttributes.getServerSortedBy()); - assertEquals(1L, explainPlanAttributes.getServerRowLimit().longValue()); - assertEquals(1, explainPlanAttributes.getClientRowLimit().intValue()); - assertEquals("CLIENT MERGE SORT", explainPlanAttributes.getClientSortAlgo()); - ExplainPlanAttributes rhsPlanAttributes = explainPlanAttributes.getRhsJoinQueryExplainPlan(); - assertEquals("PARALLEL 1-WAY", rhsPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", rhsPlanAttributes.getExplainScanType()); - assertEquals(tableName2, rhsPlanAttributes.getTableName()); - assertEquals("[COL1]", rhsPlanAttributes.getServerSortedBy()); - assertEquals(1L, rhsPlanAttributes.getServerRowLimit().longValue()); - assertEquals(1, rhsPlanAttributes.getClientRowLimit().intValue()); - assertEquals("CLIENT MERGE SORT", rhsPlanAttributes.getClientSortAlgo()); - - String limitPlan = "UNION ALL OVER 2 QUERIES\n" + " CLIENT SERIAL 1-WAY FULL SCAN OVER " - + tableName1 + "\n" + " SERVER 2 ROW LIMIT\n" + " CLIENT 2 ROW LIMIT\n" - + " CLIENT SERIAL 1-WAY FULL SCAN OVER " + tableName2 + "\n" - + " SERVER 2 ROW LIMIT\n" + " CLIENT 2 ROW LIMIT\n" + "CLIENT 2 ROW LIMIT"; - - ddl = "select a_string, col1 from " + tableName1 + " union all select a_string, col1 from " - + tableName2 + " limit 2"; - plan = conn.prepareStatement(ddl).unwrap(PhoenixPreparedStatement.class).optimizeQuery() - .getExplainPlan(); - explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("UNION ALL OVER 2 QUERIES", explainPlanAttributes.getAbstractExplainPlan()); - assertEquals("SERIAL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals(tableName1, explainPlanAttributes.getTableName()); - assertNull(explainPlanAttributes.getServerSortedBy()); - assertEquals(2L, explainPlanAttributes.getServerRowLimit().longValue()); - assertEquals(2, explainPlanAttributes.getClientRowLimit().intValue()); - rhsPlanAttributes = explainPlanAttributes.getRhsJoinQueryExplainPlan(); - assertEquals("SERIAL 1-WAY", rhsPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", rhsPlanAttributes.getExplainScanType()); - assertEquals(tableName2, rhsPlanAttributes.getTableName()); - assertNull(rhsPlanAttributes.getServerSortedBy()); - assertEquals(2L, rhsPlanAttributes.getServerRowLimit().longValue()); - assertEquals(2, rhsPlanAttributes.getClientRowLimit().intValue()); - - Statement stmt = conn.createStatement(); - stmt.setMaxRows(2); - ResultSet rs = stmt.executeQuery("explain " + ddl); - assertEquals(limitPlan, stripScanAnnotations(QueryUtil.getExplainPlan(rs))); - - ddl = "select a_string, col1 from " + tableName1 + " union all select a_string, col1 from " - + tableName2; - plan = conn.prepareStatement(ddl).unwrap(PhoenixPreparedStatement.class).optimizeQuery() - .getExplainPlan(); - explainPlanAttributes = plan.getPlanStepsAsAttributes(); - assertEquals("UNION ALL OVER 2 QUERIES", explainPlanAttributes.getAbstractExplainPlan()); - assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", explainPlanAttributes.getExplainScanType()); - assertEquals(tableName1, explainPlanAttributes.getTableName()); - rhsPlanAttributes = explainPlanAttributes.getRhsJoinQueryExplainPlan(); - assertEquals("PARALLEL 1-WAY", rhsPlanAttributes.getIteratorTypeAndScanSize()); - assertEquals("FULL SCAN ", rhsPlanAttributes.getExplainScanType()); - assertEquals(tableName2, rhsPlanAttributes.getTableName()); + String orderLimit = "select a_string, col1 from " + tableName1 + + " union all select a_string, col1 from " + tableName2 + " order by col1 limit 1"; + assertPlan(conn, orderLimit).abstractExplainPlan("UNION ALL OVER 2 QUERIES") + .iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName1).serverSortedBy("[COL1]") + .serverRowLimit(1L).clientRowLimit(1).clientSortAlgo("CLIENT MERGE SORT").rhs() + .iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName2).serverSortedBy("[COL1]") + .serverRowLimit(1L).clientRowLimit(1).clientSortAlgo("CLIENT MERGE SORT"); + + String limitOnly = "select a_string, col1 from " + tableName1 + + " union all select a_string, col1 from " + tableName2 + " limit 2"; + // The LHS branch's builder is shared with the union plan's root builder, so the LHS + // root carries both the inner branch's CLIENT 2 ROW LIMIT and the union's outer + // CLIENT 2 ROW LIMIT in its clientSteps. The RHS branch has its own separate builder. + assertPlan(conn, limitOnly).abstractExplainPlan("UNION ALL OVER 2 QUERIES") + .iteratorType("SERIAL").scanType("FULL SCAN").table(tableName1).serverSortedBy(null) + .serverRowLimit(2L).clientRowLimit(2) + .clientSteps("CLIENT 2 ROW LIMIT", "CLIENT 2 ROW LIMIT").rhs().iteratorType("SERIAL") + .scanType("FULL SCAN").table(tableName2).serverSortedBy(null).serverRowLimit(2L) + .clientRowLimit(2).clientSteps("CLIENT 2 ROW LIMIT"); + + String noLimit = "select a_string, col1 from " + tableName1 + + " union all select a_string, col1 from " + tableName2; + assertPlan(conn, noLimit).abstractExplainPlan("UNION ALL OVER 2 QUERIES") + .iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName1).rhs() + .iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName2); } } - /** - * Removes the per-scan {@code INDEX} and {@code REGIONS PLANNED} annotation lines so this - * structural plan assertion stays stable; those attributes are covered by the attribute-based - * assertions above and {@code REGIONS PLANNED} carries an environment-dependent region count. - */ - private static String stripScanAnnotations(String plan) { - return plan.replaceAll("(?m)^ *INDEX [^\n]*\n", "") - .replaceAll("(?m)^ *REGIONS PLANNED [^\n]*\n", ""); - } - @Test public void testBug2295() throws Exception { String tableName1 = generateUniqueName(); diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinGlobalIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinGlobalIndexIT.java index 108be15272..170b295a7a 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinGlobalIndexIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinGlobalIndexIT.java @@ -60,25 +60,24 @@ public class SortMergeJoinGlobalIndexIT extends SortMergeJoinIT { String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME); String itemIndex = getSchemaName() + ".idx_item"; String supplierIndex = getSchemaName() + ".idx_supplier"; - assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").scanType("FULL SCAN") - .table(supplierIndex).serverFirstKeyOnlyProjection(true) - .serverSortedBy("[\"S.:supplier_id\"]").clientSortAlgo("CLIENT MERGE SORT") - .sortMergeSkipMerge(false).rhs().abstractExplainPlan("SORT-MERGE-JOIN (INNER)") - .scanType("FULL SCAN").table(itemIndex).serverSortedBy("[\"I.:item_id\"]") - .clientSortAlgo("CLIENT MERGE SORT").sortMergeSkipMerge(true) - .clientSortedBy("[\"I.0:supplier_id\"]").rhs().scanType("FULL SCAN").table(order) - .serverWhereFilter("QUANTITY < 5000").serverSortedBy("[\"O.item_id\"]") - .clientSortAlgo("CLIENT MERGE SORT").end().end(); + assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").sortMergeSkipMerge(false) + .lhs().scanType("FULL SCAN").table(supplierIndex).serverFirstKeyOnlyProjection(true) + .serverSortedBy("[\"S.:supplier_id\"]").clientSortAlgo("CLIENT MERGE SORT").end().rhs() + .abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(true) + .clientSortedBy("[\"I.0:supplier_id\"]").lhs().scanType("FULL SCAN").table(itemIndex) + .serverSortedBy("[\"I.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end().rhs() + .scanType("FULL SCAN").table(order).serverWhereFilter("QUANTITY < 5000") + .serverSortedBy("[\"O.item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end().end(); } @Override protected void assertSelfJoinPlan(Connection conn, String query) throws Exception { String itemIndex = getSchemaName() + ".idx_item"; - assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").scanType("FULL SCAN") - .table(itemIndex).serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I1.:item_id\"]") - .clientSortAlgo("CLIENT MERGE SORT").sortMergeSkipMerge(false).rhs().scanType("FULL SCAN") - .table(itemIndex).serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I2.:item_id\"]") - .clientSortAlgo("CLIENT MERGE SORT").end(); + assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false) + .lhs().scanType("FULL SCAN").table(itemIndex).serverFirstKeyOnlyProjection(true) + .serverSortedBy("[\"I1.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end().rhs() + .scanType("FULL SCAN").table(itemIndex).serverFirstKeyOnlyProjection(true) + .serverSortedBy("[\"I2.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end(); } @Override @@ -91,10 +90,10 @@ public class SortMergeJoinGlobalIndexIT extends SortMergeJoinIT { statement.setMaxRows(4); ExplainPlanAttributes attributes = statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes(); - assertPlan(attributes).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").scanType("FULL SCAN") - .table(itemIndex).serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I.:item_id\"]") - .clientSortAlgo("CLIENT MERGE SORT").sortMergeSkipMerge(false).clientRowLimit(4).rhs() - .scanType("FULL SCAN").table(order) + assertPlan(attributes).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false) + .clientRowLimit(4).lhs().scanType("FULL SCAN").table(itemIndex) + .serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I.:item_id\"]") + .clientSortAlgo("CLIENT MERGE SORT").end().rhs().scanType("FULL SCAN").table(order) .serverSortedBy(queryIndex == 0 ? "[\"O.item_id\"]" : "[\"item_id\"]") .clientSortAlgo("CLIENT MERGE SORT").end(); } diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinLocalIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinLocalIndexIT.java index 8b3cd696eb..53abdd2569 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinLocalIndexIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinLocalIndexIT.java @@ -62,14 +62,14 @@ public class SortMergeJoinLocalIndexIT extends SortMergeJoinIT { String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME); String itemIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_ITEM_INDEX); String supplierIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_SUPPLIER_INDEX); - assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").scanType("RANGE SCAN") - .table(supplierIndex + "(" + supplier + ")").keyRanges(" [1]") + assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").sortMergeSkipMerge(false) + .lhs().scanType("RANGE SCAN").table(supplierIndex + "(" + supplier + ")").keyRanges(" [1]") .serverFirstKeyOnlyProjection(true).serverSortedBy("[\"S.:supplier_id\"]") - .clientSortAlgo("CLIENT MERGE SORT").sortMergeSkipMerge(false).rhs() - .abstractExplainPlan("SORT-MERGE-JOIN (INNER)").scanType("RANGE SCAN") + .clientSortAlgo("CLIENT MERGE SORT").end().rhs() + .abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(true) + .clientSortedBy("[\"I.0:supplier_id\"]").lhs().scanType("RANGE SCAN") .table(itemIndex + "(" + item + ")").keyRanges(" [1]").serverSortedBy("[\"I.:item_id\"]") - .clientSortAlgo("CLIENT MERGE SORT").sortMergeSkipMerge(true) - .clientSortedBy("[\"I.0:supplier_id\"]").rhs().scanType("FULL SCAN").table(order) + .clientSortAlgo("CLIENT MERGE SORT").end().rhs().scanType("FULL SCAN").table(order) .serverWhereFilter("QUANTITY < 5000").serverSortedBy("[\"O.item_id\"]") .clientSortAlgo("CLIENT MERGE SORT").end().end(); } @@ -78,12 +78,12 @@ public class SortMergeJoinLocalIndexIT extends SortMergeJoinIT { protected void assertSelfJoinPlan(Connection conn, String query) throws Exception { String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME); String itemIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_ITEM_INDEX); - assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").scanType("RANGE SCAN") + assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false) + .lhs().scanType("RANGE SCAN").table(itemIndex + "(" + item + ")").keyRanges(" [1]") + .serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I1.:item_id\"]") + .clientSortAlgo("CLIENT MERGE SORT").end().rhs().scanType("RANGE SCAN") .table(itemIndex + "(" + item + ")").keyRanges(" [1]").serverFirstKeyOnlyProjection(true) - .serverSortedBy("[\"I1.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT") - .sortMergeSkipMerge(false).rhs().scanType("RANGE SCAN").table(itemIndex + "(" + item + ")") - .keyRanges(" [1]").serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I2.:item_id\"]") - .clientSortAlgo("CLIENT MERGE SORT").end(); + .serverSortedBy("[\"I2.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end(); } @Override @@ -97,10 +97,10 @@ public class SortMergeJoinLocalIndexIT extends SortMergeJoinIT { statement.setMaxRows(4); ExplainPlanAttributes attributes = statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes(); - assertPlan(attributes).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").scanType("RANGE SCAN") - .table(itemIndex + "(" + item + ")").keyRanges(" [1]").serverFirstKeyOnlyProjection(true) - .serverSortedBy("[\"I.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT") - .sortMergeSkipMerge(false).clientRowLimit(4).rhs().scanType("FULL SCAN").table(order) + assertPlan(attributes).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false) + .clientRowLimit(4).lhs().scanType("RANGE SCAN").table(itemIndex + "(" + item + ")") + .keyRanges(" [1]").serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I.:item_id\"]") + .clientSortAlgo("CLIENT MERGE SORT").end().rhs().scanType("FULL SCAN").table(order) .serverSortedBy(queryIndex == 0 ? "[\"O.item_id\"]" : "[\"item_id\"]") .clientSortAlgo("CLIENT MERGE SORT").end(); } diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinNoIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinNoIndexIT.java index 3eb29809b5..683030f07d 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinNoIndexIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinNoIndexIT.java @@ -60,19 +60,19 @@ public class SortMergeJoinNoIndexIT extends SortMergeJoinIT { String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME); String supplier = getTableName(conn, JOIN_SUPPLIER_TABLE_FULL_NAME); String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME); - assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").scanType("FULL SCAN") - .table(supplier).sortMergeSkipMerge(false).rhs() - .abstractExplainPlan("SORT-MERGE-JOIN (INNER)").scanType("FULL SCAN").table(item) - .sortMergeSkipMerge(true).clientSortedBy("[\"I.supplier_id\"]").rhs().scanType("FULL SCAN") - .table(order).serverWhereFilter("QUANTITY < 5000").serverSortedBy("[\"O.item_id\"]") - .clientSortAlgo("CLIENT MERGE SORT").end().end(); + assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").sortMergeSkipMerge(false) + .lhs().scanType("FULL SCAN").table(supplier).end().rhs() + .abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(true) + .clientSortedBy("[\"I.supplier_id\"]").lhs().scanType("FULL SCAN").table(item).end().rhs() + .scanType("FULL SCAN").table(order).serverWhereFilter("QUANTITY < 5000") + .serverSortedBy("[\"O.item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end().end(); } @Override protected void assertSelfJoinPlan(Connection conn, String query) throws Exception { String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME); - assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").scanType("FULL SCAN") - .table(item).sortMergeSkipMerge(false).rhs().scanType("FULL SCAN").table(item) + assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false) + .lhs().scanType("FULL SCAN").table(item).end().rhs().scanType("FULL SCAN").table(item) .serverFirstKeyOnlyProjection(true).end(); } @@ -86,8 +86,8 @@ public class SortMergeJoinNoIndexIT extends SortMergeJoinIT { statement.setMaxRows(4); ExplainPlanAttributes attributes = statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes(); - assertPlan(attributes).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").scanType("FULL SCAN") - .table(item).sortMergeSkipMerge(false).clientRowLimit(4).rhs().scanType("FULL SCAN") + assertPlan(attributes).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false) + .clientRowLimit(4).lhs().scanType("FULL SCAN").table(item).end().rhs().scanType("FULL SCAN") .table(order).serverSortedBy(queryIndex == 0 ? "[\"O.item_id\"]" : "[\"item_id\"]") .clientSortAlgo("CLIENT MERGE SORT").end(); } diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SubqueryUsingSortMergeJoinIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SubqueryUsingSortMergeJoinIT.java index 74b95b8029..5f8a68d1bc 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SubqueryUsingSortMergeJoinIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SubqueryUsingSortMergeJoinIT.java @@ -184,8 +184,7 @@ public class SubqueryUsingSortMergeJoinIT extends BaseJoinIT { assertFalse(rs.next()); assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)") - .sortMergeSkipMerge(false).scanType("FULL SCAN").table(tableName5).iteratorType("PARALLEL") - .rhs().subPlan(0).scanType("FULL SCAN").table(tableName4) + .sortMergeSkipMerge(false).rhs().subPlan(0).scanType("FULL SCAN").table(tableName4) .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"item_id\"]") .clientSortAlgo("CLIENT MERGE SORT"); } finally { @@ -241,8 +240,7 @@ public class SubqueryUsingSortMergeJoinIT extends BaseJoinIT { assertFalse(rs.next()); assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)") - .sortMergeSkipMerge(false).scanType("FULL SCAN").table(tableName5).iteratorType("PARALLEL") - .rhs().subPlan(0).scanType("FULL SCAN").table(tableName4) + .sortMergeSkipMerge(false).rhs().subPlan(0).scanType("FULL SCAN").table(tableName4) .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"item_id\"]") .clientSortAlgo("CLIENT MERGE SORT"); } finally { diff --git a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainJsonNormalizer.java b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainJsonNormalizer.java index 22fa3e715f..39e18fde96 100644 --- a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainJsonNormalizer.java +++ b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainJsonNormalizer.java @@ -79,6 +79,11 @@ public final class ExplainJsonNormalizer { .replaceAll(DYNAMIC_FILTER_ALIAS_PLACEHOLDER)); } + JsonNode lhs = obj.get("lhsJoinQueryExplainPlan"); + if (lhs != null && lhs.isObject()) { + normalize(lhs); + } + JsonNode rhs = obj.get("rhsJoinQueryExplainPlan"); if (rhs != null && rhs.isObject()) { normalize(rhs); 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 46facbc4d9..1326750984 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 @@ -28,6 +28,7 @@ import static org.junit.Assert.fail; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import java.sql.Connection; import java.sql.DriverManager; @@ -225,7 +226,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { "CLIENT MERGE SORT"), scanAttrs("FULL SCAN ", "ATABLE", "") .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING]") - .put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", clientSteps("CLIENT MERGE SORT"))); } @Test @@ -239,7 +241,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { "CLIENT MERGE SORT"), scanAttrs("FULL SCAN ", "ATABLE", "").put("serverWhereFilter", "A_INTEGER = 1") .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY [ENTITY_ID, ROUND(A_TIME)]") - .put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", clientSteps("CLIENT MERGE SORT"))); } @Test @@ -250,7 +253,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { "CLIENT MERGE SORT", "CLIENT LIMIT 3"), scanAttrs("FULL SCAN ", "ATABLE", "").put("serverSortedBy", "[A_STRING DESC]") .put("serverRowLimit", 3).put("clientRowLimit", 3) - .put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", clientSteps("CLIENT MERGE SORT", "CLIENT LIMIT 3"))); } @Test @@ -263,7 +267,9 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { "CLIENT FILTER BY MAX(A_STRING) = 'a'"), scanAttrs("FULL SCAN ", "ATABLE", "") .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING, B_STRING]") - .put("clientFilterBy", "MAX(A_STRING) = 'a'").put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("clientFilterBy", "MAX(A_STRING) = 'a'").put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", + clientSteps("CLIENT MERGE SORT", "CLIENT FILTER BY MAX(A_STRING) = 'a'"))); } @Test @@ -279,7 +285,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']") .put("serverWhereFilter", "(ENTITY_ID != '00E00000000002' AND X_INTEGER = 2 AND A_INTEGER < 5)") - .put("serverRowLimit", 10).put("clientRowLimit", 10)); + .put("serverRowLimit", 10).put("clientRowLimit", 10) + .set("clientSteps", clientSteps("CLIENT 10 ROW LIMIT"))); } @Test @@ -405,7 +412,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { "CLIENT MERGE SORT", "CLIENT 5 ROW LIMIT"), scanAttrs("FULL SCAN ", "ATABLE", "") .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING]") - .put("clientRowLimit", 5).put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("clientRowLimit", 5).put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", clientSteps("CLIENT MERGE SORT", "CLIENT 5 ROW LIMIT"))); } @Test @@ -418,7 +426,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { "CLIENT MERGE SORT", "CLIENT LIMIT 10"), scanAttrs("RANGE SCAN ", "ATABLE", " ['000000000000001']").put("serverSortedBy", "[A_STRING]") .put("serverRowLimit", 10).put("clientRowLimit", 10) - .put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", clientSteps("CLIENT MERGE SORT", "CLIENT LIMIT 10"))); } @Test @@ -431,7 +440,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { "CLIENT MERGE SORT", "CLIENT LIMIT 10"), scanAttrs("RANGE SCAN ", "ATABLE", " ['000000000000001']") .put("serverSortedBy", "[A_STRING DESC NULLS LAST]").put("serverRowLimit", 10) - .put("clientRowLimit", 10).put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("clientRowLimit", 10).put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", clientSteps("CLIENT MERGE SORT", "CLIENT LIMIT 10"))); } @Test @@ -448,7 +458,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { scanAttrs("RANGE SCAN ", "ATABLE", " ['000000000000001']") .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY [ORGANIZATION_ID, ENTITY_ID, ROUND(A_DATE)]") - .put("clientRowLimit", 10).put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("clientRowLimit", 10).put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", clientSteps("CLIENT MERGE SORT", "CLIENT 10 ROW LIMIT"))); } @Test @@ -463,12 +474,19 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { scanAttrs("FULL SCAN ", "ATABLE", "").put("serverWhereFilter", "A_INTEGER = 1") .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING, B_STRING]") .put("clientFilterBy", "MAX(A_STRING) = 'a'").put("clientSortedBy", "[B_STRING]") - .put("clientOffset", 0).put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("clientOffset", 0).put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", clientSteps("CLIENT MERGE SORT", "CLIENT FILTER BY MAX(A_STRING) = 'a'", + "CLIENT SORTED BY [B_STRING]"))); } @Test public void testSortMergeJoin() throws Exception { + ObjectNode lhs = scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']"); ObjectNode rhs = scanAttrs("FULL SCAN ", "ATABLE", ""); + ObjectNode expected = defaultAttrs(); + expected.put("abstractExplainPlan", "SORT-MERGE-JOIN (INNER)"); + expected.set("lhsJoinQueryExplainPlan", lhs); + expected.set("rhsJoinQueryExplainPlan", rhs); verifyQuery("sortMergeJoin", "SELECT /*+ USE_SORT_MERGE_JOIN */ a.a_string, b.a_string FROM atable a" + " JOIN atable b ON a.organization_id = b.organization_id" @@ -478,8 +496,7 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { " INDEX ATABLE", " REGIONS PLANNED <N>", "AND", " CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", " INDEX ATABLE", " REGIONS PLANNED <N>"), - scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']") - .put("abstractExplainPlan", "SORT-MERGE-JOIN (INNER)").set("rhsJoinQueryExplainPlan", rhs)); + expected); } @Test @@ -613,7 +630,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { " REGIONS PLANNED <N>", " SERVER PROJECTION FILTER BY FIRST KEY ONLY", "CLIENT RESERVE VALUES FROM 1 SEQUENCE"), scanAttrs("FULL SCAN ", "ATABLE", "").put("serverFirstKeyOnlyProjection", true) - .put("clientSequenceCount", 1)); + .put("clientSequenceCount", 1) + .set("clientSteps", clientSteps("CLIENT RESERVE VALUES FROM 1 SEQUENCE"))); } @Test @@ -623,7 +641,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { " SALT BUCKETS 4", " REGIONS PLANNED <N>", " SERVER FILTER BY V = 7", "CLIENT MERGE SORT"), scanAttrs("FULL SCAN ", "EO_SALTED", "").put("saltBuckets", 4) - .put("serverWhereFilter", "V = 7").put("clientSortAlgo", "CLIENT MERGE SORT")); + .put("serverWhereFilter", "V = 7").put("clientSortAlgo", "CLIENT MERGE SORT") + .set("clientSteps", clientSteps("CLIENT MERGE SORT"))); } @Test @@ -637,7 +656,7 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { attrs().put("iteratorTypeAndScanSize", "SERIAL <N>-WAY").put("consistency", "STRONG") .put("explainScanType", "RANGE SCAN ").put("tableName", "EO_MT_BASE") .put("indexName", "EO_MT_VIEW").put("keyRanges", " ['tenant42']").put("serverRowLimit", 1) - .put("clientRowLimit", 1)); + .put("clientRowLimit", 1).set("clientSteps", clientSteps("CLIENT 1 ROW LIMIT"))); } @Test @@ -838,6 +857,27 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { assertTrue(nestedRhs.get("regionLocations").isNull()); } + @Test + public void testJsonNormalizerRecursesIntoLhsJoinQueryExplainPlan() { + ObjectNode root = mapper.createObjectNode(); + root.put("iteratorTypeAndScanSize", "PARALLEL 5-WAY"); + root.put("numRegionLocationLookups", 1); + ObjectNode lhs = mapper.createObjectNode(); + lhs.put("iteratorTypeAndScanSize", "PARALLEL 7-WAY"); + lhs.put("numRegionLocationLookups", 42); + lhs.set("regionLocations", mapper.createArrayNode().add(1)); + root.set("lhsJoinQueryExplainPlan", lhs); + + new ExplainJsonNormalizer().normalize(root); + + assertEquals("PARALLEL <N>-WAY", root.get("iteratorTypeAndScanSize").asText()); + assertEquals(0, root.get("numRegionLocationLookups").asInt()); + JsonNode nestedLhs = root.get("lhsJoinQueryExplainPlan"); + assertEquals("PARALLEL <N>-WAY", nestedLhs.get("iteratorTypeAndScanSize").asText()); + assertEquals(0, nestedLhs.get("numRegionLocationLookups").asInt()); + assertTrue(nestedLhs.get("regionLocations").isNull()); + } + @Test public void testJacksonFieldOrderMatchesPropertyOrderAnnotation() throws Exception { // The serialized field order must exactly follow the @JsonPropertyOrder declaration. Deriving @@ -1057,6 +1097,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { n.putNull("clientSequenceCount"); n.putNull("clientCursorName"); n.putNull("clientSortAlgo"); + n.putNull("clientSteps"); + n.putNull("lhsJoinQueryExplainPlan"); n.putNull("rhsJoinQueryExplainPlan"); n.putNull("serverMergeColumns"); n.putNull("regionLocations"); @@ -1096,6 +1138,15 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { return defaultAttrs(); } + /** Build a {@code clientSteps} JSON array for embedding into an expected attributes object. */ + private static ArrayNode clientSteps(String... steps) { + ArrayNode arr = mapper.createArrayNode(); + for (String s : steps) { + arr.add(s); + } + return arr; + } + private static ExplainPlan samplePlan(String way, String scanType) { ExplainPlanAttributes a = new ExplainPlanAttributesBuilder().setIteratorTypeAndScanSize(way) .setExplainScanType(scanType).setTableName("T") 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 0f79f98d34..8c01511018 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 @@ -357,6 +357,13 @@ public final class ExplainPlanTestUtil { return this; } + /** Navigate to the left hand side plan (e.g. sort-merge join). */ + public ExplainPlanAssert lhs() { + ExplainPlanAttributes lhs = attributes.getLhsJoinQueryExplainPlan(); + assertNotNull(at("lhsJoinQueryExplainPlan") + " must not be null", lhs); + return new ExplainPlanAssert(lhs, this, context + ".lhs"); + } + /** Navigate to the right-hand side plan (sort-merge join / union all). */ public ExplainPlanAssert rhs() { ExplainPlanAttributes rhs = attributes.getRhsJoinQueryExplainPlan(); @@ -364,6 +371,33 @@ public final class ExplainPlanTestUtil { return new ExplainPlanAssert(rhs, this, context + ".rhs"); } + /** Assert the number of ordered client-side pipeline steps on this node. */ + public ExplainPlanAssert clientStepCount(int expected) { + List<String> steps = attributes.getClientSteps(); + int actual = steps == null ? 0 : steps.size(); + assertEquals(at("clientSteps.size"), expected, actual); + return this; + } + + /** Assert the i-th ordered client-side pipeline step on this node. */ + public ExplainPlanAssert clientStep(int i, String expected) { + List<String> steps = attributes.getClientSteps(); + assertNotNull(at("clientSteps") + " must not be null", steps); + assertTrue(at("clientSteps") + " has no index " + i + " (size=" + steps.size() + ")", + i >= 0 && i < steps.size()); + assertEquals(at("clientSteps[" + i + "]"), expected, steps.get(i)); + return this; + } + + /** Assert the entire ordered client-side pipeline on this node matches {@code expected}. */ + public ExplainPlanAssert clientSteps(String... expected) { + List<String> actual = attributes.getClientSteps(); + List<String> actualOrEmpty = + actual == null ? java.util.Collections.<String> emptyList() : actual; + assertEquals(at("clientSteps"), java.util.Arrays.asList(expected), actualOrEmpty); + return this; + } + /** Assert the number of hash-join sub-plans (children). */ public ExplainPlanAssert subPlanCount(int expected) { List<ExplainPlanAttributes> subPlans = attributes.getSubPlans();
