Skip to content

Commit 5642abc

Browse files
committed
perf: cache parsed statement across .prepareStatement calls
This allows to use server-side prepared statements when application uses the same SQL multiple times
1 parent 4797114 commit 5642abc

14 files changed

+832
-275
lines changed

doc/pgjdbc.xml

+33
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,39 @@ openssl pkcs8 -topk8 -in client.key -out client.pk8 -outform DER -v1 PBE-SHA1-3D
737737
</listitem>
738738
</varlistentry>
739739

740+
<varlistentry>
741+
<term><varname>preparedStatementCacheQueries</varname> = <type>int</type></term>
742+
<listitem>
743+
<para>
744+
Determine the number of queries that are cached in each connection.
745+
The default is 256, meaning if you use more than 256 different queries
746+
in <function>prepareStatement()</function> calls, the least recently used ones
747+
will be discarded. The cache allows application to benefit from <xref linkend="server-prepare" />
748+
(see <varname>prepareThreshold</varname>) even if the prepared statement is
749+
closed after each execution. The value of 0 disables the cache.
750+
<note>
751+
<para>
752+
Each connection has its own statement cache.
753+
</para>
754+
</note>
755+
</para>
756+
</listitem>
757+
</varlistentry>
758+
759+
<varlistentry>
760+
<term><varname>preparedStatementCacheSizeMiB</varname> = <type>int</type></term>
761+
<listitem>
762+
<para>
763+
Determine the maximum size (in mebibytes) of the prepared queries cache
764+
(see <varname>preparedStatementCacheQueries</varname>).
765+
The default is 5, meaning if you happen to cache more than 5 MiB of queries
766+
the least recently used ones will be discarded.
767+
The main aim of this setting is to prevent <classname>OutOfMemoryError</classname>.
768+
The value of 0 disables the cache.
769+
</para>
770+
</listitem>
771+
</varlistentry>
772+
740773
<varlistentry>
741774
<term><varname>defaultRowFetchSize</varname> = <type>int</type></term>
742775
<listitem>

org/postgresql/PGProperty.java

+10
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ public enum PGProperty
6161
*/
6262
PREPARE_THRESHOLD("prepareThreshold", "5", "Statement prepare threshold. A value of {@code -1} stands for forceBinary"),
6363

64+
/**
65+
* Specifies the maximum number of entries in cache of prepared statements. A value of {@code 0} disables the cache.
66+
*/
67+
PREPARED_STATEMENT_CACHE_QUERIES("preparedStatementCacheQueries", "256", "Specifies the maximum number of entries in per-connection cache of prepared statements. A value of {@code 0} disables the cache."),
68+
69+
/**
70+
* Specifies the maximum size (in megabytes) of the prepared statement cache. A value of {@code 0} disables the cache.
71+
*/
72+
PREPARED_STATEMENT_CACHE_SIZE_MIB("preparedStatementCacheSizeMiB", "5", "Specifies the maximum size (in megabytes) of a per-connection prepared statement cache. A value of {@code 0} disables the cache."),
73+
6474
/**
6575
* Default parameter for {@link java.sql.Statement#getFetchSize()}. A value of {@code 0} means that need fetch all rows at once
6676
*/

org/postgresql/core/CachedQuery.java

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*-------------------------------------------------------------------------
2+
*
3+
* Copyright (c) 2015, PostgreSQL Global Development Group
4+
*
5+
*
6+
*-------------------------------------------------------------------------
7+
*/
8+
package org.postgresql.core;
9+
10+
import org.postgresql.util.CanEstimateSize;
11+
12+
/**
13+
* Stores information on the parsed JDBC query.
14+
* It is used to cut parsing overhead when executing the same query through {@link java.sql.Connection#prepareStatement(String)}.
15+
*/
16+
public class CachedQuery implements CanEstimateSize {
17+
/**
18+
* Cache key. {@link String} or {@link org.postgresql.jdbc2.CallableQueryKey}
19+
*/
20+
public final Object key;
21+
public final Query query;
22+
public final boolean isFunction;
23+
public final boolean outParmBeforeFunc;
24+
25+
private int executeCount;
26+
27+
public CachedQuery(Object key, Query query, boolean isFunction, boolean outParmBeforeFunc)
28+
{
29+
this.key = key;
30+
this.query = query;
31+
this.isFunction = isFunction;
32+
this.outParmBeforeFunc = outParmBeforeFunc;
33+
}
34+
35+
public void increaseExecuteCount() {
36+
if (executeCount < Integer.MAX_VALUE)
37+
executeCount++;
38+
}
39+
40+
public void increaseExecuteCount(int inc) {
41+
int newValue = executeCount + inc;
42+
if (newValue > 0) // if overflows, just ignore the update
43+
executeCount = newValue;
44+
}
45+
46+
/**
47+
* Number of times this statement has been used
48+
* @return number of times this statement has been used
49+
*/
50+
public int getExecuteCount() {
51+
return executeCount;
52+
}
53+
54+
@Override
55+
public long getSize()
56+
{
57+
int queryLength = String.valueOf(key).length() * 2 /* 2 bytes per char */;
58+
return queryLength * 2 /* original query and native sql */ + 100 /* entry in hash map, CachedQuery wrapper, etc */;
59+
}
60+
}

