You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@phoenix.apache.org by ap...@apache.org on 2026/06/09 22:43:28 UTC

(phoenix) branch PHOENIX-7876-feature updated: PHOENIX-7886 Server filter and projection EXPLAIN output improvements (#2507)

This is an automated email from the ASF dual-hosted git repository.

apurtell pushed a commit to branch PHOENIX-7876-feature
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/PHOENIX-7876-feature by this push:
     new 9b0fecb74e PHOENIX-7886 Server filter and projection EXPLAIN output improvements (#2507)
9b0fecb74e is described below

commit 9b0fecb74ea76d63b23d768732304b10f4d5dedd
Author: Andrew Purtell <ap...@apache.org>
AuthorDate: Tue Jun 9 15:43:22 2026 -0700

    PHOENIX-7886 Server filter and projection EXPLAIN output improvements (#2507)
---
 .../phoenix/compile/ExplainPlanAttributes.java     | 44 +++++++++--
 .../util/bson/UpdateExpressionUtils.java           |  3 +-
 .../org/apache/phoenix/iterate/ExplainTable.java   | 24 +++---
 .../apache/phoenix/end2end/BaseAggregateIT.java    |  8 +-
 .../end2end/BaseAggregateWithRegionMoves2IT.java   |  8 +-
 .../end2end/BaseAggregateWithRegionMovesIT.java    |  4 +-
 .../end2end/BaseTenantSpecificViewIndexIT.java     |  5 +-
 .../org/apache/phoenix/end2end/BaseViewIT.java     | 13 ++--
 .../java/org/apache/phoenix/end2end/Bson3IT.java   | 30 ++++----
 .../phoenix/end2end/CostBasedDecisionIT.java       | 19 ++---
 .../CountDistinctApproximateHyperLogLogIT.java     |  3 +-
 .../org/apache/phoenix/end2end/EmptyColumnIT.java  |  2 +-
 .../end2end/ExplainPlanWithStatsDisabledIT.java    | 24 +++---
 .../phoenix/end2end/FlappingLocalIndexIT.java      | 10 +--
 .../apache/phoenix/end2end/IndexExtendedIT.java    |  2 +-
 .../java/org/apache/phoenix/end2end/KeyOnlyIT.java |  2 +-
 .../phoenix/end2end/LocalIndexSplitMergeIT.java    |  9 +--
 .../apache/phoenix/end2end/QueryWithLimitIT.java   |  2 +-
 .../apache/phoenix/end2end/QueryWithOffsetIT.java  |  5 +-
 .../phoenix/end2end/QueryWithTableSampleIT.java    | 14 ++--
 .../org/apache/phoenix/end2end/ReverseScanIT.java  |  6 +-
 .../phoenix/end2end/SequenceBulkAllocationIT.java  |  2 +-
 .../org/apache/phoenix/end2end/SequenceIT.java     |  2 +-
 .../phoenix/end2end/SortMergeJoinMoreIT.java       |  7 +-
 .../phoenix/end2end/TenantSpecificViewIndexIT.java | 10 +--
 .../phoenix/end2end/UserDefinedFunctionsIT.java    |  4 +-
 .../it/java/org/apache/phoenix/end2end/ViewIT.java | 16 +---
 .../apache/phoenix/end2end/index/BaseIndexIT.java  | 16 ++--
 .../end2end/index/BaseIndexWithRegionMovesIT.java  | 16 ++--
 .../index/ChildViewsUseParentViewIndexIT.java      | 13 ++--
 .../end2end/index/GlobalIndexCheckerIT.java        | 10 +--
 .../index/GlobalIndexCheckerWithRegionMovesIT.java | 10 +--
 .../end2end/index/GlobalIndexOptimizationIT.java   | 29 ++++---
 .../apache/phoenix/end2end/index/IndexUsageIT.java | 42 +++++-----
 .../apache/phoenix/end2end/index/LocalIndexIT.java | 46 ++++++-----
 .../phoenix/end2end/index/SaltedIndexIT.java       | 18 ++---
 .../apache/phoenix/end2end/index/ViewIndexIT.java  |  4 +-
 .../end2end/join/HashJoinGlobalIndexIT.java        | 61 ++++++---------
 .../phoenix/end2end/join/HashJoinLocalIndexIT.java | 78 ++++++++-----------
 .../phoenix/end2end/join/HashJoinMoreIT.java       |  5 +-
 .../phoenix/end2end/join/HashJoinNoIndexIT.java    | 24 +++---
 .../end2end/join/SortMergeJoinGlobalIndexIT.java   | 16 ++--
 .../end2end/join/SortMergeJoinLocalIndexIT.java    | 14 ++--
 .../end2end/join/SortMergeJoinNoIndexIT.java       |  2 +-
 .../phoenix/schema/stats/BaseStatsCollectorIT.java |  3 +-
 .../phoenix/compile/JoinQueryCompilerTest.java     |  2 +-
 .../apache/phoenix/compile/QueryCompilerTest.java  | 31 ++++----
 .../compile/StatementHintsCompilationTest.java     |  3 +-
 .../TenantSpecificViewIndexCompileTest.java        |  2 +-
 .../org/apache/phoenix/query/QueryPlanTest.java    |  8 +-
 .../phoenix/query/explain/ExplainPlanTest.java     | 90 ++++++++++++----------
 .../phoenix/query/explain/ExplainPlanTestUtil.java | 36 +++++++++
 52 files changed, 433 insertions(+), 424 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 fcf858479a..6c80e407d5 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
@@ -38,7 +38,8 @@ import org.apache.phoenix.schema.PColumn;
   "keyRanges", "indexName", "indexKind", "saltBuckets", "regionsPlanned", "scanTimeRangeMin",
   "scanTimeRangeMax", "splitsChunk", "useRoundRobinIterator", "samplingRate", "hexStringRVCOffset",
   "iteratorTypeAndScanSize", "estimatedRows", "estimatedSizeInBytes", "serverWhereFilter",
-  "serverDistinctFilter", "serverMergeColumns", "serverArrayElementProjection", "serverAggregate",
+  "serverDistinctFilter", "serverMergeColumns", "serverArrayElementProjection",
+  "serverFirstKeyOnlyProjection", "serverEmptyColumnOnlyProjection", "serverAggregate",
   "serverGroupByLimit", "serverSortedBy", "serverOffset", "serverRowLimit", "clientFilterBy",
   "clientAggregate", "clientDistinctFilter", "clientAfterAggregate", "clientSortAlgo",
   "clientSortedBy", "clientOffset", "clientRowLimit", "clientSequenceCount", "clientCursorName",
@@ -73,6 +74,8 @@ public class ExplainPlanAttributes {
   private final String serverDistinctFilter;
   private final Set<PColumn> serverMergeColumns;
   private final boolean serverArrayElementProjection;
+  private final boolean serverFirstKeyOnlyProjection;
+  private final boolean serverEmptyColumnOnlyProjection;
   private final String serverAggregate;
   private final Integer serverGroupByLimit;
   private final String serverSortedBy;
@@ -134,6 +137,8 @@ public class ExplainPlanAttributes {
     this.serverDistinctFilter = null;
     this.serverMergeColumns = null;
     this.serverArrayElementProjection = false;
+    this.serverFirstKeyOnlyProjection = false;
+    this.serverEmptyColumnOnlyProjection = false;
     this.serverAggregate = null;
     this.serverGroupByLimit = null;
     this.serverSortedBy = null;
@@ -168,9 +173,10 @@ public class ExplainPlanAttributes {
     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,
-    Integer serverGroupByLimit, String serverSortedBy, Integer serverOffset, Long serverRowLimit,
-    String clientFilterBy, String clientAggregate, String clientDistinctFilter,
+    Set<PColumn> serverMergeColumns, boolean serverArrayElementProjection,
+    boolean serverFirstKeyOnlyProjection, boolean serverEmptyColumnOnlyProjection,
+    String serverAggregate, Integer serverGroupByLimit, String serverSortedBy, Integer serverOffset,
+    Long serverRowLimit, String clientFilterBy, String clientAggregate, String clientDistinctFilter,
     String clientAfterAggregate, String clientSortAlgo, String clientSortedBy, Integer clientOffset,
     Integer clientRowLimit, Integer clientSequenceCount, String clientCursorName,
     List<String> clientSteps, ExplainPlanAttributes lhsJoinQueryExplainPlan,
@@ -201,6 +207,8 @@ public class ExplainPlanAttributes {
     this.serverDistinctFilter = serverDistinctFilter;
     this.serverMergeColumns = serverMergeColumns;
     this.serverArrayElementProjection = serverArrayElementProjection;
+    this.serverFirstKeyOnlyProjection = serverFirstKeyOnlyProjection;
+    this.serverEmptyColumnOnlyProjection = serverEmptyColumnOnlyProjection;
     this.serverAggregate = serverAggregate;
     this.serverGroupByLimit = serverGroupByLimit;
     this.serverSortedBy = serverSortedBy;
@@ -324,6 +332,14 @@ public class ExplainPlanAttributes {
     return serverArrayElementProjection;
   }
 
+  public boolean isServerFirstKeyOnlyProjection() {
+    return serverFirstKeyOnlyProjection;
+  }
+
+  public boolean isServerEmptyColumnOnlyProjection() {
+    return serverEmptyColumnOnlyProjection;
+  }
+
   public String getServerAggregate() {
     return serverAggregate;
   }
@@ -457,6 +473,8 @@ public class ExplainPlanAttributes {
     private String serverDistinctFilter;
     private Set<PColumn> serverMergeColumns;
     private boolean serverArrayElementProjection;
+    private boolean serverFirstKeyOnlyProjection;
+    private boolean serverEmptyColumnOnlyProjection;
     private String serverAggregate;
     private Integer serverGroupByLimit;
     private String serverSortedBy;
@@ -512,6 +530,9 @@ public class ExplainPlanAttributes {
       this.serverDistinctFilter = explainPlanAttributes.getServerDistinctFilter();
       this.serverMergeColumns = explainPlanAttributes.getServerMergeColumns();
       this.serverArrayElementProjection = explainPlanAttributes.isServerArrayElementProjection();
+      this.serverFirstKeyOnlyProjection = explainPlanAttributes.isServerFirstKeyOnlyProjection();
+      this.serverEmptyColumnOnlyProjection =
+        explainPlanAttributes.isServerEmptyColumnOnlyProjection();
       this.serverAggregate = explainPlanAttributes.getServerAggregate();
       this.serverGroupByLimit = explainPlanAttributes.getServerGroupByLimit();
       this.serverSortedBy = explainPlanAttributes.getServerSortedBy();
@@ -657,6 +678,18 @@ public class ExplainPlanAttributes {
       return this;
     }
 
+    public ExplainPlanAttributesBuilder
+      setServerFirstKeyOnlyProjection(boolean serverFirstKeyOnlyProjection) {
+      this.serverFirstKeyOnlyProjection = serverFirstKeyOnlyProjection;
+      return this;
+    }
+
+    public ExplainPlanAttributesBuilder
+      setServerEmptyColumnOnlyProjection(boolean serverEmptyColumnOnlyProjection) {
+      this.serverEmptyColumnOnlyProjection = serverEmptyColumnOnlyProjection;
+      return this;
+    }
+
     public ExplainPlanAttributesBuilder setServerAggregate(String serverAggregate) {
       this.serverAggregate = serverAggregate;
       return this;
@@ -803,7 +836,8 @@ public class ExplainPlanAttributes {
         tableName, keyRanges, indexName, indexKind, saltBuckets, regionsPlanned, scanTimeRangeMin,
         scanTimeRangeMax, splitsChunk, useRoundRobinIterator, samplingRate, hexStringRVCOffset,
         iteratorTypeAndScanSize, estimatedRows, estimatedSizeInBytes, serverWhereFilter,
-        serverDistinctFilter, serverMergeColumns, serverArrayElementProjection, serverAggregate,
+        serverDistinctFilter, serverMergeColumns, serverArrayElementProjection,
+        serverFirstKeyOnlyProjection, serverEmptyColumnOnlyProjection, serverAggregate,
         serverGroupByLimit, serverSortedBy, serverOffset, serverRowLimit, clientFilterBy,
         clientAggregate, clientDistinctFilter, clientAfterAggregate, clientSortAlgo, clientSortedBy,
         clientOffset, clientRowLimit, clientSequenceCount, clientCursorName, clientSteps,
diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java
index 93efa2a366..a29c598ae1 100644
--- a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java
+++ b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/UpdateExpressionUtils.java
@@ -98,7 +98,8 @@ public class UpdateExpressionUtils {
   public static void updateExpression(final BsonDocument updateExpression,
     final BsonDocument bsonDocument) {
 
-    LOGGER.debug("Update Expression: {} , current bsonDocument: {}", updateExpression, bsonDocument);
+    LOGGER.debug("Update Expression: {} , current bsonDocument: {}", updateExpression,
+      bsonDocument);
 
     if (updateExpression.containsKey("$SET")) {
       executeSetExpression((BsonDocument) updateExpression.get("$SET"), bsonDocument);
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 1a1f32786f..badc779744 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
@@ -287,23 +287,23 @@ public abstract class ExplainTable {
         whereFilterStr = Bytes.toString(expBytes);
       }
     }
-    if (whereFilterStr != null) {
-      String serverWhereFilter =
-        "SERVER FILTER BY " + (firstKeyOnlyFilter == null ? "" : "FIRST KEY ONLY AND ")
-          + (emptyColumnOnlyFilter == null ? "" : "EMPTY COLUMN ONLY AND ") + whereFilterStr;
-      planSteps.add("    " + serverWhereFilter);
+    if (firstKeyOnlyFilter != null) {
+      planSteps.add("    SERVER PROJECTION FILTER BY FIRST KEY ONLY");
       if (explainPlanAttributesBuilder != null) {
-        explainPlanAttributesBuilder.setServerWhereFilter(serverWhereFilter);
+        explainPlanAttributesBuilder.setServerFirstKeyOnlyProjection(true);
       }
-    } else if (firstKeyOnlyFilter != null) {
-      planSteps.add("    SERVER FILTER BY FIRST KEY ONLY");
+    }
+    if (emptyColumnOnlyFilter != null) {
+      planSteps.add("    SERVER PROJECTION FILTER BY EMPTY COLUMN ONLY");
       if (explainPlanAttributesBuilder != null) {
-        explainPlanAttributesBuilder.setServerWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        explainPlanAttributesBuilder.setServerEmptyColumnOnlyProjection(true);
       }
-    } else if (emptyColumnOnlyFilter != null) {
-      planSteps.add("    SERVER FILTER BY EMPTY COLUMN ONLY");
+    }
+    if (whereFilterStr != null) {
+      String serverWhereFilter = "SERVER FILTER BY " + whereFilterStr;
+      planSteps.add("    " + serverWhereFilter);
       if (explainPlanAttributesBuilder != null) {
-        explainPlanAttributesBuilder.setServerWhereFilter("SERVER FILTER BY EMPTY COLUMN ONLY");
+        explainPlanAttributesBuilder.setServerWhereFilter(serverWhereFilter);
       }
     }
     if (distinctFilter != null) {
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateIT.java
index 2be8e8f772..3a40ef5818 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateIT.java
@@ -515,7 +515,7 @@ public abstract class BaseAggregateIT extends ParallelStatsDisabledIT {
       .table(tableName)
       .keyRanges(
         " ['000001111122222','333334444455555',0,*] - ['000001111122222','333334444455555',0,1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .serverFirstKeyOnlyProjection(true)
       .serverAggregate(
         "SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [MATCH_STATUS, EXTERNAL_DATASOURCE_KEY]")
       .clientFilterBy("COUNT(1) > 1");
@@ -557,7 +557,7 @@ public abstract class BaseAggregateIT extends ParallelStatsDisabledIT {
     assertEquals(4, rs.getLong(2));
     assertFalse(rs.next());
     assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 1-WAY").clientSortedBy("REVERSE")
-      .scanType("FULL SCAN").table(tableName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .scanType("FULL SCAN").table(tableName).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]")
       .regionLocationsNotEmpty();
   }
@@ -606,7 +606,7 @@ public abstract class BaseAggregateIT extends ParallelStatsDisabledIT {
     assertEquals(10, rs.getLong(2));
     assertFalse(rs.next());
     assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 1-WAY").clientSortedBy("REVERSE")
-      .scanType("FULL SCAN").table(tableName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .scanType("FULL SCAN").table(tableName).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]")
       .regionLocationsNotEmpty();
   }
@@ -665,7 +665,7 @@ public abstract class BaseAggregateIT extends ParallelStatsDisabledIT {
     assertEquals(2, rs.getDouble(2), 1e-6);
     assertFalse(rs.next());
     assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN")
-      .table(tableName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .table(tableName).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]")
       .regionLocationsNotEmpty();
     TestUtil.analyzeTable(conn, tableName);
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateWithRegionMoves2IT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateWithRegionMoves2IT.java
index 7d908eecc1..bde2c42b83 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateWithRegionMoves2IT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateWithRegionMoves2IT.java
@@ -139,7 +139,7 @@ public class BaseAggregateWithRegionMoves2IT extends ParallelStatsDisabledWithRe
       .table(tableName)
       .keyRanges(
         " ['000001111122222','333334444455555',0,*] - ['000001111122222','333334444455555',0,1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .serverFirstKeyOnlyProjection(true)
       .serverAggregate(
         "SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [MATCH_STATUS, EXTERNAL_DATASOURCE_KEY]")
       .clientFilterBy("COUNT(1) > 1");
@@ -183,7 +183,7 @@ public class BaseAggregateWithRegionMoves2IT extends ParallelStatsDisabledWithRe
     assertEquals(4, rs.getLong(2));
     assertFalse(rs.next());
     assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 1-WAY").clientSortedBy("REVERSE")
-      .scanType("FULL SCAN").table(tableName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .scanType("FULL SCAN").table(tableName).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]");
   }
 
@@ -235,7 +235,7 @@ public class BaseAggregateWithRegionMoves2IT extends ParallelStatsDisabledWithRe
     assertEquals(10, rs.getLong(2));
     assertFalse(rs.next());
     assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 1-WAY").clientSortedBy("REVERSE")
-      .scanType("FULL SCAN").table(tableName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .scanType("FULL SCAN").table(tableName).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]");
   }
 
@@ -437,7 +437,7 @@ public class BaseAggregateWithRegionMoves2IT extends ParallelStatsDisabledWithRe
     assertEquals(2, rs.getDouble(2), 1e-6);
     assertFalse(rs.next());
     assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN")
-      .table(tableName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .table(tableName).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]");
     TestUtil.analyzeTable(conn, tableName);
     List<KeyRange> splits = TestUtil.getAllSplits(conn, tableName);
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateWithRegionMovesIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateWithRegionMovesIT.java
index e363b0bf29..83910c50ed 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateWithRegionMovesIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateWithRegionMovesIT.java
@@ -364,7 +364,7 @@ public abstract class BaseAggregateWithRegionMovesIT
     assertEquals(2, rs.getDouble(2), 1e-6);
     assertFalse(rs.next());
     assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN")
-      .table(tableName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .table(tableName).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]");
     TestUtil.analyzeTable(conn, tableName);
     List<KeyRange> splits = TestUtil.getAllSplits(conn, tableName);
@@ -395,7 +395,7 @@ public abstract class BaseAggregateWithRegionMovesIT
     assertEquals(3, rs.getDouble(2), 1e-6);
     assertFalse(rs.next());
     assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN")
-      .table(tableName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .table(tableName).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]");
     TestUtil.analyzeTable(conn, tableName);
     List<KeyRange> splits = TestUtil.getAllSplits(conn, tableName);
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseTenantSpecificViewIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseTenantSpecificViewIndexIT.java
index 1fab8de99b..70708610c2 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseTenantSpecificViewIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseTenantSpecificViewIndexIT.java
@@ -195,7 +195,7 @@ public abstract class BaseTenantSpecificViewIndexIT extends SplitSystemCatalogIT
       expectedTableName = "_IDX_" + tableName;
     }
     assertPlan(conn, "SELECT k1, k2, v2 FROM " + viewName + " WHERE v2='" + valuePrefix + "v2-1'")
-      .iteratorType(iteratorTypeAndScanSize).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .iteratorType(iteratorTypeAndScanSize).serverFirstKeyOnlyProjection(true)
       .scanType("RANGE SCAN").clientSortAlgo(clientSortAlgo).table(expectedTableName)
       .keyRanges(keyRanges);
   }
@@ -209,8 +209,7 @@ public abstract class BaseTenantSpecificViewIndexIT extends SplitSystemCatalogIT
       .execute("UPSERT INTO " + viewName + "(k2,v1,v2) VALUES (-1, 'blah', 'superblah')");
     conn.commit();
     assertPlan(conn, "SELECT k1, k2, v2 FROM " + viewName + " WHERE v2='" + valuePrefix + "v2-1'")
-      .iteratorType("PARALLEL 1-WAY").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-      .scanType("RANGE SCAN")
+      .iteratorType("PARALLEL 1-WAY").serverFirstKeyOnlyProjection(true).scanType("RANGE SCAN")
       .table(SchemaUtil.getTableName(SchemaUtil.getSchemaNameFromFullName(viewName), indexName)
         + "(" + tableName + ")")
       .keyRanges(" [1," + tenantId + ",'" + valuePrefix + "v2-1']")
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseViewIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseViewIT.java
index 3b3f6f81d4..9f9a3e96a7 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseViewIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseViewIT.java
@@ -202,14 +202,14 @@ public abstract class BaseViewIT extends ParallelStatsEnabledIT {
     String clientSortAlgo;
     String expectedTableName;
     String keyRanges;
-    String serverFilterBy;
+    boolean firstKeyOnlyProjection;
 
     if (localIndex) {
       iteratorTypeAndScanSize = "PARALLEL " + (saltBuckets == null ? 1 : saltBuckets) + "-WAY";
       expectedTableName = fullTableName;
       keyRanges = "[1,51]";
       clientSortAlgo = "CLIENT MERGE SORT";
-      serverFilterBy = "SERVER FILTER BY FIRST KEY ONLY";
+      firstKeyOnlyProjection = true;
     } else {
       iteratorTypeAndScanSize =
         saltBuckets == null ? "PARALLEL 1-WAY" : "PARALLEL " + saltBuckets + "-WAY";
@@ -219,11 +219,12 @@ public abstract class BaseViewIT extends ParallelStatsEnabledIT {
         : " [0," + Short.MIN_VALUE + ",51] - [" + (saltBuckets - 1) + "," + Short.MIN_VALUE
           + ",51]";
       clientSortAlgo = saltBuckets == null ? null : "CLIENT MERGE SORT";
-      serverFilterBy = null;
+      firstKeyOnlyProjection = false;
     }
     assertPlan(conn, query).iteratorType(iteratorTypeAndScanSize).scanType("RANGE SCAN")
       .table(expectedTableName).clientSortAlgo(clientSortAlgo).keyRanges(keyRanges)
-      .serverWhereFilter(serverFilterBy);
+      .serverFirstKeyOnlyProjection(firstKeyOnlyProjection).serverEmptyColumnOnlyProjection(false)
+      .serverWhereFilter(null);
 
     String viewIndexName2 = "I_" + generateUniqueName();
     if (localIndex) {
@@ -269,8 +270,8 @@ public abstract class BaseViewIT extends ParallelStatsEnabledIT {
       clientSortAlgo = saltBuckets == null ? null : "CLIENT MERGE SORT";
     }
     assertPlan(conn, query).table(physicalTableName).iteratorType(iteratorTypeAndScanSize)
-      .scanType("RANGE SCAN").keyRanges(keyRanges)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo(clientSortAlgo);
+      .scanType("RANGE SCAN").keyRanges(keyRanges).serverFirstKeyOnlyProjection(true)
+      .clientSortAlgo(clientSortAlgo);
 
     conn.close();
     return new Pair<>(physicalTableName, scan);
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java
index 1f78df07c0..d2bd414488 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson3IT.java
@@ -232,9 +232,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
-              new BsonString("track[0].shot[2][0].city.problem[2]"),
-              new BsonDouble(529.435))))))
+            new BsonDocument().append("$ADD",
+              new BsonArray(Arrays.asList(new BsonString("track[0].shot[2][0].city.problem[2]"),
+                new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull()));
 
@@ -766,9 +766,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
-              new BsonString("track[0].shot[2][0].city.problem[2]"),
-              new BsonDouble(529.435))))))
+            new BsonDocument().append("$ADD",
+              new BsonArray(Arrays.asList(new BsonString("track[0].shot[2][0].city.problem[2]"),
+                new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull()));
 
@@ -1167,9 +1167,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
-              new BsonString("track[0].shot[2][0].city.problem[2]"),
-              new BsonDouble(529.435))))))
+            new BsonDocument().append("$ADD",
+              new BsonArray(Arrays.asList(new BsonString("track[0].shot[2][0].city.problem[2]"),
+                new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull()));
 
@@ -1542,9 +1542,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
-              new BsonString("track[0].shot[2][0].city.problem[2]"),
-              new BsonDouble(529.435))))))
+            new BsonDocument().append("$ADD",
+              new BsonArray(Arrays.asList(new BsonString("track[0].shot[2][0].city.problem[2]"),
+                new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull()));
 
@@ -1959,9 +1959,9 @@ public class Bson3IT extends ParallelStatsDisabledIT {
           .append("browserling", new BsonBinary(PDouble.INSTANCE.toBytes(-505169340.54880095)))
           .append("track[0].shot[2][0].city.standard[5]", new BsonString("soft"))
           .append("track[0].shot[2][0].city.problem[2]",
-            new BsonDocument().append("$ADD", new BsonArray(Arrays.asList(
-              new BsonString("track[0].shot[2][0].city.problem[2]"),
-              new BsonDouble(529.435))))))
+            new BsonDocument().append("$ADD",
+              new BsonArray(Arrays.asList(new BsonString("track[0].shot[2][0].city.problem[2]"),
+                new BsonDouble(529.435))))))
         .append("$UNSET",
           new BsonDocument().append("track[0].shot[2][0].city.flame", new BsonNull()));
 
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java
index e2bd9a66ed..8e9518db9b 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java
@@ -21,6 +21,7 @@ import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertMutatio
 import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan;
 import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import java.sql.Connection;
 import java.sql.DriverManager;
