EXPLAIN 打印规划器准备采用的执行计划与代价;EXPLAIN ANALYZE 则真的跑一遍语句,把估算变成测到的行数与时间。它们回答的是两个不同问题:规划器认为应该怎样做,以及数据库实际怎样做。排障时先看前者理解意图,再用后者测量现实,别把 cost 当成秒数,也别在生产库上无准备地执行一条会写数据的 ANALYZE。
计划提供需要核对的估计与观测,而不是单凭行数差异就能完成的诊断。循环次数和提前停止也会制造表面差距;估计准确时,查询仍可能受到锁等待、溢写或大量必要工作的影响。它告诉你读了哪些表、用了什么索引、预计经过多少行,以及每个节点把多少工作交给下一个节点。把计划保存进工单,和请求参数、统计信息更新时间、数据库版本一起记录,几周后的回归才有可比基线。只贴一句“慢”没有证据;保存计划能帮助定位值得检验的假设,但不能自动解释所有请求延迟。
自上而下读形状
文本计划先打印父节点,再缩进打印它的子节点。向下读可以理解节点依赖哪些输入,向上看可以理解结果怎样汇合。执行可能按需发生:上层的 LIMIT 得到足够多的行便会停止索取,下层排序却可能需要读完全部输入才能交出第一行。缩进描述依赖关系,并不是所有节点通用的执行时间顺序。
cost 使用规划器的工作量单位,不是毫秒。父节点总成本包含子节点,不能把整棵树的数字直接求和。启动成本表示返回第一行前预计投入多少工作,总成本表示完成节点的估算。实际时间和实际行数在 loops 大于一时按每次执行取平均;判断重复工作要结合循环次数,同时避免把父节点包含的子节点时间再加一次。
阅读时先问三个问题。第一,计划是否走了预期的访问路径,例如选择性很高的条件是否用了索引。第二,估算行数和实际行数是否在同一个数量级,数量级错了通常比单个节点的 cost 更值得处理。第三,数据流在哪里膨胀或等待,宽大的中间结果会把后面的排序、连接和聚合一起拖慢。沿着这三个问题读,计划就从一堆术语变成了可验证的因果链。
追踪工作量与放大点
行数最多只是线索,不能直接证明瓶颈。连续读取的大表扫描可能比随机访问索引更便宜,一个每次很快却反复执行数万次的节点也可能主导延迟。先找估算与实际第一次发生明显偏离的地方,再追踪它怎样影响后续连接、排序与内存。嵌套循环并非天然错误:外表很小、内表有匹配索引时它可能最快;危险的是外表被严重低估后仍反复访问内表。
把计划格式化为 JSON 可以让工具比较节点,手工排障先用普通 EXPLAIN 确认访问路径,再按需执行带 ANALYZE、BUFFERS 的测量。缓冲命中、缓冲读取、临时文件读写能帮助定位数据访问与排序落盘。缓冲读取不等于真实磁盘读取,因为操作系统缓存也可能提供数据;看到 read 就宣布磁盘慢,证据还不够。VERBOSE 会显示更多输出列和别名,适合定位复杂视图。
用 ANALYZE 让行数和代价对齐
EXPLAIN ANALYZE 会真实执行查询,同时给出估算与实际行数、节点耗时和循环次数。两者出入很大,可能来自统计过时、分布倾斜或列间相关性;重新收集统计有时能改善,但不能保证修好所有偏差。写入实验放到隔离数据库,并设置语句超时。事务回滚只能撤销可回滚的数据库改动,不能撤销已经分配的序列值、函数或触发器对外部系统造成的不可回滚副作用。SELECT 也可能调用带副作用的函数,不能只根据语句开头判断安全。
可以按需提高相关列的统计目标,或者建立描述同一张表中相关列的扩展统计,但这些动作也要由测量验证。扩展统计不能修复所有估计,目前也不是跨表连接选择性的通用信息来源;应先确认它改变了真正有问题的估计,再归因于统计改进。索引收益仍要根据整个查询的工作量评估。若同一语句对小租户很快、对大租户很慢,要用应用实际的预备语句路径比较通用计划和定制计划。PostgreSQL 核心并没有通用的 SQL 提示语法,不要凭空加入提示来强行选择节点。每次只改变一个变量,把结果集、计划和延迟并排保存。
坏掉的样子
第一种失败是调错了查询。一个页面发出五条语句,工程师给最熟悉的那一条加索引,用户延迟却没有变化,因为真正耗时的是另一个查询。可以按 pg_stat_statements 的累计执行时间筛选候选,再关联具体慢请求;累计耗时很高也可能只是调用频繁,不能单凭这个排名解释某次请求的尾延迟。应结合调用次数、参数和请求追踪,避免让熟悉程度代替耗时证据。估算行数远小于实际行数时,再沿循环次数和数据流找放大点,而不是先全局禁用某种连接算法。
另一个故障是把 EXPLAIN ANALYZE 当成无副作用的测速命令。有人在主库直接跑大批量更新,结果测试本身锁住业务、写入 WAL,还触发了通知或审计。修法是先复制语句到隔离副本,读查询用 BEGIN 和超时保护,写查询先用普通 EXPLAIN 估算,再用生产级回放数据测量。计划采集也要脱敏参数,避免把客户信息带进日志或工单。
另一种常见失败是参数敏感性被平均值掩盖。某个租户占了大多数数据,其他租户很小,共用的预备语句通用计划却无法同时照顾两种分布。用两个有代表性的参数分别保存计划,再检查统计、查询形状和实际执行路径。每个优化动作都应对应结果不变及资源消耗改善的证据,否则只是换了一段计划文本。
快速验证
在允许写入的临时实验数据库执行下面的事务。数据表属于当前会话,结束时回滚清理,不依赖已有订单或客户表:
BEGIN;
SET LOCAL statement_timeout = '10s';
CREATE TEMP TABLE explain_demo AS
SELECT n AS id, n % 100 AS tenant_id FROM generate_series(1, 20000) AS g(n);
ANALYZE explain_demo;
EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM explain_demo WHERE tenant_id = 17 ORDER BY id LIMIT 10;
SELECT id FROM explain_demo WHERE tenant_id = 17 ORDER BY id LIMIT 10;
CREATE INDEX explain_demo_lookup ON explain_demo (tenant_id, id);
EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM explain_demo WHERE tenant_id = 17 ORDER BY id LIMIT 10;
SELECT id FROM explain_demo WHERE tenant_id = 17 ORDER BY id LIMIT 10;
ROLLBACK;
两次普通 SELECT 都应返回从 17 到 917、间隔一百的十个编号。比较两份计划中的扫描类型、排序节点、过滤行数与缓冲访问;第二份通常会利用有序索引。不同版本和环境的精确节点形状与耗时可以不同,不要把某个毫秒数字写成硬性断言。回滚后实验对象消失;迁移到真实业务前,还需用接近生产分布的数据重复测量,并确认其他重要查询没有回退。
延伸阅读
来源
PostgreSQL EXPLAIN 文档,EXPLAIN 选项与真实执行的副作用,已核验 2026-10-15。
PostgreSQL 使用 EXPLAIN,说明计划节点、估算行数与 ANALYZE 输出,已核验 2026-10-15。
PostgreSQL 18: Extended statistics,同表列相关性及 ANALYZE 采集,已核验 2026-10-15。
PostgreSQL 18: CREATE STATISTICS,统计种类与表连接选择率估算的限制,已核验 2026-10-15。
PostgreSQL 18: pg_stat_statements,调用次数与累计执行时间,已核验 2026-10-15。