Skip to content

Commit 30e031b

Browse files
authored
perf(spanner-jdbc): cache positional to named param conversion (#14034)
Cache the result of converting a JDBC SQL string containing positional parameters (?), so we do not need to do this conversion over and over each time a SQL string is executed.
1 parent 0c482fe commit 30e031b

2 files changed

Lines changed: 80 additions & 2 deletions

File tree

java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractStatementParser.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,13 @@ private static boolean isRecordStatementCacheStats() {
483483
* Cache for parsed statements. This prevents statements that are executed multiple times by the
484484
* application to be parsed over and over again. The default maximum size is 5Mb.
485485
*/
486-
private final Cache<String, ParsedStatement> statementCache;
486+
@Nullable private final Cache<String, ParsedStatement> statementCache;
487+
488+
/**
489+
* Cache for positional parameters info. This prevents statements that are executed multiple times
490+
* using positional parameters from having to be scanned and translated repeatedly.
491+
*/
492+
@Nullable private final Cache<String, ParametersInfo> positionalParametersCache;
487493

488494
AbstractStatementParser(Set<ClientSideStatementImpl> statements) {
489495
this.statements = Collections.unmodifiableSet(statements);
@@ -502,16 +508,36 @@ private static boolean isRecordStatementCacheStats() {
502508
cacheBuilder.recordStats();
503509
}
504510
this.statementCache = cacheBuilder.build();
511+
512+
CacheBuilder<String, ParametersInfo> positionalCacheBuilder =
513+
CacheBuilder.newBuilder()
514+
.maximumWeight(maxCacheSize * 1024L * 1024L)
515+
.weigher(
516+
(String key, ParametersInfo value) ->
517+
2 * key.length() + 2 * value.sqlWithNamedParameters.length())
518+
.concurrencyLevel(Runtime.getRuntime().availableProcessors());
519+
if (isRecordStatementCacheStats()) {
520+
positionalCacheBuilder.recordStats();
521+
}
522+
this.positionalParametersCache = positionalCacheBuilder.build();
505523
} else {
506524
this.statementCache = null;
525+
this.positionalParametersCache = null;
507526
}
508527
}
509528

510529
@VisibleForTesting
530+
@Nullable
511531
CacheStats getStatementCacheStats() {
512532
return statementCache == null ? null : statementCache.stats();
513533
}
514534

535+
@VisibleForTesting
536+
@Nullable
537+
CacheStats getPositionalParametersCacheStats() {
538+
return positionalParametersCache == null ? null : positionalParametersCache.stats();
539+
}
540+
515541
@VisibleForTesting
516542
Set<ClientSideStatementImpl> getClientSideStatements() {
517543
return statements;
@@ -776,8 +802,21 @@ public static class ParametersInfo {
776802
@InternalApi
777803
public ParametersInfo convertPositionalParametersToNamedParameters(char paramChar, String sql) {
778804
Preconditions.checkNotNull(sql);
805+
if (positionalParametersCache == null) {
806+
return internalConvertPositionalParametersToNamedParameters(paramChar, sql);
807+
}
808+
String cacheKey = paramChar == '?' ? sql : paramChar + "\0" + sql;
809+
ParametersInfo info = positionalParametersCache.getIfPresent(cacheKey);
810+
if (info == null) {
811+
info = internalConvertPositionalParametersToNamedParameters(paramChar, sql);
812+
positionalParametersCache.put(cacheKey, info);
813+
}
814+
return info;
815+
}
816+
817+
ParametersInfo internalConvertPositionalParametersToNamedParameters(char paramChar, String sql) {
779818
final String namedParamPrefix = getQueryParameterPrefix();
780-
StringBuilder named = new StringBuilder(sql.length() + countOccurrencesOf(paramChar, sql));
819+
StringBuilder named = new StringBuilder(sql.length() + 32);
781820
int index = 0;
782821
int paramIndex = 1;
783822
while (index < sql.length()) {

java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/StatementParserTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.Assert.assertEquals;
2121
import static org.junit.Assert.assertFalse;
2222
import static org.junit.Assert.assertNotSame;
23+
import static org.junit.Assert.assertSame;
2324
import static org.junit.Assert.assertThrows;
2425
import static org.junit.Assert.assertTrue;
2526
import static org.junit.Assert.fail;
@@ -29,6 +30,7 @@
2930
import com.google.cloud.spanner.ErrorCode;
3031
import com.google.cloud.spanner.SpannerException;
3132
import com.google.cloud.spanner.Statement;
33+
import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo;
3234
import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
3335
import com.google.cloud.spanner.connection.AbstractStatementParser.StatementType;
3436
import com.google.cloud.spanner.connection.ClientSideStatementImpl.CompileException;
@@ -1864,6 +1866,43 @@ public void testStatementCache_ParameterizedStatement() {
18641866
assertEquals(1, stats.hitCount());
18651867
}
18661868

1869+
@Test
1870+
public void testPositionalParametersCache() {
1871+
CacheStats statsBefore = parser.getPositionalParametersCacheStats();
1872+
1873+
String sql = "select * from foo where id=? and name=? and value=" + UUID.randomUUID();
1874+
ParametersInfo info1 = parser.convertPositionalParametersToNamedParameters('?', sql);
1875+
assertEquals(2, info1.numberOfParameters);
1876+
if (dialect == Dialect.POSTGRESQL) {
1877+
assertTrue(info1.sqlWithNamedParameters.contains("$1"));
1878+
assertTrue(info1.sqlWithNamedParameters.contains("$2"));
1879+
} else {
1880+
assertTrue(info1.sqlWithNamedParameters.contains("@p1"));
1881+
assertTrue(info1.sqlWithNamedParameters.contains("@p2"));
1882+
}
1883+
1884+
ParametersInfo info2 = parser.convertPositionalParametersToNamedParameters('?', sql);
1885+
assertEquals(info1.numberOfParameters, info2.numberOfParameters);
1886+
assertEquals(info1.sqlWithNamedParameters, info2.sqlWithNamedParameters);
1887+
assertSame(info1, info2);
1888+
1889+
// Test with non-'?' parameter character.
1890+
String sqlDollar = "select * from foo where id=$ and name=$ and value=" + UUID.randomUUID();
1891+
ParametersInfo infoDollar1 =
1892+
parser.convertPositionalParametersToNamedParameters('$', sqlDollar);
1893+
assertEquals(2, infoDollar1.numberOfParameters);
1894+
ParametersInfo infoDollar2 =
1895+
parser.convertPositionalParametersToNamedParameters('$', sqlDollar);
1896+
assertSame(infoDollar1, infoDollar2);
1897+
1898+
CacheStats statsAfter = parser.getPositionalParametersCacheStats();
1899+
CacheStats stats = statsAfter.minus(statsBefore);
1900+
1901+
// Two distinct queries had cache misses. Two repeated queries had cache hits.
1902+
assertEquals(2, stats.missCount());
1903+
assertEquals(2, stats.hitCount());
1904+
}
1905+
18671906
@Test
18681907
public void testClientSideStatementWithComment() {
18691908
String sql = "-- Null (no timeout)\n" + "SET STATEMENT_TIMEOUT=null";

0 commit comments

Comments
 (0)