Skip to content

Commit

Permalink
perf: cache parsed statement across .prepareStatement calls
Browse files Browse the repository at this point in the history
This allows to use server-side prepared statements when application uses the same SQL multiple times
  • Loading branch information
vlsi committed Jun 15, 2015
1 parent 4797114 commit 5642abc
Show file tree
Hide file tree
Showing 14 changed files with 832 additions and 275 deletions.
33 changes: 33 additions & 0 deletions doc/pgjdbc.xml
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,39 @@ openssl pkcs8 -topk8 -in client.key -out client.pk8 -outform DER -v1 PBE-SHA1-3D
</listitem>
</varlistentry>

<varlistentry>
<term><varname>preparedStatementCacheQueries</varname> = <type>int</type></term>
<listitem>
<para>
Determine the number of queries that are cached in each connection.
The default is 256, meaning if you use more than 256 different queries
in <function>prepareStatement()</function> calls, the least recently used ones
will be discarded. The cache allows application to benefit from <xref linkend="server-prepare" />
(see <varname>prepareThreshold</varname>) even if the prepared statement is
closed after each execution. The value of 0 disables the cache.
<note>
<para>
Each connection has its own statement cache.
</para>
</note>
</para>
</listitem>
</varlistentry>

<varlistentry>
<term><varname>preparedStatementCacheSizeMiB</varname> = <type>int</type></term>
<listitem>
<para>
Determine the maximum size (in mebibytes) of the prepared queries cache
(see <varname>preparedStatementCacheQueries</varname>).
The default is 5, meaning if you happen to cache more than 5 MiB of queries
the least recently used ones will be discarded.
The main aim of this setting is to prevent <classname>OutOfMemoryError</classname>.
The value of 0 disables the cache.
</para>
</listitem>
</varlistentry>

<varlistentry>
<term><varname>defaultRowFetchSize</varname> = <type>int</type></term>
<listitem>
Expand Down
10 changes: 10 additions & 0 deletions org/postgresql/PGProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ public enum PGProperty
*/
PREPARE_THRESHOLD("prepareThreshold", "5", "Statement prepare threshold. A value of {@code -1} stands for forceBinary"),

/**
* Specifies the maximum number of entries in cache of prepared statements. A value of {@code 0} disables the cache.
*/
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."),

/**
* Specifies the maximum size (in megabytes) of the prepared statement cache. A value of {@code 0} disables the cache.
*/
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."),

/**
* Default parameter for {@link java.sql.Statement#getFetchSize()}. A value of {@code 0} means that need fetch all rows at once
*/
Expand Down
60 changes: 60 additions & 0 deletions org/postgresql/core/CachedQuery.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*-------------------------------------------------------------------------
*
* Copyright (c) 2015, PostgreSQL Global Development Group
*
*
*-------------------------------------------------------------------------
*/
package org.postgresql.core;

import org.postgresql.util.CanEstimateSize;

/**
* Stores information on the parsed JDBC query.
* It is used to cut parsing overhead when executing the same query through {@link java.sql.Connection#prepareStatement(String)}.
*/
public class CachedQuery implements CanEstimateSize {
/**
* Cache key. {@link String} or {@link org.postgresql.jdbc2.CallableQueryKey}
*/
public final Object key;
public final Query query;
public final boolean isFunction;
public final boolean outParmBeforeFunc;

private int executeCount;

public CachedQuery(Object key, Query query, boolean isFunction, boolean outParmBeforeFunc)
{
this.key = key;
this.query = query;
this.isFunction = isFunction;
this.outParmBeforeFunc = outParmBeforeFunc;
}

public void increaseExecuteCount() {
if (executeCount < Integer.MAX_VALUE)
executeCount++;
}

public void increaseExecuteCount(int inc) {
int newValue = executeCount + inc;
if (newValue > 0) // if overflows, just ignore the update
executeCount = newValue;
}

/**
* Number of times this statement has been used
* @return number of times this statement has been used
*/
public int getExecuteCount() {
return executeCount;
}

@Override
public long getSize()
{
int queryLength = String.valueOf(key).length() * 2 /* 2 bytes per char */;
return queryLength * 2 /* original query and native sql */ + 100 /* entry in hash map, CachedQuery wrapper, etc */;
}
}
258 changes: 258 additions & 0 deletions org/postgresql/core/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
*/
package org.postgresql.core;

import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -425,4 +430,257 @@ private static boolean subArraysEqual(final char[] arr,

return true;
}

/**
* Contains parse flags from {@link #modifyJdbcCall(String, boolean, int, int)}.
* Originally {@link #modifyJdbcCall(String, boolean, int, int)} was located in {@link org.postgresql.jdbc2.AbstractJdbc2Statement},
* however it was moved out to avoid parse on each prepareCall.
*/
public static class JdbcCallParseInfo {
private String sql;
private boolean isFunction;
private boolean outParmBeforeFunc;

public String getSql() {
return sql;
}

public boolean isFunction()
{
return isFunction;
}

public boolean isOutParmBeforeFunc()
{
return outParmBeforeFunc;
}
}

