我遇到了一个风趣的小技巧,在测试一些SQL查询时,在你的开发环境中模仿推迟。或许的用途包括验证后端推迟不会导致你的前端瘫痪,或许你的用户体验依然能够接受,等等:

#PostgreSQL的pg_sleep()函数关于模仿慢速查询和评价其对服务的影响十分实用。

把它包装成一个回来常量值的假函数,你能够简略地把它增加到你的JPQL/HQL的WHERE子句中。#Hibernatehttps://t.co/4mmkl8rggQ pic.twitter.com/u4qFuGAlaN

– Gunnar Morling (@gunnarmorling)2021年2月14日

这个处理方案是针对PostgreSQL和Hibernate的,虽然纷歧定要这样。此外,它运用了一个存储函数来处理PostgreSQL中的VOID 函数的约束,但这也能够用不同的办法来处理,不需求存储任何辅助的目录。

为了消除对Hibernate的依靠,你能够直接运用NULL 谓词来运用pg_sleep 函数,但不要这样测验

select 1
from t_book
-- Don't do this!
where pg_sleep(1) is not null;

这将使每行睡觉1秒(!)。从解说计划中能够看出。让咱们约束在3行来看看:

explain analyze
select 1
from t_book
where pg_sleep(1) is not null
limit 3;

而成果是:

Limit  (cost=0.00..1.54 rows=3 width=4) (actual time=1002.142..3005.374 rows=3 loops=1)
   ->  Seq Scan on t_book  (cost=0.00..2.05 rows=4 width=4) (actual time=1002.140..3005.366 rows=3 loops=1)

正如你所看到的,整个查询关于3行花费了大约3秒。事实上,这也是Gunnar在推特上的例子中产生的情况,仅仅他是经过ID过滤的,这 “有助于 “隐藏这种影响。

咱们能够运用Oracle所说的标量子查询缓存,事实上标量子查询能够合理地预期没有副作用(虽然pg_sleep ),这意味着一些RDBMS会在每次查询执行时缓存其成果:

explain analyze
select 1
from t_book
where (select pg_sleep(1)) is not null
limit 3;

现在的成果是:

Limit  (cost=0.01..1.54 rows=3 width=4) (actual time=1001.177..1001.178 rows=3 loops=1)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.01 rows=1 width=4) (actual time=1001.148..1001.148 rows=1 loops=1)
   ->  Result  (cost=0.00..2.04 rows=4 width=4) (actual time=1001.175..1001.176 rows=3 loops=1) 

咱们现在得到了想要的一次性过滤器。但是,我不太喜欢这个黑客,因为它依靠于一个优化,而这个优化是可选的,不是一个正式的确保。这关于快速模仿推迟来说或许足够好了,但在生产中不要轻率地依靠这种优化。

另一种好像能确保这种行为的办法是运用MATERIALIZED CTE:

explain
with s (x) as materialized (select pg_sleep(1))
select *
from t_book
where (select x from s) is not null;

我现在又运用了一个标量子查询,因为我需求访问CTE,并且我不想把它放在FROM 子句中,这样会影响我的预测。

计划是这样的:

Result  (cost=0.03..2.07 rows=4 width=943) (actual time=1001.289..1001.292 rows=4 loops=1)

同样,包含一个一次性的过滤器,这就是咱们在这儿想要的。

运用根据JDBC的办法

假如你的应用程序是根据JDBC的,你就不用经过调整查询来模仿推迟了。你能够简略地以某种方式署理JDBC。让咱们看一下这个小程序:

try (Connection c1 = db.getConnection()) {
    // A Connection proxy that intercepts preparedStatement() calls
    Connection c2 = new DefaultConnection(c1) {
        @Override
        public PreparedStatement prepareStatement(String sql) 
        throws SQLException {
            sleep(1000L);
            return super.prepareStatement(sql);
        }
    };
    long time = System.nanoTime();
    String sql = "SELECT id FROM book";
    // This call now has a 1 second "latency"
    try (PreparedStatement s = c2.prepareStatement(sql);
        ResultSet rs = s.executeQuery()) {
        while (rs.next())
            System.out.println(rs.getInt(1));
    }
    System.out.println("Time taken: " + 
       (System.nanoTime() - time) / 1_000_000L + "ms");
}

在哪里?

public static void sleep(long time) {
    try {
        Thread.sleep(time);
    }
    catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

为了简略起见,这儿运用了jOOQ的 [DefaultConnection](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/tools/jdbc/DefaultConnection.html)作为一个署理,方便地将一切的办法委托给一些委托衔接,只允许重写特定的办法。该程序的输出是:

1
2
3
4
Time taken: 1021ms

这模仿了prepareStatement() 事情的推迟。很明显,为了不使你的代码乱七八糟,你会把署理提取到一些东西中。你乃至能够在开发中署理一切的查询,只依据系统特点来启用睡觉调用。

另外,咱们也能够在executeQuery() 事情上进行模仿:

try (Connection c = db.getConnection()) {
    long time = System.nanoTime();
    // A PreparedStatement proxy intercepting executeQuery() calls
    try (PreparedStatement s = new DefaultPreparedStatement(
        c.prepareStatement("SELECT id FROM t_book")
    ) {
        @Override
        public ResultSet executeQuery() throws SQLException {
            sleep(1000L);
            return super.executeQuery();
        };
    };
        // This call now has a 1 second "latency"
        ResultSet rs = s.executeQuery()) {
        while (rs.next())
            System.out.println(rs.getInt(1));
    }
    System.out.println("Time taken: " +
        (System.nanoTime() - time) / 1_000_000L + "ms");
}

现在这是在运用jOOQ的方便类 [DefaultPreparedStatement](https://www.jooq.org/javadoc/latest/org.jooq/org/jooq/tools/jdbc/DefaultPreparedStatement.html).假如你需求这些,只需增加jOOQ开源版的依靠关系(这些类中没有任何RDBMS的特定内容),与任何根据JDBC的应用程序,包括Hibernate:

<dependency>
  <groupId>org.jooq</groupId>
  <artifactId>jooq</artifactId>
</dependency>

另外,假如你不需求整个依靠关系,只需仿制类的来历DefaultConnectionDefaultPreparedStatement ,或许你只需自己署理JDBC API。

一个根据jOOQ的处理方案

假如你已经在运用jOOQ(你应该这样做!),你能够更容易地做到这一点,经过完成一个 [ExecuteListener](https://www.jooq.org/doc/latest/manual/sql-execution/execute-listeners/).咱们的程序现在看起来就像这样:

try (Connection c = db.getConnection()) {
    DSLContext ctx = DSL.using(new DefaultConfiguration()
        .set(c)
        .set(new CallbackExecuteListener()
            .onExecuteStart(x -> sleep(1000L))
        )
    );
    long time = System.nanoTime();
    System.out.println(ctx.fetch("SELECT id FROM t_book"));
    System.out.println("Time taken: " +
        (System.nanoTime() - time) / 1_000_000L + "ms");
}

还是同样的成果:

+----+
|id  |
+----+
|1   |
|2   |
|3   |
|4   |
+----+
Time taken: 1025ms

不同的是,经过一个拦截回调,咱们现在能够把这个睡觉增加到一切类型的句子中,包括准备好的句子、静态句子、回来成果集的句子,或更新计数,或两者都是。