@@ -148,7 +149,8 @@ public class CostBasedDecisionIT extends BaseTest {
       assertEquals("RANGE SCAN ", explainPlanAttributes.getExplainScanType());
       assertEquals(indexName + "(" + tableName + ")", explainPlanAttributes.getTableName());
       assertEquals(" [1]", explainPlanAttributes.getKeyRanges());
-      assertEquals("SERVER FILTER BY FIRST KEY ONLY AND \"ROWKEY\" <= 'z'",
+      assertTrue(explainPlanAttributes.isServerFirstKeyOnlyProjection());
+      assertEquals("SERVER FILTER BY \"ROWKEY\" <= 'z'",
         explainPlanAttributes.getServerWhereFilter());
       assertEquals("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]",
         explainPlanAttributes.getServerAggregate());
@@ -355,12 +357,12 @@ public class CostBasedDecisionIT extends BaseTest {
       // Use the optimal plan based on cost when stats become available.
       assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES")
         .iteratorType("PARALLEL").scanType("RANGE SCAN").table(indexName + "(" + tableName + ")")
-        .keyRanges(" [1]").serverMergeColumns("[0.C2]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND \"ROWKEY\" <= 'z'")
+        .keyRanges(" [1]").serverMergeColumns("[0.C2]").serverFirstKeyOnlyProjection(true)
+        .serverWhereFilter("SERVER FILTER BY \"ROWKEY\" <= 'z'")
         .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]")
         .clientSortAlgo("CLIENT MERGE SORT").rhs().iteratorType("PARALLEL").scanType("RANGE SCAN")
         .table(indexName + "(" + tableName + ")").keyRanges(" [1]").serverMergeColumns("[0.C2]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND \"ROWKEY\" >= 'a'")
+        .serverFirstKeyOnlyProjection(true).serverWhereFilter("SERVER FILTER BY \"ROWKEY\" >= 'a'")
         .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]")
         .clientSortAlgo("CLIENT MERGE SORT");
     } finally {
@@ -409,13 +411,13 @@ public class CostBasedDecisionIT extends BaseTest {
       // Use the optimal plan based on cost when stats become available.
       assertPlan(conn, query).iteratorType("PARALLEL 626-WAY").scanType("RANGE SCAN")
         .table(indexName + "(" + tableName + ")").keyRanges(" [1,'X0'] - [1,'X1']")
-        .serverMergeColumns("[0.C2]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.C2]").serverFirstKeyOnlyProjection(true)
         .serverSortedBy("[\"T1.:ROWKEY\"]").clientSortAlgo("CLIENT MERGE SORT")
         .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"T1.:ROWKEY\" IN (T2.MRK)").subPlanCount(1)
         .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
         .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
         .table(indexName + "(" + tableName + ")").keyRanges(" [1]").serverMergeColumns("[0.C2]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND \"ROWKEY\" <= 'z'")
+        .serverFirstKeyOnlyProjection(true).serverWhereFilter("SERVER FILTER BY \"ROWKEY\" <= 'z'")
         .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]")
         .clientSortAlgo("CLIENT MERGE SORT");
     } finally {
@@ -440,8 +442,7 @@ public class CostBasedDecisionIT extends BaseTest {
       String hintedQuery = query.replaceFirst("SELECT",
         "SELECT  /*+ INDEX(" + tableName + " " + tableName + "_idx) */");
       String dataPlan = "[C1]";
-      String indexPlan =
-        "SERVER FILTER BY FIRST KEY ONLY AND (\"ROWKEY\" >= 1 AND \"ROWKEY\" <= 10)";
+      String indexPlan = "SERVER FILTER BY (\"ROWKEY\" >= 1 AND \"ROWKEY\" <= 10)";
 
       // Use the index table plan that opts out order-by when stats are not available.
       ExplainPlan plan = conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)
@@ -503,7 +504,7 @@ public class CostBasedDecisionIT extends BaseTest {
         .clientAggregate("CLIENT AGGREGATE INTO SINGLE ROW").lhs().iteratorType("PARALLEL 1-WAY")
         .scanType("FULL SCAN").table(testTable500).serverWhereFilter("SERVER FILTER BY COL1 < 200")
         .end().rhs().iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN").table(testTable1000)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverFirstKeyOnlyProjection(true);
     }
   }
 
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CountDistinctApproximateHyperLogLogIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CountDistinctApproximateHyperLogLogIT.java
index 0d30793c23..8b4dc54e01 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CountDistinctApproximateHyperLogLogIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CountDistinctApproximateHyperLogLogIT.java
@@ -123,8 +123,7 @@ public class CountDistinctApproximateHyperLogLogIT extends ParallelStatsDisabled
     try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
       prepareTableWithValues(conn, 100);
       assertPlan(conn, query).table(tableName).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverAggregate("SERVER AGGREGATE INTO SINGLE ROW");
