# EXPLAIN 与查询计划

> 阅读执行计划，并将估算的代价与实际行数、耗时和缓冲区进行比较。

---

LLMS 索引： [llms.txt](/llms.txt)

---

`EXPLAIN` 展示 PostgreSQL 计划执行查询的方式——它会使用哪些索引、采用哪些连接策略，以及预计每一步会得到多少行。`EXPLAIN ANALYZE` 会实际运行查询，并在估算值旁报告实际行数和耗时。估算值与实际值之间的差距能告诉你规划器的统计信息是否准确；要找出慢查询，就需要熟练阅读这些计划。

## 本章要点 {#learn}

- 自底向上阅读查询计划
- 估算行数/耗时与实际行数/耗时的区别
- 最常见的节点类型：Seq Scan、Index Scan、Bitmap Heap Scan、Nested Loop、Hash Join、Merge Join、Sort、Aggregate
- `EXPLAIN ANALYZE`、`BUFFERS` 及其他选项
- 估算值严重偏离时如何修复

## 示例 {#example}

```sql {num="45.1" caption="EXPLAIN 与查询计划"}
-- Plain EXPLAIN: estimates only
EXPLAIN SELECT * FROM orders_example WHERE customer_id = 1;

-- EXPLAIN ANALYZE: actually runs the query, shows real timing
EXPLAIN ANALYZE SELECT * FROM orders_example WHERE customer_id = 1;

-- With BUFFERS: how many pages came from cache vs. disk
EXPLAIN (ANALYZE, BUFFERS)
  SELECT c.name, count(o.id) AS orders
  FROM customers_example c
  LEFT JOIN orders_example o ON o.customer_id = c.id
  GROUP BY c.name
  ORDER BY orders DESC;

-- A more complex plan to study
EXPLAIN ANALYZE
  WITH spend AS (
    SELECT customer_id, sum(total) AS s FROM orders_example GROUP BY customer_id
  )
  SELECT c.name, s.s
  FROM customers_example c
  LEFT JOIN spend s ON s.customer_id = c.id
  ORDER BY s.s DESC NULLS LAST;
```

应从*最内层、最深的*节点向上读取计划。每一行都是一个步骤；子步骤的输出会供父步骤使用。每一行中的字段含义如下：

- **cost=startup..total**——规划器的估算值，使用任意单位。第一个数字表示第一行准备就绪的时间；第二个数字表示最后一行准备就绪的时间。
- **rows=N**——估算的行数。
- **width=N**——平均行宽，单位为字节。
- **actual time=startup..total ms**（使用 `ANALYZE` 时）——实际耗时，按每次*循环*分别计算。
- **rows=N actual**——实际行数。
- **loops=N**——该节点执行的次数（在嵌套循环连接中，内侧一方会针对外侧的每一行运行一次）。

常见的扫描类型：

- **Seq Scan**：读取整张表。对于小表没问题；对于大表，除非确实需要每一行，否则就不理想。
- **Index Scan**：遍历索引，从表中取出匹配的行。匹配行很少时最佳。
- **Index Only Scan**：索引包含所需的值，但可能仍需访问堆来检查可见性，除非可见性映射将堆页标记为全可见。
- **Bitmap Heap Scan / Bitmap Index Scan**：先构建匹配页的位图，然后读取这些页。匹配行较多时很有用。

连接策略：

- **Nested Loop**：对外侧的每一行，查找匹配的内侧行。外侧结果集较小且内侧有索引时速度很快。
- **Hash Join**：为较小的一侧构建哈希表，再用较大的一侧进行探测。中等规模连接速度很快。
- **Merge Join**：两侧都按连接键排序，并行遍历。两侧已经排序时最佳（例如由索引提供排序）。

如果估算的 `rows=N` 与实际的 `rows=M actual` 相差几个数量级，说明规划器使用的统计信息已经过时或具有误导性。运行 `ANALYZE table` 刷新统计信息。

## 运行示例 {#run}

运行方式：

```text
$ psql -f source/explain.sql postgres
                                  QUERY PLAN
-------------------------------------------------------------------------------
 Seq Scan on orders_example  (cost=0.00..1.04 rows=2 width=10)
   Filter: (customer_id = 1)
(2 rows)
 ...
```

## 常见陷阱 {#pitfalls}

- 从上到下而不是从下到上读取计划。叶节点才是实际数据源；上面的所有节点都在进行转换。
- 不要把 `cost=` 当成绝对值。这些数字没有单位——只有在同一计划内相互比较才有意义。
- `EXPLAIN ANALYZE` 会实际运行查询，包括任何 `INSERT`/`UPDATE`/`DELETE`。要预览写操作，请将其包在事务中并执行 `ROLLBACK`：`BEGIN; EXPLAIN ANALYZE UPDATE ...; ROLLBACK;`。
- 小表会让优化失去意义。请使用有代表性的数据进行测试；对 10 行执行 Seq Scan 说明不了任何问题。

> [!TIP] 提示
> 将 `EXPLAIN ANALYZE` 计划粘贴到 <https://explain.depesz.com/> 或 <https://explain.dalibo.com/>，即可获得突出显示最慢步骤的彩色可视化。对于生产环境调试不可或缺。

> [!NOTE] 动手练习
> 删除一个索引，运行 `EXPLAIN ANALYZE` 并记下计划。重新创建它并再次运行，观察计划的变化。然后运行一个带连接的查询：`EXPLAIN ANALYZE SELECT ... FROM customers_example c JOIN orders_example o ON o.customer_id = c.id;`，并确定所选的连接策略。

## 下载 {#downloads}

SQL 文件：[explain.sql](/examples/source/explain.sql) · [全部示例文件](/examples/examples.zip) · [准备工作](/#prerequisites)
