会话甲持锁,连接会话乙的线被挡住,乙等待;观察者丙指向等待侧。

查询变慢时同时检查计划与等待

一条过去很快的查询,可以在数据、统计和访问计划都没有变化时变慢。另一个事务可能持有它需要的锁,存储缓存可能比上次更冷,应用也可能在语句进入 PostgreSQL 之前就开始等待。重新检查计划很有用,但它应当放在完整的耗时比较中:先区分时间是花在执行工作上,还是花在等待获得执行机会。不能从“现在更慢”直接推导出“数据与统计一定都变了”。 保留可比较的基线 记录语句形状、有代表性的参数值、返回行数、相关会话设置,以及应用是否使用预备语句。不同参数可能命中完全不同规模的数据。手工使用常量的查询,不一定复现预备语句采用的通用计划;不同角色或搜索路径,也可能改变实际对象与权限。因此,拿一条看起来相似的语句作为对照,并不保证比较条件相同。 尽量在变慢正在发生时观察活动。pg_stat_activity 可以显示一个状态为运行中的查询正在等待锁;运行中并不等于持续消耗处理器。pg_blocking_pids 列出当前阻塞这个后端的会话。查看其他角色活动需要适当权限,因此字段不可见不等于没有工作发生。交互观察最好使用新的自动提交查询,而不是在同一个长事务中不断读取已经缓存的监控快照。 坏掉的样子 不加判断就运行 ANALYZE,相当于把每次变慢都当成统计陈旧。它可能增加维护工作,却完全没有解除真正的阻塞。一张只有一行的表,即使新增索引,也不能释放其他事务持有的行锁。准确的行数估计和很小的顺序扫描,可以与很长的应用等待同时存在;这两种观察并不矛盾。 EXPLAIN ANALYZE 会真正执行语句,写入也不例外,而且测量本身同样可能被阻塞。从生产环境复制一条大范围更新,不是无害的检查。应先看等待情况和锁的拥有者,再选择安全夹具或副作用已经明确的语句来测量。看到某个进程编号出现在阻塞列表中,也不能立即终止不熟悉的事务,因为它可能正在完成另一个有效业务操作。 没有锁等待的计划样本,也不等于端到端延迟。EXPLAIN ANALYZE 不会像普通查询那样把结果行发送给客户端;未指定 SERIALIZE 时,还不包含输出转换。TIMING OFF 减少逐节点计时开销,但仍保留总执行时间。大量结果传输、应用解码和连接池排队,需要在计划之外单独观察,不能从这里的执行时间中直接读出。 多会话过程归属于历史报告,未找回原始日志;打印观察需人工检查,超时是独立失败结果。 快速验证 打开三个新的 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,阻塞列表包含甲的进程编号。具体事件可能随争用过程变化,核心证据是乙确实被甲阻塞。该报告称甲回滚后,乙把零加成一,最后查询返回一,清理移除了模式。必须人工确认丙恰好识别出乙、阻塞列表包含甲报告的 pid,且最终值为一;SQL 只是打印这些观察,没有自动断言。 ...

一月 31, 2028 · 1 分钟 · XB Field Notes
EXPLAIN ANALYZE 会真正执行。回滚到保存点可以恢复表变更,但已经消耗的序列值不会归还。图示取自文章示例,本次未重跑。

EXPLAIN ANALYZE 把查询真的跑一遍

EXPLAIN 展示 PostgreSQL 的执行计划,EXPLAIN ANALYZE 还会真正执行语句并测量工作量。最有用的比较,是同一个节点的估计行数与实际行数。两者都不能单独当成结论:估计来自模型,实测则只描述特定数据、缓存、并发和插桩条件下的一次执行。不能把前者称为谎言,也不能把后者当成脱离环境的永恒事实。 ANALYZE 会真正执行。文章示例区分可恢复的表变更与已消耗的序列值;本图没有重新运行实验。 先比较同一种量 计划中的 cost 是相对成本单位,不是毫秒。两个成本值分别描述启动成本与完成成本,带 LIMIT 的查询可能更重视尽早得到前几行,因此选择不同计划。actual time 才使用毫秒。即使成本和耗时恰好都打印成一百,也不能把它们直接相减或求比例;它们本来就不表示同一件事。 一个节点被重复执行时,actual rows 和节点耗时是每次循环的平均值,需要结合 loops 理解总工作量。但不能把所有节点耗时简单相加,因为父节点已经包含子节点的工作,重复相加会重复计算。并行执行时更不能用这种方式猜总延迟。先沿树看输入输出、循环次数、过滤条件和行数流向,再决定哪个位置值得调查。 BUFFERS 区分 shared、local 和临时块活动。shared hit 表示 PostgreSQL 在共享缓冲区里找到了块;shared read 只表示需要读入,操作系统仍可能从页缓存满足它,不能直接断言每次都访问了物理磁盘。临时表使用本地缓冲区,排序或哈希溢出则可能读写临时文件。父节点计数包含子节点,逐行把所有计数加起来同样会夸大 I/O。 ...

