# EXPLAIN · 读懂 PostgreSQL 的执行计划

> EXPLAIN 让优化器摊开它打算怎么执行一条 SQL：走哪个索引、怎么连接、估算多少行；EXPLAIN ANALYZE 真跑一遍给出实测行数与耗时。这是排查慢查询的第一手段——看懂扫描类型、连接方式、估算与实测的偏差，你才知道慢在哪、该加什么索引。

- 作者：David（道雾轩）
- 专栏：PostgreSQL 特性详解（https://daiw.org/manual/postgresql.md）
- 最后更新：2026-10-01
- 原文：https://daiw.org/manual/postgresql/explain
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# EXPLAIN · 读懂 PostgreSQL 的执行计划

前面几章讲了索引、分区、物化视图这些「让查询变快」的手段。但用哪个、有没有生效，不能靠猜——得让数据库自己告诉你。`EXPLAIN` 就是那扇窗：它让优化器把「我打算怎么执行这条 SQL」摊开给你看。**看懂执行计划，是排查慢查询的基本功。**

## EXPLAIN 家族：从「看计划」到「真跑一遍」

```sql
EXPLAIN                      SELECT ...;  -- 只看计划,不执行
EXPLAIN ANALYZE             SELECT ...;  -- 真跑一遍,给出实测行数与耗时
EXPLAIN (ANALYZE, BUFFERS)  SELECT ...;  -- 再加上缓存命中情况
EXPLAIN (FORMAT JSON)       SELECT ...;  -- 机器可读,给工具用
```

<Callout type="warn">
  `EXPLAIN ANALYZE` 会**真正执行**语句。对 `SELECT` 无妨，但对 `UPDATE` / `DELETE` / `INSERT`，它是真的改数据！要看写语句的计划又不想改数据，把它包在事务里最后回滚：`BEGIN; EXPLAIN ANALYZE UPDATE ...; ROLLBACK;`。
</Callout>

## 一个索引的立竿见影

最能说明问题的，是「加索引前后」同一条查询的计划对比。实测灌了 20000 行订单，查 `WHERE user_id = 1`。

**没有索引时——全表扫：**

```text
Seq Scan on ea_orders  (cost=0.00..398.00 rows=19 width=26)
                       (actual time=0.023..0.997 rows=25 loops=1)
  Filter: (user_id = 1)
  Rows Removed by Filter: 19975      ← 为了找 25 行,读了 20000 行,扔掉 19975 行
Execution Time: 1.013 ms
```

**建了 `user_id` 的 B-tree 索引后：**

```text
Bitmap Heap Scan on ea_orders  (cost=4.43..57.84 rows=19 width=26)
                               (actual time=0.038..0.067 rows=25 loops=1)
  -> Bitmap Index Scan on ea_orders_user_idx  (actual rows=25)
       Index Cond: (user_id = 1)
Execution Time: 0.086 ms           ← 1.013ms → 0.086ms,快了十倍
```

关键信号是那句 **`Rows Removed by Filter: 19975`**：数据库为了返回 25 行，白读了近两万行。看到它，基本就是「这里缺个索引」的铁证。加索引后，`Bitmap Index Scan` 先从索引拿到匹配行的位置，`Bitmap Heap Scan` 再只去读那 22 个相关数据块——不再全表扫。

## 读计划：三个必看的数字

执行计划是一棵树，从最内层缩进往外读（内层先执行）。每个节点盯三样：

**① 扫描 / 连接类型（节点名）** —— 数据是怎么取的、表是怎么连的：

| 扫描类型 | 含义 |
| --- | --- |
| `Seq Scan` | 全表顺序扫（小表 OK，大表 + 高过滤 = 危险信号） |
| `Index Scan` | 走索引取行，再回表 |
| `Index Only Scan` | 只读索引就够（覆盖索引，不回表，最快） |
| `Bitmap Heap Scan` | 索引找到一批行，批量按块读（中等选择度） |

| 连接类型 | 适合 |
| --- | --- |
| `Nested Loop` | 一侧行很少 + 另一侧有索引 |
| `Hash Join` | 两侧都较大、等值连接（建哈希表） |
| `Merge Join` | 两侧都已按连接键有序 |

**② `rows` 估算 vs `actual rows` 实测** —— 优化器的「预判」准不准：

```text
(cost=... rows=19 ...) (actual ... rows=25 ...)   -- 估 19,实 25,很准
```

估算靠 `ANALYZE` 收集的统计信息。**如果 `rows` 和 `actual rows` 差了几个数量级**（估 10 行实际 100 万），说明统计信息**陈旧**——优化器基于错误预判选了烂计划。解法：`ANALYZE 表名` 重新收集统计。这是慢查询排查里最常见的一类病根。

