为什么按位置读取JDBC ResultSet的速度比按名称读取的速度快多少?

Rob*_*roj 15 java performance hibernate jdbc hibernate-6.x

Hibernate团队宣布Hibernate 6时宣称,通过在JDBC ResultSet中从按名称读取切换为按位置读取,它们可以获得性能上的好处。

高负载性能测试表明,Hibernate按名称从ResultSet中读取值的方法是其扩展吞吐量的最大限制因素。

这是否意味着他们将呼叫从更改 getString(String columnLabel)getString(int columnIndex)

为什么这样更快?

由于ResultSet是一个接口没有性能增益是否取决于执行它的JDBC驱动程序?

收益有多大?

Mar*_*eel 12

作为JDBC驱动程序维护者(我承认,做了一些概括性的归纳并不一定适用于所有JDBC驱动程序),行值通常将存储在数组或列表中,因为最自然地与从数组中接收数据的方式相匹配。数据库服务器。

结果,按索引检索值将是最简单的。它可能像这样简单(忽略实现JDBC驱动程序的一些更原始的细节):

public Object getObject(int index) throws SQLException {
    checkValidRow();
    checkValidIndex(index);
    return currentRow[index - 1];
}
Run Code Online (Sandbox Code Playgroud)

这差不多快了。

另一方面,按列名查找则需要更多工作。列名需要区分大小写,无论您使用小写还是大写进行归一化,或者使用进行不区分大小写的查找,都将产生额外的开销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;
}
Run Code Online (Sandbox Code Playgroud)

根据数据库的API和可用的语句元数据,可能需要其他处理才能确定查询的实际列标签。根据成本,这可能仅在实际需要时才确定(当按名称访问列标签时,或在检索结果集元数据时)。换句话说,的成本createOrGetIndexMap()可能会很高。

但是,即使该费用可以忽略不计(例如,从数据库服务器准备语句的语句包括列标签),将列标签映射到索引然后按索引检索的开销显然要比直接按索引检索的开销高。

驱动程序甚至可以每次都遍历结果集元数据,并使用标签匹配的第一个。这可能比为具有少量列的结果集构建和访问哈希图便宜,但是成本仍然高于按索引直接访问。

正如我所说的,这是一个笼统的概括,但是如果这种方法(按名称查找索引,然后按索引检索)不是大多数JDBC驱动程序的工作方式,我会感到惊讶,这意味着我希望按索引查找通常会更快。

快速浏览一些驱动程序,情况如下:

  • Firebird(Jaybird,披露:我维护此驱动程序)
  • MySQL(MySQL连接器/ J)
  • PostgreSQL的
  • 甲骨文
  • 数据库
  • SQL Server(用于SQL Server的Microsoft JDBC驱动程序)