/**
* this method will turn a string of the form
* { [? =] call <some_function> [(?, [?,..])] }
* into the PostgreSQL format which is
* select <some_function> (?, [?, ...]) as result
* or select * from <some_function> (?, [?, ...]) as result (7.3)
*/
public static JdbcCallParseInfo modifyJdbcCall(String p_sql, boolean stdStrings, int serverVersion, int protocolVersion) throws SQLException
{
// Mini-parser for JDBC function-call syntax (only)
// TODO: Merge with escape processing (and parameter parsing?)
// so we only parse each query once.
JdbcCallParseInfo info = new JdbcCallParseInfo();
info.sql = p_sql;
info.isFunction = false;

int len = p_sql.length();
int state = 1;
boolean inQuotes = false, inEscape = false;
info.outParmBeforeFunc = false;
int startIndex = -1, endIndex = -1;
boolean syntaxError = false;
int i = 0;

while (i < len && !syntaxError)
{
char ch = p_sql.charAt(i);

switch (state)
{
case 1: // Looking for { at start of query
if (ch == '{')
{
++i;
++state;
} else if (Character.isWhitespace(ch))
{
++i;
} else
{
// Not function-call syntax. Skip the rest of the string.
i = len;
}
break;

case 2: // After {, looking for ? or =, skipping whitespace
if (ch == '?')
{
info.outParmBeforeFunc = info.isFunction = true; // { ? = call ... } -- function with one out parameter
++i;
++state;
} else if (ch == 'c' || ch == 'C')
{ // { call ... } -- proc with no out parameters
state += 3; // Don't increase 'i'
} else if (Character.isWhitespace(ch))
{
++i;
} else
{
// "{ foo ...", doesn't make sense, complain.
syntaxError = true;
}
break;

case 3: // Looking for = after ?, skipping whitespace
if (ch == '=')
{
++i;
++state;
} else if (Character.isWhitespace(ch))
{
++i;
} else
{
syntaxError = true;
}
break;

case 4: // Looking for 'call' after '? =' skipping whitespace
if (ch == 'c' || ch == 'C')
{
++state; // Don't increase 'i'.
} else if (Character.isWhitespace(ch))
{
++i;
} else
{
syntaxError = true;
}
break;

case 5: // Should be at 'call ' either at start of string or after ?=
if ((ch == 'c' || ch == 'C') && i + 4 <= len && p_sql.substring(i, i + 4).equalsIgnoreCase("call"))
{
info.isFunction = true;
i += 4;
++state;
} else if (Character.isWhitespace(ch))
{
++i;
} else
{
syntaxError = true;
}
break;

case 6: // Looking for whitespace char after 'call'
if (Character.isWhitespace(ch))
{
// Ok, we found the start of the real call.
++i;
++state;
startIndex = i;
} else
{
syntaxError = true;
}
break;

case 7: // In "body" of the query (after "{ [? =] call ")
if (ch == '\'')
{
inQuotes = !inQuotes;
++i;
} else if (inQuotes && ch == '\\' && !stdStrings)
{
// Backslash in string constant, skip next character.
i += 2;
} else if (!inQuotes && ch == '{')
{
inEscape = !inEscape;
++i;
} else if (!inQuotes && ch == '}')
{
if (!inEscape)
{
// Should be end of string.
endIndex = i;
++i;
++state;
} else
{
inEscape = false;
}
} else if (!inQuotes && ch == ';')
{
syntaxError = true;
} else
{
// Everything else is ok.
++i;
}
break;

case 8: // At trailing end of query, eating whitespace
if (Character.isWhitespace(ch))
{
++i;
} else
{
syntaxError = true;
}
break;

default:
throw new IllegalStateException("somehow got into bad state " + state);
}
}

// We can only legally end in a couple of states here.
if (i == len && !syntaxError)
{
if (state == 1)
{
return info; // Not an escaped syntax.
}
if (state != 8)
{
syntaxError = true; // Ran out of query while still parsing
}
}

if (syntaxError)
{
throw new PSQLException(GT.tr("Malformed function or procedure escape syntax at offset {0}.", i),
PSQLState.STATEMENT_NOT_ALLOWED_IN_FUNCTION_CALL);
}

if (serverVersion < 80100 /* 8.1 */ || protocolVersion != 3)
{
info.sql = "select " + p_sql.substring(startIndex, endIndex) + " as result";
return info;
} else
{
String s = p_sql.substring(startIndex, endIndex);
StringBuilder sb = new StringBuilder(s);
if (info.outParmBeforeFunc)
{
// move the single out parameter into the function call
// so that it can be treated like all other parameters
boolean needComma = false;

// have to use String.indexOf for java 2
int opening = s.indexOf('(') + 1;
int closing = s.indexOf(')');
for (int j = opening; j < closing; j++)
{
if (!Character.isWhitespace(sb.charAt(j)))
{
needComma = true;
break;
}
}
if (needComma)
{
sb.insert(opening, "?,");
} else
{
sb.insert(opening, "?");
}

}
info.sql = "select * from " + sb.toString() + " as result";
return info;
}
}

}
Loading

0 comments on commit 5642abc

Please sign in to comment.