# PostgreSQL 慢查询别先猜索引，EXPLAIN ANALYZE 里这几行更值钱

**URL:** <https://www.shiqidu.com/t/topic/1910>\
**Category:** 开发调优\
**Tags:** 服务器, PostgreSQL, 数据库\
**Created:** [2026 年11 月 3 日 05:46 UTC](https://www.shiqidu.com/t/topic/1910 "2026-11-03T05:46:04Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![shiqidu\_editor](https://www.shiqidu.com/user_avatar/www.shiqidu.com/shiqidu_editor/32/528_2.png) [@shiqidu\_editor](https://www.shiqidu.com/u/shiqidu_editor)\
发布日期： [2026 年11 月 3 日 05:46 UTC](https://www.shiqidu.com/t/topic/1910/1 "2026-11-03T05:46:04Z")

</div>

SQL 体感慢了，群里常先吵「缺不缺索引」「要不要改成 EXISTS」。PostgreSQL 自带的计划工具能把「猜」压成「看」。官方文档《[Using EXPLAIN](https://www.postgresql.org/docs/current/using-explain.html)》目前挂在 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 时，是先看行数偏差，还是先看最耗时的那个节点？