我不知道JDBC驱动程序,按列名进行检索的代价是相等的,甚至更便宜。

  • 很好的解释。我想知道是否使用* requested *列名作为映射中的键,而不是使用小写的列名进行前期操作(因此构造了映射,因此每次调用getXxx()方法时都会添加一个条目)对于较大的结果集,它更快:它将避免不得不为结果集的每一行一次又一次地小写相同的键。 (2认同)
  • @JBNizet 可能吧。这就是我们实际上[在 Jaybird 中所做的](https://github.com/FirebirdSQL/jaybird/blob/master/src/main/org/firebirdsql/jdbc/AbstractResultSet.java#L630),但我不想把这里的东西弄乱:) (2认同)

Luk*_*der 12

在制作jOOQ的早期,我考虑过ResultSet通过索引或通过名称访问 JDBC 值的两种选择。我选择通过索引访问事物的原因如下:

\n

关系数据库管理系统支持

\n

并非所有 JDBC 驱动程序实际上都支持按名称访问列。我忘记了哪些没有,如果仍然没有,因为我已经 13 年没有再接触过 JDBC 的 API 的那部分了。但有些人没有,这对我来说已经是一个阻碍。

\n

名称的语义

\n

此外,在支持列名的那些中,列名有不同的语义,主要有两种,即 JDBC 所称的:

\n\n

尽管我认为意图很明确,但上述两者的实现存在很多含糊之处:

\n
    \n
  • 列名应该生成列的名称,而不管别名如何,例如,TITLE如果投影表达式是BOOK.TITLE AS X
  • \n
  • 列标签应该生成列的标签(或别名),或者如果没有可用的别名,则生成名称,例如,X如果投影表达式是BOOK.TITLE AS X
  • \n
\n

因此,名称/标签的含糊性已经非常令人困惑和令人担忧。一般来说,ORM 似乎不应该依赖它,尽管在 Hibernate 的情况下,人们可以说 Hibernate 控制着大多数正在生成的 SQL,至少是为获取实体而生成的 SQL。但是,如果用户编写 HQL 或本机 SQL 查询,我将不愿意依赖名称/标签 - 至少不ResultSetMetaData首先在 中查找内容。

\n

歧义

\n

在 SQL 中,顶层有不明确的列名是完全可以的,例如:

\n
SELECT id, id, not_the_id AS id\nFROM book\n
Run Code Online (Sandbox Code Playgroud)\n

这是完全有效的 SQL。您不能将此查询嵌套为派生表,其中不允许有歧义,但在顶层SELECT可以。现在,您将如何处理ID顶层的那些重复标签?当按名称访问事物时,您无法确定会得到哪一个。前两个可能是相同的,但第三个则非常不同。

\n

明确区分列的唯一方法是通过索引,该索引是唯一的:1, 2, 3

\n

表现

\n

当时我也尝试过表演。我不再有基准测试结果,但快速编写另一个基准测试很容易。在下面的基准测试中,我在 H2 内存实例上运行一个简单的查询,并使用ResultSet访问内容:

\n
    \n
  • 按指数
  • \n
  • 按名字
  • \n
\n

结果是惊人的:

\n
Benchmark                            Mode  Cnt        Score       Error  Units\nJDBCResultSetBenchmark.indexAccess  thrpt    7  1130734.076 \xc2\xb1  9035.404  ops/s\nJDBCResultSetBenchmark.nameAccess   thrpt    7   600540.553 \xc2\xb1 13217.954  ops/s\n
Run Code Online (Sandbox Code Playgroud)\n

尽管基准测试在每次调用时运行整个查询,但通过索引的访问速度几乎是两倍!你可以看一下H2的代码,它是开源的。它执行此操作(版本 2.1.212):

\n
Benchmark                            Mode  Cnt        Score       Error  Units\nJDBCResultSetBenchmark.indexAccess  thrpt    7  1130734.076 \xc2\xb1  9035.404  ops/s\nJDBCResultSetBenchmark.nameAccess   thrpt    7   600540.553 \xc2\xb1 13217.954  ops/s\n
Run Code Online (Sandbox Code Playgroud)\n

所以。有一个带有大写字母的哈希图,并且每次查找也执行大写字母。至少,它在准备好的语句中缓存了映射,所以:

\n
    \n
  • 您可以在每一行上重复使用它
  • \n
  • 您可以在该语句的多次执行中重用它(至少我是这样解释代码的)
  • \n
\n

因此,对于非常大的结果集,它可能不再那么重要,但对于小结果集,它肯定很重要。

\n

ORM 的结论

\n

像 Hibernate 或jOOQ这样的 ORM控制着大量 SQL 和结果集。它确切地知道哪一列在什么位置,这项工作在生成 SQL 查询时就已经完成了。因此,当结果集从数据库服务器返回时,绝对没有理由进一步依赖列名。每个值都将位于预期位置。

\n

在 Hibernate 中使用列名肯定是历史性的事情。这可能也是他们用来生成这些不太可读的列别名的原因,以确保每个别名都是明确的。

\n

无论现实世界(非基准)查询中的实际收益如何,这似乎都是一个明显的改进。即使改进只有 2%,也是值得的,因为它会影响每个基于 Hibernate 的应用程序的每个查询执行。

\n

下面的基准代码,用于复制

\n
private int getColumnIndex(String columnLabel) {\n    checkClosed();\n    if (columnLabel == null) {\n        throw DbException.getInvalidValueException("columnLabel", null);\n    }\n    if (columnCount >= 3) {\n        // use a hash table if more than 2 columns\n        if (columnLabelMap == null) {\n            HashMap<String, Integer> map = new HashMap<>();\n            // [ ... ]\n\n            columnLabelMap = map;\n            if (preparedStatement != null) {\n                preparedStatement.setCachedColumnLabelMap(columnLabelMap);\n            }\n        }\n        Integer index = columnLabelMap.get(StringUtils.toUpperEnglish(columnLabel));\n        if (index == null) {\n            throw DbException.get(ErrorCode.COLUMN_NOT_FOUND_1, columnLabel);\n        }\n        return index + 1;\n    }\n    // [ ... ]\n
Run Code Online (Sandbox Code Playgroud)\n