十二月 16, 2027 · 2 分钟 · XB Field Notes
代表性计划对比 Limit、Sort、Scan 三级依赖树与 Limit、有序索引访问两级树。两者都保留相同的十个租户 17 编号。第二种路径常见但形状并非固定;本图没有重跑实验。

EXPLAIN 让你看清 Postgres 会怎么做

EXPLAIN 打印规划器准备采用的执行计划与代价;EXPLAIN ANALYZE 则真的跑一遍语句,在估算旁增加实测行数与时间。它们回答的是两个不同问题:规划器认为应该怎样做,以及数据库实际怎样做。排障时先看前者理解意图,再用后者测量现实,别把 cost 当成秒数,也别在生产库上无准备地执行一条会写数据的 ANALYZE。 计划提供需要核对的估计与观测,而不是单凭行数差异就能完成的诊断。循环次数和提前停止也会制造表面差距;估计准确时,查询仍可能受到锁等待、溢写或大量必要工作的影响。它告诉你读了哪些表、用了什么索引、预计经过多少行,以及每个节点把多少工作交给下一个节点。把计划保存进工单,和请求参数、统计信息更新时间、数据库版本一起记录,几周后的回归才有可比基线。只贴一句“慢”没有证据;保存计划能帮助定位值得检验的假设,但不能自动解释所有请求延迟。 先读输入依赖,再向上追踪结果。有序索引常会改变工作路径,但十个有序编号必须相同;这是概念对比,并非新一次执行。 自上而下读形状 文本计划先打印父节点,再缩进打印它的子节点。向下读可以理解节点依赖哪些输入,向上看可以理解结果怎样汇合。执行可能按需发生:上层的 LIMIT 得到足够多的行便会停止索取,下层排序却可能需要读完全部输入才能交出第一行。缩进描述依赖关系,并不是所有节点通用的执行时间顺序。 cost 使用规划器的工作量单位,不是毫秒。父节点总成本包含子节点,不能把整棵树的数字直接求和。启动成本表示返回第一行前预计投入多少工作,总成本表示完成节点的估算。实际时间和实际行数在 loops 大于一时按每次执行取平均;判断重复工作要结合循环次数,同时避免把父节点包含的子节点时间再加一次。 阅读时先问三个问题。第一,计划是否走了预期的访问路径,例如选择性很高的条件是否用了索引。第二,估算行数和实际行数是否在同一个数量级,数量级错了通常比单个节点的 cost 更值得处理。第三,数据流在哪里膨胀或等待,宽大的中间结果会把后面的排序、连接和聚合一起拖慢。沿着这三个问题读,计划就从一堆术语变成了可验证的因果链。 追踪工作量与放大点 行数最多只是线索,不能直接证明瓶颈。连续读取的大表扫描可能比随机访问索引更便宜,一个每次很快却反复执行数万次的节点也可能主导延迟。先找估算与实际第一次发生明显偏离的地方,再追踪它怎样影响后续连接、排序与内存。嵌套循环并非天然错误:外表很小、内表有匹配索引时它可能最快;危险的是外表被严重低估后仍反复访问内表。 把计划格式化为 JSON 可以让工具比较节点,手工排障先用普通 EXPLAIN 确认访问路径,再按需执行带 ANALYZE、BUFFERS 的测量。缓冲命中、缓冲读取、临时文件读写能帮助定位数据访问与排序落盘。缓冲读取不等于真实磁盘读取,因为操作系统缓存也可能提供数据;看到 read 就宣布磁盘慢,证据还不够。VERBOSE 会显示更多输出列和别名,适合定位复杂视图。 ...

七月 8, 2027 · 1 分钟 · XB Field Notes