高负载性能测试表明,Hibernate从ResultSet按名称读取值的方法是其在扩展吞吐量方面最具限制性的因素。
这是否意味着他们将调用从getString(String columnLabel)
更改为getString(int columnIndex)
?
这样做为什么更快?
由于ResultSet
是一个接口,性能提升是否取决于JDBC驱动程序的实现?
收益有多大?
高负载性能测试表明,Hibernate从ResultSet按名称读取值的方法是其在扩展吞吐量方面最具限制性的因素。
这是否意味着他们将调用从getString(String columnLabel)
更改为getString(int columnIndex)
?
这样做为什么更快?
由于ResultSet
是一个接口,性能提升是否取决于JDBC驱动程序的实现?
收益有多大?
作为JDBC驱动程序的维护者(我承认,我的一些概括性描述可能并不适用于所有的JDBC驱动程序),行值通常会存储在数组或列表中,因为这最自然地匹配了从数据库服务器接收数据的方式。
因此,通过索引检索值将是最简单的。它可能就像这样简单(忽略实现JDBC驱动程序的某些更糟糕的细节):
public Object getObject(int index) throws SQLException {
checkValidRow();
checkValidIndex(index);
return currentRow[index - 1];
}
这已经是最快的速度了。
另一方面,按列名查找更加费力。列名需要进行大小写不敏感处理,无论是使用小写规范化还是大写规范化,或者使用一个 TreeMap
进行不区分大小写的查找,都会增加额外的成本。
一个简单的实现可能如下:
public Object getObject(String columnLabel) throws SQLException {
return getObject(getIndexByLabel(columnLabel));
}
private int getIndexByLabel(String columnLabel) {
Map<String, Integer> indexMap = createOrGetIndexMap();
Integer columnIndex = indexMap.get(columnLabel.toLowerCase());
if (columnIndex == null) {
throw new SQLException("Column label " + columnLabel + " does not exist in the result set");
}
return columnIndex;
}
private Map<String, Integer> createOrGetIndexMap() throws SQLException {
if (this.indexMap != null) {
return this.indexMap;
}
ResultSetMetaData rsmd = getMetaData();
Map<String, Integer> map = new HashMap<>(rsmd.getColumnCount());
// reverse loop to ensure first occurrence of a column label is retained
for (int idx = rsmd.getColumnCount(); idx > 0; idx--) {
String label = rsmd.getColumnLabel(idx).toLowerCase();
map.put(label, idx);
}
return this.indexMap = map;
}
根据数据库API和可用的语句元数据,确定查询的实际列标签可能需要额外的处理。根据代价,这通常只在实际需要时确定(通过名称访问列标签或检索结果集元数据时)。换句话说,createOrGetIndexMap()的成本可能相当高。但是,即使这种成本可以忽略不计(例如来自数据库服务器的语句准备元数据包括列标签),将列标签映射到索引,然后通过索引检索的开销显然比直接通过索引检索要高。驱动程序甚至可以每次循环遍历结果集元数据,并使用第一个标签匹配的元数据;对于具有少量列的结果集,这可能比构建和访问哈希映射更便宜,但成本仍然比直接访问索引高。这是我所说的概括性说法,但如果JDBC驱动程序的大多数都是这样工作的(通过名称查找索引,然后通过索引检索),那么我会感到惊讶,这意味着我认为按索引查找通常会更快。经过快速查看多个驱动程序之后,以下驱动程序符合此情况:
Firebird(Jaybird,披露:我维护此驱动程序)
MySQL(MySQL Connector/J)
PostgreSQL
Oracle
HSQLDB
SQL Server(Microsoft JDBC Driver for SQL Server)
我不知道有哪些JDBC驱动程序可以通过列名检索等效或者成本更低。ResultSet
值的两种选项。出于以下原因,我选择了通过索引访问内容:
并非所有 JDBC 驱动程序都支持通过名称访问列。我忘记了哪些不支持,以及它们是否仍然不支持,因为在 13 年中我从未再次接触过 JDBC 的 API 的这一部分。但是有些驱动程序不支持,这对我来说已经是一个停机点。
此外,在那些支持列名称的驱动程序中,存在不同的列名称语义,主要有两种,JDBC 称之为:
ResultSetMetaData::getColumnName
中所示的列名称ResultSetMetaData::getColumnLabel
中所示的列标签关于上述两个的实现存在很多歧义,尽管我认为意图非常清晰:
BOOK.TITLE AS X
,则应该产生 TITLE
BOOK.TITLE AS X
,则应该生成 X
因此,名称/标签是什么的不确定性已经非常令人困惑和担忧。这似乎并不是 ORM 在一般情况下应该依赖的东西,尽管在 Hibernate 的情况下,可以认为 Hibernate 控制着生成的大部分 SQL,至少是用于获取实体的 SQL。但是,如果用户编写 HQL 或本地 SQL 查询,我将不愿意依赖名称/标签-至少不在未首先在 ResultSetMetaData
中查找信息的情况下。
在 SQL 中,顶层可以有模糊的列名称,例如:
SELECT id, id, not_the_id AS id
FROM book
Benchmark Mode Cnt Score Error Units
JDBCResultSetBenchmark.indexAccess thrpt 7 1130734.076 ± 9035.404 ops/s
JDBCResultSetBenchmark.nameAccess thrpt 7 600540.553 ± 13217.954 ops/s
尽管基准测试每次调用都会运行整个查询,但通过索引访问的速度几乎是快了一倍!您可以查看H2的代码,它是开源的。它采用了如下方式(版本2.1.212):
private int getColumnIndex(String columnLabel) {
checkClosed();
if (columnLabel == null) {
throw DbException.getInvalidValueException("columnLabel", null);
}
if (columnCount >= 3) {
// use a hash table if more than 2 columns
if (columnLabelMap == null) {
HashMap<String, Integer> map = new HashMap<>();
// [ ... ]
columnLabelMap = map;
if (preparedStatement != null) {
preparedStatement.setCachedColumnLabelMap(columnLabelMap);
}
}
Integer index = columnLabelMap.get(StringUtils.toUpperEnglish(columnLabel));
if (index == null) {
throw DbException.get(ErrorCode.COLUMN_NOT_FOUND_1, columnLabel);
}
return index + 1;
}
// [ ... ]
package org.jooq.test.benchmarks.local;
import java.io.*;
import java.sql.*;
import java.util.Properties;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.*;
@Fork(value = 1)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 7, time = 3)
public class JDBCResultSetBenchmark {
@State(Scope.Benchmark)
public static class BenchmarkState {
Connection connection;
@Setup(Level.Trial)
public void setup() throws Exception {
try (InputStream is = BenchmarkState.class.getResourceAsStream("/config.properties")) {
Properties p = new Properties();
p.load(is);
connection = DriverManager.getConnection(
p.getProperty("db.url"),
p.getProperty("db.username"),
p.getProperty("db.password")
);
}
}
@TearDown(Level.Trial)
public void teardown() throws Exception {
connection.close();
}
}
@FunctionalInterface
interface ThrowingConsumer<T> {
void accept(T t) throws SQLException;
}
private void run(BenchmarkState state, ThrowingConsumer<ResultSet> c) throws SQLException {
try (Statement s = state.connection.createStatement();
ResultSet rs = s.executeQuery("select c as c1, c as c2, c as c3, c as c4 from system_range(1, 10) as t(c);")) {
c.accept(rs);
}
}
@Benchmark
public void indexAccess(Blackhole blackhole, BenchmarkState state) throws SQLException {
run(state, rs -> {
while (rs.next()) {
blackhole.consume(rs.getInt(1));
blackhole.consume(rs.getInt(2));
blackhole.consume(rs.getInt(3));
blackhole.consume(rs.getInt(4));
}
});
}
@Benchmark
public void nameAccess(Blackhole blackhole, BenchmarkState state) throws SQLException {
run(state, rs -> {
while (rs.next()) {
blackhole.consume(rs.getInt("C1"));
blackhole.consume(rs.getInt("C2"));
blackhole.consume(rs.getInt("C3"));
blackhole.consume(rs.getInt("C4"));
}
});
}
}
getXXX(String)
应该通过列标签而不是列名来检索。不幸的是,驱动程序中的向后兼容性使错误的行为仍然存在。 - Mark RotteveelSELECT *
查询的JOIN顺序,或者用USING
替换ON
。突然之间,一个之前运行良好的查询会因为ID
列出现的顺序改变而改变行为。 - Lukas Eder