一条过去很快的查询,可以在数据、统计和访问计划都没有变化时变慢。另一个事务可能持有它需要的锁,存储缓存可能比上次更冷,应用也可能在语句进入 PostgreSQL 之前就开始等待。重新检查计划很有用,但它应当放在完整的耗时比较中:先区分时间是花在执行工作上,还是花在等待获得执行机会。不能从“现在更慢”直接推导出“数据与统计一定都变了”。
保留可比较的基线
记录语句形状、有代表性的参数值、返回行数、相关会话设置,以及应用是否使用预备语句。不同参数可能命中完全不同规模的数据。手工使用常量的查询,不一定复现预备语句采用的通用计划;不同角色或搜索路径,也可能改变实际对象与权限。因此,拿一条看起来相似的语句作为对照,并不保证比较条件相同。
尽量在变慢正在发生时观察活动。pg_stat_activity 可以显示一个状态为运行中的查询正在等待锁;运行中并不等于持续消耗处理器。pg_blocking_pids 列出当前阻塞这个后端的会话。查看其他角色活动需要适当权限,因此字段不可见不等于没有工作发生。交互观察最好使用新的自动提交查询,而不是在同一个长事务中不断读取已经缓存的监控快照。
坏掉的样子
不加判断就运行 ANALYZE,相当于把每次变慢都当成统计陈旧。它可能增加维护工作,却完全没有解除真正的阻塞。一张只有一行的表,即使新增索引,也不能释放其他事务持有的行锁。准确的行数估计和很小的顺序扫描,可以与很长的应用等待同时存在;这两种观察并不矛盾。
EXPLAIN ANALYZE 会真正执行语句,写入也不例外,而且测量本身同样可能被阻塞。从生产环境复制一条大范围更新,不是无害的检查。应先看等待情况和锁的拥有者,再选择安全夹具或副作用已经明确的语句来测量。看到某个进程编号出现在阻塞列表中,也不能立即终止不熟悉的事务,因为它可能正在完成另一个有效业务操作。
快速验证
打开三个新的 psql 会话,分别记作甲、乙、丙,连接同一个可丢弃的 PostgreSQL 18 数据库并使用相同角色,保留通常的自动提交行为。名为 query_recheck_demo 的模式必须尚不存在。务必区分会话:把所有代码块顺序粘贴到同一个连接,无法证明事务之间的阻塞。
- 在甲中建立夹具,然后开启事务,修改唯一一条记录但暂不提交。
\set ON_ERROR_STOP on
CREATE SCHEMA query_recheck_demo;
CREATE TABLE query_recheck_demo.items (id integer PRIMARY KEY,value integer);
INSERT INTO query_recheck_demo.items VALUES (1,0);
ANALYZE query_recheck_demo.items;
BEGIN;
UPDATE query_recheck_demo.items SET value=10 WHERE id=1;
SELECT pg_backend_pid() AS blocker_pid;
- 在乙中发起带执行测量的更新。它会等待甲结束事务,或者等到六十秒的语句超时。
SET application_name='xbcatm_recheck_waiter';
SET statement_timeout='60s';
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)
UPDATE query_recheck_demo.items SET value=value+1 WHERE id=1;
- 乙仍在等待且尚未超时时,在丙中执行观察查询。实验专用应用名称用于定位乙,不需要展示整条查询文本。
SELECT pid,state,wait_event_type,wait_event,pg_blocking_pids(pid) AS blockers
FROM pg_stat_activity WHERE application_name='xbcatm_recheck_waiter';
- 回到甲,撤销尚未提交的变更并释放锁,然后收集乙执行完毕后的计划。
ROLLBACK;
- 确认乙已经成功结束后,在丙中检查最终数据并清理夹具。
SELECT value FROM query_recheck_demo.items WHERE id=1;
DROP SCHEMA query_recheck_demo CASCADE;
PostgreSQL 18.6 实测中,乙的状态为运行中,等待类型是 Lock,具体等待事件是 transactionid,阻塞列表包含甲的进程编号。具体事件可能随争用过程变化,核心证据是乙确实被甲阻塞。甲回滚后,乙把原本的零加成一,最后查询返回一,清理语句移除了整个实验模式。
乙的扫描估计并读取一行;最上层 Update 节点实际返回零行,是因为没有 RETURNING 子句,不表示没有修改数据。总体执行时间包含人为制造的等待,长短取决于甲何时释放事务,不能把这个数字当作扫描一行的固有成本。如果乙先超时,就先回滚甲,并用丙的最后一个代码块清理夹具后再重新开始。取消的更新并不等同于成功加一,不能沿用成功情况的结果解释。
顺着观察到的原因继续
如果证据指向锁等待,先调查事务持有时间和归属,再考虑访问路径。如果实际处理的行太多,则比较过滤选择率与当前统计。如果排序或哈希操作发生溢写,就检查内存和临时读写。最后还要把数据库执行时间与应用请求延迟比较,识别连接建立、连接池排队或客户端处理的部分。只有新计划能够解释测得的回归,并保持所需结果,计划变化才构成有效改进。
来源
- PostgreSQL 18:监控数据库活动,活动会话与等待观察,已核验 2026-09-10。
- PostgreSQL 18:使用 EXPLAIN,行数、循环次数与计划解读,已核验 2026-09-10。