org/postgresql/core/Parser.java

+258
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
*/
88
package org.postgresql.core;
99

10+
import org.postgresql.util.GT;
11+
import org.postgresql.util.PSQLException;
12+
import org.postgresql.util.PSQLState;
13+
14+
import java.sql.SQLException;
1015
import java.util.ArrayList;
1116
import java.util.Collections;
1217
import java.util.List;
@@ -425,4 +430,257 @@ private static boolean subArraysEqual(final char[] arr,
425430

426431
return true;
427432
}
433+
434+
/**
435+
* Contains parse flags from {@link #modifyJdbcCall(String, boolean, int, int)}.
436+
* Originally {@link #modifyJdbcCall(String, boolean, int, int)} was located in {@link org.postgresql.jdbc2.AbstractJdbc2Statement},
437+
* however it was moved out to avoid parse on each prepareCall.
438+
*/
439+
public static class JdbcCallParseInfo {
440+
private String sql;
441+
private boolean isFunction;
442+
private boolean outParmBeforeFunc;
443+
444+
public String getSql() {
445+
return sql;
446+
}
447+
448+
public boolean isFunction()
449+
{
450+
return isFunction;
451+
}
452+
453+
public boolean isOutParmBeforeFunc()
454+
{
455+
return outParmBeforeFunc;
456+
}
457+
}
458+
459+
/**
460+
* this method will turn a string of the form
461+
* { [? =] call <some_function> [(?, [?,..])] }
462+
* into the PostgreSQL format which is
463+
* select <some_function> (?, [?, ...]) as result
464+
* or select * from <some_function> (?, [?, ...]) as result (7.3)
465+
*/
466+
public static JdbcCallParseInfo modifyJdbcCall(String p_sql, boolean stdStrings, int serverVersion, int protocolVersion) throws SQLException
467+
{
468+
// Mini-parser for JDBC function-call syntax (only)
469+
// TODO: Merge with escape processing (and parameter parsing?)
470+
// so we only parse each query once.
471+
JdbcCallParseInfo info = new JdbcCallParseInfo();
472+
info.sql = p_sql;
473+
info.isFunction = false;
474+
475+
int len = p_sql.length();
476+
int state = 1;
477+
boolean inQuotes = false, inEscape = false;
478+
info.outParmBeforeFunc = false;
479+
int startIndex = -1, endIndex = -1;
480+
boolean syntaxError = false;
481+
int i = 0;
482+
483+
while (i < len && !syntaxError)
484+
{
485+
char ch = p_sql.charAt(i);
486+
487+
switch (state)
488+
{
489+
case 1: // Looking for { at start of query
490+
if (ch == '{')
491+
{
492+
++i;
493+
++state;
494+
} else if (Character.isWhitespace(ch))
495+
{
496+
++i;
497+
} else
498+
{
499+
// Not function-call syntax. Skip the rest of the string.
500+
i = len;
501+
}
502+
break;
503+
504+
case 2: // After {, looking for ? or =, skipping whitespace
505+
if (ch == '?')
506+
{
507+
info.outParmBeforeFunc = info.isFunction = true; // { ? = call ... } -- function with one out parameter
508+
++i;
509+
++state;
510+
} else if (ch == 'c' || ch == 'C')
511+
{ // { call ... } -- proc with no out parameters
512+
state += 3; // Don't increase 'i'
513+
} else if (Character.isWhitespace(ch))
514+
{
515+
++i;
516+
} else
517+
{
518+
// "{ foo ...", doesn't make sense, complain.
519+
syntaxError = true;
520+
}
521+
break;
522+
523+
case 3: // Looking for = after ?, skipping whitespace
524+
if (ch == '=')
525+
{
526+
++i;
527+
++state;
528+
} else if (Character.isWhitespace(ch))
529+
{
530+
++i;
531+
} else
532+
{
533+
syntaxError = true;
534+
}
535+
break;
536+
537+
case 4: // Looking for 'call' after '? =' skipping whitespace
538+
if (ch == 'c' || ch == 'C')
539+
{
540+
++state; // Don't increase 'i'.
541+
} else if (Character.isWhitespace(ch))
542+
{
543+
++i;
544+
} else
545+
{
546+
syntaxError = true;
547+
}
548+
break;
549+
550+
case 5: // Should be at 'call ' either at start of string or after ?=
551+
if ((ch == 'c' || ch == 'C') && i + 4 <= len && p_sql.substring(i, i + 4).equalsIgnoreCase("call"))
552+
{
553+
info.isFunction = true;
554+
i += 4;
555+
++state;
556+
} else if (Character.isWhitespace(ch))
557+
{
558+
++i;
559+
} else
560+
{
561+
syntaxError = true;
562+
}
563+
break;
564+
565+
case 6: // Looking for whitespace char after 'call'
566+
if (Character.isWhitespace(ch))
567+
{
568+
// Ok, we found the start of the real call.
569+
++i;
570+
++state;
571+
startIndex = i;
572+
} else
573+
{
574+
syntaxError = true;
575+
}
576+
break;
577+
578+
case 7: // In "body" of the query (after "{ [? =] call ")
579+
if (ch == '\'')
580+
{
581+
inQuotes = !inQuotes;
582+
++i;
583+
} else if (inQuotes && ch == '\\' && !stdStrings)
584+
{
585+
// Backslash in string constant, skip next character.
586+
i += 2;
587+
} else if (!inQuotes && ch == '{')
588+
{
589+
inEscape = !inEscape;
590+
++i;
591+
} else if (!inQuotes && ch == '}')
592+
{
593+
if (!inEscape)
594+
{
595+
// Should be end of string.
596+
endIndex = i;
597+
++i;
598+
++state;
599+
} else
600+
{
601+
inEscape = false;
602+
}
603+
} else if (!inQuotes && ch == ';')
604+
{
605+
syntaxError = true;
606+
} else
607+
{
608+
// Everything else is ok.
609+
++i;
610+
}
611+
break;
612+
613+
case 8: // At trailing end of query, eating whitespace
614+
if (Character.isWhitespace(ch))
615+
{
616+
++i;
617+
} else
618+
{
619+
syntaxError = true;
620+
}
621+
break;
622+
623+
default:
624+
throw new IllegalStateException("somehow got into bad state " + state);
625+
}
626+
}
627+
628+
// We can only legally end in a couple of states here.
629+
if (i == len && !syntaxError)
630+
{
631+
if (state == 1)
632+
{
633+
return info; // Not an escaped syntax.
634+
}
635+
if (state != 8)
636+
{
637+
syntaxError = true; // Ran out of query while still parsing
638+
}
639+
}
640+
641+
if (syntaxError)
642+
{
643+
throw new PSQLException(GT.tr("Malformed function or procedure escape syntax at offset {0}.", i),
644+
PSQLState.STATEMENT_NOT_ALLOWED_IN_FUNCTION_CALL);
645+
}
646+
647+
if (serverVersion < 80100 /* 8.1 */ || protocolVersion != 3)
648+
{
649+
info.sql = "select " + p_sql.substring(startIndex, endIndex) + " as result";
650+
return info;
651+
} else
652+
{
653+
String s = p_sql.substring(startIndex, endIndex);
654+
StringBuilder sb = new StringBuilder(s);
655+
if (info.outParmBeforeFunc)
656+
{
657+
// move the single out parameter into the function call
658+
// so that it can be treated like all other parameters
659+
boolean needComma = false;
660+
661+
// have to use String.indexOf for java 2
662+
int opening = s.indexOf('(') + 1;
663+
int closing = s.indexOf(')');
664+
for (int j = opening; j < closing; j++)
665+
{
666+
if (!Character.isWhitespace(sb.charAt(j)))
667+
{
668+
needComma = true;
669+
break;
670+
}
671+
}
672+
if (needComma)
673+
{
674+
sb.insert(opening, "?,");
675+
} else
676+
{
677+
sb.insert(opening, "?");
678+
}
679+
680+
}
681+
info.sql = "select * from " + sb.toString() + " as result";
682+
return info;
683+
}
684+
}
685+
428686
}

0 commit comments

Comments
 (0)