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 1e32251fbf4c0c21ca7dff104752e88cfb58c92f
Author: Andrew Purtell <[email protected]>
AuthorDate: Fri Jun 5 17:54:07 2026 -0700

    [WIP] Add per-scan INDEX, SALT BUCKETS, and REGIONS PLANNED to EXPLAIN
    
    Emit the chosen index name with its kind (GLOBAL/LOCAL/UNCOVERED GLOBAL),
    salt bucket count, and planned region count for each scan, exposed in both
    the EXPLAIN text and ExplainPlanAttributes. Update the backward-compat
    baselines, normalize the environment-dependent REGIONS PLANNED count, and
    add dedicated tests covering data-table, salted, and all index-kind scans.
---
 .../phoenix/compile/ExplainPlanAttributes.java     |  78 +++++-
 .../phoenix/iterate/BaseResultIterators.java       |   9 +
 .../org/apache/phoenix/iterate/ExplainTable.java   |  92 +++----
 .../query/explain/ExplainJsonNormalizer.java       |   5 +
 .../phoenix/query/explain/ExplainPlanTest.java     | 276 +++++++++++++++++----
 .../query/explain/ExplainTextNormalizer.java       |   5 +
 6 files changed, 358 insertions(+), 107 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 683369d854..af0feeccc6 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,7 +35,8 @@ import org.apache.phoenix.schema.PColumn;
 @JsonPropertyOrder({ "abstractExplainPlan", "hint", "explainScanType", 
"consistency", "tableName",
   "keyRanges", "scanTimeRangeMin", "scanTimeRangeMax", "splitsChunk", 
"useRoundRobinIterator",
   "samplingRate", "hexStringRVCOffset", "iteratorTypeAndScanSize", 
"estimatedRows",
-  "estimatedSizeInBytes", "serverWhereFilter", "serverDistinctFilter", 
"serverMergeColumns",
+  "estimatedSizeInBytes", "indexName", "indexKind", "saltBuckets", 
"regionsPlanned",
+  "serverWhereFilter", "serverDistinctFilter", "serverMergeColumns",
   "serverArrayElementProjection", "serverAggregate", "serverGroupByLimit", 
"serverSortedBy",
   "serverOffset", "serverRowLimit", "clientFilterBy", "clientAggregate", 
"clientDistinctFilter",
   "clientAfterAggregate", "clientSortAlgo", "clientSortedBy", "clientOffset", 
"clientRowLimit",
@@ -61,6 +62,12 @@ public class ExplainPlanAttributes {
   private final Long estimatedRows;
   private final Long estimatedSizeInBytes;
 
+  // Per-scan index and structural attributes
+  private final String indexName;
+  private final String indexKind;
+  private final Integer saltBuckets;
+  private final Integer regionsPlanned;
+
   // Server-side operations
   private final String serverWhereFilter;
   private final String serverDistinctFilter;
@@ -114,6 +121,10 @@ public class ExplainPlanAttributes {
     this.iteratorTypeAndScanSize = null;
     this.estimatedRows = null;
     this.estimatedSizeInBytes = null;
+    this.indexName = null;
+    this.indexKind = null;
+    this.saltBuckets = null;
+    this.regionsPlanned = null;
     this.serverWhereFilter = null;
     this.serverDistinctFilter = null;
     this.serverMergeColumns = null;
@@ -147,7 +158,8 @@ public class ExplainPlanAttributes {
     Consistency consistency, String tableName, String keyRanges, Long 
scanTimeRangeMin,
     Long scanTimeRangeMax, Integer splitsChunk, boolean useRoundRobinIterator, 
Double samplingRate,
     String hexStringRVCOffset, String iteratorTypeAndScanSize, Long 
estimatedRows,
-    Long estimatedSizeInBytes, String serverWhereFilter, String 
serverDistinctFilter,
+    Long estimatedSizeInBytes, String indexName, String indexKind, Integer 
saltBuckets,
+    Integer regionsPlanned, String serverWhereFilter, String 
serverDistinctFilter,
     Set<PColumn> serverMergeColumns, boolean serverArrayElementProjection, 
String serverAggregate,
     Integer serverGroupByLimit, String serverSortedBy, Integer serverOffset, 
Long serverRowLimit,
     String clientFilterBy, String clientAggregate, String clientDistinctFilter,
@@ -172,6 +184,10 @@ public class ExplainPlanAttributes {
     this.iteratorTypeAndScanSize = iteratorTypeAndScanSize;
     this.estimatedRows = estimatedRows;
     this.estimatedSizeInBytes = estimatedSizeInBytes;
+    this.indexName = indexName;
+    this.indexKind = indexKind;
+    this.saltBuckets = saltBuckets;
+    this.regionsPlanned = regionsPlanned;
     this.serverWhereFilter = serverWhereFilter;
     this.serverDistinctFilter = serverDistinctFilter;
     this.serverMergeColumns = serverMergeColumns;
@@ -261,6 +277,22 @@ public class ExplainPlanAttributes {
     return estimatedSizeInBytes;
   }
 
+  public String getIndexName() {
+    return indexName;
+  }
+
+  public String getIndexKind() {
+    return indexKind;
+  }
+
+  public Integer getSaltBuckets() {
+    return saltBuckets;
+  }
+
+  public Integer getRegionsPlanned() {
+    return regionsPlanned;
+  }
+
   public String getServerWhereFilter() {
     return serverWhereFilter;
   }
@@ -391,6 +423,10 @@ public class ExplainPlanAttributes {
     private String iteratorTypeAndScanSize;
     private Long estimatedRows;
     private Long estimatedSizeInBytes;
+    private String indexName;
+    private String indexKind;
+    private Integer saltBuckets;
+    private Integer regionsPlanned;
     private String serverWhereFilter;
     private String serverDistinctFilter;
     private Set<PColumn> serverMergeColumns;
@@ -439,6 +475,10 @@ public class ExplainPlanAttributes {
       this.iteratorTypeAndScanSize = 
explainPlanAttributes.getIteratorTypeAndScanSize();
       this.estimatedRows = explainPlanAttributes.getEstimatedRows();
       this.estimatedSizeInBytes = 
explainPlanAttributes.getEstimatedSizeInBytes();
+      this.indexName = explainPlanAttributes.getIndexName();
+      this.indexKind = explainPlanAttributes.getIndexKind();
+      this.saltBuckets = explainPlanAttributes.getSaltBuckets();
+      this.regionsPlanned = explainPlanAttributes.getRegionsPlanned();
       this.serverWhereFilter = explainPlanAttributes.getServerWhereFilter();
       this.serverDistinctFilter = 
explainPlanAttributes.getServerDistinctFilter();
       this.serverMergeColumns = explainPlanAttributes.getServerMergeColumns();
@@ -543,6 +583,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 setServerWhereFilter(String 
serverWhereFilter) {
       this.serverWhereFilter = serverWhereFilter;
       return this;
@@ -684,13 +744,13 @@ public class ExplainPlanAttributes {
       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, 
rhsJoinQueryExplainPlan, subPlans,
-        dynamicServerFilter, afterJoinFilter, joinScannerLimit, 
sortMergeSkipMerge, regionLocations,
-        numRegionLocationLookups);
+        estimatedRows, estimatedSizeInBytes, indexName, indexKind, 
saltBuckets, regionsPlanned,
+        serverWhereFilter, 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);
     }
   }
 }
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..1384ab4484 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
@@ -646,6 +646,15 @@ public abstract class BaseResultIterators extends 
ExplainTable implements Result
     else return scans;
   }
 
+  public int getSplitCount() {
+    return splits == null ? 0 : splits.size();
+  }
+
+  @Override
+  protected Integer getRegionsPlanned() {
+    return splits == null ? 0 : splits.size();
+  }
+
   private List<HRegionLocation> getRegionBoundaries(ParallelScanGrouper 
scanGrouper,
     byte[] startRegionBoundaryKey, byte[] stopRegionBoundaryKey) throws 
SQLException {
     return scanGrouper.getRegionBoundaries(context, physicalTableName, 
startRegionBoundaryKey,
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 221a69c8dd..9a936dcda8 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
@@ -53,6 +53,7 @@ import org.apache.phoenix.query.QueryServices;
 import org.apache.phoenix.query.QueryServicesOptions;
 import org.apache.phoenix.schema.PColumn;
 import org.apache.phoenix.schema.PTable;
+import org.apache.phoenix.schema.PTableType;
 import org.apache.phoenix.schema.RowKeySchema;
 import org.apache.phoenix.schema.SortOrder;
 import org.apache.phoenix.schema.TableRef;
@@ -180,6 +181,50 @@ public abstract class ExplainTable {
         explainPlanAttributesBuilder.setKeyRanges(appendKeyRanges());
       }
     }
+    PTable table = tableRef.getTable();
+    String idxName = table.getName().getString();
+    if (table.getIndexType() == PTable.IndexType.LOCAL
+      && table.getViewIndexId() != null
+      && idxName.contains(QueryConstants.CHILD_VIEW_INDEX_NAME_SEPARATOR)) {
+      int lastIdx = 
idxName.lastIndexOf(QueryConstants.CHILD_VIEW_INDEX_NAME_SEPARATOR);
+      idxName = idxName.substring(lastIdx + 1);
+    }
+    StringBuilder indexBuf = new StringBuilder("INDEX ").append(idxName);
+    String indexKindStr = null;
+    if (table.getType() == PTableType.INDEX) {
+      PTable.IndexType idxType = table.getIndexType();
+      if (idxType == PTable.IndexType.LOCAL) {
+        indexKindStr = "LOCAL";
+      } else if (context.isUncoveredIndex()
+        || idxType == PTable.IndexType.UNCOVERED_GLOBAL) {
+        indexKindStr = "UNCOVERED GLOBAL";
+      } else if (idxType == PTable.IndexType.GLOBAL) {
+        indexKindStr = "GLOBAL";
+      }
+      if (indexKindStr != null) {
+        indexBuf.append(' ').append(indexKindStr);
+      }
+    }
+    planSteps.add("    " + indexBuf.toString());
+    if (explainPlanAttributesBuilder != null) {
+      explainPlanAttributesBuilder.setIndexName(idxName);
+      explainPlanAttributesBuilder.setIndexKind(indexKindStr);
+    }
+    Integer bucketNum = table.getBucketNum();
+    if (bucketNum != null && bucketNum > 0) {
+      planSteps.add("    SALT BUCKETS " + bucketNum);
+      if (explainPlanAttributesBuilder != null) {
+        explainPlanAttributesBuilder.setSaltBuckets(bucketNum);
+      }
+    }
+    Integer regionsPlannedVal = getRegionsPlanned();
+    if (regionsPlannedVal != null) {
+      planSteps.add("    REGIONS PLANNED " + regionsPlannedVal);
+      if (explainPlanAttributesBuilder != null) {
+        explainPlanAttributesBuilder.setRegionsPlanned(regionsPlannedVal);
+      }
+    }
+
     if (context.getScan() != null && 
tableRef.getTable().getRowTimestampColPos() != -1) {
       TimeRange range = context.getScan().getTimeRange();
       planSteps.add("    ROW TIMESTAMP FILTER [" + range.getMin() + ", " + 
range.getMax() + ")");
@@ -455,49 +500,6 @@ public abstract class ExplainTable {
     return (useLongViewIndex ? (Long) s : (Short) s) + Short.MAX_VALUE + 2;
   }
 
-  private static class RowKeyValueIterator implements Iterator<byte[]> {
-    private final RowKeySchema schema;
-    private ImmutableBytesWritable ptr = new ImmutableBytesWritable();
-    private int position = 0;
-    private final int maxOffset;
-    private byte[] nextValue;
-
-    public RowKeyValueIterator(RowKeySchema schema, byte[] rowKey) {
-      this.schema = schema;
-      this.maxOffset = schema.iterator(rowKey, ptr);
-      iterate();
-    }
-
-    private void iterate() {
-      if (schema.next(ptr, position++, maxOffset) == null) {
-        nextValue = null;
-      } else {
-        nextValue = ptr.copyBytes();
-      }
-    }
-
-    @Override
-    public boolean hasNext() {
-      return nextValue != null;
-    }
-
-    @Override
-    public byte[] next() {
-      if (nextValue == null) {
-        throw new NoSuchElementException();
-      }
-      byte[] value = nextValue;
-      iterate();
-      return value;
-    }
-
-    @Override
-    public void remove() {
-      throw new UnsupportedOperationException();
-    }
-
-  }
-
   private void appendScanRow(StringBuilder buf, Bound bound) {
     ScanRanges scanRanges = context.getScanRanges();
     Iterator<byte[]> minMaxIterator = Collections.emptyIterator();
@@ -531,6 +533,10 @@ public abstract class ExplainTable {
     }
   }
 
+  protected Integer getRegionsPlanned() {
+    return null;
+  }
+
   private String appendKeyRanges() {
     final StringBuilder buf = new StringBuilder();
     ScanRanges scanRanges = context.getScanRanges();
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 eccde4303a..22fa3e715f 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
@@ -56,6 +56,11 @@ public final class ExplainJsonNormalizer {
     if (obj.has("splitsChunk")) {
       obj.set("splitsChunk", NullNode.getInstance());
     }
+    // The planned region/split count is dependent on the split count that 
drives <N>-WAY, so it
+    // is collapsed for comparison.
+    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 df26951ff3..f653d3d69d 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
@@ -73,6 +73,11 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   private static final String MT_BASE = "EO_MT_BASE";
   private static final String MT_VIEW = "EO_MT_VIEW";
   private static final String TENANT_ID = "tenant42";
+  // Base table plus three index flavors used to exercise the per-scan INDEX 
<kind> classifier.
+  private static final String IDX_BASE = "EO_IDX_BASE";
+  private static final String GLOBAL_IDX = "EO_GIDX";
+  private static final String LOCAL_IDX = "EO_LIDX";
+  private static final String UNCOVERED_IDX = "EO_UIDX";
 
   private static ExplainOracle oracle;
   private static ObjectMapper mapper;
@@ -86,6 +91,14 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       conn.createStatement().execute("CREATE TABLE IF NOT EXISTS " + SALTED
         + " (k VARCHAR NOT NULL PRIMARY KEY, v INTEGER) SALT_BUCKETS=4");
       conn.createStatement().execute("CREATE SEQUENCE IF NOT EXISTS " + SEQ);
+      conn.createStatement().execute("CREATE TABLE IF NOT EXISTS " + IDX_BASE
+        + " (k VARCHAR NOT NULL PRIMARY KEY, a INTEGER, b INTEGER)");
+      conn.createStatement()
+        .execute("CREATE INDEX IF NOT EXISTS " + GLOBAL_IDX + " ON " + 
IDX_BASE + " (a) INCLUDE(b)");
+      conn.createStatement()
+        .execute("CREATE LOCAL INDEX IF NOT EXISTS " + LOCAL_IDX + " ON " + 
IDX_BASE + " (a)");
+      conn.createStatement()
+        .execute("CREATE UNCOVERED INDEX IF NOT EXISTS " + UNCOVERED_IDX + " 
ON " + IDX_BASE + " (a)");
       conn.createStatement()
         .execute("CREATE TABLE IF NOT EXISTS " + MT_BASE + " (" + "  tenant_id 
VARCHAR(8) NOT NULL,"
           + "  userid INTEGER NOT NULL," + "  username VARCHAR NOT NULL," + "  
col VARCHAR"
@@ -109,8 +122,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 +134,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));
   }
 
@@ -131,7 +145,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       "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 ']"),
+        + " ['00D000000000001','00E00000000002!'] - 
['00D000000000001','00E00000000008 ']",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>"),
       scanAttrs("RANGE SCAN ", "ATABLE",
         " ['00D000000000001','00E00000000002!'] - 
['00D000000000001','00E00000000008 ']"));
   }
@@ -140,7 +155,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 +169,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 +178,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 +199,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 +207,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 +218,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"));
@@ -212,7 +231,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", "")
@@ -224,8 +244,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"));
@@ -235,7 +256,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", "")
@@ -249,7 +271,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']")
@@ -261,8 +284,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));
   }
 
@@ -273,7 +296,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
         + " 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']"),
+        + " ['000000000000001','000000000000003'] - 
['000000000000001','000000000000005']",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>"),
       scanAttrs("RANGE SCAN ", "ATABLE",
         " ['000000000000001','000000000000003'] - 
['000000000000001','000000000000005']"));
   }
