在排查慢SQL时,很多人第一步就是执行EXPLAIN ANALYZE,看到一大堆带数字的输出后却不知道从何看起。有的人只盯着最外层节点的总耗时,有的人把每行的Actual Time直接相加,结果得出完全错误的结论。这篇文章带你系统地解读EXPLAIN ANALYZE的输出结果,搞清楚每个字段的真实含义,尤其是Actual Time两列数字与loops的乘法关系,帮你准确找到查询真正的瓶颈所在。

一、EXPLAIN ANALYZE与普通EXPLAIN有什么区别
普通的EXPLAIN只生成执行计划,不真正执行SQL,所以它给出的成本估算(Cost列)是优化器基于统计信息的预测值,可能与实际情况偏差很大,尤其是统计信息过期或者涉及复杂JOIN时。而EXPLAIN ANALYZE会真正把SQL执行一遍(SELECT语句会完整执行,带增删改的语句默认会执行后回滚),并在执行过程中收集每个节点的真实数据。
正因为它是真执行,输出中会比普通EXPLAIN多出几类关键信息:每个节点的实际启动耗时和总耗时(Actual Time)、循环次数(loops)、实际处理的行数(actual rows)、内存使用情况,以及在ANALYZE、BUFFERS等选项开启时的额外统计。这些真实数据才是性能诊断的核心依据。基本用法如下:
EXPLAIN (ANALYZE, BUFFERS) SELECT o.order_id, c.customer_name FROM orders o JOIN customers c ON o.customer_id = c.customer_id WHERE o.created_at >= '2024-01-01';
需要特别注意的一点是,EXPLAIN ANALYZE本身会带来额外的执行开销。每个节点执行时都要记录时间戳和行计数,对IO密集型查询影响不大,但对原本只跑几毫秒的热点小查询,测量结果可能被明显放大。因此不要用它来评估一个本就极快的SQL到底耗时几毫秒,它更适合诊断那些耗时数百毫秒以上的慢查询。
二、Actual Time两列数字的真实含义
看一个典型的输出片段:
Nested Loop (cost=0.86..1250.40 rows=300 width=52)
(actual time=0.038..18.924 rows=280 loops=1)
-> Index Scan using idx_orders_created on orders o
(cost=0.43..890.12 rows=300 width=28)
(actual time=0.029..5.210 rows=280 loops=1)
-> Index Scan using customers_pkey on customers c
(cost=0.43..1.20 rows=1 width=24)
(actual time=0.021..0.024 rows=1 loops=280)第一列数字(0.038)是startup time,表示该节点产出第一行所花的时间;第二列(18.924)是total time,表示该节点产出全部行所花的时间。两者相减大致就是这个节点从第一行输出到最后一行输出之间的增量耗时。对Sort、HashAggregate这类必须读完全部输入才能吐出结果的节点,startup time会非常接近total time,这本身就是一个识别信号:排序节点几乎所有的耗时都在启动阶段。
最容易被忽视的是loops这个数字。关键规则只有一句话:actual time报告的是平均每次循环的耗时,不是总耗时。上面例子中内层的Index Scan,actual time是0.021..0.024,看起来微不足道,但它循环了280次(loops=280),所以这个节点贡献的真实总耗时约为0.024乘以280,大约6.7毫秒,这才是它在整棵计划树中的真实成本。如果不乘loops,把每个节点的actual time直接相加,得出的耗时分布会严重失真。
同理,actual rows也是平均值,表示每次循环平均返回的行数。对比rows(估算行数)和actual rows(实际行数)是判断统计信息是否准确的重要手段。比如优化器估算rows=300,实际是280,说明统计信息基本靠谱;如果估算1行实际返回10万行,执行计划几乎必然劣化,此时首先要做的就是ANALYZE表或调整统计目标(default_statistics_target)。
三、如何快速定位真正的耗时瓶颈
拿到输出后,推荐的阅读顺序不是从上往下,而是先看最外层节点的总耗时(也就是整条SQL的Execution Time),然后沿着计划树自顶向下,找那些自身耗时占比高的节点。计算某个节点自身耗时,可以用它的total time减去所有直接子节点的total time(子节点的total time同样要乘以各自的loops)。差值大的节点,往往就是排序、哈希、过滤这类真正消耗CPU的地方。
实际排查时有几个高频问题模式值得记住。第一,Seq Scan加上巨大的Rows Removed by Filter数字,说明表扫描后大量行被条件过滤掉了,索引缺失或条件写法导致索引失效的典型表现。第二,Nested Loop的内层节点loops数值极大且每次循环都不便宜,说明优化器可能低估了外层行数,本该走Hash Join却选了Nested Loop,这时优先核对估算行数偏差。第三,Sort节点耗时高且触发了external merge Disk,表示排序内存不足落盘了,可以调大work_mem。配合BUFFERS选项还能直接看到shared hit与read的比例,判断瓶颈在缓存命中还是磁盘IO:
Sort (actual time=890.123..920.456 rows=500000 loops=1) Sort Method: external merge Disk: 98240kB Buffers: shared hit=1234 read=45678 -> Seq Scan on orders (actual time=0.020..210.310 rows=500000 loops=1)
最后提醒两个容易踩的坑。一是EXPLAIN ANALYZE会真的执行SQL,对INSERT、UPDATE、DELETE这类语句,一定要包在事务里再执行,看完计划后ROLLBACK,避免误改数据:
BEGIN; EXPLAIN ANALYZE UPDATE orders SET status = 'done' WHERE status = 'pending'; ROLLBACK;
二是不要把Planning Time和Execution Time混为一谈。Planning Time是生成计划的时间,如果一条SQL本身执行很快但Planning Time高达几十毫秒,问题出在优化器阶段,常见于表上有大量分区或大量继承子表、prepared statement首次硬解析的场景,这与执行阶段的优化是完全不同的方向。掌握actual time乘loops、估算行数对比、节点自身耗时差值这三个技巧,绝大多数慢查询的瓶颈都能在几分钟内定位清楚。
PostgreSQLEXPLAIN ANALYZE执行计划修改时间:2026-09-14 01:08:51