我遇到了一个风趣的小技巧,在测试一些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>
另外,假如你不需求整个依靠关系,只需仿制类的来历DefaultConnection
或DefaultPreparedStatement
,或许你只需自己署理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
不同的是,经过一个拦截回调,咱们现在能够把这个睡觉增加到一切类型的句子中,包括准备好的句子、静态句子、回来成果集的句子,或更新计数,或两者都是。