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 cbc7a758fde39b9ad3890c7fcfb254ee287531ff
Author: Andrew Purtell <[email protected]>
AuthorDate: Tue Jun 9 12:02:12 2026 -0700

    PHOENIX-7882 Per scan EXPLAIN output improvements (#2505)
---
 .../phoenix/compile/ExplainPlanAttributes.java     | 100 +++++++---
 .../phoenix/iterate/BaseResultIterators.java       |   5 +
 .../org/apache/phoenix/iterate/ExplainTable.java   |  76 +++++++-
 .../phoenix/end2end/SortMergeJoinMoreIT.java       |   4 +-
 .../end2end/join/SortMergeJoinGlobalIndexIT.java   |   9 +-
 .../end2end/join/SortMergeJoinLocalIndexIT.java    |   7 +-
 .../query/explain/ExplainJsonNormalizer.java       |   3 +
 .../phoenix/query/explain/ExplainPlanTest.java     | 203 +++++++++++++++------
 .../phoenix/query/explain/ExplainPlanTestUtil.java |  27 +++
 .../query/explain/ExplainTextNormalizer.java       |   6 +-
 10 files changed, 341 insertions(+), 99 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 74cd983332..fcf858479a 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
@@ -35,16 +35,16 @@ import org.apache.phoenix.schema.PColumn;
  * Strings containing entire plan.
  */
 @JsonPropertyOrder({ "abstractExplainPlan", "hint", "explainScanType", 
"consistency", "tableName",
-  "keyRanges", "scanTimeRangeMin", "scanTimeRangeMax", "splitsChunk", 
"useRoundRobinIterator",
-  "samplingRate", "hexStringRVCOffset", "iteratorTypeAndScanSize", 
"estimatedRows",
-  "estimatedSizeInBytes", "serverWhereFilter", "serverDistinctFilter", 
"serverMergeColumns",
-  "serverArrayElementProjection", "serverAggregate", "serverGroupByLimit", 
"serverSortedBy",
-  "serverOffset", "serverRowLimit", "clientFilterBy", "clientAggregate", 
"clientDistinctFilter",
-  "clientAfterAggregate", "clientSortAlgo", "clientSortedBy", "clientOffset", 
"clientRowLimit",
-  "clientSequenceCount", "clientCursorName", "clientSteps", 
"lhsJoinQueryExplainPlan",
-  "rhsJoinQueryExplainPlan", "subPlans", "dynamicServerFilter", 
"afterJoinFilter",
-  "joinScannerLimit", "sortMergeSkipMerge", "regionLocations", 
"regionLocationsTotalSize",
-  "numRegionLocationLookups" })
+  "keyRanges", "indexName", "indexKind", "saltBuckets", "regionsPlanned", 
"scanTimeRangeMin",
+  "scanTimeRangeMax", "splitsChunk", "useRoundRobinIterator", "samplingRate", 
"hexStringRVCOffset",
+  "iteratorTypeAndScanSize", "estimatedRows", "estimatedSizeInBytes", 
"serverWhereFilter",
+  "serverDistinctFilter", "serverMergeColumns", 
"serverArrayElementProjection", "serverAggregate",
+  "serverGroupByLimit", "serverSortedBy", "serverOffset", "serverRowLimit", 
"clientFilterBy",
+  "clientAggregate", "clientDistinctFilter", "clientAfterAggregate", 
"clientSortAlgo",
+  "clientSortedBy", "clientOffset", "clientRowLimit", "clientSequenceCount", 
"clientCursorName",
+  "clientSteps", "lhsJoinQueryExplainPlan", "rhsJoinQueryExplainPlan", 
"subPlans",
+  "dynamicServerFilter", "afterJoinFilter", "joinScannerLimit", 
"sortMergeSkipMerge",
+  "regionLocations", "regionLocationsTotalSize", "numRegionLocationLookups" })
 public class ExplainPlanAttributes {
 
   // Plan identity and scan-level metadata
@@ -54,6 +54,10 @@ public class ExplainPlanAttributes {
   private final Consistency consistency;
   private final String tableName;
   private final String keyRanges;
+  private final String indexName;
+  private final String indexKind;
+  private final Integer saltBuckets;
+  private final Integer regionsPlanned;
   private final Long scanTimeRangeMin;
   private final Long scanTimeRangeMax;
   private final Integer splitsChunk;
@@ -113,6 +117,10 @@ public class ExplainPlanAttributes {
     this.consistency = null;
     this.tableName = null;
     this.keyRanges = null;
+    this.indexName = null;
+    this.indexKind = null;
+    this.saltBuckets = null;
+    this.regionsPlanned = null;
     this.scanTimeRangeMin = null;
     this.scanTimeRangeMax = null;
     this.splitsChunk = null;
@@ -155,8 +163,9 @@ public class ExplainPlanAttributes {
   }
 
   public ExplainPlanAttributes(String abstractExplainPlan, Hint hint, String 
explainScanType,
-    Consistency consistency, String tableName, String keyRanges, Long 
scanTimeRangeMin,
-    Long scanTimeRangeMax, Integer splitsChunk, boolean useRoundRobinIterator, 
Double samplingRate,
+    Consistency consistency, String tableName, String keyRanges, String 
indexName, String indexKind,
+    Integer saltBuckets, Integer regionsPlanned, Long scanTimeRangeMin, Long 
scanTimeRangeMax,
+    Integer splitsChunk, boolean useRoundRobinIterator, Double samplingRate,
     String hexStringRVCOffset, String iteratorTypeAndScanSize, Long 
estimatedRows,
     Long estimatedSizeInBytes, String serverWhereFilter, String 
serverDistinctFilter,
     Set<PColumn> serverMergeColumns, boolean serverArrayElementProjection, 
String serverAggregate,
@@ -175,6 +184,10 @@ public class ExplainPlanAttributes {
     this.consistency = consistency;
     this.tableName = tableName;
     this.keyRanges = keyRanges;
+    this.indexName = indexName;
+    this.indexKind = indexKind;
+    this.saltBuckets = saltBuckets;
+    this.regionsPlanned = regionsPlanned;
     this.scanTimeRangeMin = scanTimeRangeMin;
     this.scanTimeRangeMax = scanTimeRangeMax;
     this.splitsChunk = splitsChunk;
@@ -242,6 +255,22 @@ public class ExplainPlanAttributes {
     return keyRanges;
   }
 
+  public String getIndexName() {
+    return indexName;
+  }
+
+  public String getIndexKind() {
+    return indexKind;
+  }
+
+  public Integer getSaltBuckets() {
+    return saltBuckets;
+  }
+
+  public Integer getRegionsPlanned() {
+    return regionsPlanned;
+  }
+
   public Long getScanTimeRangeMin() {
     return scanTimeRangeMin;
   }
@@ -411,6 +440,10 @@ public class ExplainPlanAttributes {
     private Consistency consistency;
     private String tableName;
     private String keyRanges;
+    private String indexName;
+    private String indexKind;
+    private Integer saltBuckets;
+    private Integer regionsPlanned;
     private Long scanTimeRangeMin;
     private Long scanTimeRangeMax;
     private Integer splitsChunk;
@@ -462,6 +495,10 @@ public class ExplainPlanAttributes {
       this.consistency = explainPlanAttributes.getConsistency();
       this.tableName = explainPlanAttributes.getTableName();
       this.keyRanges = explainPlanAttributes.getKeyRanges();
+      this.indexName = explainPlanAttributes.getIndexName();
+      this.indexKind = explainPlanAttributes.getIndexKind();
+      this.saltBuckets = explainPlanAttributes.getSaltBuckets();
+      this.regionsPlanned = explainPlanAttributes.getRegionsPlanned();
       this.scanTimeRangeMin = explainPlanAttributes.getScanTimeRangeMin();
       this.scanTimeRangeMax = explainPlanAttributes.getScanTimeRangeMax();
       this.splitsChunk = explainPlanAttributes.getSplitsChunk();
@@ -534,6 +571,26 @@ public class ExplainPlanAttributes {
       return this;
     }
 
+    public ExplainPlanAttributesBuilder setIndexName(String indexName) {
+      this.indexName = indexName;
+      return this;
+    }
+
+    public ExplainPlanAttributesBuilder setIndexKind(String indexKind) {
+      this.indexKind = indexKind;
+      return this;
+    }
+
+    public ExplainPlanAttributesBuilder setSaltBuckets(Integer saltBuckets) {
+      this.saltBuckets = saltBuckets;
+      return this;
+    }
+
+    public ExplainPlanAttributesBuilder setRegionsPlanned(Integer 
regionsPlanned) {
+      this.regionsPlanned = regionsPlanned;
+      return this;
+    }
+
     public ExplainPlanAttributesBuilder setScanTimeRangeMin(Long 
scanTimeRangeMin) {
       this.scanTimeRangeMin = scanTimeRangeMin;
       return this;
@@ -743,15 +800,16 @@ public class ExplainPlanAttributes {
 
     public ExplainPlanAttributes build() {
       return new ExplainPlanAttributes(abstractExplainPlan, hint, 
explainScanType, consistency,
-        tableName, keyRanges, scanTimeRangeMin, scanTimeRangeMax, splitsChunk,
-        useRoundRobinIterator, samplingRate, hexStringRVCOffset, 
iteratorTypeAndScanSize,
-        estimatedRows, estimatedSizeInBytes, serverWhereFilter, 
serverDistinctFilter,
-        serverMergeColumns, serverArrayElementProjection, serverAggregate, 
serverGroupByLimit,
-        serverSortedBy, serverOffset, serverRowLimit, clientFilterBy, 
clientAggregate,
-        clientDistinctFilter, clientAfterAggregate, clientSortAlgo, 
clientSortedBy, clientOffset,
-        clientRowLimit, clientSequenceCount, clientCursorName, clientSteps, 
lhsJoinQueryExplainPlan,
-        rhsJoinQueryExplainPlan, subPlans, dynamicServerFilter, 
afterJoinFilter, joinScannerLimit,
-        sortMergeSkipMerge, regionLocations, regionLocationsTotalSize, 
numRegionLocationLookups);
+        tableName, keyRanges, indexName, indexKind, saltBuckets, 
regionsPlanned, scanTimeRangeMin,
+        scanTimeRangeMax, splitsChunk, useRoundRobinIterator, samplingRate, 
hexStringRVCOffset,
+        iteratorTypeAndScanSize, estimatedRows, estimatedSizeInBytes, 
serverWhereFilter,
+        serverDistinctFilter, serverMergeColumns, 
serverArrayElementProjection, serverAggregate,
+        serverGroupByLimit, serverSortedBy, serverOffset, serverRowLimit, 
clientFilterBy,
+        clientAggregate, clientDistinctFilter, clientAfterAggregate, 
clientSortAlgo, clientSortedBy,
+        clientOffset, clientRowLimit, clientSequenceCount, clientCursorName, 
clientSteps,
+        lhsJoinQueryExplainPlan, rhsJoinQueryExplainPlan, subPlans, 
dynamicServerFilter,
+        afterJoinFilter, joinScannerLimit, sortMergeSkipMerge, regionLocations,
+        regionLocationsTotalSize, numRegionLocationLookups);
     }
   }
 }
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/BaseResultIterators.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/BaseResultIterators.java
index 45e160fac8..7e81d3a534 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/BaseResultIterators.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/BaseResultIterators.java
@@ -640,6 +640,11 @@ public abstract class BaseResultIterators extends 
ExplainTable implements Result
     else return splits;
   }
 
+  @Override
+  protected int getSplitCount() {
+    return splits == null ? 0 : splits.size();
+  }
+
   @Override
   public List<List<Scan>> getScans() {
     if (scans == null) return Collections.emptyList();
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java
index 8382100561..1a1f32786f 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java
@@ -122,6 +122,33 @@ public abstract class ExplainTable {
     return buf.toString();
   }
 
+  /**
+   * Number of region scan splits the plan will hit, used to render the {@code 
REGIONS PLANNED}
+   * per-scan line.
+   * @return the split count, or 0 when unknown
+   */
+  protected int getSplitCount() {
+    return 0;
+  }
+
+  /**
+   * Logical name used to render a table or index in EXPLAIN output. Shared by 
both the scan
+   * {@code OVER} line's local index decoration and the per scan {@code INDEX} 
line.
+   * @param table the scanned table or index
+   * @return the display name with any child-view local-index prefix stripped
+   */
+  private static String getExplainIndexName(PTable table) {
+    String indexName = table.getName().getString();
+    if (
+      table.getIndexType() == PTable.IndexType.LOCAL && table.getViewIndexId() 
!= null
+        && indexName.contains(QueryConstants.CHILD_VIEW_INDEX_NAME_SEPARATOR)
+    ) {
+      int lastIndexOf = 
indexName.lastIndexOf(QueryConstants.CHILD_VIEW_INDEX_NAME_SEPARATOR);
+      indexName = indexName.substring(lastIndexOf + 1);
+    }
+    return indexName;
+  }
+
   protected void explain(String prefix, List<String> planSteps,
     ExplainPlanAttributesBuilder explainPlanAttributesBuilder,
     List<HRegionLocation> regionLocations) {
@@ -148,15 +175,7 @@ public abstract class ExplainTable {
 
     String tableName = tableRef.getTable().getPhysicalName().getString();
     if (tableRef.getTable().getIndexType() == PTable.IndexType.LOCAL) {
-      String indexName = tableRef.getTable().getName().getString();
-      if (
-        tableRef.getTable().getViewIndexId() != null
-          && indexName.contains(QueryConstants.CHILD_VIEW_INDEX_NAME_SEPARATOR)
-      ) {
-        int lastIndexOf = 
indexName.lastIndexOf(QueryConstants.CHILD_VIEW_INDEX_NAME_SEPARATOR);
-        indexName = indexName.substring(lastIndexOf + 1);
-      }
-      tableName = indexName + "(" + tableName + ")";
+      tableName = getExplainIndexName(tableRef.getTable()) + "(" + tableName + 
")";
     }
     buf.append("OVER ").append(tableName);
 
@@ -178,6 +197,45 @@ public abstract class ExplainTable {
         explainPlanAttributesBuilder.setKeyRanges(appendKeyRanges());
       }
     }
+
+    PTable.IndexType indexType = tableRef.getTable().getIndexType();
+    String explainIndexName = getExplainIndexName(tableRef.getTable());
+    String indexKind = null;
+    if (indexType != null) {
+      switch (indexType) {
+        case LOCAL:
+          indexKind = "LOCAL";
+          break;
+        case GLOBAL:
+          indexKind = "GLOBAL";
+          break;
+        case UNCOVERED_GLOBAL:
+          indexKind = "UNCOVERED GLOBAL";
+          break;
+        default:
+          indexKind = null;
+      }
+    }
+    planSteps.add("    INDEX " + explainIndexName + (indexKind == null ? "" : 
" " + indexKind));
+    Integer bucketNum = tableRef.getTable().getBucketNum();
+    if (bucketNum != null) {
+      planSteps.add("    SALT BUCKETS " + bucketNum);
+    }
+    int splitCount = getSplitCount();
+    if (splitCount > 0) {
+      planSteps.add("    REGIONS PLANNED " + splitCount);
+    }
+    if (explainPlanAttributesBuilder != null) {
+      explainPlanAttributesBuilder.setIndexName(explainIndexName);
+      explainPlanAttributesBuilder.setIndexKind(indexKind);
+      if (bucketNum != null) {
+        explainPlanAttributesBuilder.setSaltBuckets(bucketNum);
+      }
+      if (splitCount > 0) {
+        explainPlanAttributesBuilder.setRegionsPlanned(splitCount);
+      }
+    }
+
     if (context.getScan() != null && 
tableRef.getTable().getRowTimestampColPos() != -1) {
       TimeRange range = context.getScan().getTimeRange();
       planSteps.add("    ROW TIMESTAMP FILTER [" + range.getMin() + ", " + 
range.getMax() + ")");
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 1a57ae39e0..26621c1c9b 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
@@ -434,8 +434,8 @@ public class SortMergeJoinMoreIT extends 
ParallelStatsDisabledIT {
           .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]")
-          .end().rhs().iteratorType("PARALLEL").scanType("SKIP SCAN ON 2 
RANGES").table(t[i])
+          .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[BUCKET, 
TIMESTAMP]").end().rhs()
+          .iteratorType("PARALLEL").scanType("SKIP SCAN ON 2 
RANGES").table(t[i])
           .keyRanges(rhsKeyRanges)
           .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND SRC_LOCATION 
= DST_LOCATION")
           .serverDistinctFilter(
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 59d69aa160..612768aabc 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
@@ -65,9 +65,8 @@ public class SortMergeJoinGlobalIndexIT extends 
SortMergeJoinIT {
       .serverWhereFilter("SERVER FILTER BY FIRST KEY 
ONLY").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()
+      .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("SERVER FILTER BY 
QUANTITY < 5000")
       .serverSortedBy("[\"O.item_id\"]").clientSortAlgo("CLIENT MERGE 
SORT").end().end();
   }
@@ -96,8 +95,8 @@ public class SortMergeJoinGlobalIndexIT extends 
SortMergeJoinIT {
     assertPlan(attributes).abstractExplainPlan("SORT-MERGE-JOIN 
(INNER)").sortMergeSkipMerge(false)
       .clientRowLimit(4).lhs().scanType("FULL SCAN").table(itemIndex)
       .serverWhereFilter("SERVER FILTER BY FIRST KEY 
ONLY").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().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 ec31377e1a..95d5f44a2a 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
@@ -67,10 +67,9 @@ public class SortMergeJoinLocalIndexIT extends 
SortMergeJoinIT {
       .serverWhereFilter("SERVER FILTER BY FIRST KEY 
ONLY").serverSortedBy("[\"S.:supplier_id\"]")
       .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")
-      .end().rhs().scanType("FULL SCAN").table(order)
+      .clientSortedBy("[\"I.0:supplier_id\"]").lhs().scanType("RANGE SCAN")
+      .table(itemIndex + "(" + item + ")").keyRanges(" 
[1]").serverSortedBy("[\"I.:item_id\"]")
+      .clientSortAlgo("CLIENT MERGE SORT").end().rhs().scanType("FULL 
SCAN").table(order)
       .serverWhereFilter("SERVER FILTER BY QUANTITY < 
5000").serverSortedBy("[\"O.item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").end().end();
   }
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 38f44fabea..eb96912f95 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
@@ -63,6 +63,9 @@ public final class ExplainJsonNormalizer {
     if (obj.has("splitsChunk")) {
       obj.set("splitsChunk", NullNode.getInstance());
     }
+    if (obj.has("regionsPlanned")) {
+      obj.set("regionsPlanned", NullNode.getInstance());
+    }
     if (obj.has("estimatedRows")) {
       obj.set("estimatedRows", NullNode.getInstance());
     }
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 66fe6b8b71..8e57faa750 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
@@ -109,8 +109,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       "SELECT a_string, b_string FROM atable"
         + " WHERE organization_id = '00D000000000001' AND entity_id = 
'00E00000000001'"
         + " AND x_integer = 2 AND a_integer < 5",
-      text("CLIENT PARALLEL <N>-WAY POINT LOOKUP ON 1 KEY OVER ATABLE",
-        "    SERVER FILTER BY (X_INTEGER = 2 AND A_INTEGER < 5)"),
+      text("CLIENT PARALLEL <N>-WAY POINT LOOKUP ON 1 KEY OVER ATABLE", "    
INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER FILTER BY (X_INTEGER = 2 AND 
A_INTEGER < 5)"),
       scanAttrs("POINT LOOKUP ON 1 KEY ", "ATABLE", 
null).put("serverWhereFilter",
         "SERVER FILTER BY (X_INTEGER = 2 AND A_INTEGER < 5)"));
   }
@@ -121,7 +121,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       "SELECT a_string, b_string FROM atable"
         + " WHERE organization_id IN ('00D000000000001', '00D000000000005')"
         + " AND entity_id IN ('00E00000000000X','00E00000000000Z')",
-      text("CLIENT PARALLEL <N>-WAY POINT LOOKUP ON 4 KEYS OVER ATABLE"),
+      text("CLIENT PARALLEL <N>-WAY POINT LOOKUP ON 4 KEYS OVER ATABLE", "    
INDEX ATABLE",
+        "    REGIONS PLANNED <N>"),
       scanAttrs("POINT LOOKUP ON 4 KEYS ", "ATABLE", null));
   }
 
@@ -130,8 +131,10 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("rangeScan",
       "SELECT a_string FROM atable WHERE organization_id = '00D000000000001'"
         + " AND entity_id > '00E00000000002' AND entity_id < '00E00000000008'",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE"
-        + " ['00D000000000001','00E00000000002!'] - 
['00D000000000001','00E00000000008 ']"),
+      text(
+        "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE"
+          + " ['00D000000000001','00E00000000002!'] - 
['00D000000000001','00E00000000008 ']",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>"),
       scanAttrs("RANGE SCAN ", "ATABLE",
         " ['00D000000000001','00E00000000002!'] - 
['00D000000000001','00E00000000008 ']"));
   }
@@ -140,7 +143,7 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   public void testSkipScanKeys() throws Exception {
     verifyQuery("skipScanKeys", "SELECT host FROM ptsdb3 WHERE host IN 
('na1','na2','na3')",
       text("CLIENT PARALLEL <N>-WAY SKIP SCAN ON 3 KEYS OVER PTSDB3 [~'na3'] - 
[~'na1']",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
+        "    INDEX PTSDB3", "    REGIONS PLANNED <N>", "    SERVER FILTER BY 
FIRST KEY ONLY"),
       scanAttrs("SKIP SCAN ON 3 KEYS ", "PTSDB3", " [~'na3'] - 
[~'na1']").put("serverWhereFilter",
         "SERVER FILTER BY FIRST KEY ONLY"));
   }
@@ -154,7 +157,7 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       text(
         "CLIENT PARALLEL <N>-WAY SKIP SCAN ON 6 RANGES OVER PTSDB"
           + " ['na1','a','2013-01-01'] - ['na3','b','2013-01-02']",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
+        "    INDEX PTSDB", "    REGIONS PLANNED <N>", "    SERVER FILTER BY 
FIRST KEY ONLY"),
       scanAttrs("SKIP SCAN ON 6 RANGES ", "PTSDB",
         " ['na1','a','2013-01-01'] - 
['na3','b','2013-01-02']").put("serverWhereFilter",
           "SERVER FILTER BY FIRST KEY ONLY"));
@@ -163,15 +166,17 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   @Test
   public void testFullScan() throws Exception {
     verifyQuery("fullScan", "SELECT * FROM atable",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE"), scanAttrs("FULL 
SCAN ", "ATABLE", ""));
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>"),
+      scanAttrs("FULL SCAN ", "ATABLE", ""));
   }
 
   @Test
   public void testReverseScan() throws Exception {
     verifyQuery("reverseScan",
       "SELECT inst,\"DATE\" FROM ptsdb2 WHERE inst = 'na1' ORDER BY inst DESC, 
\"DATE\" DESC",
-      text("CLIENT PARALLEL <N>-WAY REVERSE RANGE SCAN OVER PTSDB2 ['na1']",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
+      text("CLIENT PARALLEL <N>-WAY REVERSE RANGE SCAN OVER PTSDB2 ['na1']", " 
   INDEX PTSDB2",
+        "    REGIONS PLANNED <N>", "    SERVER FILTER BY FIRST KEY ONLY"),
       scanAttrs("RANGE SCAN ", "PTSDB2", " ['na1']")
         .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY")
         .put("clientSortedBy", "REVERSE"));
@@ -182,7 +187,7 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("smallHint",
       "SELECT /*+ SMALL */ host FROM ptsdb3 WHERE host IN ('na1','na2','na3')",
       text("CLIENT PARALLEL <N>-WAY SMALL SKIP SCAN ON 3 KEYS OVER PTSDB3 
[~'na3'] - [~'na1']",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
+        "    INDEX PTSDB3", "    REGIONS PLANNED <N>", "    SERVER FILTER BY 
FIRST KEY ONLY"),
       scanAttrs("SKIP SCAN ON 3 KEYS ", "PTSDB3", " [~'na3'] - 
[~'na1']").put("hint", "SMALL")
         .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY"));
   }
@@ -190,7 +195,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   @Test
   public void testAggregateSingleRow() throws Exception {
     verifyQuery("aggregateSingleRow", "SELECT count(*) FROM atable",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    SERVER FILTER 
BY FIRST KEY ONLY",
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER FILTER BY FIRST KEY ONLY",
         "    SERVER AGGREGATE INTO SINGLE ROW"),
       scanAttrs("FULL SCAN ", "ATABLE", "")
         .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY")
@@ -200,8 +206,9 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   @Test
   public void testAggregateOrderedDistinct() throws Exception {
     verifyQuery("aggregateOrderedDistinct", "SELECT count(1) FROM atable GROUP 
BY a_string",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
-        "    SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING]", "CLIENT MERGE 
SORT"),
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER AGGREGATE INTO DISTINCT ROWS BY 
[A_STRING]",
+        "CLIENT MERGE SORT"),
       scanAttrs("FULL SCAN ", "ATABLE", "")
         .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY 
[A_STRING]")
         .put("clientSortAlgo", "CLIENT MERGE SORT")
@@ -213,7 +220,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("aggregateHashDistinct",
       "SELECT count(1) FROM atable WHERE a_integer = 1"
         + " GROUP BY ROUND(a_time,'HOUR',2), entity_id",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    SERVER FILTER 
BY A_INTEGER = 1",
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER FILTER BY A_INTEGER = 1",
         "    SERVER AGGREGATE INTO DISTINCT ROWS BY [ENTITY_ID, 
ROUND(A_TIME)]",
         "CLIENT MERGE SORT"),
       scanAttrs("FULL SCAN ", "ATABLE", "")
@@ -226,8 +234,9 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   @Test
   public void testTopNSortedBy() throws Exception {
     verifyQuery("topNSortedBy", "SELECT a_string FROM atable ORDER BY a_string 
DESC LIMIT 3",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
-        "    SERVER TOP 3 ROWS SORTED BY [A_STRING DESC]", "CLIENT MERGE 
SORT", "CLIENT LIMIT 3"),
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER TOP 3 ROWS SORTED BY [A_STRING 
DESC]",
+        "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")
@@ -238,7 +247,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   public void testClientFilterByMax() throws Exception {
     verifyQuery("clientFilterByMax",
       "SELECT count(1) FROM atable GROUP BY a_string, b_string HAVING 
max(a_string) = 'a'",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>",
         "    SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING, B_STRING]", 
"CLIENT MERGE SORT",
         "CLIENT FILTER BY MAX(A_STRING) = 'a'"),
       scanAttrs("FULL SCAN ", "ATABLE", "")
@@ -254,7 +264,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       "SELECT a_string, b_string FROM atable"
         + " WHERE organization_id = '00D000000000001' AND entity_id != 
'00E00000000002'"
         + " AND x_integer = 2 AND a_integer < 5 LIMIT 10",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']",
+      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>",
         "    SERVER FILTER BY (ENTITY_ID != '00E00000000002' AND X_INTEGER = 2 
AND A_INTEGER < 5)",
         "    SERVER 10 ROW LIMIT", "CLIENT 10 ROW LIMIT"),
       scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']")
@@ -267,8 +278,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   @Test
   public void testArrayElementProjection() throws Exception {
     verifyQuery("arrayElementProjection", "SELECT a_string_array[1] FROM 
table_with_array",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER TABLE_WITH_ARRAY",
-        "    SERVER ARRAY ELEMENT PROJECTION"),
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER TABLE_WITH_ARRAY", "    
INDEX TABLE_WITH_ARRAY",
+        "    REGIONS PLANNED <N>", "    SERVER ARRAY ELEMENT PROJECTION"),
       scanAttrs("FULL SCAN ", "TABLE_WITH_ARRAY", 
"").put("serverArrayElementProjection", true));
   }
 
@@ -278,8 +289,10 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       "SELECT a_string,b_string FROM atable WHERE organization_id = 
'000000000000001'"
         + " AND entity_id > '000000000000002' AND entity_id < 
'000000000000008'"
         + " AND (organization_id,entity_id) <= 
('000000000000001','000000000000005')",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE"
-        + " ['000000000000001','000000000000003'] - 
['000000000000001','000000000000005']"),
+      text(
+        "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE"
+          + " ['000000000000001','000000000000003'] - 
['000000000000001','000000000000005']",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>"),
       scanAttrs("RANGE SCAN ", "ATABLE",
         " ['000000000000001','000000000000003'] - 
['000000000000001','000000000000005']"));
   }
@@ -293,6 +306,7 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       text(
         "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE"
           + " ['000000000000003000000000000005'] - [*]",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>",
         "    SERVER FILTER BY (ENTITY_ID > '000000000000002' AND ENTITY_ID < 
'000000000000008')"),
       scanAttrs("RANGE SCAN ", "ATABLE", " ['000000000000003000000000000005'] 
- [*]").put(
         "serverWhereFilter",
@@ -304,7 +318,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("rangeScanNullNotNull",
       "SELECT host FROM PTSDB WHERE inst IS NULL AND host IS NOT NULL"
         + " AND \"DATE\" >= to_date('2013-01-01')",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER PTSDB [null,not null]",
+      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER PTSDB [null,not null]", "  
  INDEX PTSDB",
+        "    REGIONS PLANNED <N>",
         "    SERVER FILTER BY FIRST KEY ONLY AND \"DATE\" >= DATE '2013-01-01 
00:00:00.000'"),
       scanAttrs("RANGE SCAN ", "PTSDB", " [null,not 
null]").put("serverWhereFilter",
         "SERVER FILTER BY FIRST KEY ONLY AND \"DATE\" >= DATE '2013-01-01 
00:00:00.000'"));
@@ -315,7 +330,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("rangeScanNotNull",
       "SELECT host FROM PTSDB WHERE inst IS NOT NULL AND host IS NULL"
         + " AND \"DATE\" >= to_date('2013-01-01')",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER PTSDB [not null]",
+      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER PTSDB [not null]", "    
INDEX PTSDB",
+        "    REGIONS PLANNED <N>",
         "    SERVER FILTER BY FIRST KEY ONLY AND (HOST IS NULL"
           + " AND \"DATE\" >= DATE '2013-01-01 00:00:00.000')"),
       scanAttrs("RANGE SCAN ", "PTSDB", " [not null]").put("serverWhereFilter",
@@ -331,7 +347,7 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       text(
         "CLIENT PARALLEL <N>-WAY SKIP SCAN ON 2 RANGES OVER PTSDB"
           + " ['na','a','2013-01-01'] - ['nb','b','2013-01-02']",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
+        "    INDEX PTSDB", "    REGIONS PLANNED <N>", "    SERVER FILTER BY 
FIRST KEY ONLY"),
       scanAttrs("SKIP SCAN ON 2 RANGES ", "PTSDB",
         " ['na','a','2013-01-01'] - 
['nb','b','2013-01-02']").put("serverWhereFilter",
           "SERVER FILTER BY FIRST KEY ONLY"));
@@ -342,6 +358,7 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("skipScanRegexpRanges",
       "SELECT inst,host FROM PTSDB WHERE REGEXP_SUBSTR(INST, '[^-]+', 1) IN 
('na1', 'na2','na3')",
       text("CLIENT PARALLEL <N>-WAY SKIP SCAN ON 3 RANGES OVER PTSDB ['na1'] - 
['na4']",
+        "    INDEX PTSDB", "    REGIONS PLANNED <N>",
         "    SERVER FILTER BY FIRST KEY ONLY AND"
           + " REGEXP_SUBSTR(INST, '[^-]+', 1) IN ('na1','na2','na3')"),
       scanAttrs("SKIP SCAN ON 3 RANGES ", "PTSDB", " ['na1'] - 
['na4']").put("serverWhereFilter",
@@ -353,8 +370,10 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("rangeScanSubstrBounds",
       "SELECT a_string FROM atable WHERE organization_id='000000000000001'"
         + " AND SUBSTR(entity_id,1,3) > '002' AND SUBSTR(entity_id,1,3) <= 
'003'",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE"
-        + " ['000000000000001','003            '] - ['000000000000001','004    
        ']"),
+      text(
+        "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE"
+          + " ['000000000000001','003            '] - ['000000000000001','004  
          ']",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>"),
       scanAttrs("RANGE SCAN ", "ATABLE",
         " ['000000000000001','003            '] - ['000000000000001','004      
      ']"));
   }
@@ -364,17 +383,19 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("skipScanTwoKeys",
       "SELECT a_string,b_string FROM atable"
         + " WHERE organization_id IN ('000000000000001', '000000000000005')",
-      text("CLIENT PARALLEL <N>-WAY SKIP SCAN ON 2 KEYS OVER ATABLE"
-        + " ['000000000000001'] - ['000000000000005']"),
+      text(
+        "CLIENT PARALLEL <N>-WAY SKIP SCAN ON 2 KEYS OVER ATABLE"
+          + " ['000000000000001'] - ['000000000000005']",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>"),
       scanAttrs("SKIP SCAN ON 2 KEYS ", "ATABLE", " ['000000000000001'] - 
['000000000000005']"));
   }
 
   @Test
   public void testGroupByClientLimit() throws Exception {
     verifyQuery("groupByClientLimit", "SELECT count(1) FROM atable GROUP BY 
a_string LIMIT 5",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
-        "    SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING]", "CLIENT MERGE 
SORT",
-        "CLIENT 5 ROW LIMIT"),
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER AGGREGATE INTO DISTINCT ROWS BY 
[A_STRING]",
+        "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")
@@ -386,8 +407,9 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("topNAscNullsFirstLimit",
       "SELECT a_string,b_string FROM atable WHERE organization_id = 
'000000000000001'"
         + " ORDER BY a_string ASC NULLS FIRST LIMIT 10",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['000000000000001']",
-        "    SERVER TOP 10 ROWS SORTED BY [A_STRING]", "CLIENT MERGE SORT", 
"CLIENT LIMIT 10"),
+      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['000000000000001']", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER TOP 10 ROWS SORTED BY 
[A_STRING]",
+        "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")
@@ -399,9 +421,9 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("topNDescNullsLastLimit",
       "SELECT a_string,b_string FROM atable WHERE organization_id = 
'000000000000001'"
         + " ORDER BY a_string DESC NULLS LAST LIMIT 10",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['000000000000001']",
-        "    SERVER TOP 10 ROWS SORTED BY [A_STRING DESC NULLS LAST]", "CLIENT 
MERGE SORT",
-        "CLIENT LIMIT 10"),
+      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['000000000000001']", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER TOP 10 ROWS SORTED BY [A_STRING 
DESC NULLS LAST]",
+        "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")
@@ -414,7 +436,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       "SELECT max(a_integer) FROM atable WHERE organization_id = 
'000000000000001'"
         + " GROUP BY organization_id,entity_id,ROUND(a_date,'HOUR')"
         + " ORDER BY entity_id NULLS LAST LIMIT 10",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['000000000000001']",
+      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['000000000000001']", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>",
         "    SERVER AGGREGATE INTO DISTINCT ROWS BY"
           + " [ORGANIZATION_ID, ENTITY_ID, ROUND(A_DATE)]",
         "CLIENT MERGE SORT", "CLIENT 10 ROW LIMIT"),
@@ -430,7 +453,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("clientSortedByHaving",
       "SELECT count(1) FROM atable WHERE a_integer = 1 GROUP BY 
a_string,b_string"
         + " HAVING max(a_string) = 'a' ORDER BY b_string",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    SERVER FILTER 
BY A_INTEGER = 1",
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER FILTER BY A_INTEGER = 1",
         "    SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING, B_STRING]", 
"CLIENT MERGE SORT",
         "CLIENT FILTER BY MAX(A_STRING) = 'a'", "CLIENT SORTED BY [B_STRING]"),
       scanAttrs("FULL SCAN ", "ATABLE", "")
@@ -457,8 +481,10 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
         + " JOIN atable b ON a.organization_id = b.organization_id"
         + " WHERE a.organization_id = '00D000000000001'",
       text("SORT-MERGE-JOIN (INNER) TABLES",
-        "    CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']", "AND",
-        "    CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE"),
+        "    CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']",
+        "        INDEX ATABLE", "        REGIONS PLANNED <N>", "AND",
+        "    CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "        INDEX 
ATABLE",
+        "        REGIONS PLANNED <N>"),
       expected);
   }
 
@@ -476,8 +502,10 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       "SELECT a.a_string, b.a_string FROM atable a"
         + " JOIN atable b ON a.organization_id = b.organization_id"
         + " WHERE a.organization_id = '00D000000000001'",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']",
-        "    PARALLEL INNER-JOIN TABLE 0", "        CLIENT PARALLEL <N>-WAY 
FULL SCAN OVER ATABLE",
+      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    PARALLEL INNER-JOIN TABLE 0",
+        "        CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "            
INDEX ATABLE",
+        "            REGIONS PLANNED <N>",
         "    DYNAMIC SERVER FILTER BY A.ORGANIZATION_ID IN 
(B.ORGANIZATION_ID)"),
       expected);
   }
@@ -497,9 +525,10 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     verifyQuery("hashJoinSemiInSubquery",
       "SELECT a_string FROM atable"
         + " WHERE organization_id IN (SELECT organization_id FROM atable WHERE 
a_integer = 1)",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    
SKIP-SCAN-JOIN TABLE 0",
-        "        CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
-        "            SERVER FILTER BY A_INTEGER = 1",
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SKIP-SCAN-JOIN TABLE 0",
+        "        CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "            
INDEX ATABLE",
+        "            REGIONS PLANNED <N>", "            SERVER FILTER BY 
A_INTEGER = 1",
         "            SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY 
[ORGANIZATION_ID]",
         "    DYNAMIC SERVER FILTER BY ATABLE.ORGANIZATION_ID IN ($1.$2)"),
       expected);
@@ -513,7 +542,9 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
         + " SELECT a_string FROM atable WHERE organization_id = 
'00D000000000002'",
       text("UNION ALL OVER 2 QUERIES",
         "    CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']",
-        "    CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000002']"),
+        "        INDEX ATABLE", "        REGIONS PLANNED <N>",
+        "    CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000002']",
+        "        INDEX ATABLE", "        REGIONS PLANNED <N>"),
       scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']")
         .put("abstractExplainPlan", "UNION ALL OVER 2 QUERIES")
         .set("rhsJoinQueryExplainPlan", rhs));
@@ -534,7 +565,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
         + " SELECT organization_id, entity_id, a_string FROM atable"
         + " WHERE organization_id = '00D000000000001'",
       false,
-      text("UPSERT SELECT", "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']"),
+      text("UPSERT SELECT", "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>"),
       scanAttrs("RANGE SCAN ", "ATABLE", " 
['00D000000000001']").put("abstractExplainPlan",
         "UPSERT SELECT"));
   }
@@ -546,7 +578,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
         + " SELECT organization_id, entity_id, a_string FROM atable"
         + " WHERE organization_id = '00D000000000001'",
       true,
-      text("UPSERT ROWS", "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']"),
+      text("UPSERT ROWS", "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>"),
       scanAttrs("RANGE SCAN ", "ATABLE", " 
['00D000000000001']").put("abstractExplainPlan",
         "UPSERT ROWS"));
   }
@@ -564,6 +597,7 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   public void testDeleteServer() throws Exception {
     verifyMutation("deleteServer", "DELETE FROM atable WHERE entity_id = 
'abc'", true,
       text("DELETE ROWS SERVER SELECT", "CLIENT PARALLEL <N>-WAY FULL SCAN 
OVER ATABLE",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>",
         "    SERVER FILTER BY FIRST KEY ONLY AND ENTITY_ID = 'abc'"),
       scanAttrs("FULL SCAN ", "ATABLE", "").put("abstractExplainPlan", "DELETE 
ROWS SERVER SELECT")
         .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY AND 
ENTITY_ID = 'abc'"));
@@ -573,6 +607,7 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   public void testDeleteClient() throws Exception {
     verifyMutation("deleteClient", "DELETE FROM atable WHERE entity_id = 
'abc'", false,
       text("DELETE ROWS CLIENT SELECT", "CLIENT PARALLEL <N>-WAY FULL SCAN 
OVER ATABLE",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>",
         "    SERVER FILTER BY FIRST KEY ONLY AND ENTITY_ID = 'abc'"),
       scanAttrs("FULL SCAN ", "ATABLE", "").put("abstractExplainPlan", "DELETE 
ROWS CLIENT SELECT")
         .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY AND 
ENTITY_ID = 'abc'"));
@@ -581,7 +616,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   @Test
   public void testSequenceNextValue() throws Exception {
     verifyQuery("sequenceNextValue", "SELECT NEXT VALUE FOR " + SEQ + " FROM 
atable",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    SERVER FILTER 
BY FIRST KEY ONLY",
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
+        "    REGIONS PLANNED <N>", "    SERVER FILTER BY FIRST KEY ONLY",
         "CLIENT RESERVE VALUES FROM 1 SEQUENCE"),
       scanAttrs("FULL SCAN ", "ATABLE", "")
         .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY 
ONLY").put("clientSequenceCount", 1)
@@ -591,9 +627,11 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   @Test
   public void testSaltedTableScan() throws Exception {
     verifyQuery("saltedTableScan", "SELECT * FROM " + SALTED + " WHERE v = 7",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER EO_SALTED", "    SERVER 
FILTER BY V = 7",
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER EO_SALTED", "    INDEX 
EO_SALTED",
+        "    SALT BUCKETS 4", "    REGIONS PLANNED <N>", "    SERVER FILTER BY 
V = 7",
         "CLIENT MERGE SORT"),
-      scanAttrs("FULL SCAN ", "EO_SALTED", "").put("serverWhereFilter", 
"SERVER FILTER BY V = 7")
+      scanAttrs("FULL SCAN ", "EO_SALTED", "").put("saltBuckets", 4)
+        .put("serverWhereFilter", "SERVER FILTER BY V = 7")
         .put("clientSortAlgo", "CLIENT MERGE SORT")
         .set("clientSteps", clientSteps("CLIENT MERGE SORT")));
   }
@@ -604,12 +642,56 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     tenantProps.setProperty(DATE_FORMAT_ATTRIB, "yyyy-MM-dd");
     tenantProps.setProperty(TENANT_ID_ATTRIB, TENANT_ID);
     verifyQuery("multiTenantView", "SELECT * FROM " + MT_VIEW + " LIMIT 1", 
tenantProps,
-      text("CLIENT SERIAL <N>-WAY RANGE SCAN OVER EO_MT_BASE ['tenant42']",
-        "    SERVER 1 ROW LIMIT", "CLIENT 1 ROW LIMIT"),
+      text("CLIENT SERIAL <N>-WAY RANGE SCAN OVER EO_MT_BASE ['tenant42']", "  
  INDEX EO_MT_VIEW",
+        "    REGIONS PLANNED <N>", "    SERVER 1 ROW LIMIT", "CLIENT 1 ROW 
LIMIT"),
       attrs().put("iteratorTypeAndScanSize", "SERIAL 
<N>-WAY").put("consistency", "STRONG")
         .put("explainScanType", "RANGE SCAN ").put("tableName", "EO_MT_BASE")
-        .put("keyRanges", " ['tenant42']").put("serverRowLimit", 
1).put("clientRowLimit", 1)
-        .set("clientSteps", clientSteps("CLIENT 1 ROW LIMIT")));
+        .put("indexName", "EO_MT_VIEW").put("keyRanges", " 
['tenant42']").put("serverRowLimit", 1)
+        .put("clientRowLimit", 1).set("clientSteps", clientSteps("CLIENT 1 ROW 
LIMIT")));
+  }
+
+  @Test
+  public void testIndexKindGlobal() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps());
+      java.sql.Statement stmt = conn.createStatement()) {
+      String base = generateUniqueName();
+      String idx = generateUniqueName();
+      stmt.execute("CREATE TABLE " + base + " (k VARCHAR PRIMARY KEY, v1 
VARCHAR, v2 VARCHAR)");
+      stmt.execute("CREATE INDEX " + idx + " ON " + base + " (v1) INCLUDE 
(v2)");
+      ExplainPlanTestUtil.assertPlan(conn, "SELECT v1, v2 FROM " + base + " 
WHERE v1 = 'x'")
+        .table(idx).indexName(idx).indexKind("GLOBAL");
+    }
+  }
+
+  @Test
+  public void testIndexKindLocal() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps());
+      java.sql.Statement stmt = conn.createStatement()) {
+      String base = generateUniqueName();
+      String idx = generateUniqueName();
+      stmt.execute("CREATE TABLE " + base + " (k VARCHAR PRIMARY KEY, v1 
VARCHAR, v2 VARCHAR)");
+      stmt.execute("CREATE LOCAL INDEX " + idx + " ON " + base + " (v1)");
+      // The OVER line decorates a local index as <idx>(<phys>); the INDEX 
line prints just <idx>.
+      ExplainPlanTestUtil
+        .assertPlan(conn,
+          "SELECT /*+ INDEX(" + base + " " + idx + ") */ k, v1 FROM " + base + 
" WHERE v1 = 'x'")
+        .indexName(idx).indexKind("LOCAL");
+    }
+  }
+
+  @Test
+  public void testIndexKindUncoveredGlobal() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps());
+      java.sql.Statement stmt = conn.createStatement()) {
+      String base = generateUniqueName();
+      String idx = generateUniqueName();
+      stmt.execute("CREATE TABLE " + base + " (k VARCHAR PRIMARY KEY, v1 
VARCHAR, v2 VARCHAR)");
+      stmt.execute("CREATE UNCOVERED INDEX " + idx + " ON " + base + " (v1)");
+      ExplainPlanTestUtil
+        .assertPlan(conn,
+          "SELECT /*+ INDEX(" + base + " " + idx + ") */ k, v2 FROM " + base + 
" WHERE v1 = 'x'")
+        .indexName(idx).indexKind("UNCOVERED GLOBAL");
+    }
   }
 
   @Test
@@ -908,6 +990,10 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     n.putNull("explainScanType");
     n.putNull("tableName");
     n.putNull("keyRanges");
+    n.putNull("indexName");
+    n.putNull("indexKind");
+    n.putNull("saltBuckets");
+    n.putNull("regionsPlanned");
     n.putNull("scanTimeRangeMin");
     n.putNull("scanTimeRangeMax");
     n.putNull("serverWhereFilter");
@@ -957,6 +1043,9 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     n.put("consistency", "STRONG");
     n.put("explainScanType", scanType);
     n.put("tableName", table);
+    // For a data table scan the per scan INDEX line names the same entity as 
tableName. View and
+    // index scans that diverge override indexName on the returned node.
+    n.put("indexName", table);
     if (keys != null) {
       n.put("keyRanges", keys);
     }
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 7c55ecbafc..2f44a07e58 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
@@ -175,6 +175,33 @@ public final class ExplainPlanTestUtil {
       return this;
     }
 
+    /** Assert the chosen per-scan index (or data table) name. */
+    public ExplainPlanAssert indexName(String expected) {
+      assertEquals(at("indexName"), expected, attributes.getIndexName());
+      return this;
+    }
+
+    /**
+     * Assert the per-scan index kind token: {@code "LOCAL"}, {@code 
"GLOBAL"}, or
+     * {@code "UNCOVERED GLOBAL"} (null for a data-table target).
+     */
+    public ExplainPlanAssert indexKind(String expected) {
+      assertEquals(at("indexKind"), expected, attributes.getIndexKind());
+      return this;
+    }
+
+    /** Assert the salt bucket count of the scanned table (null when not 
salted). */
+    public ExplainPlanAssert saltBuckets(Integer expected) {
+      assertEquals(at("saltBuckets"), expected, attributes.getSaltBuckets());
+      return this;
+    }
+
+    /** Assert the number of regions the scan is planned to hit (null when 
unknown). */
+    public ExplainPlanAssert regionsPlanned(Integer expected) {
+      assertEquals(at("regionsPlanned"), expected, 
attributes.getRegionsPlanned());
+      return this;
+    }
+
     /** Assert the hex-string row-value-constructor offset marker. */
     public ExplainPlanAssert hexStringRVCOffset(String expected) {
       assertEquals(at("hexStringRVCOffset"), expected, 
attributes.getHexStringRVCOffset());
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainTextNormalizer.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainTextNormalizer.java
index 24c3bee4a9..c8e1d1ef46 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainTextNormalizer.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainTextNormalizer.java
@@ -33,9 +33,12 @@ public final class ExplainTextNormalizer {
   // Matches the iterator parallelism count.
   private static final Pattern WAY_COUNT = Pattern.compile("\\b\\d+-WAY\\b");
 
-  // Matches the stats-row-count gated row count and byte count.
+  // Matches the stats row and byte counts.
   private static final Pattern ROWS_BYTES = Pattern.compile("\\d+ ROWS \\d+ 
BYTES\\s*");
 
+  // Matches the planned regions count on the REGIONS PLANNED line.
+  private static final Pattern REGIONS_PLANNED = Pattern.compile("REGIONS 
PLANNED \\d+");
+
   // Matches the region locations line.
   private static final String REGION_LOCATIONS_PREFIX = " (region locations = 
";
 
@@ -62,6 +65,7 @@ public final class ExplainTextNormalizer {
       normalized = CHUNK_COUNT.matcher(normalized).replaceAll("<N>-CHUNK");
       normalized = WAY_COUNT.matcher(normalized).replaceAll("<N>-WAY");
       normalized = ROWS_BYTES.matcher(normalized).replaceAll("");
+      normalized = REGIONS_PLANNED.matcher(normalized).replaceAll("REGIONS 
PLANNED <N>");
       normalized = aliases.rewrite(normalized);
       out.add(normalized);
     }

Reply via email to