+        .serverFirstKeyOnlyProjection(true).serverAggregate("SERVER AGGREGATE INTO SINGLE ROW");
     }
   }
 
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/EmptyColumnIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/EmptyColumnIT.java
index 45ba6fc22d..d52749615d 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/EmptyColumnIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/EmptyColumnIT.java
@@ -748,7 +748,7 @@ public class EmptyColumnIT extends ParallelStatsDisabledIT {
         EnvironmentEdgeManager.injectEdge(injectEdge);
         String distinctQuery = "SELECT DISTINCT id1 FROM " + dataTableName;
         ExplainPlanAttributes attributes = getExplainAttributes(conn, distinctQuery);
-        assertPlan(attributes).serverWhereFilter("SERVER FILTER BY EMPTY COLUMN ONLY");
+        assertPlan(attributes).serverEmptyColumnOnlyProjection(true);
         assertNotNull(attributes.getServerDistinctFilter());
         try (ResultSet rs = conn.createStatement().executeQuery(distinctQuery)) {
           // all the rows should have been masked
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/ExplainPlanWithStatsDisabledIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/ExplainPlanWithStatsDisabledIT.java
index 7606aec6fd..0bee64c4eb 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/ExplainPlanWithStatsDisabledIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/ExplainPlanWithStatsDisabledIT.java
@@ -256,7 +256,7 @@ public class ExplainPlanWithStatsDisabledIT extends ParallelStatsDisabledIT {
       assertPlan(conn, query).scanType("RANGE SCAN").table("FOO")
         .keyRanges(
           " [X'00','a',~'2016-01-28 23:59:59.999'] -" + " [X'13','a',~'2016-01-28 00:00:00.000']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+        .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
     }
   }
 
@@ -276,7 +276,7 @@ public class ExplainPlanWithStatsDisabledIT extends ParallelStatsDisabledIT {
       assertPlan(conn, query).useRoundRobinIterator(true).scanType("RANGE SCAN").table(tableName)
         .keyRanges(
           " [X'00','a',~'2016-01-28 23:59:59.999'] -" + " [X'13','a',~'2016-01-28 00:00:00.000']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverFirstKeyOnlyProjection(true);
     }
   }
 
@@ -341,32 +341,32 @@ public class ExplainPlanWithStatsDisabledIT extends ParallelStatsDisabledIT {
       assertTrue(rs.next());
       assertEquals(53, rs.getInt(1));
       assertPlan(conn, query).scanType("RANGE SCAN").table(tableName).keyRanges(" [*] - ['b']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverAggregate("SERVER AGGREGATE INTO SINGLE ROW").numRegionLocationLookups(2);
+        .serverFirstKeyOnlyProjection(true).serverAggregate("SERVER AGGREGATE INTO SINGLE ROW")
+        .numRegionLocationLookups(2);
 
       query = "select count(*) from " + tableName + " where PK1 <= 'cd'";
       rs = conn.createStatement().executeQuery(query);
       assertTrue(rs.next());
       assertEquals(128, rs.getInt(1));
       assertPlan(conn, query).scanType("RANGE SCAN").table(tableName).keyRanges(" [*] - ['cd']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverAggregate("SERVER AGGREGATE INTO SINGLE ROW").numRegionLocationLookups(3);
+        .serverFirstKeyOnlyProjection(true).serverAggregate("SERVER AGGREGATE INTO SINGLE ROW")
+        .numRegionLocationLookups(3);
 
       query = "select count(*) from " + tableName + " where PK1 LIKE 'ef%'";
       rs = conn.createStatement().executeQuery(query);
       assertTrue(rs.next());
       assertEquals(25, rs.getInt(1));
       assertPlan(conn, query).scanType("RANGE SCAN").table(tableName).keyRanges(" ['ef'] - ['eg']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverAggregate("SERVER AGGREGATE INTO SINGLE ROW").numRegionLocationLookups(1);
+        .serverFirstKeyOnlyProjection(true).serverAggregate("SERVER AGGREGATE INTO SINGLE ROW")
+        .numRegionLocationLookups(1);
 
       query = "select count(*) from " + tableName + " where PK1 > 'de'";
       rs = conn.createStatement().executeQuery(query);
       assertTrue(rs.next());
       assertEquals(75, rs.getInt(1));
       assertPlan(conn, query).scanType("RANGE SCAN").table(tableName).keyRanges(" ['de'] - [*]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverAggregate("SERVER AGGREGATE INTO SINGLE ROW").numRegionLocationLookups(1);
+        .serverFirstKeyOnlyProjection(true).serverAggregate("SERVER AGGREGATE INTO SINGLE ROW")
+        .numRegionLocationLookups(1);
     }
   }
 
@@ -461,8 +461,8 @@ public class ExplainPlanWithStatsDisabledIT extends ParallelStatsDisabledIT {
         assertEquals(50, rs.getInt(1));
 
         assertPlan(tenantConn, query).scanType("RANGE SCAN").table(tableName).keyRanges(" ['ab12']")
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-          .serverAggregate("SERVER AGGREGATE INTO SINGLE ROW").numRegionLocationLookups(1);
+          .serverFirstKeyOnlyProjection(true).serverAggregate("SERVER AGGREGATE INTO SINGLE ROW")
+          .numRegionLocationLookups(1);
       }
 
       try (Connection tenantConn = getTenantConnection("cd12")) {
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/FlappingLocalIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/FlappingLocalIndexIT.java
index 9fa2e49bd5..b0adc333fc 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/FlappingLocalIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/FlappingLocalIndexIT.java
@@ -169,7 +169,7 @@ public class FlappingLocalIndexIT extends BaseLocalIndexIT {
       // full number of regions is reported via regionLocationsTotalSize.
       assertPlan(conn1, query).iteratorType("PARALLEL " + numRegions + "-WAY")
         .scanType("RANGE SCAN").table(indexTableName + "(" + indexPhysicalTableName + ")")
-        .keyRanges(" [1,'a'] - [1,'b']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .keyRanges(" [1,'a'] - [1,'b']").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE SORT").regionLocationCount(trimmedRegionLocations)
         .regionLocationsTotalSize(numRegions);
 
@@ -191,7 +191,7 @@ public class FlappingLocalIndexIT extends BaseLocalIndexIT {
 
       assertPlan(conn1, query).iteratorType("PARALLEL " + numRegions + "-WAY")
         .scanType("RANGE SCAN").table(indexTableName + "(" + indexPhysicalTableName + ")")
-        .keyRanges(" [1,'a']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .keyRanges(" [1,'a']").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE SORT");
 
       rs = conn1.createStatement().executeQuery(query);
@@ -208,7 +208,7 @@ public class FlappingLocalIndexIT extends BaseLocalIndexIT {
 
       assertPlan(conn1, query).iteratorType("PARALLEL " + numRegions + "-WAY")
         .scanType("RANGE SCAN").table(indexTableName + "(" + indexPhysicalTableName + ")")
-        .keyRanges(" [1,*] - [1,'z']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .keyRanges(" [1,*] - [1,'z']").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE SORT").serverSortedBy("[\"K3\"]");
 
       rs = conn1.createStatement().executeQuery(query);
@@ -230,8 +230,8 @@ public class FlappingLocalIndexIT extends BaseLocalIndexIT {
 
       assertPlan(conn1, query).iteratorType("PARALLEL " + numRegions + "-WAY")
         .scanType("RANGE SCAN").table(indexTableName + "(" + indexPhysicalTableName + ")")
-        .keyRanges(" [1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .clientSortAlgo("CLIENT MERGE SORT").serverSortedBy(null);
+        .keyRanges(" [1]").serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT")
+        .serverSortedBy(null);
 
       rs = conn1.createStatement().executeQuery(query);
       assertTrue(rs.next());
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/IndexExtendedIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/IndexExtendedIT.java
index eda03aaa9c..90317a3cd0 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/IndexExtendedIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/IndexExtendedIT.java
@@ -250,7 +250,7 @@ public class IndexExtendedIT extends BaseTest {
       String query = "SELECT pk3 from " + dataTableFullName + " ORDER BY pk3";
 
       assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN")
-        .table(indexTableFullName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .table(indexTableFullName).serverFirstKeyOnlyProjection(true);
 
       ResultSet rs = conn.createStatement().executeQuery(query);
       assertTrue(rs.next());
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/KeyOnlyIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/KeyOnlyIT.java
index dffcea8379..584827d92e 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/KeyOnlyIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/KeyOnlyIT.java
@@ -144,7 +144,7 @@ public class KeyOnlyIT extends ParallelStatsEnabledIT {
     assertFalse(rs.next());
 
     assertPlan(conn, query).iteratorType("SERIAL 1-WAY").scanType("FULL SCAN").table(tableName)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverRowLimit(1L).clientRowLimit(1);
+      .serverFirstKeyOnlyProjection(true).serverRowLimit(1L).clientRowLimit(1);
   }
 
   @Test
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/LocalIndexSplitMergeIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/LocalIndexSplitMergeIT.java
index eebcfaf6fd..f8df8a1e79 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/LocalIndexSplitMergeIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/LocalIndexSplitMergeIT.java
@@ -143,15 +143,14 @@ public class LocalIndexSplitMergeIT extends BaseTest {
 
         assertPlan(conn1, query).iteratorType("PARALLEL " + (4 + i) + "-WAY").scanType("RANGE SCAN")
           .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1]")
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+          .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
 
         query = "SELECT t_id,k1,k3 FROM " + tableName;
         assertPlan(conn1, query)
           .iteratorType(
             "PARALLEL " + ((strings[3 * i].compareTo("j") < 0) ? (4 + i) : (4 + i - 1)) + "-WAY")
           .scanType("RANGE SCAN").table(fullIndexName + "_2(" + indexPhysicalTableName + ")")
-          .keyRanges(" [2]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-          .clientSortAlgo("CLIENT MERGE SORT");
+          .keyRanges(" [2]").serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
 
         rs = conn1.createStatement().executeQuery(query);
         Thread.sleep(1000);
@@ -229,12 +228,12 @@ public class LocalIndexSplitMergeIT extends BaseTest {
 
       assertPlan(conn1, query).iteratorType("PARALLEL 3-WAY").scanType("RANGE SCAN")
         .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+        .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
 
       query = "SELECT t_id,k1,k3 FROM " + tableName;
       assertPlan(conn1, query).iteratorType("PARALLEL 3-WAY").scanType("RANGE SCAN")
         .table(fullIndexName + "_2(" + indexPhysicalTableName + ")").keyRanges(" [2]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+        .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
 
       rs = conn1.createStatement().executeQuery(query);
       Thread.sleep(1000);
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithLimitIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithLimitIT.java
index e7666f9a68..cef4e03c8c 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithLimitIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithLimitIT.java
@@ -90,7 +90,7 @@ public class QueryWithLimitIT extends BaseTest {
       assertFalse(rs.next());
 
       assertPlan(conn, query).iteratorType("SERIAL 1-WAY").scanType("FULL SCAN").table(tableName)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverRowLimit(1L).clientRowLimit(1);
+        .serverFirstKeyOnlyProjection(true).serverRowLimit(1L).clientRowLimit(1);
     } finally {
       conn.close();
     }
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithOffsetIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithOffsetIT.java
index 6cda98ab04..232b9dc264 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithOffsetIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithOffsetIT.java
@@ -129,11 +129,10 @@ public class QueryWithOffsetIT extends ParallelStatsDisabledIT {
     String query = "SELECT t_id from " + tableName + " offset " + offset;
     if (!isSalted) {
       assertPlan(conn, query).scanType("FULL SCAN").table(tableName)
-        .serverWhereFilter("SERVER FILTER BY EMPTY COLUMN ONLY").iteratorType("SERIAL 1-WAY")
-        .serverOffset(offset);
+        .serverEmptyColumnOnlyProjection(true).iteratorType("SERIAL 1-WAY").serverOffset(offset);
     } else {
       assertPlan(conn, query).scanType("FULL SCAN").table(tableName)
-        .serverWhereFilter("SERVER FILTER BY EMPTY COLUMN ONLY").iteratorType("PARALLEL 10-WAY")
+        .serverEmptyColumnOnlyProjection(true).iteratorType("PARALLEL 10-WAY")
         .clientSortAlgo("CLIENT MERGE SORT").clientOffset(offset);
     }
 
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithTableSampleIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithTableSampleIT.java
index ddbf213c45..93702373e9 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithTableSampleIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithTableSampleIT.java
@@ -212,7 +212,7 @@ public class QueryWithTableSampleIT extends ParallelStatsEnabledIT {
       prepareTableWithValues(conn, 100);
       String query = "SELECT i1, i2 FROM " + tableName + " tablesample (45) ";
       assertPlan(conn, query).iteratorType("PARALLEL").scanType("FULL SCAN").samplingRate(0.45d)
-        .table(tableName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .table(tableName).serverFirstKeyOnlyProjection(true);
     }
   }
 
@@ -227,9 +227,9 @@ public class QueryWithTableSampleIT extends ParallelStatsEnabledIT {
           + tableName + " tablesample (2) where i2<6000";
       assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES").scanType("RANGE SCAN")
         .table(tableName).keyRanges(" [*] - [2]").samplingRate(1.0d)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").rhs().scanType("FULL SCAN")
-        .table(tableName).samplingRate(0.02d)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND I2 < 6000").end();
+        .serverFirstKeyOnlyProjection(true).rhs().scanType("FULL SCAN").table(tableName)
+        .samplingRate(0.02d).serverFirstKeyOnlyProjection(true)
+        .serverWhereFilter("SERVER FILTER BY I2 < 6000").end();
     } finally {
       conn.close();
     }
@@ -245,12 +245,10 @@ public class QueryWithTableSampleIT extends ParallelStatsEnabledIT {
         + joinedTableName + " as B tablesample (75) where A.i1=B.i1";
 
       assertPlan(conn, query).scanType("FULL SCAN").table(tableName).samplingRate(0.45d)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverAggregate("SERVER AGGREGATE INTO SINGLE ROW")
+        .serverFirstKeyOnlyProjection(true).serverAggregate("SERVER AGGREGATE INTO SINGLE ROW")
         .dynamicServerFilter("DYNAMIC SERVER FILTER BY A.I1 IN (B.I1)").subPlanCount(1).subPlan(0)
         .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0 (SKIP MERGE)").scanType("FULL SCAN")
-        .table(joinedTableName).samplingRate(0.75d)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+        .table(joinedTableName).samplingRate(0.75d).serverFirstKeyOnlyProjection(true).end();
     } finally {
       conn.close();
     }
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/ReverseScanIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/ReverseScanIT.java
index d3eb617786..c1bab87569 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/ReverseScanIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/ReverseScanIT.java
@@ -80,8 +80,8 @@ public class ReverseScanIT extends ParallelStatsDisabledIT {
       assertFalse(rs.next());
 
       assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").clientSortedBy("REVERSE")
-        .scanType("FULL SCAN").table(tableName)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND ENTITY_ID >= '00A323122312312'");
+        .scanType("FULL SCAN").table(tableName).serverFirstKeyOnlyProjection(true)
+        .serverWhereFilter("SERVER FILTER BY ENTITY_ID >= '00A323122312312'");
 
       PreparedStatement statement = conn.prepareStatement("SELECT entity_id FROM " + tableName
         + " WHERE organization_id = ? AND entity_id >= ? ORDER BY organization_id DESC, entity_id DESC");
@@ -179,7 +179,7 @@ public class ReverseScanIT extends ParallelStatsDisabledIT {
 
       assertPlan(conn, query).iteratorType("SERIAL 1-WAY").clientSortedBy("REVERSE")
         .scanType("RANGE SCAN").table(indexName).keyRanges(" [not null]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverRowLimit(1L).clientRowLimit(1);
+        .serverFirstKeyOnlyProjection(true).serverRowLimit(1L).clientRowLimit(1);
     }
 
   }
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SequenceBulkAllocationIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SequenceBulkAllocationIT.java
index 697b1aad8b..4080d5f30a 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SequenceBulkAllocationIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SequenceBulkAllocationIT.java
@@ -824,7 +824,7 @@ public class SequenceBulkAllocationIT extends ParallelStatsDisabledIT {
 
     // Assert output for Explain Plain result is as expected
     assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN").table(tableName)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSequenceCount(1);
+      .serverFirstKeyOnlyProjection(true).clientSequenceCount(1);
   }
 
   /**
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SequenceIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SequenceIT.java
index 0ff1bb53db..83f1fae5b9 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SequenceIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SequenceIT.java
@@ -823,7 +823,7 @@ public class SequenceIT extends ParallelStatsDisabledIT {
       DriverManager.getConnection(getUrl(), PropertiesUtil.deepCopy(TEST_PROPERTIES));
     String query = "SELECT NEXT VALUE FOR " + sequenceName + " FROM " + tableName;
     assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN").table(tableName)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSequenceCount(1);
+      .serverFirstKeyOnlyProjection(true).clientSequenceCount(1);
 
     ResultSet rs = conn.createStatement().executeQuery(
       "SELECT sequence_name, current_value FROM \"SYSTEM\".\"SEQUENCE\" WHERE sequence_name='"
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 26621c1c9b..46c0747d77 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
@@ -429,15 +429,14 @@ public class SortMergeJoinMoreIT extends ParallelStatsDisabledIT {
         assertPlan(conn, q).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(true)
           .clientAggregate("CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY [E.BUCKET, E.TIMESTAMP]")
           .lhs().iteratorType("PARALLEL").scanType("SKIP SCAN ON 2 RANGES")
-          .table(eventCountTableName).keyRanges(lhsKeyRanges)
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+          .table(eventCountTableName).keyRanges(lhsKeyRanges).serverFirstKeyOnlyProjection(true)
           .serverDistinctFilter("SERVER DISTINCT PREFIX FILTER OVER [BUCKET, TIMESTAMP, LOCATION]")
           .serverAggregate(
             "SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [BUCKET, TIMESTAMP, LOCATION]")
           .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[BUCKET, TIMESTAMP]").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")
+          .keyRanges(rhsKeyRanges).serverFirstKeyOnlyProjection(true)
+          .serverWhereFilter("SERVER FILTER BY SRC_LOCATION = DST_LOCATION")
           .serverDistinctFilter(
             "SERVER DISTINCT PREFIX FILTER OVER [BUCKET, \"TIMESTAMP\", SRC_LOCATION, DST_LOCATION]")
           .serverAggregate(
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/TenantSpecificViewIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/TenantSpecificViewIndexIT.java
index 0223b8bca5..000dad55ab 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/TenantSpecificViewIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/TenantSpecificViewIndexIT.java
@@ -224,7 +224,7 @@ public class TenantSpecificViewIndexIT extends BaseTenantSpecificViewIndexIT {
     explainPlanAttributes = plan.getPlanStepsAsAttributes();
     assertEquals("PARALLEL 1-WAY", explainPlanAttributes.getIteratorTypeAndScanSize());
     assertEquals("RANGE SCAN ", explainPlanAttributes.getExplainScanType());
-    assertEquals("SERVER FILTER BY FIRST KEY ONLY", explainPlanAttributes.getServerWhereFilter());
+    assertTrue(explainPlanAttributes.isServerFirstKeyOnlyProjection());
     if (localIndex) {
       assertEquals(fullIndexName + "("
         + SchemaUtil.getPhysicalTableName(Bytes.toBytes(tableName), isNamespaceMapped).toString()
@@ -366,8 +366,8 @@ public class TenantSpecificViewIndexIT extends BaseTenantSpecificViewIndexIT {
         .table(expectedIndexName)
         .keyRanges(" ['tenant1        ','001','2011-01-01 00:00:00.001']"
           + " - ['tenant1        ','001','2016-10-31 00:00:00.000']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverRowLimit(501L)
-        .clientRowLimit(501).clientSteps("CLIENT 501 ROW LIMIT");
+        .serverFirstKeyOnlyProjection(true).serverRowLimit(501L).clientRowLimit(501)
+        .clientSteps("CLIENT 501 ROW LIMIT");
 
       String query2 = "SELECT PARENT_ID FROM " + viewName + " WHERE PARENT_TYPE='001' "
         + " AND (CREATED_DATE >= to_date('2011-01-01') AND CREATED_DATE <= to_date('2016-01-01'))"
@@ -377,8 +377,8 @@ public class TenantSpecificViewIndexIT extends BaseTenantSpecificViewIndexIT {
         .table(expectedIndexName)
         .keyRanges(" ['tenant1        ','001','2012-10-21 00:00:00.001']"
           + " - ['tenant1        ','001','2016-01-01 00:00:00.000']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverRowLimit(501L)
-        .clientRowLimit(501).clientSteps("CLIENT 501 ROW LIMIT");
+        .serverFirstKeyOnlyProjection(true).serverRowLimit(501L).clientRowLimit(501)
+        .clientSteps("CLIENT 501 ROW LIMIT");
     }
   }
 
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UserDefinedFunctionsIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UserDefinedFunctionsIT.java
index 231d877ecd..88980c4dfa 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UserDefinedFunctionsIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UserDefinedFunctionsIT.java
@@ -841,7 +841,7 @@ public class UserDefinedFunctionsIT extends BaseOwnClusterIT {
     String query = "select myreverse5(lastname_reverse) from t5";
 
     assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN").table("IDX")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+      .serverFirstKeyOnlyProjection(true);
 
     ResultSet rs = stmt.executeQuery(query);
     assertTrue(rs.next());
@@ -852,7 +852,7 @@ public class UserDefinedFunctionsIT extends BaseOwnClusterIT {
       "select k,k1,myreverse5(lastname_reverse) from t5 where myreverse5(lastname_reverse)='kcoj'";
 
     assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN").table("IDX2(T5)")
-      .keyRanges(" [1,'kcoj']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .keyRanges(" [1,'kcoj']").serverFirstKeyOnlyProjection(true)
       .clientSortAlgo("CLIENT MERGE SORT");
 
     rs = stmt.executeQuery(query);
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/ViewIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/ViewIT.java
index e3bf620dc6..656dffc847 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/ViewIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/ViewIT.java
@@ -967,9 +967,7 @@ public class ViewIT extends SplitSystemCatalogIT {
         assertPlan(conn, query).scanType("RANGE SCAN")
           .iteratorType("PARALLEL " + (saltBuckets == null ? 1 : saltBuckets) + "-WAY")
           .table(viewIndexFullName1 + "(" + fullTableName + ")").keyRanges(" [1,51]")
-          .serverWhereFilterAnyOf("SERVER FILTER BY FIRST KEY ONLY",
-            "SERVER FILTER BY EMPTY COLUMN ONLY")
-          .clientSortAlgo("CLIENT MERGE SORT");
+          .serverProjectionFilterAnyOf().clientSortAlgo("CLIENT MERGE SORT");
       } else if (saltBuckets == null) {
         assertPlan(conn, query).scanType("RANGE SCAN").iteratorType("PARALLEL 1-WAY")
           .table(viewIndexPhysicalName).keyRanges(" [" + Short.MIN_VALUE + ",51]")
@@ -1012,23 +1010,17 @@ public class ViewIT extends SplitSystemCatalogIT {
 
       if (localIndex) {
         physicalTableName = fullTableName;
-        assertPlan(conn, query).scanType("RANGE SCAN")
-          .serverWhereFilterAnyOf("SERVER FILTER BY FIRST KEY ONLY",
-            "SERVER FILTER BY EMPTY COLUMN ONLY")
+        assertPlan(conn, query).scanType("RANGE SCAN").serverProjectionFilterAnyOf()
           .iteratorType("PARALLEL " + (saltBuckets == null ? 1 : saltBuckets) + "-WAY")
           .table(viewIndexFullName2 + "(" + fullTableName + ")").keyRanges(" [" + (2) + ",'foo']");
       } else if (saltBuckets == null) {
         physicalTableName = viewIndexPhysicalName;
-        assertPlan(conn, query).scanType("RANGE SCAN")
-          .serverWhereFilterAnyOf("SERVER FILTER BY FIRST KEY ONLY",
-            "SERVER FILTER BY EMPTY COLUMN ONLY")
+        assertPlan(conn, query).scanType("RANGE SCAN").serverProjectionFilterAnyOf()
           .iteratorType("PARALLEL 1-WAY").table(viewIndexPhysicalName)
           .keyRanges(" [" + (Short.MIN_VALUE + 1) + ",'foo']").clientSortAlgo(null);
       } else {
         physicalTableName = viewIndexPhysicalName;
-        assertPlan(conn, query).scanType("RANGE SCAN")
-          .serverWhereFilterAnyOf("SERVER FILTER BY FIRST KEY ONLY",
-            "SERVER FILTER BY EMPTY COLUMN ONLY")
+        assertPlan(conn, query).scanType("RANGE SCAN").serverProjectionFilterAnyOf()
           .iteratorType("PARALLEL " + saltBuckets + "-WAY").table(viewIndexPhysicalName)
           .keyRanges(" [X'00'," + (Short.MIN_VALUE + 1) + ",'foo'] - ["
             + PVarbinary.INSTANCE.toStringLiteral(new byte[] { (byte) (saltBuckets - 1) }) + ","
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexIT.java
index 3a1ffdcc9e..a29b8c5028 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexIT.java
@@ -154,8 +154,7 @@ public abstract class BaseIndexIT extends ParallelStatsDisabledIT {
         assertPlan(conn, query).iteratorType("PARALLEL 1-WAY");
       if (!uncovered) {
         // Optimizer would not select the uncovered index for this query
-        basePlan.serverWhereFilter(
-          columnEncoded ? "SERVER FILTER BY FIRST KEY ONLY" : "SERVER FILTER BY EMPTY COLUMN ONLY");
+        basePlan.serverProjectionFilter(columnEncoded);
       }
       if (localIndex) {
         basePlan.table(fullIndexName + "(" + fullTableName + ")").keyRanges(" [1]")
@@ -572,9 +571,8 @@ public abstract class BaseIndexIT extends ParallelStatsDisabledIT {
       String query =
         "SELECT" + (uncovered ? " /*+ INDEX(" + fullTableName + " " + indexName + ")*/ " : " ")
           + "int_pk from " + fullTableName;
-      ExplainPlanTestUtil.ExplainPlanAssert basePlan =
-        assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").serverWhereFilter(
-          columnEncoded ? "SERVER FILTER BY FIRST KEY ONLY" : "SERVER FILTER BY EMPTY COLUMN ONLY");
+      ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, query)
+        .iteratorType("PARALLEL 1-WAY").serverProjectionFilter(columnEncoded);
       if (localIndex) {
         basePlan.scanType("RANGE SCAN").table(fullIndexName + "(" + fullTableName + ")")
           .clientSortAlgo("CLIENT MERGE SORT").keyRanges(" [1]");
@@ -593,8 +591,8 @@ public abstract class BaseIndexIT extends ParallelStatsDisabledIT {
       assertFalse(rs.next());
 
       query = "SELECT date_col from " + fullTableName + " order by date_col";
-      basePlan = assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").serverWhereFilter(
-        columnEncoded ? "SERVER FILTER BY FIRST KEY ONLY" : "SERVER FILTER BY EMPTY COLUMN ONLY");
+      basePlan = assertPlan(conn, query).iteratorType("PARALLEL 1-WAY")
+        .serverProjectionFilter(columnEncoded);
       if (localIndex) {
         basePlan.scanType("RANGE SCAN").table(fullIndexName + "(" + fullTableName + ")")
           .clientSortAlgo("CLIENT MERGE SORT").keyRanges(" [1]");
@@ -910,8 +908,8 @@ public abstract class BaseIndexIT extends ParallelStatsDisabledIT {
       // make sure the index is working as expected
       query = "SELECT" + (uncovered ? " /*+ INDEX(" + testTable + " " + indexName + ")*/ " : " ")
         + "* FROM " + testTable;
-      ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, query).serverWhereFilter(
-        columnEncoded ? "SERVER FILTER BY FIRST KEY ONLY" : "SERVER FILTER BY EMPTY COLUMN ONLY");
+      ExplainPlanTestUtil.ExplainPlanAssert basePlan =
+        assertPlan(conn, query).serverProjectionFilter(columnEncoded);
       if (localIndex) {
         basePlan.iteratorType("PARALLEL 2-WAY").scanType("RANGE SCAN")
           .table(fullIndexName + "(" + testTable + ")").keyRanges(" [1]")
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexWithRegionMovesIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexWithRegionMovesIT.java
index 256e4ef8d2..c8c768b17f 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexWithRegionMovesIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexWithRegionMovesIT.java
@@ -170,8 +170,7 @@ public abstract class BaseIndexWithRegionMovesIT extends ParallelStatsDisabledWi
         assertPlan(conn, query).iteratorType("PARALLEL 1-WAY");
       if (!uncovered) {
         // Optimizer would not select the uncovered index for this query
-        basePlan.serverWhereFilter(
-          columnEncoded ? "SERVER FILTER BY FIRST KEY ONLY" : "SERVER FILTER BY EMPTY COLUMN ONLY");
+        basePlan.serverProjectionFilter(columnEncoded);
       }
       if (localIndex) {
         basePlan.table(fullTableName).keyRanges(" [1]").clientSortAlgo("CLIENT MERGE SORT")
@@ -632,9 +631,8 @@ public abstract class BaseIndexWithRegionMovesIT extends ParallelStatsDisabledWi
       String query =
         "SELECT" + (uncovered ? " /*+ INDEX(" + fullTableName + " " + indexName + ")*/ " : " ")
           + "int_pk from " + fullTableName;
-      ExplainPlanTestUtil.ExplainPlanAssert basePlan =
-        assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").serverWhereFilter(
-          columnEncoded ? "SERVER FILTER BY FIRST KEY ONLY" : "SERVER FILTER BY EMPTY COLUMN ONLY");
+      ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, query)
+        .iteratorType("PARALLEL 1-WAY").serverProjectionFilter(columnEncoded);
       if (localIndex) {
         basePlan.scanType("RANGE SCAN").table(fullTableName).clientSortAlgo("CLIENT MERGE SORT")
           .keyRanges(" [1]");
@@ -656,8 +654,8 @@ public abstract class BaseIndexWithRegionMovesIT extends ParallelStatsDisabledWi
       assertFalse(rs.next());
 
       query = "SELECT date_col from " + fullTableName + " order by date_col";
-      basePlan = assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").serverWhereFilter(
-        columnEncoded ? "SERVER FILTER BY FIRST KEY ONLY" : "SERVER FILTER BY EMPTY COLUMN ONLY");
+      basePlan = assertPlan(conn, query).iteratorType("PARALLEL 1-WAY")
+        .serverProjectionFilter(columnEncoded);
       if (localIndex) {
         basePlan.scanType("RANGE SCAN").table(fullTableName).clientSortAlgo("CLIENT MERGE SORT")
           .keyRanges(" [1]");
@@ -1004,8 +1002,8 @@ public abstract class BaseIndexWithRegionMovesIT extends ParallelStatsDisabledWi
       // make sure the index is working as expected
       query = "SELECT" + (uncovered ? " /*+ INDEX(" + testTable + " " + indexName + ")*/ " : " ")
         + "* FROM " + testTable;
-      ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, query).serverWhereFilter(
-        columnEncoded ? "SERVER FILTER BY FIRST KEY ONLY" : "SERVER FILTER BY EMPTY COLUMN ONLY");
+      ExplainPlanTestUtil.ExplainPlanAssert basePlan =
+        assertPlan(conn, query).serverProjectionFilter(columnEncoded);
       if (localIndex) {
         basePlan.iteratorType("PARALLEL 2-WAY").scanType("RANGE SCAN").table(testTable)
           .keyRanges(" [1]").clientSortAlgo("CLIENT MERGE SORT");
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ChildViewsUseParentViewIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ChildViewsUseParentViewIndexIT.java
index 5a8c41b266..f0bcbab7ee 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ChildViewsUseParentViewIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ChildViewsUseParentViewIndexIT.java
@@ -164,10 +164,10 @@ public class ChildViewsUseParentViewIndexIT extends ParallelStatsDisabledIT {
     String sql =
       "SELECT A0, A1, A2, A4 FROM " + viewName + " WHERE A4 IN ('1', '2', '3') ORDER BY A4, A2";
     String childViewScanKey = isChildView ? ",'Y'" : "";
-    assertPlan(conn, sql).iteratorType("PARALLEL 1-WAY")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").scanType("SKIP SCAN ON 3 KEYS")
-      .table("_IDX_" + baseTableName).keyRanges(" [" + Short.MIN_VALUE + ",'1'" + childViewScanKey
-        + "] - [" + Short.MIN_VALUE + ",'3'" + childViewScanKey + "]");
+    assertPlan(conn, sql).iteratorType("PARALLEL 1-WAY").serverFirstKeyOnlyProjection(true)
+      .scanType("SKIP SCAN ON 3 KEYS").table("_IDX_" + baseTableName)
+      .keyRanges(" [" + Short.MIN_VALUE + ",'1'" + childViewScanKey + "] - [" + Short.MIN_VALUE
+        + ",'3'" + childViewScanKey + "]");
 
     ResultSet rs = conn.createStatement().executeQuery(sql);
     assertTrue(rs.next());
@@ -261,9 +261,8 @@ public class ChildViewsUseParentViewIndexIT extends ParallelStatsDisabledIT {
     String sql = "SELECT WO_ID FROM " + viewName
       + " WHERE WO_ID IN ('003xxxxxxxxxxx1', '003xxxxxxxxxxx2', '003xxxxxxxxxxx3', '003xxxxxxxxxxx4', '003xxxxxxxxxxx5') "
       + " AND (A_DATE > TO_DATE('2016-01-01 06:00:00.0')) " + " ORDER BY WO_ID, A_DATE DESC";
-    assertPlan(conn, sql).iteratorType("PARALLEL 1-WAY")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").scanType("SKIP SCAN ON 5 RANGES")
-      .table("_IDX_" + baseTableName)
+    assertPlan(conn, sql).iteratorType("PARALLEL 1-WAY").serverFirstKeyOnlyProjection(true)
+      .scanType("SKIP SCAN ON 5 RANGES").table("_IDX_" + baseTableName)
       .keyRanges(" [" + Short.MIN_VALUE + ",'00Dxxxxxxxxxxx1','003xxxxxxxxxxx1',*] - ["
         + Short.MIN_VALUE + ",'00Dxxxxxxxxxxx1','003xxxxxxxxxxx5',~'2016-01-01 06:00:00.000']");
 
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexCheckerIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexCheckerIT.java
index e174c37c56..31e55b55d3 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexCheckerIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexCheckerIT.java
@@ -1566,12 +1566,10 @@ public class GlobalIndexCheckerIT extends BaseTest {
         "SELECT id, val3 from " + dataTableName + " WHERE val1 = 'bc' and val2 = 'bcd' ";
       // Verify that we will read from the index table with a first-key-only/empty-column filter
       ExplainPlanAttributes attributes = getExplainAttributes(conn, selectSql);
-      assertPlan(attributes).scanType("RANGE SCAN").table(indexName);
-      String expectedFilter =
-        String.format("SERVER FILTER BY %s ONLY AND", encoded ? "FIRST KEY" : "EMPTY COLUMN");
-      assertTrue("serverWhereFilter=" + attributes.getServerWhereFilter(),
-        attributes.getServerWhereFilter() != null
-          && attributes.getServerWhereFilter().contains(expectedFilter));
+      assertPlan(attributes).scanType("RANGE SCAN").table(indexName)
+        .serverProjectionFilter(encoded);
+      assertNotNull("serverWhereFilter=" + attributes.getServerWhereFilter(),
+        attributes.getServerWhereFilter());
       try (ResultSet rs = conn.createStatement().executeQuery(selectSql)) {
         assertTrue(rs.next());
         assertEquals("b", rs.getString("id"));
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexCheckerWithRegionMovesIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexCheckerWithRegionMovesIT.java
index c5240d9ff0..3fbc8f4a62 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexCheckerWithRegionMovesIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexCheckerWithRegionMovesIT.java
@@ -1526,12 +1526,10 @@ public class GlobalIndexCheckerWithRegionMovesIT extends BaseTest {
         "SELECT id, val3 from " + dataTableName + " WHERE val1 = 'bc' and val2 = 'bcd' ";
       // Verify that we will read from the index table with a first-key-only/empty-column filter
       ExplainPlanAttributes attributes = getExplainAttributes(conn, selectSql);
-      assertPlan(attributes).scanType("RANGE SCAN").table(indexName);
-      String expectedFilter =
-        String.format("SERVER FILTER BY %s ONLY AND", encoded ? "FIRST KEY" : "EMPTY COLUMN");
-      assertTrue("serverWhereFilter=" + attributes.getServerWhereFilter(),
-        attributes.getServerWhereFilter() != null
-          && attributes.getServerWhereFilter().contains(expectedFilter));
+      assertPlan(attributes).scanType("RANGE SCAN").table(indexName)
+        .serverProjectionFilter(encoded);
+      assertNotNull("serverWhereFilter=" + attributes.getServerWhereFilter(),
+        attributes.getServerWhereFilter());
       try (ResultSet rs = conn.createStatement().executeQuery(selectSql)) {
         assertTrue(rs.next());
         moveRegionsOfTable(dataTableName);
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexOptimizationIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexOptimizationIT.java
index a9a43e3204..1f900e2f5f 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexOptimizationIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/GlobalIndexOptimizationIT.java
@@ -82,7 +82,7 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
       }
       assertMutationPlan(conn1, query).abstractExplainPlan("DELETE ROWS CLIENT SELECT")
         .scanType("RANGE SCAN").table(indexTableName + "L(" + dataTableName + ")")
-        .keyRanges(" [1,*] - [1,100]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .keyRanges(" [1,*] - [1,100]").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE SORT");
       ResultSet rs = conn1.createStatement().executeQuery("SELECT COUNT(*) FROM " + dataTableName);
       rs.next();
@@ -154,7 +154,7 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
       String query = "SELECT /*+ INDEX(" + dataTableName + " " + indexTableName + ")*/ * FROM "
         + dataTableName + " where v1='a'";
       assertPlan(conn1, query).scanType("RANGE SCAN").table(indexTableName).keyRanges(" ['a']")
-        .serverMergeColumns("[0.K3]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true);
 
       ResultSet rs = conn1.createStatement().executeQuery(query);
       assertTrue(rs.next());
@@ -172,7 +172,7 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
       query = "SELECT /*+ INDEX(" + dataTableName + " " + indexTableName + ")*/ * FROM "
         + dataTableName + " where v1='a'";
       assertPlan(conn1, query).scanType("RANGE SCAN").table(indexTableName).keyRanges(" ['a']")
-        .serverMergeColumns("[0.K3]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true);
 
       rs = conn1.createStatement().executeQuery(query);
       assertTrue(rs.next());
@@ -192,8 +192,8 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
       query = "SELECT /*+ INDEX(" + dataTableName + " " + indexTableName + ")*/ * FROM "
         + dataTableName + " where v1='a' limit 1";
       assertPlan(conn1, query).iteratorType("SERIAL").scanType("RANGE SCAN").table(indexTableName)
-        .keyRanges(" ['a']").serverMergeColumns("[0.K3]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverRowLimit(1L).clientRowLimit(1);
+        .keyRanges(" ['a']").serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
+        .serverRowLimit(1L).clientRowLimit(1);
 
       rs = conn1.createStatement().executeQuery(query);
       assertTrue(rs.next());
@@ -208,8 +208,8 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
         + ")*/ t_id, k1, k2, k3, V1 from " + dataTableFullName
         + "  where v1<='z' and k3 > 1 order by V1,t_id";
       assertPlan(conn1, query).scanType("RANGE SCAN").table(indexTableName)
-        .keyRanges(" [*] - ['z']").serverMergeColumns("[0.K3]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND \"K3\" > 1");
+        .keyRanges(" [*] - ['z']").serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
+        .serverWhereFilter("SERVER FILTER BY \"K3\" > 1");
 
       rs = conn1.createStatement().executeQuery(query);
       assertTrue(rs.next());
@@ -242,7 +242,7 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
         .clientSortAlgo("CLIENT MERGE SORT");
       skipScanJoinPlan.subPlanCount(1).subPlan(0).abstractExplainPlan("SKIP-SCAN-JOIN TABLE 0")
         .scanType("RANGE SCAN").table(indexTableName).keyRanges(" [*] - ['z']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+        .serverFirstKeyOnlyProjection(true).end();
       // The dynamic server filter references compiler-generated positional aliases ($N.$N) whose
       // numbers are not stable, so match the structural shape of the attribute with a regex.
       String dynamicServerFilter = skipScanJoinPlan.attributes().getDynamicServerFilter();
@@ -279,8 +279,7 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
       query = "SELECT /*+ INDEX(" + dataTableName + " " + indexTableName + ")*/ t_id, V1, k3 from "
         + dataTableFullName + "  where v1 <='z' group by v1,t_id, k3";
       assertPlan(conn1, query).scanType("RANGE SCAN").table(indexTableName)
-        .keyRanges(" [*] - ['z']").serverMergeColumns("[0.K3]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .keyRanges(" [*] - ['z']").serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
         .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"V1\", \"T_ID\", \"K3\"]")
         .clientSortAlgo("CLIENT MERGE SORT");
 
@@ -308,8 +307,7 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
         + dataTableFullName + " where v1 <='z'  group by v1 order by v1";
 
       assertPlan(conn1, query).scanType("RANGE SCAN").table(indexTableName)
-        .keyRanges(" [*] - ['z']").serverMergeColumns("[0.K3]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .keyRanges(" [*] - ['z']").serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
         .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"V1\"]");
 
       rs = conn1.createStatement().executeQuery(query);
@@ -343,8 +341,7 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
       String query = "SELECT /*+ INDEX(" + dataTableName + " " + indexTableName
         + ")*/ k1,k2,k3,v1 FROM " + dataTableName + " where v1='a'";
       assertPlan(conn1, query).scanType("RANGE SCAN").table(indexTableName)
-        .keyRanges(" ['tid1','a']").serverMergeColumns("[0.K3]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .keyRanges(" ['tid1','a']").serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true);
 
       ResultSet rs = conn1.createStatement().executeQuery(query);
       assertTrue(rs.next());
@@ -397,7 +394,7 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
 
       assertPlan(conn1, query).scanType("SKIP SCAN ON 2 KEYS").table("_IDX_" + dataTableName)
         .keyRanges(" [" + Short.MIN_VALUE + ",1] - [" + Short.MIN_VALUE + ",2]")
-        .serverMergeColumns("[0.K3]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true);
 
       rs = conn1.createStatement().executeQuery(query);
       assertTrue(rs.next());
@@ -435,7 +432,7 @@ public class GlobalIndexOptimizationIT extends ParallelStatsDisabledIT {
       String query = "SELECT /*+ INDEX(" + dataTableName + " " + indexTableName
         + ")*/ t_id, k1, k2, V1 FROM " + dataTableName + " where v1='a'";
       assertPlan(conn1, query).scanType("RANGE SCAN").table(indexTableName).keyRanges(" ['a']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverFirstKeyOnlyProjection(true);
 
       ResultSet rs = conn1.createStatement().executeQuery(query);
       assertTrue(rs.next());
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/IndexUsageIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/IndexUsageIT.java
index 0abd7ca913..9acbc4848e 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/IndexUsageIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/IndexUsageIT.java
@@ -131,7 +131,7 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
         + " GROUP BY (int_col1+int_col2)";
 
       ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, groupBySql)
-        .iteratorType("PARALLEL 1-WAY").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .iteratorType("PARALLEL 1-WAY").serverFirstKeyOnlyProjection(true)
         .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY "
           + "[TO_BIGINT(\"(A.INT_COL1 + B.INT_COL2)\")]");
       if (localIndex) {
@@ -185,8 +185,7 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
       String sql = "SELECT distinct int_col1+1 FROM " + fullDataTableName + " where int_col1+1 > 0";
 
       ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, sql)
-        .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN").serverFirstKeyOnlyProjection(true)
         .serverDistinctFilter(
           "SERVER DISTINCT PREFIX FILTER OVER " + "[TO_BIGINT(\"(A.INT_COL1 + 1)\")]")
         .serverAggregate(
@@ -243,9 +242,8 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
       conn.createStatement().execute(ddl);
       String sql = "SELECT int_col1+1 FROM " + fullDataTableName + " where int_col1+1 IN (2)";
 
-      ExplainPlanTestUtil.ExplainPlanAssert basePlan =
-        assertPlan(conn, sql).iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+      ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, sql)
+        .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN").serverFirstKeyOnlyProjection(true);
       if (localIndex) {
         basePlan.table("INDEX_TEST." + indexName + "(" + fullDataTableName + ")")
           .keyRanges(" [1,2]").clientSortAlgo("CLIENT MERGE SORT");
@@ -297,8 +295,8 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
       conn.createStatement().execute(ddl);
       String sql = "SELECT int_col1+1 AS foo FROM " + fullDataTableName + " ORDER BY foo";
 
-      ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, sql)
-        .iteratorType("PARALLEL 1-WAY").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+      ExplainPlanTestUtil.ExplainPlanAssert basePlan =
+        assertPlan(conn, sql).iteratorType("PARALLEL 1-WAY").serverFirstKeyOnlyProjection(true);
       if (localIndex) {
         basePlan.scanType("RANGE SCAN")
           .table("INDEX_TEST." + indexName + "(" + fullDataTableName + ")").keyRanges(" [1]")
@@ -476,7 +474,7 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
       if (localIndex) {
         basePlan.scanType("RANGE SCAN")
           .table("INDEX_TEST." + indexName + "(" + fullDataTableName + ")").keyRanges(" [1,2]")
-          .clientSortAlgo("CLIENT MERGE SORT").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+          .clientSortAlgo("CLIENT MERGE SORT").serverFirstKeyOnlyProjection(true);
       } else {
         basePlan.scanType("FULL SCAN").table(fullDataTableName).clientSortAlgo(null)
           .serverWhereFilter("SERVER FILTER BY (A.INT_COL1 + 1) = 2");
@@ -551,7 +549,7 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
 
       query = "SELECT k1, k2, s1||'_'||s2 FROM " + viewName + " WHERE (s1||'_'||s2)='foo2_bar2'";
       basePlan = assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverFirstKeyOnlyProjection(true);
       if (local) {
         basePlan.table(indexName2 + "(" + dataTableName + ")")
           .keyRanges(" [" + (2) + ",'foo2_bar2']").clientSortAlgo("CLIENT MERGE SORT");
@@ -614,8 +612,7 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
         "SELECT s2||'_'||s3 FROM " + viewName + " WHERE k2=1 AND (s2||'_'||s3)='abc_cab'";
 
       assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
-        .table(indexName2).keyRanges(" [1,'abc_cab','foo']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .table(indexName2).keyRanges(" [1,'abc_cab','foo']").serverFirstKeyOnlyProjection(true);
 
       rs = conn.createStatement().executeQuery(query);
       assertTrue(rs.next());
@@ -625,8 +622,8 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
       conn.createStatement().execute("ALTER VIEW " + viewName + " DROP COLUMN s4");
       // i2 cannot be used since s4 has been dropped from the view, so i1 will be used
       assertPlan(conn, query).scanType("RANGE SCAN").tableContains(indexName1).keyRanges(" [1]")
-        .serverWhereFilter(
-          "SERVER FILTER BY FIRST KEY ONLY AND ((\"S2\" || '_' || \"S3\") = 'abc_cab' AND \"S1\" = 'foo')");
+        .serverFirstKeyOnlyProjection(true).serverWhereFilter(
+          "SERVER FILTER BY ((\"S2\" || '_' || \"S3\") = 'abc_cab' AND \"S1\" = 'foo')");
       rs = conn.createStatement().executeQuery(query);
       assertTrue(rs.next());
       assertEquals("abc_cab", rs.getString(1));
@@ -710,9 +707,8 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
 
       query = "SELECT k FROM " + dataTableName + " WHERE REGEXP_SUBSTR(v,'id:\\\\w+') = 'id:id1'";
 
-      ExplainPlanTestUtil.ExplainPlanAssert basePlan =
-        assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+      ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, query)
+        .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN").serverFirstKeyOnlyProjection(true);
       if (localIndex) {
         basePlan.table(indexName + "(" + dataTableName + ")").keyRanges(" [1,'id:id1']")
           .clientSortAlgo("CLIENT MERGE SORT");
@@ -783,16 +779,14 @@ public class IndexUsageIT extends ParallelStatsDisabledIT {
       if (localIndex) {
         assertPlan(conn, query).scanType("RANGE SCAN")
           .tableContains(indexName + "(" + tableName + ")").keyRanges(" [1,'1David']")
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-          .subPlanCount(1).subPlan(0).scanType("RANGE SCAN")
-          .tableContains(indexName + "(" + tableName + ")").keyRanges(" [1]")
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
+          .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1)
+          .subPlan(0).scanType("RANGE SCAN").tableContains(indexName + "(" + tableName + ")")
+          .keyRanges(" [1]").serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT")
           .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0 (SKIP MERGE)").end();
       } else {
         assertPlan(conn, query).scanType("RANGE SCAN").tableContains(indexName)
-          .keyRanges(" ['1David']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-          .subPlanCount(1).subPlan(0).scanType("FULL SCAN").tableContains(indexName)
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+          .keyRanges(" ['1David']").serverFirstKeyOnlyProjection(true).subPlanCount(1).subPlan(0)
+          .scanType("FULL SCAN").tableContains(indexName).serverFirstKeyOnlyProjection(true)
           .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0 (SKIP MERGE)").end();
       }
 
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/LocalIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/LocalIndexIT.java
index fffacd4fc3..02c38fe89e 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/LocalIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/LocalIndexIT.java
@@ -443,7 +443,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
     assertPlan(conn, "SELECT v2 FROM " + tableName + " WHERE pk1 = 3 AND pk2 = 4")
       .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
       .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,3,4]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+      .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
 
     // 3. same prefix length, but there's a column not on the index
     assertPlan(conn, "SELECT v2 FROM " + tableName + " WHERE pk1 = 3 AND pk2 = 4 AND v3 = 1")
@@ -455,9 +455,8 @@ public class LocalIndexIT extends BaseLocalIndexIT {
       "SELECT v2 FROM " + tableName + " WHERE pk1 = 3 AND pk2 = 4 AND v1 = 3 AND v3 = 1")
         .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
         .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,3,4,3]")
-        .serverMergeColumns("[0.V3]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND \"V3\" = 1")
-        .clientSortAlgo("CLIENT MERGE SORT");
+        .serverMergeColumns("[0.V3]").serverFirstKeyOnlyProjection(true)
+        .serverWhereFilter("SERVER FILTER BY \"V3\" = 1").clientSortAlgo("CLIENT MERGE SORT");
   }
 
   @Test
@@ -486,7 +485,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
     assertPlan(conn, "SELECT pk1, pk2, pk3, v1 FROM " + tableName + " WHERE pk1 = 2 AND pk3 = 3")
       .iteratorType("PARALLEL").scanType("RANGE SCAN")
       .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,2,3]")
-      .serverMergeColumns("[0.V1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .serverMergeColumns("[0.V1]").serverFirstKeyOnlyProjection(true)
       .clientSortAlgo("CLIENT MERGE SORT");
   }
 
@@ -513,7 +512,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
     // 1. COUNT(*) should still use the index - fewer bytes to scan
     assertPlan(conn, "SELECT COUNT(*) FROM " + tableName).iteratorType("PARALLEL 1-WAY")
       .scanType("RANGE SCAN").table(fullIndexName + "(" + indexPhysicalTableName + ")")
-      .keyRanges(" [1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .keyRanges(" [1]").serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO SINGLE ROW");
 
     // 2. All column projected, no filtering by indexed column, not using the index
@@ -523,8 +522,8 @@ public class LocalIndexIT extends BaseLocalIndexIT {
     // 3. if the index can avoid a sort operation, use it
     assertPlan(conn, "SELECT * FROM " + tableName + " ORDER BY v2").iteratorType("PARALLEL 1-WAY")
       .scanType("RANGE SCAN").table(fullIndexName + "(" + indexPhysicalTableName + ")")
-      .keyRanges(" [1]").serverMergeColumns("[0.V1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+      .keyRanges(" [1]").serverMergeColumns("[0.V1]").serverFirstKeyOnlyProjection(true)
+      .clientSortAlgo("CLIENT MERGE SORT");
 
     // 4. but can't use the index if not ORDERing by a prefix of the index key.
     assertPlan(conn, "SELECT * FROM " + tableName + " ORDER BY v3").iteratorType("PARALLEL 1-WAY")
@@ -535,7 +534,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
     assertPlan(conn, "SELECT * FROM " + tableName + " WHERE v2 = 2 ORDER BY v3")
       .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
       .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,2]")
-      .serverMergeColumns("[0.V1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .serverMergeColumns("[0.V1]").serverFirstKeyOnlyProjection(true)
       .clientSortAlgo("CLIENT MERGE SORT");
 
     // 6. Filtering by a non-indexed column will not use the index
@@ -551,24 +550,23 @@ public class LocalIndexIT extends BaseLocalIndexIT {
     // 8. Filtering along a prefix of the index key can use the index
     assertPlan(conn, "SELECT * FROM " + tableName + " WHERE v2 = 2").iteratorType("PARALLEL 1-WAY")
       .scanType("RANGE SCAN").table(fullIndexName + "(" + indexPhysicalTableName + ")")
-      .keyRanges(" [1,2]").serverMergeColumns("[0.V1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+      .keyRanges(" [1,2]").serverMergeColumns("[0.V1]").serverFirstKeyOnlyProjection(true)
+      .clientSortAlgo("CLIENT MERGE SORT");
 
     // 9. Make sure a gap in the index columns still uses the index as long as a prefix is specified
     assertPlan(conn, "SELECT * FROM " + tableName + " WHERE v2 = 2 AND v4 = 4")
       .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
       .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,2]")
-      .serverMergeColumns("[0.V1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND TO_INTEGER(\"V4\") = 4")
+      .serverMergeColumns("[0.V1]").serverFirstKeyOnlyProjection(true)
+      .serverWhereFilter("SERVER FILTER BY TO_INTEGER(\"V4\") = 4")
       .clientSortAlgo("CLIENT MERGE SORT");
 
     // 10. Use index even when also filtering on non-indexed column
     assertPlan(conn, "SELECT * FROM " + tableName + " WHERE v2 = 2 AND v1 = 3")
       .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
       .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,2]")
-      .serverMergeColumns("[0.V1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND \"V1\" = 3.0")
-      .clientSortAlgo("CLIENT MERGE SORT");
+      .serverMergeColumns("[0.V1]").serverFirstKeyOnlyProjection(true)
+      .serverWhereFilter("SERVER FILTER BY \"V1\" = 3.0").clientSortAlgo("CLIENT MERGE SORT");
 
     // 11. Another case of not using a prefix of the index key
     assertPlan(conn, "SELECT * FROM " + tableName + " WHERE v1 = 3 AND v3 = 1 AND v4 = 1")
@@ -780,7 +778,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
       String query = "SELECT * FROM " + tableName + " ORDER BY V1";
       assertPlan(conn1, query).scanType("RANGE SCAN")
         .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1]")
-        .serverMergeColumns("[0.K3]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE SORT");
 
       ResultSet rs = conn1.createStatement().executeQuery(query);
@@ -799,7 +797,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
       query = "SELECT * FROM " + tableName + " ORDER BY V1 DESC NULLS LAST";
       assertPlan(conn1, query).scanType("RANGE SCAN").clientSortedBy("REVERSE")
         .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1]")
-        .serverMergeColumns("[0.K3]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE SORT");
 
       rs = conn1.createStatement().executeQuery(query);
@@ -840,7 +838,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
       String query = "SELECT V1 FROM " + tableName + " ORDER BY V1 DESC NULLS LAST";
       assertPlan(conn1, query).scanType("RANGE SCAN").clientSortedBy("REVERSE")
         .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+        .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
 
       ResultSet rs = conn1.createStatement().executeQuery(query);
       String v = "zz";
@@ -885,7 +883,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
       String query = "SELECT t_id, k1, k2, k3, V1 FROM " + tableName + " where v1='a'";
       assertPlan(conn1, query).scanType("RANGE SCAN")
         .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,'a']")
-        .serverMergeColumns("[0.K3]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE SORT");
 
       rs = conn1.createStatement().executeQuery(query);
@@ -904,7 +902,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
       query = "SELECT t_id, k1, k2, k3, V1 from " + tableName + "  where v1<='z' order by V1,t_id";
       assertPlan(conn1, query).scanType("RANGE SCAN")
         .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,*] - [1,'z']")
-        .serverMergeColumns("[0.K3]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE SORT");
 
       rs = conn1.createStatement().executeQuery(query);
@@ -936,7 +934,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
       query = "SELECT t_id, V1, k3 from " + tableName + "  where v1 <='z' group by v1,t_id, k3";
       assertPlan(conn1, query).scanType("RANGE SCAN")
         .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,*] - [1,'z']")
-        .serverMergeColumns("[0.K3]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
         .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"V1\", \"T_ID\", \"K3\"]")
         .clientSortAlgo("CLIENT MERGE SORT");
 
@@ -962,7 +960,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
 
       assertPlan(conn1, query).scanType("RANGE SCAN")
         .table(fullIndexName + "(" + indexPhysicalTableName + ")").keyRanges(" [1,*] - [1,'z']")
-        .serverMergeColumns("[0.K3]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.K3]").serverFirstKeyOnlyProjection(true)
         .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"V1\"]")
         .clientSortAlgo("CLIENT MERGE SORT");
 
@@ -1009,7 +1007,7 @@ public class LocalIndexIT extends BaseLocalIndexIT {
     assertPlan(conn1, query).iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
       .table(
         SchemaUtil.getPhysicalTableName(Bytes.toBytes(indexTableName), isNamespaceMapped) + "2")
-      .keyRanges(" ['a']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+      .keyRanges(" ['a']").serverFirstKeyOnlyProjection(true);
     conn1.close();
   }
 
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/SaltedIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/SaltedIndexIT.java
index c908e50b92..3dbd13f5bc 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/SaltedIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/SaltedIndexIT.java
@@ -160,15 +160,14 @@ public class SaltedIndexIT extends ParallelStatsDisabledIT {
 
     if (indexSaltBuckets == null) {
       assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
-        .table(indexTableFullName).keyRanges(" [~'y']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .table(indexTableFullName).keyRanges(" [~'y']").serverFirstKeyOnlyProjection(true);
     } else {
       assertPlan(conn, query).iteratorType("PARALLEL 4-WAY").scanType("RANGE SCAN")
         .table(indexTableFullName)
         .keyRanges(" [X'00',~'y'] - ["
           + PVarbinary.INSTANCE.toStringLiteral(new byte[] { (byte) (indexSaltBuckets - 1) })
           + ",~'y']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+        .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
     }
 
     // Will use index, so rows returned in DESC order.
@@ -185,15 +184,14 @@ public class SaltedIndexIT extends ParallelStatsDisabledIT {
     assertFalse(rs.next());
     if (indexSaltBuckets == null) {
       assertPlan(conn, query).iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
-        .table(indexTableFullName).keyRanges(" [*] - [~'x']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .table(indexTableFullName).keyRanges(" [*] - [~'x']").serverFirstKeyOnlyProjection(true);
     } else {
       assertPlan(conn, query).iteratorType("PARALLEL 4-WAY").scanType("RANGE SCAN")
         .table(indexTableFullName)
         .keyRanges(" [X'00',*] - ["
           + PVarbinary.INSTANCE.toStringLiteral(new byte[] { (byte) (indexSaltBuckets - 1) })
           + ",~'x']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+        .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
     }
 
     // Use data table, since point lookup trumps order by
@@ -232,12 +230,12 @@ public class SaltedIndexIT extends ParallelStatsDisabledIT {
     query = "SELECT * FROM " + dataTableFullName + " ORDER BY v DESC LIMIT 1";
     if (indexSaltBuckets == null) {
       assertPlan(conn, query).iteratorType("SERIAL 1-WAY").scanType("FULL SCAN")
-        .table(indexTableFullName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverRowLimit(1L).clientRowLimit(1);
+        .table(indexTableFullName).serverFirstKeyOnlyProjection(true).serverRowLimit(1L)
+        .clientRowLimit(1);
     } else {
       assertPlan(conn, query).iteratorType("PARALLEL 4-WAY").scanType("FULL SCAN")
-        .table(indexTableFullName).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverRowLimit(1L).clientSortAlgo("CLIENT MERGE SORT").clientRowLimit(1);
+        .table(indexTableFullName).serverFirstKeyOnlyProjection(true).serverRowLimit(1L)
+        .clientSortAlgo("CLIENT MERGE SORT").clientRowLimit(1);
     }
   }
 }
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ViewIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ViewIndexIT.java
index 6da69d4316..7566387a62 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ViewIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ViewIndexIT.java
@@ -259,8 +259,8 @@ public class ViewIndexIT extends SplitSystemCatalogIT {
       assertPlan(conn1, sql).iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN")
         .table(fullIndexName + "("
           + SchemaUtil.getPhysicalTableName(Bytes.toBytes(fullTableName), isNamespaceMapped) + ")")
-        .keyRanges(" [1,'10',100]").serverMergeColumns("[0.V1]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+        .keyRanges(" [1,'10',100]").serverMergeColumns("[0.V1]").serverFirstKeyOnlyProjection(true)
+        .clientSortAlgo("CLIENT MERGE SORT");
       ResultSet rs = conn1.prepareStatement(sql).executeQuery();
       assertTrue(rs.next());
       assertFalse(rs.next());
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinGlobalIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinGlobalIndexIT.java
index 8c6c5bc759..a3ee5b48be 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinGlobalIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinGlobalIndexIT.java
@@ -63,7 +63,7 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
       .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"I.0:NAME\"]")
       .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN").table(idxItem)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
@@ -74,15 +74,14 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
       .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"I.:item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[SUM(O.QUANTITY) DESC]").subPlanCount(1)
       .subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN")
-      .table(idxItem).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .table(idxItem).serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
   protected void assertLeftJoinWithAggPlan3(Connection conn, String query) throws Exception {
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(item)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"I.item_id\"]")
       .clientSortedBy("[SUM(O.QUANTITY) DESC NULLS LAST, \"I.item_id\"]").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN").table(order).end();
@@ -92,8 +91,7 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
   protected void assertRightJoinWithAggPlan1(Connection conn, String query) throws Exception {
     String idxItem = getSchemaName() + ".idx_item";
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(idxItem)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(idxItem).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"I.0:NAME\"]")
       .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0")
       .scanType("FULL SCAN").table(order).end();
@@ -103,8 +101,7 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
   protected void assertRightJoinWithAggPlan2(Connection conn, String query) throws Exception {
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(item)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"I.item_id\"]")
       .clientSortedBy("[SUM(O.QUANTITY) DESC NULLS LAST, \"I.item_id\"]").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN").table(order).end();
@@ -124,9 +121,9 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
     String idxItem = getSchemaName() + ".idx_item";
     String idxSupplier = getSchemaName() + ".idx_supplier";
     assertPlan(conn, query).scanType("RANGE SCAN").table(idxItem).keyRanges(" ['T1'] - ['T5']")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").subPlanCount(1).subPlan(0)
+      .serverFirstKeyOnlyProjection(true).subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("RANGE SCAN").table(idxSupplier)
-      .keyRanges(" ['S1'] - ['S5']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .keyRanges(" ['S1'] - ['S5']").serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
@@ -136,8 +133,7 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
     assertPlan(conn, query).scanType("SKIP SCAN ON 2 KEYS").table(idxItem)
       .keyRanges(" ['T1'] - ['T5']").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("SKIP SCAN ON 2 KEYS")
-      .table(idxSupplier).keyRanges(" ['S1'] - ['S5']")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .table(idxSupplier).keyRanges(" ['S1'] - ['S5']").serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
@@ -149,7 +145,7 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
       .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0 (SKIP MERGE)").scanType("FULL SCAN")
       .table(order).serverWhereFilter("SERVER FILTER BY QUANTITY < 5000").end().subPlan(1)
       .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 1").scanType("FULL SCAN").table(idxSupplier)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
@@ -159,15 +155,13 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
     assertPlan(conn, query).scanType("FULL SCAN").table(item)
       .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"I1.item_id\" IN (\"I2.:item_id\")")
       .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
-      .scanType("FULL SCAN").table(idxItem).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-      .end();
+      .scanType("FULL SCAN").table(idxItem).serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
   protected void assertSelfJoinPlan2(Connection conn, String query) throws Exception {
     String idxItem = getSchemaName() + ".idx_item";
-    assertPlan(conn, query).scanType("FULL SCAN").table(idxItem)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(idxItem).serverFirstKeyOnlyProjection(true)
       .serverSortedBy("[\"I1.0:NAME\", \"I2.0:NAME\"]").clientSortAlgo("CLIENT MERGE SORT")
       .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
       .scanType("FULL SCAN").table(idxItem).end();
@@ -182,17 +176,16 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
     if (!noStarJoin) {
       assertPlan(conn, query).scanType("FULL SCAN").table(order).subPlanCount(2).subPlan(0)
         .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN").table(idxCustomer)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end().subPlan(1)
+        .serverFirstKeyOnlyProjection(true).end().subPlan(1)
         .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 1").scanType("FULL SCAN").table(idxItem)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+        .serverFirstKeyOnlyProjection(true).end();
     } else {
       assertPlan(conn, query).scanType("FULL SCAN").table(idxItem)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverSortedBy("[\"O.order_id\"]")
+        .serverFirstKeyOnlyProjection(true).serverSortedBy("[\"O.order_id\"]")
         .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
         .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN").table(order)
         .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
-        .scanType("FULL SCAN").table(idxCustomer)
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end().end();
+        .scanType("FULL SCAN").table(idxCustomer).serverFirstKeyOnlyProjection(true).end().end();
     }
   }
 
@@ -223,7 +216,7 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
       .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [I.NAME]")
       .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN").table(idxItem)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
@@ -234,16 +227,14 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
       .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [O.IID]")
       .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[SUM(O.QUANTITY) DESC]").subPlanCount(1)
       .subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0 (SKIP MERGE)")
-      .scanType("FULL SCAN").table(idxItem).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-      .end();
+      .scanType("FULL SCAN").table(idxItem).serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
   protected void assertSubqueryAggPlan3(Connection conn, String query) throws Exception {
     String idxItem = getSchemaName() + ".idx_item";
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(idxItem)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(idxItem).serverFirstKeyOnlyProjection(true)
       .serverSortedBy("[O.Q DESC NULLS LAST, I.IID]").clientSortAlgo("CLIENT MERGE SORT")
       .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0")
       .scanType("FULL SCAN").table(order)
@@ -255,11 +246,10 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
   protected void assertSubqueryAggPlan4(Connection conn, String query) throws Exception {
     String idxItem = getSchemaName() + ".idx_item";
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(idxItem)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverSortedBy("[O.Q DESC, I.IID]")
-      .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
-      .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN").table(order)
-      .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"item_id\"]")
+    assertPlan(conn, query).scanType("FULL SCAN").table(idxItem).serverFirstKeyOnlyProjection(true)
+      .serverSortedBy("[O.Q DESC, I.IID]").clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1)
+      .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN")
+      .table(order).serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
@@ -314,10 +304,9 @@ public class HashJoinGlobalIndexIT extends HashJoinIT {
     statement.setMaxRows(4);
     ExplainPlanAttributes attributes =
       statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes();
-    assertPlan(attributes).scanType("FULL SCAN").table(idxItem)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientRowLimit(4).joinScannerLimit(4L)
-      .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
-      .scanType("FULL SCAN").table(order).end();
+    assertPlan(attributes).scanType("FULL SCAN").table(idxItem).serverFirstKeyOnlyProjection(true)
+      .clientRowLimit(4).joinScannerLimit(4L).subPlanCount(1).subPlan(0)
+      .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN").table(order).end();
   }
 
   @Override
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinLocalIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinLocalIndexIT.java
index a44ac276e9..9f46bea9a8 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinLocalIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinLocalIndexIT.java
@@ -78,9 +78,8 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"I.0:NAME\"]")
       .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("RANGE SCAN")
-      .table(itemIndex + "(" + item + ")").keyRanges(" [1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-      .end();
+      .table(itemIndex + "(" + item + ")").keyRanges(" [1]").serverFirstKeyOnlyProjection(true)
+      .clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
@@ -92,17 +91,15 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"I.:item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[SUM(O.QUANTITY) DESC]").subPlanCount(1)
       .subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("RANGE SCAN")
-      .table(itemIndex + "(" + item + ")").keyRanges(" [1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-      .end();
+      .table(itemIndex + "(" + item + ")").keyRanges(" [1]").serverFirstKeyOnlyProjection(true)
+      .clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
   protected void assertLeftJoinWithAggPlan3(Connection conn, String query) throws Exception {
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(item)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"I.item_id\"]")
       .clientSortedBy("[SUM(O.QUANTITY) DESC NULLS LAST, \"I.item_id\"]").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN").table(order).end();
@@ -114,7 +111,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
     String itemIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_ITEM_INDEX);
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
     assertPlan(conn, query).scanType("RANGE SCAN").table(itemIndex + "(" + item + ")")
-      .keyRanges(" [1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .keyRanges(" [1]").serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"I.0:NAME\"]")
       .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN").table(order).end();
@@ -124,8 +121,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
   protected void assertRightJoinWithAggPlan2(Connection conn, String query) throws Exception {
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(item)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"I.item_id\"]")
       .clientSortedBy("[SUM(O.QUANTITY) DESC NULLS LAST, \"I.item_id\"]").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN").table(order).end();
@@ -147,12 +143,11 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
     String supplier = getTableName(conn, JOIN_SUPPLIER_TABLE_FULL_NAME);
     String supplierIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_SUPPLIER_INDEX);
     assertPlan(conn, query).scanType("RANGE SCAN").table(itemIndex + "(" + item + ")")
-      .keyRanges(" [1,'T1'] - [1,'T5']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .keyRanges(" [1,'T1'] - [1,'T5']").serverFirstKeyOnlyProjection(true)
       .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("RANGE SCAN")
       .table(supplierIndex + "(" + supplier + ")").keyRanges(" [1,'S1'] - [1,'S5']")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-      .end();
+      .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
@@ -165,8 +160,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       .keyRanges(" [1,'T1'] - [1,'T5']").clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1)
       .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("SKIP SCAN ON 2 KEYS")
       .table(supplierIndex + "(" + supplier + ")").keyRanges(" [1,'S1'] - [1,'S5']")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-      .end();
+      .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
@@ -183,8 +177,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       .scanType("FULL SCAN").table(order).serverWhereFilter("SERVER FILTER BY QUANTITY < 5000")
       .end().subPlan(1).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 1").scanType("RANGE SCAN")
       .table(supplierIndex + "(" + supplier + ")").keyRanges(" [1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-      .end();
+      .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
@@ -195,8 +188,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"I1.item_id\" IN (\"I2.:item_id\")")
       .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
       .scanType("RANGE SCAN").table(itemIndex + "(" + item + ")").keyRanges(" [1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-      .end();
+      .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
@@ -204,7 +196,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
     String itemIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_ITEM_INDEX);
     assertPlan(conn, query).scanType("RANGE SCAN").table(itemIndex + "(" + item + ")")
-      .keyRanges(" [1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .keyRanges(" [1]").serverFirstKeyOnlyProjection(true)
       .serverSortedBy("[\"I1.0:NAME\", \"I2.0:NAME\"]").clientSortAlgo("CLIENT MERGE SORT")
       .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"I1.:item_id\" IN (\"I2.0:supplier_id\")")
       .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
@@ -224,22 +216,20 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       assertPlan(conn, query).scanType("FULL SCAN").table(order).subPlanCount(2).subPlan(0)
         .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("RANGE SCAN")
         .table(customerIndex + "(" + customer + ")").keyRanges(" [1]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-        .end().subPlan(1).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 1").scanType("RANGE SCAN")
-        .table(itemIndex + "(" + item + ")").keyRanges(" [1]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-        .end();
+        .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT").end().subPlan(1)
+        .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 1").scanType("RANGE SCAN")
+        .table(itemIndex + "(" + item + ")").keyRanges(" [1]").serverFirstKeyOnlyProjection(true)
+        .clientSortAlgo("CLIENT MERGE SORT").end();
     } else {
       assertPlan(conn, query).scanType("RANGE SCAN").table(itemIndex + "(" + item + ")")
-        .keyRanges(" [1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverSortedBy("[\"O.order_id\"]").clientSortAlgo("CLIENT MERGE SORT")
+        .keyRanges(" [1]").serverFirstKeyOnlyProjection(true).serverSortedBy("[\"O.order_id\"]")
+        .clientSortAlgo("CLIENT MERGE SORT")
         .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"I.:item_id\" IN (\"O.item_id\")")
         .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
         .scanType("FULL SCAN").table(order).subPlanCount(1).subPlan(0)
         .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("RANGE SCAN")
         .table(customerIndex + "(" + customer + ")").keyRanges(" [1]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-        .end().end();
+        .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT").end().end();
     }
   }
 
@@ -273,9 +263,8 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [I.NAME]")
       .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("RANGE SCAN")
-      .table(itemIndex + "(" + item + ")").keyRanges(" [1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-      .end();
+      .table(itemIndex + "(" + item + ")").keyRanges(" [1]").serverFirstKeyOnlyProjection(true)
+      .clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
@@ -288,8 +277,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[SUM(O.QUANTITY) DESC]").subPlanCount(1)
       .subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0 (SKIP MERGE)")
       .scanType("RANGE SCAN").table(itemIndex + "(" + item + ")").keyRanges(" [1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT")
-      .end();
+      .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
@@ -298,7 +286,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
     String itemIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_ITEM_INDEX);
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
     assertPlan(conn, query).scanType("RANGE SCAN").table(itemIndex + "(" + item + ")")
-      .keyRanges(" [1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .keyRanges(" [1]").serverFirstKeyOnlyProjection(true)
       .serverSortedBy("[O.Q DESC NULLS LAST, I.IID]").clientSortAlgo("CLIENT MERGE SORT")
       .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0")
       .scanType("FULL SCAN").table(order)
@@ -312,10 +300,10 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
     String itemIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_ITEM_INDEX);
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
     assertPlan(conn, query).scanType("RANGE SCAN").table(itemIndex + "(" + item + ")")
-      .keyRanges(" [1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-      .serverSortedBy("[O.Q DESC, I.IID]").clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1)
-      .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN")
-      .table(order).serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"item_id\"]")
+      .keyRanges(" [1]").serverFirstKeyOnlyProjection(true).serverSortedBy("[O.Q DESC, I.IID]")
+      .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
+      .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN").table(order)
+      .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
@@ -378,8 +366,8 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
     ExplainPlanAttributes attributes =
       statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes();
     assertPlan(attributes).scanType("RANGE SCAN").table(itemIndex + "(" + item + ")")
-      .keyRanges(" [1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-      .clientSortAlgo("CLIENT MERGE SORT").clientRowLimit(4)
+      .keyRanges(" [1]").serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT")
+      .clientRowLimit(4)
       .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"I.:item_id\" IN (\"O.item_id\")")
       .joinScannerLimit(4L).subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN").table(order).end();
@@ -451,7 +439,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       assertFalse(rs.next());
       assertPlan(conn, query).scanType("RANGE SCAN")
         .table(supplierIndex + "(" + supplierTable + ")").keyRanges(" [1,'S1']")
-        .serverMergeColumns("[0.PHONE]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.PHONE]").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE SORT")
         .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"S.:supplier_id\" IN (\"I.0:supplier_id\")")
         .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
@@ -468,7 +456,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       assertFalse(rs.next());
       assertPlan(conn, query).scanType("RANGE SCAN")
         .table(supplierIndex + "(" + supplierTable + ")").keyRanges(" [1,'S1']")
-        .serverMergeColumns("[0.PHONE]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.PHONE]").serverFirstKeyOnlyProjection(true)
         .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"S.PHONE\"]")
         .clientSortAlgo("CLIENT MERGE SORT")
         .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"S.:supplier_id\" IN (\"I.0:supplier_id\")")
@@ -486,7 +474,7 @@ public class HashJoinLocalIndexIT extends HashJoinIT {
       assertFalse(rs.next());
       assertPlan(conn, query).scanType("RANGE SCAN")
         .table(supplierIndex + "(" + supplierTable + ")").keyRanges(" [1,*] - [1,'S3']")
-        .serverMergeColumns("[0.PHONE]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .serverMergeColumns("[0.PHONE]").serverFirstKeyOnlyProjection(true)
         .serverAggregate("SERVER AGGREGATE INTO SINGLE ROW").subPlanCount(1).subPlan(0)
         .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("RANGE SCAN")
         .table(itemIndex + "(" + itemTable + ")").keyRanges(" [1,*] - [1,'T6']")
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinMoreIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinMoreIT.java
index 16f0144c69..4e338f31d5 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinMoreIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinMoreIT.java
@@ -782,12 +782,13 @@ public class HashJoinMoreIT extends ParallelStatsDisabledIT {
 
         assertPlan(conn, q).scanType("SKIP SCAN ON 2 RANGES").table("EVENT_COUNT").keyRanges(
           " [X'00','5SEC',~1462993520000000000,'Tr/Bal'] - [X'01','5SEC',~1462993420000000000,'Tr/Bal']")
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+          .serverFirstKeyOnlyProjection(true)
           .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"E.TIMESTAMP\", E.BUCKET]")
           .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
           .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0 (SKIP MERGE)")
           .scanType("SKIP SCAN ON 2 RANGES").table(t[i]).keyRanges(innerKeyRanges)
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND SRC_LOCATION = DST_LOCATION")
+          .serverFirstKeyOnlyProjection(true)
+          .serverWhereFilter("SERVER FILTER BY SRC_LOCATION = DST_LOCATION")
           .clientSortAlgo("CLIENT MERGE SORT").end();
 
         ResultSet rs = conn.createStatement().executeQuery(q);
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinNoIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinNoIndexIT.java
index 166cc338ad..87da5547da 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinNoIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/HashJoinNoIndexIT.java
@@ -73,15 +73,14 @@ public class HashJoinNoIndexIT extends HashJoinIT {
       .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"I.item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[SUM(O.QUANTITY) DESC]").subPlanCount(1)
       .subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN")
-      .table(item).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .table(item).serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
   protected void assertLeftJoinWithAggPlan3(Connection conn, String query) throws Exception {
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(item)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"I.item_id\"]")
       .clientSortedBy("[SUM(O.QUANTITY) DESC NULLS LAST, \"I.item_id\"]").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN").table(order).end();
@@ -101,8 +100,7 @@ public class HashJoinNoIndexIT extends HashJoinIT {
   protected void assertRightJoinWithAggPlan2(Connection conn, String query) throws Exception {
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(item)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"I.item_id\"]")
       .clientSortedBy("[SUM(O.QUANTITY) DESC NULLS LAST, \"I.item_id\"]").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN").table(order).end();
@@ -156,7 +154,7 @@ public class HashJoinNoIndexIT extends HashJoinIT {
     assertPlan(conn, query).scanType("FULL SCAN").table(item)
       .dynamicServerFilter("DYNAMIC SERVER FILTER BY \"I1.item_id\" IN (\"I2.item_id\")")
       .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0")
-      .scanType("FULL SCAN").table(item).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
@@ -228,15 +226,14 @@ public class HashJoinNoIndexIT extends HashJoinIT {
       .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [O.IID]")
       .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[SUM(O.QUANTITY) DESC]").subPlanCount(1)
       .subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0 (SKIP MERGE)")
-      .scanType("FULL SCAN").table(item).serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
   protected void assertSubqueryAggPlan3(Connection conn, String query) throws Exception {
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(item)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+    assertPlan(conn, query).scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true)
       .serverSortedBy("[O.Q DESC NULLS LAST, I.IID]").clientSortAlgo("CLIENT MERGE SORT")
       .subPlanCount(1).subPlan(0).abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0")
       .scanType("FULL SCAN").table(order)
@@ -248,11 +245,10 @@ public class HashJoinNoIndexIT extends HashJoinIT {
   protected void assertSubqueryAggPlan4(Connection conn, String query) throws Exception {
     String order = getTableName(conn, JOIN_ORDER_TABLE_FULL_NAME);
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
-    assertPlan(conn, query).scanType("FULL SCAN").table(item)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverSortedBy("[O.Q DESC, I.IID]")
-      .clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1).subPlan(0)
-      .abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN").table(order)
-      .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"item_id\"]")
+    assertPlan(conn, query).scanType("FULL SCAN").table(item).serverFirstKeyOnlyProjection(true)
+      .serverSortedBy("[O.Q DESC, I.IID]").clientSortAlgo("CLIENT MERGE SORT").subPlanCount(1)
+      .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("FULL SCAN")
+      .table(order).serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [\"item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
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 612768aabc..a0ee9bc490 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
@@ -61,9 +61,8 @@ public class SortMergeJoinGlobalIndexIT extends SortMergeJoinIT {
     String itemIndex = getSchemaName() + ".idx_item";
     String supplierIndex = getSchemaName() + ".idx_supplier";
     assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").sortMergeSkipMerge(false)
-      .lhs().scanType("FULL SCAN").table(supplierIndex)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverSortedBy("[\"S.:supplier_id\"]")
-      .clientSortAlgo("CLIENT MERGE SORT").end().rhs()
+      .lhs().scanType("FULL SCAN").table(supplierIndex).serverFirstKeyOnlyProjection(true)
+      .serverSortedBy("[\"S.:supplier_id\"]").clientSortAlgo("CLIENT MERGE SORT").end().rhs()
       .abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(true)
       .clientSortedBy("[\"I.0:supplier_id\"]").lhs().scanType("FULL SCAN").table(itemIndex)
       .serverSortedBy("[\"I.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end().rhs()
@@ -75,11 +74,10 @@ public class SortMergeJoinGlobalIndexIT extends SortMergeJoinIT {
   protected void assertSelfJoinPlan(Connection conn, String query) throws Exception {
     String itemIndex = getSchemaName() + ".idx_item";
     assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false)
-      .lhs().scanType("FULL SCAN").table(itemIndex)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverSortedBy("[\"I1.:item_id\"]")
-      .clientSortAlgo("CLIENT MERGE SORT").end().rhs().scanType("FULL SCAN").table(itemIndex)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverSortedBy("[\"I2.:item_id\"]")
-      .clientSortAlgo("CLIENT MERGE SORT").end();
+      .lhs().scanType("FULL SCAN").table(itemIndex).serverFirstKeyOnlyProjection(true)
+      .serverSortedBy("[\"I1.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end().rhs()
+      .scanType("FULL SCAN").table(itemIndex).serverFirstKeyOnlyProjection(true)
+      .serverSortedBy("[\"I2.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
@@ -94,7 +92,7 @@ public class SortMergeJoinGlobalIndexIT extends SortMergeJoinIT {
       statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes();
     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\"]")
+      .serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I.:item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").end().rhs().scanType("FULL SCAN").table(order)
       .serverSortedBy(queryIndex == 0 ? "[\"O.item_id\"]" : "[\"item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").end();
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinLocalIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinLocalIndexIT.java
index 95d5f44a2a..8e5fbb489a 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
@@ -64,7 +64,7 @@ public class SortMergeJoinLocalIndexIT extends SortMergeJoinIT {
     String supplierIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_SUPPLIER_INDEX);
     assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (LEFT)").sortMergeSkipMerge(false)
       .lhs().scanType("RANGE SCAN").table(supplierIndex + "(" + supplier + ")").keyRanges(" [1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverSortedBy("[\"S.:supplier_id\"]")
+      .serverFirstKeyOnlyProjection(true).serverSortedBy("[\"S.:supplier_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").end().rhs()
       .abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(true)
       .clientSortedBy("[\"I.0:supplier_id\"]").lhs().scanType("RANGE SCAN")
@@ -80,11 +80,10 @@ public class SortMergeJoinLocalIndexIT extends SortMergeJoinIT {
     String itemIndex = SchemaUtil.getTableName(getSchemaName(), JOIN_ITEM_INDEX);
     assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false)
       .lhs().scanType("RANGE SCAN").table(itemIndex + "(" + item + ")").keyRanges(" [1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverSortedBy("[\"I1.:item_id\"]")
+      .serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I1.:item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").end().rhs().scanType("RANGE SCAN")
-      .table(itemIndex + "(" + item + ")").keyRanges(" [1]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").serverSortedBy("[\"I2.:item_id\"]")
-      .clientSortAlgo("CLIENT MERGE SORT").end();
+      .table(itemIndex + "(" + item + ")").keyRanges(" [1]").serverFirstKeyOnlyProjection(true)
+      .serverSortedBy("[\"I2.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end();
   }
 
   @Override
@@ -100,9 +99,8 @@ public class SortMergeJoinLocalIndexIT extends SortMergeJoinIT {
       statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes();
     assertPlan(attributes).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false)
       .clientRowLimit(4).lhs().scanType("RANGE SCAN").table(itemIndex + "(" + item + ")")
-      .keyRanges(" [1]").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-      .serverSortedBy("[\"I.:item_id\"]").clientSortAlgo("CLIENT MERGE SORT").end().rhs()
-      .scanType("FULL SCAN").table(order)
+      .keyRanges(" [1]").serverFirstKeyOnlyProjection(true).serverSortedBy("[\"I.:item_id\"]")
+      .clientSortAlgo("CLIENT MERGE SORT").end().rhs().scanType("FULL SCAN").table(order)
       .serverSortedBy(queryIndex == 0 ? "[\"O.item_id\"]" : "[\"item_id\"]")
       .clientSortAlgo("CLIENT MERGE SORT").end();
   }
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinNoIndexIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinNoIndexIT.java
index 4cd6b59dbb..20c7fe620c 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinNoIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/join/SortMergeJoinNoIndexIT.java
@@ -73,7 +73,7 @@ public class SortMergeJoinNoIndexIT extends SortMergeJoinIT {
     String item = getTableName(conn, JOIN_ITEM_TABLE_FULL_NAME);
     assertPlan(conn, query).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(false)
       .lhs().scanType("FULL SCAN").table(item).end().rhs().scanType("FULL SCAN").table(item)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+      .serverFirstKeyOnlyProjection(true).end();
   }
 
   @Override
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/schema/stats/BaseStatsCollectorIT.java b/phoenix-core/src/it/java/org/apache/phoenix/schema/stats/BaseStatsCollectorIT.java
index ab8f625b3e..0b731fff6e 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/schema/stats/BaseStatsCollectorIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/schema/stats/BaseStatsCollectorIT.java
@@ -256,8 +256,7 @@ public abstract class BaseStatsCollectorIT extends BaseTest {
     collectStatistics(conn, fullTableName);
     assertPlan(conn, "SELECT * FROM " + fullTableName).splitsChunk(1).estimatedRows(0L)
       .estimatedBytes(20L).iteratorType("PARALLEL 1-WAY").scanType("FULL SCAN")
-      .table(physicalTableName).serverWhereFilter(
-        "SERVER FILTER BY " + (columnEncoded ? "FIRST KEY ONLY" : "EMPTY COLUMN ONLY"));
+      .table(physicalTableName).serverProjectionFilter(columnEncoded);
     conn.close();
   }
 
diff --git a/phoenix-core/src/test/java/org/apache/phoenix/compile/JoinQueryCompilerTest.java b/phoenix-core/src/test/java/org/apache/phoenix/compile/JoinQueryCompilerTest.java
index 8ee65259d4..c280192ee3 100644
--- a/phoenix-core/src/test/java/org/apache/phoenix/compile/JoinQueryCompilerTest.java
+++ b/phoenix-core/src/test/java/org/apache/phoenix/compile/JoinQueryCompilerTest.java
@@ -82,7 +82,7 @@ public class JoinQueryCompilerTest extends BaseConnectionlessQueryTest {
         + " s ON s.\"supplier_id\" = i.\"supplier_id\" WHERE i.name LIKE 'T%'";
     // RIGHT JOIN drives the scan over SUPPLIER, with the rest of the join tree nested as sub-plans.
     assertPlan(conn, query).scanType("FULL SCAN").table(JOIN_SUPPLIER_TABLE_DISPLAY_NAME)
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+      .serverFirstKeyOnlyProjection(true)
       .afterJoinFilter("AFTER-JOIN SERVER FILTER BY I.NAME LIKE 'T%'").subPlanCount(1).subPlan(0)
       .abstractExplainPlan("PARALLEL LEFT-JOIN TABLE 0").scanType("FULL SCAN")
       .table(JOIN_ORDER_TABLE_DISPLAY_NAME).subPlanCount(2).subPlan(0)
diff --git a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java
index 0016d9d4cc..8100742e67 100644
--- a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java
+++ b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java
@@ -2548,20 +2548,18 @@ public class QueryCompilerTest extends BaseConnectionlessQueryTest {
       conn.createStatement().execute("CREATE INDEX idx ON t1 (col1 || col2)");
       assertPlan(conn, "SELECT a.k from t1 a where a.col1 || a.col2 = 'foobar'")
         .scanType("RANGE SCAN").table("IDX").keyRanges(" ['foobar']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverFirstKeyOnlyProjection(true);
       assertPlan(conn, "SELECT k,j from t3 b join t1 a ON k = j where a.col1 || a.col2 = 'foobar'")
-        .scanType("FULL SCAN").table("T3").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+        .scanType("FULL SCAN").table("T3").serverFirstKeyOnlyProjection(true)
         .dynamicServerFilter("DYNAMIC SERVER FILTER BY B.J IN (\"A.:K\")").subPlanCount(1)
         .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("RANGE SCAN")
-        .table("IDX").keyRanges(" ['foobar']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .end();
+        .table("IDX").keyRanges(" ['foobar']").serverFirstKeyOnlyProjection(true).end();
       assertPlan(conn,
         "SELECT a.k,b.k from t2 b join t1 a ON a.k = b.k where a.col1 || a.col2 = 'foobar'")
-          .scanType("FULL SCAN").table("T2").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
+          .scanType("FULL SCAN").table("T2").serverFirstKeyOnlyProjection(true)
           .dynamicServerFilter("DYNAMIC SERVER FILTER BY B.K IN (\"A.:K\")").subPlanCount(1)
           .subPlan(0).abstractExplainPlan("PARALLEL INNER-JOIN TABLE 0").scanType("RANGE SCAN")
-          .table("IDX").keyRanges(" ['foobar']")
-          .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+          .table("IDX").keyRanges(" ['foobar']").serverFirstKeyOnlyProjection(true).end();
     } finally {
       conn.close();
     }
@@ -7150,7 +7148,7 @@ public class QueryCompilerTest extends BaseConnectionlessQueryTest {
         + " where ts >= TIMESTAMP '2023-02-23 13:30:00'  and ts < TIMESTAMP '2023-02-23 13:40:00'";
       assertPlan(conn, query).scanType("RANGE SCAN").table(indexName)
         .keyRanges(" [~1,677,159,600,000] - [~1,677,159,000,000]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverFirstKeyOnlyProjection(true);
     }
   }
 
@@ -7168,7 +7166,7 @@ public class QueryCompilerTest extends BaseConnectionlessQueryTest {
       assertEquals("\\x9E\\x9E\\x9F\\x00", Bytes.toStringBinary(openScan.getStartRow()));
       assertEquals("\\x9E\\xFF", Bytes.toStringBinary(openScan.getStopRow()));
       assertPlan(conn, openQry).scanType("RANGE SCAN").table(tableName)
-        .keyRanges(" [~'aaa'] - [~'a']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .keyRanges(" [~'aaa'] - [~'a']").serverFirstKeyOnlyProjection(true);
 
       String closedQry = "select * from " + tableName + " where k >= 'a' and k <= 'aaa'";
       Scan closedScan =
@@ -7176,7 +7174,7 @@ public class QueryCompilerTest extends BaseConnectionlessQueryTest {
       assertEquals("\\x9E\\x9E\\x9E\\xFF", Bytes.toStringBinary(closedScan.getStartRow()));
       assertEquals("\\x9F\\x00", Bytes.toStringBinary(closedScan.getStopRow()));
       assertPlan(conn, closedQry).scanType("RANGE SCAN").table(tableName)
-        .keyRanges(" [~'aaa'] - [~'a']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .keyRanges(" [~'aaa'] - [~'a']").serverFirstKeyOnlyProjection(true);
     }
   }
 
@@ -7193,10 +7191,9 @@ public class QueryCompilerTest extends BaseConnectionlessQueryTest {
       String query = "select /*+ index(dd ii) */ k1, k2, k3, k4, v1, v2, v3, v4 from dd"
         + " where k4=1 and k2=1 order by k1 asc, v1 asc limit  1";
       assertPlan(conn, query).scanType("RANGE SCAN").table("II").keyRanges(" [1]")
-        .serverMergeColumns("[0.V1, 0.V2, 0.V3, 0.V4]")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND \"K2\" = 1")
-        .serverSortedBy("[\"K1\", \"V1\"]").serverRowLimit(1L).clientRowLimit(1)
-        .clientSortAlgo("CLIENT MERGE SORT");
+        .serverMergeColumns("[0.V1, 0.V2, 0.V3, 0.V4]").serverFirstKeyOnlyProjection(true)
+        .serverWhereFilter("SERVER FILTER BY \"K2\" = 1").serverSortedBy("[\"K1\", \"V1\"]")
+        .serverRowLimit(1L).clientRowLimit(1).clientSortAlgo("CLIENT MERGE SORT");
     }
   }
 
@@ -7219,8 +7216,8 @@ public class QueryCompilerTest extends BaseConnectionlessQueryTest {
         .dynamicServerFilter("DYNAMIC SERVER FILTER BY (\"D.K1\", \"D.K2\", \"D.K3\", \"D.K4\")"
           + " IN (($2.$4, $2.$5, $2.$6, $2.$7))")
         .subPlanCount(1).subPlan(0).abstractExplainPlan("SKIP-SCAN-JOIN TABLE 0")
-        .scanType("RANGE SCAN").table("I").keyRanges(" ['XXX']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+        .scanType("RANGE SCAN").table("I").keyRanges(" ['XXX']").serverFirstKeyOnlyProjection(true)
+        .end();
     }
   }
 
@@ -7249,7 +7246,7 @@ public class QueryCompilerTest extends BaseConnectionlessQueryTest {
           + " IN (($2.$4, $2.$5, $2.$6, $2.$7))")
         .subPlanCount(1).subPlan(0).abstractExplainPlan("SKIP-SCAN-JOIN TABLE 0")
         .scanType("RANGE SCAN").table("IDX_PHOENIX_6986").keyRanges(" ['XXX']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").end();
+        .serverFirstKeyOnlyProjection(true).end();
     }
   }
 
diff --git a/phoenix-core/src/test/java/org/apache/phoenix/compile/StatementHintsCompilationTest.java b/phoenix-core/src/test/java/org/apache/phoenix/compile/StatementHintsCompilationTest.java
index 4cdd5dbf92..abbb829c9a 100644
--- a/phoenix-core/src/test/java/org/apache/phoenix/compile/StatementHintsCompilationTest.java
+++ b/phoenix-core/src/test/java/org/apache/phoenix/compile/StatementHintsCompilationTest.java
@@ -103,7 +103,8 @@ public class StatementHintsCompilationTest extends BaseConnectionlessQueryTest {
     assertPlan(conn, query).scanType("RANGE SCAN").table("EH")
       .keyRanges(" ['111111111111111','foo            ','2012-11-01 00:00:00.000']"
         + " - ['111111111111111','fop            ','2012-11-30 00:00:00.000']")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY AND (CREATED_DATE >= DATE"
+      .serverFirstKeyOnlyProjection(true)
+      .serverWhereFilter("SERVER FILTER BY (CREATED_DATE >= DATE"
         + " '2012-11-01 00:00:00.000' AND CREATED_DATE < DATE '2012-11-30 00:00:00.000')")
       .serverSortedBy("[ORGANIZATION_ID, PARENT_ID, CREATED_DATE DESC, ENTITY_HISTORY_ID]")
       .serverRowLimit(100L).clientSortAlgo("CLIENT MERGE SORT").clientRowLimit(100);
diff --git a/phoenix-core/src/test/java/org/apache/phoenix/compile/TenantSpecificViewIndexCompileTest.java b/phoenix-core/src/test/java/org/apache/phoenix/compile/TenantSpecificViewIndexCompileTest.java
index c043617840..1a3e82c9c2 100644
--- a/phoenix-core/src/test/java/org/apache/phoenix/compile/TenantSpecificViewIndexCompileTest.java
+++ b/phoenix-core/src/test/java/org/apache/phoenix/compile/TenantSpecificViewIndexCompileTest.java
@@ -194,7 +194,7 @@ public class TenantSpecificViewIndexCompileTest extends BaseConnectionlessQueryT
     assertPlan(conn, "SELECT v2 FROM v WHERE v2 > 'a' and k2 = 'a' ORDER BY v2,k2")
       .iteratorType("PARALLEL 1-WAY").scanType("RANGE SCAN").table("_IDX_T")
       .keyRanges(" [-9223372036854775808,'me','a'] - [-9223372036854775808,'me',*]")
-      .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+      .serverFirstKeyOnlyProjection(true);
 
     // Won't use index b/c v1 is not in index, but should optimize out k2 still from the order by
     // K2 will still be referenced in the filter, as these are automatically tacked on to the where
diff --git a/phoenix-core/src/test/java/org/apache/phoenix/query/QueryPlanTest.java b/phoenix-core/src/test/java/org/apache/phoenix/query/QueryPlanTest.java
index a783ccc0a1..09849e3db1 100644
--- a/phoenix-core/src/test/java/org/apache/phoenix/query/QueryPlanTest.java
+++ b/phoenix-core/src/test/java/org/apache/phoenix/query/QueryPlanTest.java
@@ -82,7 +82,7 @@ public class QueryPlanTest extends BaseConnectionlessQueryTest {
       assertPlan(conn, query).scanType("RANGE SCAN").table("FOO")
         .keyRanges(
           " [X'00','a',~'2016-01-28 23:59:59.999'] -" + " [X'13','a',~'2016-01-28 00:00:00.000']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY").clientSortAlgo("CLIENT MERGE SORT");
+        .serverFirstKeyOnlyProjection(true).clientSortAlgo("CLIENT MERGE SORT");
     } finally {
       conn.close();
     }
@@ -107,7 +107,7 @@ public class QueryPlanTest extends BaseConnectionlessQueryTest {
       assertPlan(conn, query).useRoundRobinIterator(true).scanType("RANGE SCAN").table(tableName)
         .keyRanges(
           " [X'00','a',~'2016-01-28 23:59:59.999'] -" + " [X'13','a',~'2016-01-28 00:00:00.000']")
-        .serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY");
+        .serverFirstKeyOnlyProjection(true);
     } finally {
       conn.close();
     }
@@ -126,8 +126,8 @@ public class QueryPlanTest extends BaseConnectionlessQueryTest {
       // The SERIAL hint is ignored for a non-rowkey ORDER BY, so the iterator stays PARALLEL and a
       // server sort + client merge sort are planned.
       assertPlan(conn, query).iteratorType("PARALLEL").scanType("RANGE SCAN").table("FOO")
-        .keyRanges(" ['a']").serverWhereFilter("SERVER FILTER BY FIRST KEY ONLY")
-        .serverSortedBy("[B, C]").clientSortAlgo("CLIENT MERGE SORT");
+        .keyRanges(" ['a']").serverFirstKeyOnlyProjection(true).serverSortedBy("[B, C]")
+        .clientSortAlgo("CLIENT MERGE SORT");
     } finally {
       conn.close();
     }
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 8e57faa750..5445b5b855 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
@@ -143,9 +143,10 @@ 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']",
-        "    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"));
+        "    INDEX PTSDB3", "    REGIONS PLANNED <N>",
+        "    SERVER PROJECTION FILTER BY FIRST KEY ONLY"),
+      scanAttrs("SKIP SCAN ON 3 KEYS ", "PTSDB3", " [~'na3'] - [~'na1']")
+        .put("serverFirstKeyOnlyProjection", true));
   }
 
   @Test
@@ -157,10 +158,11 @@ 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']",
-        "    INDEX PTSDB", "    REGIONS PLANNED <N>", "    SERVER FILTER BY FIRST KEY ONLY"),
+        "    INDEX PTSDB", "    REGIONS PLANNED <N>",
+        "    SERVER PROJECTION 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"));
+        " ['na1','a','2013-01-01'] - ['na3','b','2013-01-02']").put("serverFirstKeyOnlyProjection",
+          true));
   }
 
   @Test
@@ -176,9 +178,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest {
     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']", "    INDEX PTSDB2",
-        "    REGIONS PLANNED <N>", "    SERVER FILTER BY FIRST KEY ONLY"),
-      scanAttrs("RANGE SCAN ", "PTSDB2", " ['na1']")
-        .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY")
+        "    REGIONS PLANNED <N>", "    SERVER PROJECTION FILTER BY FIRST KEY ONLY"),
+      scanAttrs("RANGE SCAN ", "PTSDB2", " ['na1']").put("serverFirstKeyOnlyProjection", true)
         .put("clientSortedBy", "REVERSE"));
   }
 
@@ -187,19 +188,19 @@ 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']",
-        "    INDEX PTSDB3", "    REGIONS PLANNED <N>", "    SERVER FILTER BY FIRST KEY ONLY"),
+        "    INDEX PTSDB3", "    REGIONS PLANNED <N>",
+        "    SERVER PROJECTION 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"));
+        .put("serverFirstKeyOnlyProjection", true));
   }
 
   @Test
   public void testAggregateSingleRow() throws Exception {
     verifyQuery("aggregateSingleRow", "SELECT count(*) FROM atable",
       text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
-        "    REGIONS PLANNED <N>", "    SERVER FILTER BY FIRST KEY ONLY",
+        "    REGIONS PLANNED <N>", "    SERVER PROJECTION FILTER BY FIRST KEY ONLY",
         "    SERVER AGGREGATE INTO SINGLE ROW"),
-      scanAttrs("FULL SCAN ", "ATABLE", "")
-        .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY")
+      scanAttrs("FULL SCAN ", "ATABLE", "").put("serverFirstKeyOnlyProjection", true)
         .put("serverAggregate", "SERVER AGGREGATE INTO SINGLE ROW"));
   }
 
@@ -319,10 +320,11 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest {
       "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]", "    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'"));
+        "    REGIONS PLANNED <N>", "    SERVER PROJECTION FILTER BY FIRST KEY ONLY",
+        "    SERVER FILTER BY \"DATE\" >= DATE '2013-01-01 00:00:00.000'"),
+      scanAttrs("RANGE SCAN ", "PTSDB", " [null,not null]")
+        .put("serverFirstKeyOnlyProjection", true)
+        .put("serverWhereFilter", "SERVER FILTER BY \"DATE\" >= DATE '2013-01-01 00:00:00.000'"));
   }
 
   @Test
@@ -331,12 +333,11 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest {
       "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]", "    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"
-          + " AND \"DATE\" >= DATE '2013-01-01 00:00:00.000')"));
+        "    REGIONS PLANNED <N>", "    SERVER PROJECTION FILTER BY FIRST KEY ONLY",
+        "    SERVER FILTER BY (HOST IS NULL" + " AND \"DATE\" >= DATE '2013-01-01 00:00:00.000')"),
+      scanAttrs("RANGE SCAN ", "PTSDB", " [not null]").put("serverFirstKeyOnlyProjection", true)
+        .put("serverWhereFilter",
+          "SERVER FILTER BY (HOST IS NULL" + " AND \"DATE\" >= DATE '2013-01-01 00:00:00.000')"));
   }
 
   @Test
@@ -347,10 +348,11 @@ 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']",
-        "    INDEX PTSDB", "    REGIONS PLANNED <N>", "    SERVER FILTER BY FIRST KEY ONLY"),
+        "    INDEX PTSDB", "    REGIONS PLANNED <N>",
+        "    SERVER PROJECTION 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"));
+        " ['na','a','2013-01-01'] - ['nb','b','2013-01-02']").put("serverFirstKeyOnlyProjection",
+          true));
   }
 
   @Test
@@ -359,10 +361,11 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest {
       "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",
-        "SERVER FILTER BY FIRST KEY ONLY AND REGEXP_SUBSTR(INST, '[^-]+', 1) IN ('na1','na2','na3')"));
+        "    SERVER PROJECTION FILTER BY FIRST KEY ONLY",
+        "    SERVER FILTER BY REGEXP_SUBSTR(INST, '[^-]+', 1) IN ('na1','na2','na3')"),
+      scanAttrs("SKIP SCAN ON 3 RANGES ", "PTSDB", " ['na1'] - ['na4']")
+        .put("serverFirstKeyOnlyProjection", true).put("serverWhereFilter",
+          "SERVER FILTER BY REGEXP_SUBSTR(INST, '[^-]+', 1) IN ('na1','na2','na3')"));
   }
 
   @Test
@@ -598,9 +601,10 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest {
     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'"),
+        "    SERVER PROJECTION FILTER BY FIRST KEY ONLY", "    SERVER FILTER BY 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'"));
+        .put("serverFirstKeyOnlyProjection", true)
+        .put("serverWhereFilter", "SERVER FILTER BY ENTITY_ID = 'abc'"));
   }
 
   @Test
@@ -608,19 +612,20 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest {
     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'"),
+        "    SERVER PROJECTION FILTER BY FIRST KEY ONLY", "    SERVER FILTER BY 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'"));
+        .put("serverFirstKeyOnlyProjection", true)
+        .put("serverWhereFilter", "SERVER FILTER BY ENTITY_ID = 'abc'"));
   }
 
   @Test
   public void testSequenceNextValue() throws Exception {
     verifyQuery("sequenceNextValue", "SELECT NEXT VALUE FOR " + SEQ + " FROM atable",
       text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    INDEX ATABLE",
-        "    REGIONS PLANNED <N>", "    SERVER FILTER BY FIRST KEY ONLY",
+        "    REGIONS PLANNED <N>", "    SERVER PROJECTION 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)
+      scanAttrs("FULL SCAN ", "ATABLE", "").put("serverFirstKeyOnlyProjection", true)
+        .put("clientSequenceCount", 1)
         .set("clientSteps", clientSteps("CLIENT RESERVE VALUES FROM 1 SEQUENCE")));
   }
 
