查询变慢时同时检查计划与等待
一条过去很快的查询,可以在数据、统计和访问计划都没有变化时变慢。另一个事务可能持有它需要的锁,存储缓存可能比上次更冷,应用也可能在语句进入 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,阻塞列表包含甲的进程编号。具体事件可能随争用过程变化,核心证据是乙确实被甲阻塞。甲回滚后,乙把原本的零加成一,最后查询返回一,清理语句移除了整个实验模式。 ...