**③ `cost`（成本）** —— 优化器的选择依据：

```text
cost=0.00..398.00   -- 启动成本..总成本
```

<Callout type="info">
  `cost` 是**优化器内部的相对单位，不是毫秒**——它以「顺序读一个页 = 1.0」为基准折算 I/O 与 CPU。两个数是「启动成本..总成本」：启动成本是「吐出第一行前的花费」（比如排序要先读完全部才能出第一行，启动成本就高）。优化器的工作，就是在所有候选计划里挑**总成本最低**的那个。你要真实耗时，得看 `EXPLAIN ANALYZE` 里的 `actual time` 和 `Execution Time`。
</Callout>

加上 `BUFFERS` 还能看到 `shared hit=148`（命中缓存的页）vs `read=2`（从磁盘读的页）——`read` 高说明数据没在内存里，可能需要更大的 `shared_buffers` 或更好的索引。

## 估算从哪来：统计信息

优化器不是瞎猜。`ANALYZE` 会采样每张表，把「某列有多少不同值、最常见的值是哪些、数据怎么分布」记进系统目录(`pg_statistic`)。查 `WHERE user_id = 1` 时，它据此估「大概匹配多少行」，再决定走索引还是全表扫。

<Callout type="info">
  日常里 **autovacuum** 会自动触发 `ANALYZE`，多数时候不用管。但**大批量导入 / 删除之后**统计会瞬间过时——这时手动跑一次 `ANALYZE`，能避免优化器拿着旧地图选错路。排查「昨天还好好的、今天突然变慢」的查询，先看 `EXPLAIN ANALYZE` 里估算和实测差多少，往往就是统计过时。
</Callout>

## 横向对比

| 能力 | PostgreSQL | MySQL | Oracle | SQL Server |
| --- | --- | --- | --- | --- |
| 看计划 | `EXPLAIN` | `EXPLAIN` | `EXPLAIN PLAN` | 执行计划（图形 / 文本） |
| 实测执行 | `EXPLAIN ANALYZE` | `EXPLAIN ANALYZE`(8.0.18+) | `DBMS_XPLAN` + 实测 | Actual Execution Plan |
| 缓存 / IO 明细 | `BUFFERS` | 部分 | 强 | 强 |
| 机器可读 | `FORMAT JSON/YAML` | `FORMAT=JSON` | XML | XML / 图形 |

各家都有执行计划工具，思路一致。PostgreSQL 的 `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` 信息密度很高，配合 [explain.dalibo.com](https://explain.dalibo.com) 这类可视化工具，读起来相当顺手。

## 应用场景

- **慢查询定位**：线上某接口慢，把它的 SQL 拿去 `EXPLAIN ANALYZE`，看是全表扫、缺索引，还是估算跑偏。
- **验证索引是否生效**：建了索引却没变快？`EXPLAIN` 看它到底走没走索引——很多时候是查询写法（函数包裹列、类型不匹配）让索引失效。
- **JOIN 顺序 / 方式调优**：大表连接慢，看是不是选了糟糕的 `Nested Loop`，或统计过时导致连接顺序错。
- **上线前体检**：核心查询上线前先 `EXPLAIN` 过一遍，别等生产环境全表扫才发现。

## 小结与下一章

`EXPLAIN` 是数据库调优的听诊器：`Rows Removed by Filter` 大 → 缺索引；`rows` 与 `actual` 差远 → 统计过时；`Seq Scan` 大表 → 警惕。学会读这三样，你就从「盲目加索引」进阶到「对症下药」。

规模与性能这一站到此结束。下一站转向另一个维度——**把逻辑放进数据库里**：用 PL/pgSQL 写函数与触发器，让数据库不只是存数据，还能主动执行逻辑。👉 [PL/pgSQL 与触发器：让数据库自己干活](https://daiw.org/manual/postgresql/plpgsql-triggers)。

## 参考文献

- [1] PostgreSQL 官方文档.「14.1. Using EXPLAIN.」https://www.postgresql.org/docs/current/using-explain.html —— 计划节点、成本、`ANALYZE`、`BUFFERS` 解读。
- [2] PostgreSQL 官方文档.「14.2. Statistics Used by the Planner.」https://www.postgresql.org/docs/current/planner-stats.html —— `ANALYZE` 与 `pg_statistic`。
- [3] PostgreSQL 官方文档.「EXPLAIN(SQL Command).」https://www.postgresql.org/docs/current/sql-explain.html —— 全部选项与输出格式。
