SQL 体感慢了,群里常先吵「缺不缺索引」「要不要改成 EXISTS」。PostgreSQL 自带的计划工具能把「猜」压成「看」。官方文档《Using EXPLAIN》目前挂在 PostgreSQL 18 文档树下,把计划怎么读、EXPLAIN ANALYZE 多出来什么、有哪些坑写得很具体。下面按那一节梳理,不编造「超过多少毫秒就算慢」的阈值。
先分清两种输出
普通 EXPLAIN 只让优化器给出计划树,不真正跑这条语句。树是一串计划节点:最底下常见是各种 Scan(顺序扫、索引扫、位图索引扫等),上面再叠 Join、Sort、Aggregate 之类。每一行节点旁边有括号里的代价估计:启动代价、总代价、估计输出行数、平均行宽。代价单位是规划器自己的一套参数换算出来的,传统上跟磁盘页读取挂钩,不能拿去跟墙上的毫秒一一对应。
上层节点的代价会把子节点算进去。文档也提醒:计划代价不管结果转成文本、也不管传到客户端花了多久——那些规划器改不了,所以估的时候直接忽略。
加上 ANALYZE 变成 EXPLAIN ANALYZE 以后,语句会真实执行,节点旁多出实际耗时、实际行数、loops 等。写操作(INSERT / UPDATE / DELETE / MERGE)一样会改表。文档给的做法是先 BEGIN;,跑完 EXPLAIN ANALYZE …,再 ROLLBACK;,这样能看到计划又不留下数据变更。
ANALYZE 还会顺带打开缓冲区统计(可用 BUFFERS OFF 关掉)。输出末尾常见 Planning Time 和 Execution Time:前者是生成并优化计划的时间,不含解析重写。后者含执行器启停和部分触发器时间,默认不含把结果编码发给客户端的开销。需要把序列化也量进去时,文档提到可加 SERIALIZE。
真正该盯的几处
- 节点类型:
Seq Scan、Index Scan、Bitmap Heap Scan等直接说明访问路径。有Filter/Index Cond/Recheck Cond时,看条件是下推到索引里了,还是扫完再过滤。外连接计划里还可能同时出现Join Filter和普通Filter,语义不一样,文档 Basics 里有对照。 - 估计行数 vs 实际行数:文档反复强调,两边差太多时,后面的 Join、Hash、内存估算都会跟着歪。统计信息过期、表达式难估,都可能拉开差距。收集统计的
ANALYZE命令,和计划里的EXPLAIN ANALYZE,不是一回事,别混。 actual time与loops:子节点可能执行多次(嵌套循环的内表就是典型)。输出里的时间和行数常常是单次平均,总花费要自己乘loops。文档例子里写得很清楚。- 额外统计行:Sort 会标明内存排序还是落盘、用了多少内存。Hash 会标桶数、批次数、峰值内存。过滤条件可能出现
Rows Removed by Filter。索引相关节点还可能有Index Searches。这些比孤立盯最外层一个毫秒数有用。
代价估的是「跑完这个节点」。碰上 LIMIT,上层可能提前停掉,子节点的估计仍按跑完显示,实际行数却很少——文档 Caveats 里专门提醒,这不是估错,是展示口径不同。
Caveats 里几条别忽略
EXPLAIN ANALYZE的计时本身有开销,慢机器上gettimeofday成本不低,可以用pg_test_timing摸一下本机开销。- 结果不会发给客户端,所以默认测量里没有网络传输。
SERIALIZE才把显示转换算进去。 - 玩具表上的计划推不到大表:单页小表几乎总会选顺序扫,有索引也不一定用。规划器代价也不是线性外推。
- Merge Join 在重复键、提前耗尽一侧输入时,实际行数读起来容易误会。BitmapAnd / BitmapOr 的实际行数会显示成 0,是实现限制,不是「没扫到行」。
- 分区表上若执行器判定某些子计划不可能产出行,输出里可能出现
Subplans Removed,对应节点直接省略。
具体该不该加索引、改写 JOIN,要回到你自己库上的计划和统计,别把别人帖子里的耗时当标准。你们排查慢 SQL 时,是先看行数偏差,还是先看最耗时的那个节点?