为单行查询设置JDBC Statement.setFetchSize()为1的方法指南

Vladimir Sitnikov的一个有趣提示让我想到了jOOQ的一个新基准:

有趣的想法,你是否已经准备好了一个基准,也许?

- Lukas Eder (@lukaseder)June 23, 2021

基准应该检查单行查询是否应该有一个JDBC [Statement.setFetchSize(1)](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Statement.html#setFetchSize(int))调用,该方法的Javadoc说:

给JDBC驱动一个提示,当需要更多的行时,应该从数据库中获取ResultSet 对象产生的行数,Statement 。如果指定的值是0,那么这个提示就会被忽略。默认值是零。

如果一个ORM(例如jOOQ)知道 它将只获取1行,或者它知道 只能有1行,那么这个提示当然是有意义的。jOOQ中的例子包括:

  • 当用户调用 [ResultQuery.fetchSingle()](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ResultQuery.html#fetchSingle()), 或 [fetchOne()](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ResultQuery.html#fetchOne()),或 [fetchOptional()](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/ResultQuery.html#fetchOptional()),或任何类似的方法,那么期望只返回0-1行是合理的。在这些方法返回超过1行的情况下,会抛出一个异常,所以即使有更多的行,最多只能获取2行。
  • 当用户在顶层查询中添加LIMIT 1 子句时,永远不可能有超过1行。
  • 当查询是琐碎的(没有连接,或者只有对一连接,没有GROUP BY GROUPING SETS ,没有UNION ,等等),并且在UNIQUE 约束上有一个平等的谓词,也不能有超过1行。

数据库优化器也知道所有这些事情。如果你在一个查询中添加了LIMIT 1 ,那么可以合理地期望优化器将其作为一个关于结果集大小的强烈提示。但是JDBC驱动不知道这些东西(或者至少不应该期望它知道),因为它不可能解析SQL并计算其统计数据,或者考虑这种优化的元数据。

所以,用户可以暗示一下。因为这对用户来说是非常繁琐的,甚至更好,ORM(例如jOOQ)应该提示。或者看起来是这样。

基准测试

但它应该吗?真的值得这么麻烦吗?下面是Vladimir对pgjdbc驱动的评估,他不指望现在有什么改进,但也许将来会有:

以防万一,pgjdbc中的单元格是 "收到时 "创建的,所以setFetchSize()不会导致所有50'000个byte[]数组的实例化。

- Vladimir Sitnikov (@VladimirSitnikv)June 23, 2021

比起做假设,让我们用一个JMH基准来测量。JMH通常用于对JVM上的东西进行微观基准测试,以测试关于JIT运行时行为的假设。这显然不是一个微观基准测试,但我仍然喜欢JMH的方法和输出,它包括标准偏差和误差,以及忽略预热惩罚等。

首先是结果

由于一些商业RDBMS的基准测试结果不能公布(至少在RDBMS之间进行比较时不能公布),我对结果进行了规范化处理,因此在RDBMS之间比较实际执行速度是不可能的。也就是说,对于每个RDBMS来说,执行速度较快的是1,较慢的是1的某个分数。这样,RDBMS只和自己做比较,这是公平的。

结果如下:我们正在测量吞吐量,所以越低越好:

Db2
---
Benchmark                            Mode   Score 
JDBCFetchSizeBenchmark.fetchSize1   thrpt   0.677
JDBCFetchSizeBenchmark.noFetchSize  thrpt   1.000

MySQL
-----
Benchmark                            Mode   Score 
JDBCFetchSizeBenchmark.fetchSize1   thrpt   0.985
JDBCFetchSizeBenchmark.noFetchSize  thrpt   1.000

Oracle
------
Benchmark                            Mode   Score 
JDBCFetchSizeBenchmark.fetchSize1   thrpt   0.485
JDBCFetchSizeBenchmark.noFetchSize  thrpt   1.000

PostgreSQL
----------
Benchmark                            Mode   Score 
JDBCFetchSizeBenchmark.fetchSize1   thrpt   1.000
JDBCFetchSizeBenchmark.noFetchSize  thrpt   0.998

SQL Server
----------
Benchmark                            Mode   Score 
JDBCFetchSizeBenchmark.fetchSize1   thrpt   0.972
JDBCFetchSizeBenchmark.noFetchSize  thrpt   1.000

对于每个RDBMS,我都运行了一个微不足道的查询,产生一个带有1列的单行。每次,我都重新创建了一个JDBCStatement ,并获取了ResultSet 。在fetchSize1 ,我指定了获取大小提示。在noFetchSize ,我没有动用默认值。正如可以总结的那样。

在这些RDBMS中,没有任何影响

  • MySQL
  • PostgreSQL
  • SQL Server

在这些RDBMS中,情况明显变差 (而不是变好!)

  • Db2
  • Oracle

这是相当令人惊讶的,因为该基准包括在服务器上运行整个语句,所以我本来以为最多是一个可以忽略不计的结果。

在这个基准测试中,我使用了这些服务器和JDBC驱动程序的版本

  • Db2 11.5.6.0与jcc-11.5.6.0
  • MySQL 8.0.29,带mysql-connector-java-8.0.28
  • Oracle 21c 与 ojdbc11-21.5.0.0 一起使用
  • 带有postgresql-42.3.3的PostgreSQL 14.1
  • SQL Server 2019,带mssql-jdbc-10.2.0

基准逻辑在这里:

package org.jooq.test.benchmarks.local;

import java.sql.*;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

@Fork(value = 1)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 7, time = 3)
public class JDBCFetchSizeBenchmark {

    @State(Scope.Benchmark)
    public static class BenchmarkState {

        Connection connection;

        @Setup(Level.Trial)
        public void setup() throws Exception {
            Class.forName("org.postgresql.Driver");
            connection = DriverManager.getConnection(
                "jdbc:postgresql://localhost:5432/postgres",
                "postgres",
                "test"
            );
        }

        @TearDown(Level.Trial)
        public void teardown() throws Exception {
            connection.close();
        }
    }

    @FunctionalInterface
    interface ThrowingConsumer<T> {
        void accept(T t) throws SQLException;
    }

    private void run(
        Blackhole blackhole,
        BenchmarkState state,
        ThrowingConsumer<Statement> c
    ) throws SQLException {
        try (Statement s = state.connection.createStatement()) {
            c.accept(s);

            try (ResultSet rs = s.executeQuery(
                "select title from t_book where id = 1")
            ) {
                while (rs.next())
                    blackhole.consume(rs.getString(1));
            }
        }
    }

    @Benchmark
    public void fetchSize1(Blackhole blackhole, BenchmarkState state)
    throws SQLException {
        run(blackhole, state, s -> s.setFetchSize(1));
    }

    @Benchmark
    public void noFetchSize(Blackhole blackhole, BenchmarkState state)
    throws SQLException {
        run(blackhole, state, s -> {});
    }
}

几点意见:

  • 该查询决不是生产工作负载的代表。但是,如果事情确实通过fetchSize 标志得到了改善,那么这种改善应该已经体现出来了
  • 该基准没有使用准备好的语句,这可能会消除一些副作用,或者增加一些副作用。请随意使用准备好的语句来重复该基准。
  • 目前还不明白为什么有些事情在某些驱动中不重要,或者为什么在其他驱动中重要。对于结论来说,“为什么 "并不太重要,因为这篇博文的结果不会有任何改变。如果你知道原因(很遗憾,db2驱动和ojdbc代码没有开放源代码),我会很好奇。

总结

优化是一种棘手的野兽。有些事情在推理时似乎很有意义,但在实际测量中,看似更优化的事情实际上更糟糕,或者不相关。

在这个案例中,一开始,我们似乎应该提示JDBC驱动我们只获取1行的意图。我不知道为什么JDBC驱动程序的表现比我不提示它的时候更差。也许它分配的缓冲区太小,不得不增加,而不是分配的缓冲区太大,但足够大。

我过去也做过类似的基准测试,试图 "优化 "ArrayListStringBuilder 的初始大小。我几乎无法持续地超越默认值。有时,"改进 "似乎确实改善了情况。有时,它又使事情变得更糟。

由于没有明显的胜利(可以理解,也不要盲目相信基准结果,即使你赢了!),我对这些改进失去了信心,最后也没有实施。这里的情况也是如此。我没能实现改进,但在2/5的案例中,情况明显变差了。

后续报道

在/r/java上,曾有过关于这篇文章的讨论,它建议进行2个额外的检查。

1.尝试使用2的fetchSize

你可能会认为其他的取数大小仍然是合适的,例如2 ,以防止潜在的缓冲区大小的增加。我刚刚试了一下,只用了Oracle,产生了:

JDBCFetchSizeBenchmark.fetchSize1   thrpt  0.513
JDBCFetchSizeBenchmark.fetchSize2   thrpt  0.968
JDBCFetchSizeBenchmark.noFetchSize  thrpt  1.000

虽然将fetchSize 设置为1 的惩罚消失了,但与默认值相比,又没有改进。

2.2.尝试使用PreparedStatements

在我看来,PreparedStatement 的使用对于这个特定的基准来说不应该是重要的,这就是为什么我最初把它们排除在外。在reddit的讨论中,有人急于把所有的钱放在单PreparedStatement 卡上,所以这里有一个更新的结果,还是只用Oracle,比较静态语句和准备语句(下面是更新的基准代码):

Benchmark                                    Mode     Score
JDBCFetchSizeBenchmark.fetchSizePrepared1    thrpt    0.503
JDBCFetchSizeBenchmark.fetchSizeStatic1      thrpt    0.518
JDBCFetchSizeBenchmark.fetchSizePrepared2    thrpt    0.939
JDBCFetchSizeBenchmark.fetchSizeStatic2      thrpt    0.994
JDBCFetchSizeBenchmark.noFetchSizePrepared   thrpt    1.000
JDBCFetchSizeBenchmark.noFetchSizeStatic     thrpt    0.998

两者的结果是一样的。不仅如此,可以看到在我的特定设置中(在本地docker中查询Oracle XE 21c),在这种情况下,使用静态语句和准备好的语句完全没有区别。

研究一下为什么会这样,也是很有意思的,假设可能包括,例如:

  • ojdbc在准备好的语句缓存中也缓存了静态语句
  • 在只运行一条语句的基准中,缓存准备好的语句的效果可以忽略不计,这远远不能代表生产工作负载
  • 与服务器端游标缓存的好处相比,准备好的语句在客户端的影响是无关紧要的,或者与将fetchSize 的不利影响相比也是如此。1

更新后的基准代码:

package org.jooq.test.benchmarks.local;

import java.sql.*;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

@Fork(value = 1)
@Warmup(iterations = 3, time = 3)
@Measurement(iterations = 7, time = 3)
public class JDBCFetchSizeBenchmark {

    @State(Scope.Benchmark)
    public static class BenchmarkState {

        Connection connection;

        @Setup(Level.Trial)
        public void setup() throws Exception {
            Class.forName("oracle.jdbc.OracleDriver");
            connection = DriverManager.getConnection(
                "jdbc:oracle:thin:@localhost:1521/XEPDB1",
                "TEST",
                "TEST"
            );
        }

        @TearDown(Level.Trial)
        public void teardown() throws Exception {
            connection.close();
        }
    }

    @FunctionalInterface
    interface ThrowingConsumer<T> {
        void accept(T t) throws SQLException;
    }

    private void runPrepared(
        Blackhole blackhole,
        BenchmarkState state,
        ThrowingConsumer<Statement> c
    ) throws SQLException {
        try (PreparedStatement s = state.connection.prepareStatement(
            "select title from t_book where id = 1")
        ) {
            c.accept(s);

            try (ResultSet rs = s.executeQuery()) {
                while (rs.next())
                    blackhole.consume(rs.getString(1));
            }
        }
    }

    private void runStatic(
        Blackhole blackhole,
        BenchmarkState state,
        ThrowingConsumer<Statement> c
    ) throws SQLException {
        try (Statement s = state.connection.createStatement()) {
            c.accept(s);

            try (ResultSet rs = s.executeQuery(
                "select title from t_book where id = 1")
            ) {
                while (rs.next())
                    blackhole.consume(rs.getString(1));
            }
        }
    }

    @Benchmark
    public void fetchSizeStatic1(Blackhole blackhole, BenchmarkState state)
    throws SQLException {
        runStatic(blackhole, state, s -> s.setFetchSize(1));
    }

    @Benchmark
    public void fetchSizeStatic2(Blackhole blackhole, BenchmarkState state)
    throws SQLException {
        runStatic(blackhole, state, s -> s.setFetchSize(2));
    }

    @Benchmark
    public void noFetchSizeStatic(Blackhole blackhole, BenchmarkState state)
    throws SQLException {
        runStatic(blackhole, state, s -> {});
    }

    @Benchmark
    public void fetchSizePrepared1(Blackhole blackhole, BenchmarkState state)
    throws SQLException {
        runPrepared(blackhole, state, s -> s.setFetchSize(1));
    }

    @Benchmark
    public void fetchSizePrepared2(Blackhole blackhole, BenchmarkState state)
    throws SQLException {
        runPrepared(blackhole, state, s -> s.setFetchSize(2));
    }

    @Benchmark
    public void noFetchSizePrepared(Blackhole blackhole, BenchmarkState state)
    throws SQLException {
        runPrepared(blackhole, state, s -> {});
    }
}

猜你喜欢

转载自juejin.im/post/7126040511835537421
今日推荐