@@ -924,7 +929,7 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest {
     } catch (AssertionError expected) {
       String msg = expected.getMessage();
       assertTrue(msg.contains("Text mismatch for case 'x'"));
-      assertTrue(msg.contains("SERVER FILTER BY FIRST KEY ONLY"));
+      assertTrue(msg.contains("SERVER PROJECTION FILTER BY FIRST KEY ONLY"));
       assertTrue(msg.contains("SERVER FILTER BY (X = 9)"));
     } catch (Exception e) {
       fail("Unexpected exception type: " + e);
@@ -1001,6 +1006,8 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest {
     n.putNull("serverOffset");
     n.putNull("serverRowLimit");
     n.put("serverArrayElementProjection", false);
+    n.put("serverFirstKeyOnlyProjection", false);
+    n.put("serverEmptyColumnOnlyProjection", false);
     n.putNull("serverAggregate");
     n.putNull("clientFilterBy");
     n.putNull("clientAggregate");
@@ -1068,10 +1075,9 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest {
 
   private static ExplainPlan samplePlan(String way, String scanType) {
     ExplainPlanAttributes a = new ExplainPlanAttributesBuilder().setIteratorTypeAndScanSize(way)
-      .setExplainScanType(scanType).setTableName("T")
-      .setServerWhereFilter("SERVER FILTER BY FIRST KEY ONLY").build();
+      .setExplainScanType(scanType).setTableName("T").setServerFirstKeyOnlyProjection(true).build();
     return new ExplainPlan(Arrays.asList("CLIENT " + way + " " + scanType.trim() + " OVER T",
-      "    SERVER FILTER BY FIRST KEY ONLY"), a);
+      "    SERVER PROJECTION FILTER BY FIRST KEY ONLY"), a);
   }
 
   private static PColumn column(String family, String name) {
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 2f44a07e58..25a3347255 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
@@ -343,6 +343,42 @@ public final class ExplainPlanTestUtil {
       return this;
     }
 
+    public ExplainPlanAssert serverFirstKeyOnlyProjection(boolean expected) {
+      assertEquals(at("serverFirstKeyOnlyProjection"), expected,
+        attributes.isServerFirstKeyOnlyProjection());
+      return this;
+    }
+
+    /**
+     * Assert when {@code firstKeyOnly} is true the scan carries the {@code FIRST KEY ONLY}
+     * projection, otherwise it carries the {@code EMPTY COLUMN ONLY} projection. Convenience for
+     * callers that pick the expected kind from a flag.
+     */
+    public ExplainPlanAssert serverProjectionFilter(boolean firstKeyOnly) {
+      return firstKeyOnly
+        ? serverFirstKeyOnlyProjection(true)
+        : serverEmptyColumnOnlyProjection(true);
+    }
+
+    /**
+     * Assert either the {@code FIRST KEY ONLY} or the {@code EMPTY COLUMN ONLY} projection
+     * optimization is present.
+     */
+    public ExplainPlanAssert serverProjectionFilterAnyOf() {
+      assertTrue(
+        at("serverFirstKeyOnlyProjection|serverEmptyColumnOnlyProjection")
+          + " expected one to be true",
+        attributes.isServerFirstKeyOnlyProjection()
+          || attributes.isServerEmptyColumnOnlyProjection());
+      return this;
+    }
+
+    public ExplainPlanAssert serverEmptyColumnOnlyProjection(boolean expected) {
+      assertEquals(at("serverEmptyColumnOnlyProjection"), expected,
+        attributes.isServerEmptyColumnOnlyProjection());
+      return this;
+    }
+
     public ExplainPlanAssert useRoundRobinIterator(boolean expected) {
       assertEquals(at("useRoundRobinIterator"), expected, attributes.isUseRoundRobinIterator());
       return this;