Skip to content

Commit 284fbfc

Browse files
committed
perf: qualify statements without removing comments
Determine the type of statement without first removing all comments and hints. This prevents the creation of new strings and stepping through the entire SQL string for each statement that is not found in the statement cache. Benchmark Mode Cnt Score Error Units StatementParserBenchmark.isQueryTest thrpt 5 547904.501 ± 1970.170 ops/s StatementParserBenchmark.longDmlTest thrpt 5 114806.782 ± 826.881 ops/s StatementParserBenchmark.longQueryTest thrpt 5 112666.992 ± 700.783 ops/s
1 parent 4cf5261 commit 284fbfc

File tree

9 files changed

+2447
-772
lines changed

9 files changed

+2447
-772
lines changed

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

Lines changed: 88 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,28 @@
2727
import com.google.cloud.spanner.SpannerExceptionFactory;
2828
import com.google.cloud.spanner.Statement;
2929
import com.google.cloud.spanner.connection.AbstractBaseUnitOfWork.InterceptorsUsage;
30+
import com.google.cloud.spanner.connection.SimpleParser.Result;
3031
import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
3132
import com.google.cloud.spanner.connection.UnitOfWork.CallType;
3233
import com.google.common.annotations.VisibleForTesting;
3334
import com.google.common.base.Preconditions;
35+
import com.google.common.base.Suppliers;
3436
import com.google.common.cache.Cache;
3537
import com.google.common.cache.CacheBuilder;
3638
import com.google.common.cache.CacheStats;
3739
import com.google.common.cache.Weigher;
3840
import com.google.common.collect.ImmutableMap;
3941
import com.google.common.collect.ImmutableSet;
4042
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
43+
import java.nio.CharBuffer;
4144
import java.util.Collection;
4245
import java.util.Collections;
4346
import java.util.HashMap;
4447
import java.util.Map;
4548
import java.util.Objects;
4649
import java.util.Set;
4750
import java.util.concurrent.Callable;
51+
import java.util.function.Supplier;
4852
import java.util.logging.Level;
4953
import java.util.logging.Logger;
5054
import javax.annotation.Nullable;
@@ -179,24 +183,24 @@ public static class ParsedStatement {
179183
private final StatementType type;
180184
private final ClientSideStatementImpl clientSideStatement;
181185
private final Statement statement;
182-
private final String sqlWithoutComments;
183-
private final boolean returningClause;
186+
private final Supplier<String> sqlWithoutComments;
187+
private final Supplier<Boolean> returningClause;
184188
private final ReadQueryUpdateTransactionOption[] optionsFromHints;
185189

186190
private static ParsedStatement clientSideStatement(
187191
ClientSideStatementImpl clientSideStatement,
188192
Statement statement,
189-
String sqlWithoutComments) {
193+
Supplier<String> sqlWithoutComments) {
190194
return new ParsedStatement(clientSideStatement, statement, sqlWithoutComments);
191195
}
192196

193-
private static ParsedStatement ddl(Statement statement, String sqlWithoutComments) {
197+
private static ParsedStatement ddl(Statement statement, Supplier<String> sqlWithoutComments) {
194198
return new ParsedStatement(StatementType.DDL, statement, sqlWithoutComments);
195199
}
196200

197201
private static ParsedStatement query(
198202
Statement statement,
199-
String sqlWithoutComments,
203+
Supplier<String> sqlWithoutComments,
200204
QueryOptions defaultQueryOptions,
201205
ReadQueryUpdateTransactionOption[] optionsFromHints) {
202206
return new ParsedStatement(
@@ -205,57 +209,66 @@ private static ParsedStatement query(
205209
statement,
206210
sqlWithoutComments,
207211
defaultQueryOptions,
208-
false,
212+
Suppliers.ofInstance(false),
209213
optionsFromHints);
210214
}
211215

212216
private static ParsedStatement update(
213217
Statement statement,
214-
String sqlWithoutComments,
215-
boolean returningClause,
218+
Supplier<String> sqlWithoutComments,
219+
Supplier<Boolean> returningClause,
216220
ReadQueryUpdateTransactionOption[] optionsFromHints) {
217221
return new ParsedStatement(
218222
StatementType.UPDATE, statement, sqlWithoutComments, returningClause, optionsFromHints);
219223
}
220224

221-
private static ParsedStatement unknown(Statement statement, String sqlWithoutComments) {
225+
private static ParsedStatement unknown(
226+
Statement statement, Supplier<String> sqlWithoutComments) {
222227
return new ParsedStatement(StatementType.UNKNOWN, statement, sqlWithoutComments);
223228
}
224229

225230
private ParsedStatement(
226231
ClientSideStatementImpl clientSideStatement,
227232
Statement statement,
228-
String sqlWithoutComments) {
233+
Supplier<String> sqlWithoutComments) {
229234
Preconditions.checkNotNull(clientSideStatement);
230235
Preconditions.checkNotNull(statement);
231236
this.type = StatementType.CLIENT_SIDE;
232237
this.clientSideStatement = clientSideStatement;
233238
this.statement = statement;
234-
this.sqlWithoutComments = Preconditions.checkNotNull(sqlWithoutComments);
235-
this.returningClause = false;
239+
this.sqlWithoutComments = sqlWithoutComments;
240+
this.returningClause = Suppliers.ofInstance(false);
236241
this.optionsFromHints = EMPTY_OPTIONS;
237242
}
238243

239244
private ParsedStatement(
240245
StatementType type,
241246
Statement statement,
242-
String sqlWithoutComments,
243-
boolean returningClause,
247+
Supplier<String> sqlWithoutComments,
248+
Supplier<Boolean> returningClause,
244249
ReadQueryUpdateTransactionOption[] optionsFromHints) {
245250
this(type, null, statement, sqlWithoutComments, null, returningClause, optionsFromHints);
246251
}
247252

248-
private ParsedStatement(StatementType type, Statement statement, String sqlWithoutComments) {
249-
this(type, null, statement, sqlWithoutComments, null, false, EMPTY_OPTIONS);
253+
private ParsedStatement(
254+
StatementType type, Statement statement, Supplier<String> sqlWithoutComments) {
255+
this(
256+
type,
257+
null,
258+
statement,
259+
sqlWithoutComments,
260+
null,
261+
Suppliers.ofInstance(false),
262+
EMPTY_OPTIONS);
250263
}
251264

252265
private ParsedStatement(
253266
StatementType type,
254267
ClientSideStatementImpl clientSideStatement,
255268
Statement statement,
256-
String sqlWithoutComments,
269+
Supplier<String> sqlWithoutComments,
257270
QueryOptions defaultQueryOptions,
258-
boolean returningClause,
271+
Supplier<Boolean> returningClause,
259272
ReadQueryUpdateTransactionOption[] optionsFromHints) {
260273
Preconditions.checkNotNull(type);
261274
this.type = type;
@@ -315,7 +328,7 @@ public StatementType getType() {
315328
/** @return whether the statement has a returning clause or not. */
316329
@InternalApi
317330
public boolean hasReturningClause() {
318-
return this.returningClause;
331+
return this.returningClause.get();
319332
}
320333

321334
@InternalApi
@@ -413,7 +426,7 @@ Statement mergeQueryOptions(Statement statement, QueryOptions defaultQueryOption
413426
/** @return the SQL statement with all comments removed from the SQL string. */
414427
@InternalApi
415428
public String getSqlWithoutComments() {
416-
return sqlWithoutComments;
429+
return sqlWithoutComments.get();
417430
}
418431

419432
ClientSideStatement getClientSideStatement() {
@@ -464,7 +477,7 @@ private static boolean isRecordStatementCacheStats() {
464477
// We do length*2 because Java uses 2 bytes for each char.
465478
.weigher(
466479
(Weigher<String, ParsedStatement>)
467-
(key, value) -> 2 * key.length() + 2 * value.sqlWithoutComments.length())
480+
(key, value) -> 2 * key.length() + 2 * value.statement.getSql().length())
468481
.concurrencyLevel(Runtime.getRuntime().availableProcessors());
469482
if (isRecordStatementCacheStats()) {
470483
cacheBuilder.recordStats();
@@ -511,28 +524,62 @@ ParsedStatement parse(Statement statement, QueryOptions defaultQueryOptions) {
511524
return parsedStatement.copy(statement, defaultQueryOptions);
512525
}
513526

514-
private ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) {
515-
StatementHintParser statementHintParser =
516-
new StatementHintParser(getDialect(), statement.getSql());
527+
ParsedStatement internalParse(Statement statement, QueryOptions defaultQueryOptions) {
528+
String sql = statement.getSql();
529+
StatementHintParser statementHintParser = new StatementHintParser(getDialect(), sql);
517530
ReadQueryUpdateTransactionOption[] optionsFromHints = EMPTY_OPTIONS;
518531
if (statementHintParser.hasStatementHints()
519532
&& !statementHintParser.getClientSideStatementHints().isEmpty()) {
520533
statement =
521534
statement.toBuilder().replace(statementHintParser.getSqlWithoutClientSideHints()).build();
522535
optionsFromHints = convertHintsToOptions(statementHintParser.getClientSideStatementHints());
523536
}
524-
String sql = removeCommentsAndTrim(statement.getSql());
525-
ClientSideStatementImpl client = parseClientSideStatement(sql);
537+
// Create a supplier that will actually remove all comments and hints from the SQL string to be
538+
// backwards compatible with anything that really needs the SQL string without comments.
539+
Supplier<String> sqlWithoutCommentsSupplier =
540+
Suppliers.memoize(() -> removeCommentsAndTrim(sql));
541+
542+
// Get rid of any spaces/comments at the start of the string.
543+
SimpleParser simpleParser = new SimpleParser(getDialect(), sql);
544+
simpleParser.skipWhitespaces();
545+
// Create a wrapper around the SQL string from the point after the first whitespace.
546+
CharBuffer charBuffer = CharBuffer.wrap(sql, simpleParser.getPos(), sql.length());
547+
ClientSideStatementImpl client = parseClientSideStatement(charBuffer);
548+
526549
if (client != null) {
527-
return ParsedStatement.clientSideStatement(client, statement, sql);
528-
} else if (isQuery(sql)) {
529-
return ParsedStatement.query(statement, sql, defaultQueryOptions, optionsFromHints);
530-
} else if (isUpdateStatement(sql)) {
531-
return ParsedStatement.update(statement, sql, checkReturningClause(sql), optionsFromHints);
532-
} else if (isDdlStatement(sql)) {
533-
return ParsedStatement.ddl(statement, sql);
550+
return ParsedStatement.clientSideStatement(client, statement, sqlWithoutCommentsSupplier);
551+
} else {
552+
// Find the first keyword in the SQL statement.
553+
Result keywordResult = simpleParser.eatNextKeyword();
554+
if (keywordResult.isValid()) {
555+
// Determine the statement type based on the first keyword.
556+
String keyword = keywordResult.getValue().toUpperCase();
557+
if (keywordResult.isInParenthesis()) {
558+
// If the first keyword is inside one or more parentheses, then only a subset of all
559+
// keywords are allowed.
560+
if (SELECT_STATEMENTS_ALLOWING_PRECEDING_BRACKETS.contains(keyword)) {
561+
return ParsedStatement.query(
562+
statement, sqlWithoutCommentsSupplier, defaultQueryOptions, optionsFromHints);
563+
}
564+
} else {
565+
if (selectStatements.contains(keyword)) {
566+
return ParsedStatement.query(
567+
statement, sqlWithoutCommentsSupplier, defaultQueryOptions, optionsFromHints);
568+
} else if (dmlStatements.contains(keyword)) {
569+
return ParsedStatement.update(
570+
statement,
571+
sqlWithoutCommentsSupplier,
572+
// TODO: Make the returning clause check work without removing comments
573+
Suppliers.memoize(() -> checkReturningClause(sqlWithoutCommentsSupplier.get())),
574+
optionsFromHints);
575+
} else if (ddlStatements.contains(keyword)) {
576+
return ParsedStatement.ddl(statement, sqlWithoutCommentsSupplier);
577+
}
578+
}
579+
}
534580
}
535-
return ParsedStatement.unknown(statement, sql);
581+
// Fallthrough: Return an unknown statement.
582+
return ParsedStatement.unknown(statement, sqlWithoutCommentsSupplier);
536583
}
537584

538585
/**
@@ -546,7 +593,7 @@ private ParsedStatement internalParse(Statement statement, QueryOptions defaultQ
546593
* statement.
547594
*/
548595
@VisibleForTesting
549-
ClientSideStatementImpl parseClientSideStatement(String sql) {
596+
ClientSideStatementImpl parseClientSideStatement(CharSequence sql) {
550597
for (ClientSideStatementImpl css : statements) {
551598
if (css.matches(sql)) {
552599
return css;
@@ -563,8 +610,10 @@ ClientSideStatementImpl parseClientSideStatement(String sql) {
563610
* @param sql The statement to check (without any comments).
564611
* @return <code>true</code> if the statement is a DDL statement (i.e. starts with 'CREATE',
565612
* 'ALTER' or 'DROP').
613+
* @deprecated Use {@link #parse(Statement)} instead
566614
*/
567615
@InternalApi
616+
@Deprecated
568617
public boolean isDdlStatement(String sql) {
569618
return statementStartsWith(sql, ddlStatements);
570619
}
@@ -576,8 +625,10 @@ public boolean isDdlStatement(String sql) {
576625
*
577626
* @param sql The statement to check (without any comments).
578627
* @return <code>true</code> if the statement is a SELECT statement (i.e. starts with 'SELECT').
628+
* @deprecated Use {@link #parse(Statement)} instead
579629
*/
580630
@InternalApi
631+
@Deprecated
581632
public boolean isQuery(String sql) {
582633
// Skip any query hints at the beginning of the query.
583634
// We only do this if we actually know that it starts with a hint to prevent unnecessary
@@ -600,8 +651,10 @@ public boolean isQuery(String sql) {
600651
* @param sql The statement to check (without any comments).
601652
* @return <code>true</code> if the statement is a DML update statement (i.e. starts with
602653
* 'INSERT', 'UPDATE' or 'DELETE').
654+
* @deprecated Use {@link #parse(Statement)} instead
603655
*/
604656
@InternalApi
657+
@Deprecated
605658
public boolean isUpdateStatement(String sql) {
606659
// Skip any query hints at the beginning of the query.
607660
if (sql.startsWith("@")) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public ClientSideStatementType getStatementType() {
193193
return statementType;
194194
}
195195

196-
boolean matches(String statement) {
196+
boolean matches(CharSequence statement) {
197197
Preconditions.checkState(pattern != null, "This statement has not been compiled");
198198
return pattern.matcher(statement).matches();
199199
}

0 commit comments

Comments
 (0)