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

virajjasani pushed a commit to branch 5.3
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/5.3 by this push:
     new 048ab25f46 PHOENIX-8007 Perf bottleneck - Server side Index metadata 
lookup overhead (#2625)
048ab25f46 is described below

commit 048ab25f462712ef6fb90474387e9ad10a1abe3c
Author: Viraj Jasani <[email protected]>
AuthorDate: Sun Oct 4 17:20:27 2026 -0700

    PHOENIX-8007 Perf bottleneck - Server side Index metadata lookup overhead 
(#2625)
---
 .../phoenix/index/PhoenixIndexMetaDataBuilder.java |  26 ++-
 .../phoenix/end2end/UCFWithServerMetadataIT.java   | 256 +++++++++++++++++++++
 2 files changed, 281 insertions(+), 1 deletion(-)

diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/index/PhoenixIndexMetaDataBuilder.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/index/PhoenixIndexMetaDataBuilder.java
index 3d38a5a394..ad3d1b611e 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/index/PhoenixIndexMetaDataBuilder.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/index/PhoenixIndexMetaDataBuilder.java
@@ -19,6 +19,7 @@ package org.apache.phoenix.index;
 
 import java.io.IOException;
 import java.sql.Connection;
+import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.List;
@@ -53,6 +54,12 @@ public class PhoenixIndexMetaDataBuilder {
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(PhoenixIndexMetaDataBuilder.class);
 
+  /**
+   * Lazily-computed, cached JDBC URL used to open server-side connections for 
index metadata
+   * lookups.
+   */
+  private static volatile String serverConnectionUrl;
+
   private final RegionCoprocessorEnvironment env;
 
   PhoenixIndexMetaDataBuilder(RegionCoprocessorEnvironment env) {
@@ -154,7 +161,8 @@ public class PhoenixIndexMetaDataBuilder {
       if (tenantId != null) {
         props.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
       }
-      try (Connection conn = QueryUtil.getConnectionOnServer(props, 
env.getConfiguration())) {
+      QueryUtil.setServerConnection(props);
+      try (Connection conn = 
DriverManager.getConnection(getServerConnectionUrl(env), props)) {
         PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
         PTable dataTable = pconn.getTable(tenantId, fullTableName);
         final List<IndexMaintainer> indexMaintainers =
@@ -213,4 +221,20 @@ public class PhoenixIndexMetaDataBuilder {
     }
     return indexMaintainers;
   }
+
+  private static String getServerConnectionUrl(RegionCoprocessorEnvironment 
env)
+    throws SQLException {
+    String url = serverConnectionUrl;
+    if (url == null) {
+      synchronized (PhoenixIndexMetaDataBuilder.class) {
+        url = serverConnectionUrl;
+        if (url == null) {
+          url = QueryUtil.getConnectionUrl(new Properties(), 
env.getConfiguration());
+          LOGGER.info("Cached server-side JDBC url for index metadata lookups: 
{}", url);
+          serverConnectionUrl = url;
+        }
+      }
+    }
+    return url;
+  }
 }
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UCFWithServerMetadataIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UCFWithServerMetadataIT.java
index fe29929456..62e6844931 100644
--- 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UCFWithServerMetadataIT.java
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UCFWithServerMetadataIT.java
@@ -19,12 +19,16 @@ package org.apache.phoenix.end2end;
 
 import static 
org.apache.phoenix.hbase.index.IndexRegionObserver.PHOENIX_INDEX_CDC_CONSUMER_ENABLED;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import com.google.protobuf.RpcCallback;
 import com.google.protobuf.RpcController;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
@@ -37,8 +41,11 @@ import 
org.apache.phoenix.coprocessor.generated.MetaDataProtos.GetTableRequest;
 import 
org.apache.phoenix.coprocessor.generated.MetaDataProtos.MetaDataResponse;
 import 
org.apache.phoenix.coprocessor.generated.ServerCachingProtos.AddServerCacheRequest;
 import 
org.apache.phoenix.coprocessor.generated.ServerCachingProtos.AddServerCacheResponse;
+import org.apache.phoenix.jdbc.PhoenixPreparedStatement;
 import org.apache.phoenix.query.BaseTest;
 import org.apache.phoenix.query.QueryServices;
+import org.apache.phoenix.util.MetaDataUtil;
+import org.apache.phoenix.util.PhoenixRuntime;
 import org.apache.phoenix.util.PropertiesUtil;
 import org.apache.phoenix.util.ReadOnlyProps;
 import org.apache.phoenix.util.TestUtil;
@@ -205,6 +212,255 @@ public class UCFWithServerMetadataIT extends BaseTest {
     }
   }
 
+  @Test
+  public void testTenantViewIndexServerMetadata() throws Exception {
+    String baseTableName = generateUniqueName();
+    String tenant1 = generateUniqueName();
+    String tenant2 = generateUniqueName();
+    String viewName1 = "V1_" + generateUniqueName();
+    String viewName2 = "V2_" + generateUniqueName();
+    String coveredIndex1 = "TCI1_" + generateUniqueName();
+    String uncoveredIndex1 = "TUI1_" + generateUniqueName();
+    String coveredIndex2 = "TCI2_" + generateUniqueName();
+    String viewIndexPhysical = MetaDataUtil.VIEW_INDEX_TABLE_PREFIX + 
baseTableName;
+
+    Properties props = PropertiesUtil.deepCopy(TestUtil.TEST_PROPERTIES);
+    props.setProperty(QueryServices.INDEX_USE_SERVER_METADATA_ATTRIB,
+      Boolean.toString(useServerMetadata));
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      createMultiTenantBaseTable(conn, baseTableName);
+      attachCustomCoprocessor(conn, baseTableName);
+    }
+
+    try (Connection tenant1Conn = getTenantConnection(tenant1)) {
+      tenant1Conn.createStatement()
+        .execute(String.format("CREATE VIEW %s AS SELECT * FROM %s", 
viewName1, baseTableName));
+      tenant1Conn.createStatement().execute(String
+        .format("CREATE INDEX %s ON %s (name) INCLUDE (age, city)", 
coveredIndex1, viewName1));
+      tenant1Conn.createStatement()
+        .execute(String.format("CREATE INDEX %s ON %s (salary)", 
uncoveredIndex1, viewName1));
+
+      PreparedStatement upsert = tenant1Conn.prepareStatement(String.format(
+        "UPSERT INTO %s (id, name, age, city, salary, department) VALUES (?, 
?, ?, ?, ?, ?)",
+        viewName1));
+
+      // Warm-up (row id=1) primes both the client and the server PTable 
caches so that, under a
+      // large UPDATE_CACHE_FREQUENCY, the measured window observes no further 
getTable() RPCs.
+      upsertTenantViewRow(upsert, 1);
+      tenant1Conn.commit();
+      assertCoveredIndexRow(tenant1Conn, viewName1, viewIndexPhysical, 1);
+      assertUncoveredIndexRow(tenant1Conn, viewName1, viewIndexPhysical, 1);
+
+      long startGetTableCalls = getTableCallCount.get();
+      long startAddServerCacheCalls = addServerCacheCallCount.get();
+
+      // Measured window: upsert rows 2..6 through the tenant connection, 
single-row or batched
+      // consistent with the singleRowUpdate dimension (a batch of 5 exceeds 
the mutate-batch
+      // threshold so the without-server-metadata batch case uses 
addServerCache()).
+      for (int i = 2; i <= 6; i++) {
+        upsertTenantViewRow(upsert, i);
+        if (singleRowUpdate) {
+          tenant1Conn.commit();
+        }
+      }
+      if (!singleRowUpdate) {
+        tenant1Conn.commit();
+      }
+      // Correctness: each view index must return exactly the right 
rows/columns for this tenant.
+      assertCoveredIndexRow(tenant1Conn, viewName1, viewIndexPhysical, 3);
+      assertUncoveredIndexRow(tenant1Conn, viewName1, viewIndexPhysical, 5);
+
+      long getTableDelta = getTableCallCount.get() - startGetTableCalls;
+      long addServerCacheDelta = addServerCacheCallCount.get() - 
startAddServerCacheCalls;
+
+      // With server metadata, index writes must never trigger a client 
addServerCache() RPC. This
+      // is the counting assertion the production change (global vs per-tenant 
connection) could
+      // regress. Without server metadata, only batched writes above the 
threshold use it.
+      if (useServerMetadata) {
+        assertEquals("Tenant view index writes must not use addServerCache() 
with server metadata",
+          0L, addServerCacheDelta);
+      } else if (singleRowUpdate) {
+        assertEquals("Single-row writes stay below the mutate-batch 
threshold", 0L,
+          addServerCacheDelta);
+      } else {
+        assertTrue("Batched writes without server metadata should use 
addServerCache()",
+          addServerCacheDelta >= 1);
+      }
+
+      // getTable() behavior on the tenant-view path. The server-metadata path 
now resolves tenant
+      // views/indexes with a per-tenant-scoped reusable server connection, so 
metadata resolution
+      // honors UPDATE_CACHE_FREQUENCY exactly as it does for non-tenant 
tables. With UCF=ALWAYS
+      // every batch re-resolves the tenant view (on the client for the 
non-server path, on the
+      // RegionServer for the server-metadata path), so the count grows. With 
a large UCF, once the
+      // warm-up row has populated the shared server/client PTable caches, the 
measured window
+      // re-resolves nothing and issues no getTable() RPCs. Exact counts under 
ALWAYS depend on the
+      // number of batches, so we assert the robust direction invariant.
+      if ("ALWAYS".equals(updateCacheFrequency)) {
+        assertTrue("With UCF=ALWAYS the tenant view path must re-resolve 
metadata (getTable grows)",
+          getTableDelta > 0);
+      } else {
+        assertEquals("With a large UCF the warm tenant view path must not 
issue getTable() RPCs",
+          0L, getTableDelta);
+      }
+
+      // Tenant isolation: a second tenant's view on the same base table with 
a same-named row must
+      // not leak across the shared view-index physical table.
+      try (Connection tenant2Conn = getTenantConnection(tenant2)) {
+        tenant2Conn.createStatement()
+          .execute(String.format("CREATE VIEW %s AS SELECT * FROM %s", 
viewName2, baseTableName));
+        tenant2Conn.createStatement().execute(String
+          .format("CREATE INDEX %s ON %s (name) INCLUDE (age, city)", 
coveredIndex2, viewName2));
+        PreparedStatement upsert2 = tenant2Conn.prepareStatement(String.format(
+          "UPSERT INTO %s (id, name, age, city, salary, department) VALUES (?, 
?, ?, ?, ?, ?)",
+          viewName2));
+        // Same id/name as tenant1's row id=3 but a distinct age/city.
+        upsert2.setInt(1, 3);
+        upsert2.setString(2, "Name3");
+        upsert2.setInt(3, 999);
+        upsert2.setString(4, "CityZ");
+        upsert2.setInt(5, 999000);
+        upsert2.setString(6, "DeptZ");
+        upsert2.executeUpdate();
+        tenant2Conn.commit();
+
+        try (ResultSet rs = tenant2Conn.createStatement().executeQuery(
+          String.format("SELECT age, city FROM %s WHERE name = 'Name3'", 
viewName2))) {
+          assertTrue(rs.next());
+          assertEquals(999, rs.getInt(1));
+          assertEquals("CityZ", rs.getString(2));
+          assertFalse("tenant2 covered index must return exactly its own row", 
rs.next());
+        }
+      }
+
+      // tenant1's covered-index query for the same name must still return 
only tenant1's own data.
+      try (ResultSet rs = tenant1Conn.createStatement()
+        .executeQuery(String.format("SELECT age, city FROM %s WHERE name = 
'Name3'", viewName1))) {
+        assertTrue(rs.next());
+        assertEquals("tenant1 must not see tenant2's index entries", 23, 
rs.getInt(1));
+        assertEquals("City3", rs.getString(2));
+        assertFalse("tenant1 covered index must return exactly its own row", 
rs.next());
+      }
+    }
+  }
+
+  @Test
+  public void testPartialIndexOnTenantView() throws Exception {
+    String baseTableName = generateUniqueName();
+    String tenant = generateUniqueName();
+    String viewName = "PV_" + generateUniqueName();
+    String partialIndex = "PI_" + generateUniqueName();
+    String viewIndexPhysical = MetaDataUtil.VIEW_INDEX_TABLE_PREFIX + 
baseTableName;
+
+    Properties props = PropertiesUtil.deepCopy(TestUtil.TEST_PROPERTIES);
+    props.setProperty(QueryServices.INDEX_USE_SERVER_METADATA_ATTRIB,
+      Boolean.toString(useServerMetadata));
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      createMultiTenantBaseTable(conn, baseTableName);
+    }
+
+    try (Connection tenantConn = getTenantConnection(tenant)) {
+      tenantConn.createStatement()
+        .execute(String.format("CREATE VIEW %s AS SELECT * FROM %s", viewName, 
baseTableName));
+      // Partial index on the leading key column (age), covering city, for 
rows with age >= 50.
+      tenantConn.createStatement().execute(String.format(
+        "CREATE INDEX %s ON %s (age) INCLUDE (city) WHERE age >= 50", 
partialIndex, viewName));
+
+      PreparedStatement upsert = tenantConn.prepareStatement(String.format(
+        "UPSERT INTO %s (id, name, age, city, salary, department) VALUES (?, 
?, ?, ?, ?, ?)",
+        viewName));
+      // Rows id=1..6 with age=10*id: ids 5 and 6 match the predicate (age 50, 
60); ids 1..4 do
+      // not. Maintenance of the partial index runs on the server-metadata 
path for these upserts.
+      for (int id = 1; id <= 6; id++) {
+        upsert.setInt(1, id);
+        upsert.setString(2, "Name" + id);
+        upsert.setInt(3, 10 * id);
+        upsert.setString(4, "City" + id);
+        upsert.setInt(5, 1000 * id);
+        upsert.setString(6, "Dept" + (id % 5));
+        upsert.executeUpdate();
+        if (singleRowUpdate) {
+          tenantConn.commit();
+        }
+      }
+      if (!singleRowUpdate) {
+        tenantConn.commit();
+      }
+
+      String query =
+        String.format("SELECT age, city FROM %s WHERE age >= 50 ORDER BY age", 
viewName);
+      assertEquals("Partial view index should be used", viewIndexPhysical,
+        
tenantConn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery()
+          .getExplainPlan().getPlanStepsAsAttributes().getTableName());
+      try (ResultSet rs = tenantConn.createStatement().executeQuery(query)) {
+        assertTrue(rs.next());
+        assertEquals(50, rs.getInt(1));
+        assertEquals("City5", rs.getString(2));
+        assertTrue(rs.next());
+        assertEquals(60, rs.getInt(1));
+        assertEquals("City6", rs.getString(2));
+        assertFalse("Partial index must contain only predicate-matching rows", 
rs.next());
+      }
+    }
+  }
+
+  private void createMultiTenantBaseTable(Connection conn, String 
baseTableName)
+    throws SQLException {
+    conn.createStatement().execute(String.format(
+      "CREATE TABLE %s (tenant_id VARCHAR NOT NULL, id INTEGER NOT NULL, name 
VARCHAR(50), "
+        + "age INTEGER, city VARCHAR(50), salary INTEGER, department 
VARCHAR(50) "
+        + "CONSTRAINT pk PRIMARY KEY (tenant_id, id)) MULTI_TENANT=true, 
UPDATE_CACHE_FREQUENCY=%s",
+      baseTableName, updateCacheFrequency));
+  }
+
+  private Connection getTenantConnection(String tenantId) throws SQLException {
+    Properties props = PropertiesUtil.deepCopy(TestUtil.TEST_PROPERTIES);
+    props.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
+    props.setProperty(QueryServices.INDEX_USE_SERVER_METADATA_ATTRIB,
+      Boolean.toString(useServerMetadata));
+    return DriverManager.getConnection(getUrl(), props);
+  }
+
+  private static void upsertTenantViewRow(PreparedStatement upsert, int id) 
throws SQLException {
+    upsert.setInt(1, id);
+    upsert.setString(2, "Name" + id);
+    upsert.setInt(3, 20 + id);
+    upsert.setString(4, "City" + id);
+    upsert.setInt(5, 1000 * id);
+    upsert.setString(6, "Dept" + (id % 5));
+    upsert.executeUpdate();
+  }
+
+  private void assertCoveredIndexRow(Connection conn, String viewName, String 
viewIndexPhysical,
+    int id) throws SQLException {
+    String query =
+      String.format("SELECT name, age, city FROM %s WHERE name = 'Name%d'", 
viewName, id);
+    assertEquals("Covered view index should be used", viewIndexPhysical,
+      
conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery()
+        .getExplainPlan().getPlanStepsAsAttributes().getTableName());
+    try (ResultSet rs = conn.createStatement().executeQuery(query)) {
+      assertTrue(rs.next());
+      assertEquals("Name" + id, rs.getString(1));
+      assertEquals(20 + id, rs.getInt(2));
+      assertEquals("City" + id, rs.getString(3));
+      assertFalse("Covered index query must return exactly one row", 
rs.next());
+    }
+  }
+
+  private void assertUncoveredIndexRow(Connection conn, String viewName, 
String viewIndexPhysical,
+    int id) throws SQLException {
+    String query =
+      String.format("SELECT id, salary FROM %s WHERE salary = %d", viewName, 
1000 * id);
+    assertEquals("Uncovered view index should be used", viewIndexPhysical,
+      
conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery()
+        .getExplainPlan().getPlanStepsAsAttributes().getTableName());
+    try (ResultSet rs = conn.createStatement().executeQuery(query)) {
+      assertTrue(rs.next());
+      assertEquals(id, rs.getInt(1));
+      assertEquals(1000 * id, rs.getInt(2));
+      assertFalse("Uncovered index query must return exactly one row", 
rs.next());
+    }
+  }
+
   private void attachCustomCoprocessor(Connection conn, String dataTableName) 
throws Exception {
     TestUtil.removeCoprocessor(conn, "SYSTEM.CATALOG", 
MetaDataEndpointImpl.class);
     TestUtil.addCoprocessor(conn, "SYSTEM.CATALOG", 
TrackingMetaDataEndpointImpl.class);

Reply via email to