@@ -287,6 +311,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",
@@ -298,7 +323,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'"));
@@ -309,8 +335,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]",
-        "    SERVER FILTER BY FIRST KEY ONLY AND (HOST IS 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",
         "SERVER FILTER BY FIRST KEY ONLY AND (HOST IS NULL"
@@ -325,7 +351,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"));
@@ -336,7 +362,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']",
-        "    SERVER FILTER BY FIRST KEY ONLY AND"
+        "    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",
         "SERVER FILTER BY FIRST KEY ONLY AND REGEXP_SUBSTR(INST, '[^-]+', 1) 
IN ('na1','na2','na3')"));
@@ -348,7 +374,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       "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    
        ']"),
+        + " ['000000000000001','003            '] - ['000000000000001','004    
        ']",
+        "    INDEX ATABLE", "    REGIONS PLANNED <N>"),
       scanAttrs("RANGE SCAN ", "ATABLE",
         " ['000000000000001','003            '] - ['000000000000001','004      
      ']"));
   }
@@ -359,16 +386,17 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       "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']"),
+        + " ['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"));
@@ -379,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"));
@@ -391,9 +420,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"));
@@ -405,8 +434,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']",
-        "    SERVER AGGREGATE INTO DISTINCT ROWS BY"
+      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"),
       scanAttrs("RANGE SCAN ", "ATABLE", " ['000000000000001']")
