Permalink
Browse files

perf: cache result set column mapping for prepared statements

For server-prepared statements, Map<ColumnName, ColumnPosition> is reused, thus
resultSet.getXXX(String) is faster.

Benchmarks show reduced heap allocation, however the response time seems to be not affected.

closes #614
closes #607
  • Loading branch information...
Chrriis authored and vlsi committed Aug 3, 2016
1 parent ff4bfda commit 88fbbc59132090ad7602377014f7d55fde3aa487
@@ -9,6 +9,8 @@
package org.postgresql.core;
import java.util.Map;
/**
* Abstraction of a generic Query, hiding the details of any protocol-version-specific data needed
* to execute the query efficiently.
@@ -57,4 +59,12 @@
* @return number of times <code>addBatch()</code> has been called.
*/
int getBatchSize();
/**
* Get a map that a result set can use to find the index associated to a name.
*
* @return null if the query implementation does not support this method.
*/
Map<String, Integer> getResultSetColumnNameIndexMap();
}
@@ -18,6 +18,7 @@
import org.postgresql.core.SqlCommandType;
import java.util.List;
import java.util.Map;
/**
* Query implementation for all queries via the V2 protocol.
@@ -67,6 +68,11 @@ public int getBatchSize() {
return 1;
}
@Override
public Map<String, Integer> getResultSetColumnNameIndexMap() {
return null; // unsupported
}
private static final ParameterList NO_PARAMETERS = new SimpleParameterList(0, false);
private final NativeQuery nativeQuery;
@@ -11,6 +11,8 @@
import org.postgresql.core.ParameterList;
import java.util.Map;
/**
* V3 Query implementation for queries that involve multiple statements. We split it up into one
* SimpleQuery per statement, and wrap the corresponding per-statement SimpleParameterList objects
@@ -77,6 +79,11 @@ public int getBatchSize() {
return 0; // no-op, unsupported
}
@Override
public Map<String, Integer> getResultSetColumnNameIndexMap() {
return null; // unsupported
}
private final SimpleQuery[] subqueries;
private final int[] offsets;
}
@@ -10,6 +10,7 @@
package org.postgresql.core.v3;
import org.postgresql.PGNotification;
import org.postgresql.PGProperty;
import org.postgresql.core.Encoding;
import org.postgresql.core.Logger;
import org.postgresql.core.PGStream;
@@ -44,6 +45,7 @@
// default value for server versions that don't report standard_conforming_strings
this.standardConformingStrings = false;
this.cancelSignalTimeout = cancelSignalTimeout;
this.isSanitiserDisabled = PGProperty.DISABLE_COLUMN_SANITISER.getBoolean(info);
}
public HostSpec getHostSpec() {
@@ -246,6 +248,10 @@ public TimeZone getTimeZone() {
return timeZone;
}
boolean isSanitiserDisabled() {
return isSanitiserDisabled;
}
void setApplicationName(String applicationName) {
this.applicationName = applicationName;
}
@@ -287,6 +293,8 @@ public String getApplicationName() {
private final Logger logger;
private final int cancelSignalTimeout;
private boolean isSanitiserDisabled;
/**
* TimeZone of the current connection (TimeZone backend parameter)
*/
@@ -14,8 +14,10 @@
import org.postgresql.core.Oid;
import org.postgresql.core.ParameterList;
import org.postgresql.core.Utils;
import org.postgresql.jdbc.PgResultSet;
import java.lang.ref.PhantomReference;
import java.util.Map;
/**
* V3 Query implementation for a single-statement query. This also holds the state of any associated
@@ -169,6 +171,7 @@ boolean hasUnresolvedTypes() {
*/
void setFields(Field[] fields) {
this.fields = fields;
this.resultSetColumnNameIndexMap = null;
this.cachedMaxResultRowSize = null;
this.needUpdateFieldFormats = fields != null;
this.hasBinaryFields = false; // just in case
@@ -252,6 +255,7 @@ void unprepare() {
statementName = null;
encodedStatementName = null;
fields = null;
this.resultSetColumnNameIndexMap = null;
portalDescribed = false;
statementDescribed = false;
cachedMaxResultRowSize = null;
@@ -269,6 +273,22 @@ public final int getBindCount() {
return nativeQuery.bindPositions.length * getBatchSize();
}
private Map<String, Integer> resultSetColumnNameIndexMap;
@Override
public Map<String, Integer> getResultSetColumnNameIndexMap() {
Map<String, Integer> columnPositions = this.resultSetColumnNameIndexMap;
if (columnPositions == null) {
columnPositions =
PgResultSet.createColumnNameIndexMap(fields, protoConnection.isSanitiserDisabled());
if (statementName != null) {
// Cache column positions for server-prepared statements only
this.resultSetColumnNameIndexMap = columnPositions;
}
}
return columnPositions;
}
private final NativeQuery nativeQuery;
private final ProtocolConnectionImpl protoConnection;
@@ -123,7 +123,7 @@
protected int fetchSize; // Current fetch size (might be 0).
protected ResultCursor cursor; // Cursor for fetching additional data.
private HashMap<String, Integer> columnNameIndexMap; // Speed up findColumn by caching lookups
private Map<String, Integer> columnNameIndexMap; // Speed up findColumn by caching lookups
private ResultSetMetaData rsMetaData;
@@ -2603,19 +2603,30 @@ public int findColumn(String columnName) throws SQLException {
return col;
}
public static Map<String, Integer> createColumnNameIndexMap(Field[] fields,
boolean isSanitiserDisabled) {
Map<String, Integer> columnNameIndexMap = new HashMap<String, Integer>(fields.length * 2);
// The JDBC spec says when you have duplicate columns names,
// the first one should be returned. So load the map in
// reverse order so the first ones will overwrite later ones.
for (int i = fields.length - 1; i >= 0; i--) {
String columnLabel = fields[i].getColumnLabel();
if (isSanitiserDisabled) {
columnNameIndexMap.put(columnLabel, i + 1);
} else {
columnNameIndexMap.put(columnLabel.toLowerCase(Locale.US), i + 1);
}
}
return columnNameIndexMap;
}
private int findColumnIndex(String columnName) {
if (columnNameIndexMap == null) {
columnNameIndexMap = new HashMap<String, Integer>(fields.length * 2);
// The JDBC spec says when you have duplicate columns names,
// the first one should be returned. So load the map in
// reverse order so the first ones will overwrite later ones.
boolean isSanitiserDisabled = connection.isColumnSanitiserDisabled();
for (int i = fields.length - 1; i >= 0; i--) {
if (isSanitiserDisabled) {
columnNameIndexMap.put(fields[i].getColumnLabel(), i + 1);
} else {
columnNameIndexMap.put(fields[i].getColumnLabel().toLowerCase(Locale.US), i + 1);
}
if (originalQuery != null) {
columnNameIndexMap = originalQuery.getResultSetColumnNameIndexMap();
}
if (columnNameIndexMap == null) {
columnNameIndexMap = createColumnNameIndexMap(fields, connection.isColumnSanitiserDisabled());
}
}
@@ -13,11 +13,14 @@
import junit.framework.TestCase;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Locale;
import java.util.Map;
/*
* ResultSet tests.
@@ -716,4 +719,101 @@ public void testUpdateWithPGobject() throws SQLException {
stmt.close();
}
/**
* Test the behavior of the result set column mapping cache for simple statements.
*/
public void testStatementResultSetColumnMappingCache() throws SQLException {
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("select * from testrs");
Map<String, Integer> columnNameIndexMap;
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertEquals(null, columnNameIndexMap);
assertTrue(rs.next());
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertEquals(null, columnNameIndexMap);
rs.getInt("ID");
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertNotNull(columnNameIndexMap);
rs.getInt("id");
assertSame(columnNameIndexMap, getResultSetColumnNameIndexMap(rs));
rs.close();
rs = stmt.executeQuery("select * from testrs");
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertEquals(null, columnNameIndexMap);
assertTrue(rs.next());
rs.getInt("Id");
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertNotNull(columnNameIndexMap);
rs.close();
stmt.close();
}
/**
* Test the behavior of the result set column mapping cache for prepared statements.
*/
public void testPreparedStatementResultSetColumnMappingCache() throws SQLException {
PreparedStatement pstmt = con.prepareStatement("SELECT id FROM testrs");
ResultSet rs = pstmt.executeQuery();
Map<String, Integer> columnNameIndexMap;
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertEquals(null, columnNameIndexMap);
assertTrue(rs.next());
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertEquals(null, columnNameIndexMap);
rs.getInt("id");
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertNotNull(columnNameIndexMap);
rs.close();
rs = pstmt.executeQuery();
assertTrue(rs.next());
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertEquals(null, columnNameIndexMap);
rs.getInt("id");
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertNotNull(columnNameIndexMap);
rs.close();
pstmt.close();
}
/**
* Test the behavior of the result set column mapping cache for prepared statements once the
* statement is named.
*/
public void testNamedPreparedStatementResultSetColumnMappingCache() throws SQLException {
PreparedStatement pstmt = con.prepareStatement("SELECT id FROM testrs");
ResultSet rs;
// Make sure the prepared statement is named.
// This ensures column mapping cache is reused across different result sets.
for (int i = 0; i < 5; i++) {
rs = pstmt.executeQuery();
rs.close();
}
rs = pstmt.executeQuery();
assertTrue(rs.next());
rs.getInt("id");
Map<String, Integer> columnNameIndexMap;
columnNameIndexMap = getResultSetColumnNameIndexMap(rs);
assertNotNull(columnNameIndexMap);
rs.close();
rs = pstmt.executeQuery();
assertTrue(rs.next());
rs.getInt("id");
assertSame(
"Cached mapping should be same between different result sets of same named prepared statement",
columnNameIndexMap, getResultSetColumnNameIndexMap(rs));
rs.close();
pstmt.close();
}
@SuppressWarnings("unchecked")
private Map<String, Integer> getResultSetColumnNameIndexMap(ResultSet stmt) {
try {
Field columnNameIndexMapField = stmt.getClass().getDeclaredField("columnNameIndexMap");
columnNameIndexMapField.setAccessible(true);
return (Map<String, Integer>) columnNameIndexMapField.get(stmt);
} catch (Exception e) {
}
return null;
}
}

0 comments on commit 88fbbc5

Please sign in to comment.