@@ -420,7 +449,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", "")
@@ -438,8 +468,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>"),
       scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']")
         .put("abstractExplainPlan", "SORT-MERGE-JOIN 
(INNER)").set("rhsJoinQueryExplainPlan", rhs));
   }
@@ -458,8 +490,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);
   }
@@ -478,9 +512,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 ($<N>.$<N>)"),
       expected);
@@ -494,7 +529,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));
@@ -515,7 +552,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"));
   }
@@ -527,7 +565,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"));
   }
@@ -545,6 +584,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'"));
@@ -554,6 +594,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'"));
@@ -562,7 +603,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));
@@ -571,9 +613,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"));
   }
 
@@ -583,11 +627,127 @@ 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));
+        .put("indexName", "EO_MT_VIEW").put("keyRanges", " 
['tenant42']").put("serverRowLimit", 1)
+        .put("clientRowLimit", 1));
+  }
+
+  @Test
+  public void testIndexLineDataTableNoKind() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps())) {
+      ExplainPlan plan = ExplainPlanTestUtil.getExplainPlan(conn, "SELECT * 
FROM atable");
+      ExplainPlanAttributes attrs = plan.getPlanStepsAsAttributes();
+      // A data-table scan reports the table as the chosen INDEX and emits no 
kind token.
+      assertEquals("ATABLE", attrs.getIndexName());
+      assertEquals(null, attrs.getIndexKind());
+      assertEquals(null, attrs.getSaltBuckets());
+      assertTrue("regionsPlanned should be populated", 
attrs.getRegionsPlanned() != null);
+      assertTrue("regionsPlanned should be positive", 
attrs.getRegionsPlanned() >= 1);
+      assertTrue("text must carry bare INDEX line",
+        plan.getPlanSteps().contains("    INDEX ATABLE"));
+      // No kind token and no SALT BUCKETS line for an unsalted data table.
+      for (String line : plan.getPlanSteps()) {
+        assertTrue("unexpected SALT BUCKETS line: " + line, 
!line.contains("SALT BUCKETS"));
+      }
+    }
+  }
+
+  @Test
+  public void testSaltBucketsLine() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps())) {
+      ExplainPlan plan = ExplainPlanTestUtil.getExplainPlan(conn, "SELECT * 
FROM " + SALTED);
+      ExplainPlanAttributes attrs = plan.getPlanStepsAsAttributes();
+      assertEquals(SALTED, attrs.getIndexName());
+      assertEquals(Integer.valueOf(4), attrs.getSaltBuckets());
+      assertTrue("text must carry SALT BUCKETS 4",
+        plan.getPlanSteps().contains("    SALT BUCKETS 4"));
+      // SALT BUCKETS sits between INDEX and REGIONS PLANNED in the per-scan 
order.
+      List<String> steps = plan.getPlanSteps();
+      int idxLine = steps.indexOf("    INDEX " + SALTED);
+      int saltLine = steps.indexOf("    SALT BUCKETS 4");
+      assertTrue("INDEX line present", idxLine >= 0);
+      assertTrue("SALT BUCKETS after INDEX", saltLine > idxLine);
+    }
+  }
+
+  @Test
+  public void testRegionsPlannedAlwaysEmitted() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps())) {
+      List<String> steps =
+        ExplainPlanTestUtil.getPlanSteps(conn, "SELECT * FROM atable WHERE 
organization_id = '00D'");
+      boolean hasRegionsPlanned = false;
+      for (String line : steps) {
+        if (line.trim().startsWith("REGIONS PLANNED ")) {
+          hasRegionsPlanned = true;
+          // The value is a positive integer.
+          int n = Integer.parseInt(line.trim().substring("REGIONS PLANNED 
".length()));
+          assertTrue("REGIONS PLANNED value must be positive", n >= 1);
+        }
+      }
+      assertTrue("every scan emits REGIONS PLANNED", hasRegionsPlanned);
+    }
+  }
+
+  @Test
+  public void testIndexKindGlobal() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps())) {
+      // Selecting only covered columns lets the covered global index be 
chosen.
+      ExplainPlan plan = ExplainPlanTestUtil.getExplainPlan(conn,
+        "SELECT /*+ INDEX(" + IDX_BASE + " " + GLOBAL_IDX + ") */ a, b FROM " 
+ IDX_BASE);
+      ExplainPlanAttributes attrs = plan.getPlanStepsAsAttributes();
+      assertEquals(GLOBAL_IDX, attrs.getIndexName());
+      assertEquals("GLOBAL", attrs.getIndexKind());
+      assertTrue("text must carry INDEX <name> GLOBAL",
+        plan.getPlanSteps().contains("    INDEX " + GLOBAL_IDX + " GLOBAL"));
+    }
+  }
+
+  @Test
+  public void testIndexKindLocal() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps())) {
+      ExplainPlan plan = ExplainPlanTestUtil.getExplainPlan(conn,
+        "SELECT /*+ INDEX(" + IDX_BASE + " " + LOCAL_IDX + ") */ a FROM " + 
IDX_BASE
+          + " WHERE a = 1");
+      ExplainPlanAttributes attrs = plan.getPlanStepsAsAttributes();
+      // For a local index the INDEX line carries just the index name with the 
LOCAL kind token,
+      // while the OVER clause keeps the disambiguating idx(phys) form.
+      assertEquals(LOCAL_IDX, attrs.getIndexName());
+      assertEquals("LOCAL", attrs.getIndexKind());
+      assertTrue("text must carry INDEX <name> LOCAL",
+        plan.getPlanSteps().contains("    INDEX " + LOCAL_IDX + " LOCAL"));
+    }
+  }
+
+  @Test
+  public void testIndexKindUncoveredGlobal() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps())) {
+      ExplainPlan plan = ExplainPlanTestUtil.getExplainPlan(conn,
+        "SELECT /*+ INDEX(" + IDX_BASE + " " + UNCOVERED_IDX + ") */ a FROM " 
+ IDX_BASE
+          + " WHERE a = 1");
+      ExplainPlanAttributes attrs = plan.getPlanStepsAsAttributes();
+      assertEquals(UNCOVERED_IDX, attrs.getIndexName());
+      assertEquals("UNCOVERED GLOBAL", attrs.getIndexKind());
+      assertTrue("text must carry INDEX <name> UNCOVERED GLOBAL",
+        plan.getPlanSteps().contains("    INDEX " + UNCOVERED_IDX + " 
UNCOVERED GLOBAL"));
+    }
+  }
+
+  @Test
+  public void testTextNormalizerCollapsesRegionsPlanned() {
+    assertEquals(
+      Collections.singletonList("    REGIONS PLANNED <N>"),
+      new ExplainTextNormalizer().normalize(Arrays.asList("    REGIONS PLANNED 
20")));
+  }
+
+  @Test
+  public void testJsonNormalizerErasesRegionsPlanned() {
+    ObjectNode root = mapper.createObjectNode();
+    root.put("regionsPlanned", 7);
+    new ExplainJsonNormalizer().normalize(root);
+    assertTrue(root.get("regionsPlanned").isNull());
   }
 
   @Test
@@ -861,6 +1021,10 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     n.putNull("splitsChunk");
     n.putNull("estimatedRows");
     n.putNull("estimatedSizeInBytes");
+    n.putNull("indexName");
+    n.putNull("indexKind");
+    n.putNull("saltBuckets");
+    n.putNull("regionsPlanned");
     n.putNull("iteratorTypeAndScanSize");
     n.putNull("samplingRate");
     n.put("useRoundRobinIterator", false);
@@ -915,6 +1079,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     n.put("consistency", "STRONG");
     n.put("explainScanType", scanType);
     n.put("tableName", table);
+    // For a data table scan the chosen INDEX <name> is the logical table name.
+    n.put("indexName", table);
     if (keys != null) {
       n.put("keyRanges", keys);
     }
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 446e1fe2fd..9099f197bd 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
@@ -35,6 +35,10 @@ public final class ExplainTextNormalizer {
   // PARALLEL 400-WAY -> PARALLEL <N>-WAY ; matches the iterator parallelism 
count.
   private static final Pattern WAY_COUNT = Pattern.compile("\\b\\d+-WAY\\b");
 
+  // REGIONS PLANNED 20 -> REGIONS PLANNED <N> the planned region/split count 
is dependent
+  // on the split count that drives <N>-WAY, so it is collapsed for 
cross-environment comparison.
+  private static final Pattern REGIONS_PLANNED = Pattern.compile("REGIONS 
PLANNED \\d+");
+
   // 1234 ROWS 5678 BYTES (stats-row-count gated; we strip when present).
   private static final Pattern ROWS_BYTES = Pattern.compile("\\d+ ROWS \\d+ 
BYTES\\s*");
 
@@ -67,6 +71,7 @@ public final class ExplainTextNormalizer {
       String normalized = line;
       normalized = CHUNK_COUNT.matcher(normalized).replaceAll("<N>-CHUNK");
       normalized = WAY_COUNT.matcher(normalized).replaceAll("<N>-WAY");
+      normalized = REGIONS_PLANNED.matcher(normalized).replaceAll("REGIONS 
PLANNED <N>");
       normalized = ROWS_BYTES.matcher(normalized).replaceAll("");
       normalized =
         
DYNAMIC_FILTER_ALIAS.matcher(normalized).replaceAll(DYNAMIC_FILTER_ALIAS_PLACEHOLDER);

Reply via email to