# 优化器如何选择路径

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

---

SQL 描述结果关系，planner 则要在等价实现中选择一棵可执行树。选择发生在当前 catalog、statistics、parameter visibility、planner GUC 和 cost constants 下；环境改变，最便宜路径也可能改变。

## 7.1.1 扫描、连接、排序、聚合与物化节点 {#item-7-1-1}

叶子节点产生基础行：

- Seq Scan 顺序访问 relation page，并在节点上应用 filter；
- Index Scan 按 index 找 tuple，再访问 heap 取可见行/列；
- Index Only Scan 仍需 visibility map 证明可跳过 heap；
- Bitmap Index + Heap Scan 先收集 TID，再按 page 批量访问；
- Function/Values/CTE/Subquery Scan 从非普通表来源产行。

没有“高级节点一定更快”。小表或低选择性查询用 Seq Scan 很合理；返回大量 heap row 时，随机 index fetch 可能更贵。`Rows Removed by Filter` 说明读到但未输出的行，不能与节点 `rows` 混为一谈。

中间节点转换数据流：

| 责任 | 常见节点 | 关键观察 |
|---|---|---|
| join | Nested Loop / Hash Join / Merge Join | outer rows、inner loops、hash/sort 输入 |
| order | Sort / Incremental Sort | key、method、memory、disk spill |
| aggregate | Aggregate / HashAggregate / GroupAggregate | group estimate、batches、memory/disk |
| reuse | Materialize / Memoize | 重复读取是否被缓存，命中/溢出 |
| combine | Append / Merge Append | child/partition 数与裁剪 |
| parallel | Gather / Gather Merge | planned/launched workers、每 worker rows |

Nested Loop 的 inner child 通常执行 outer row 次数，所以要看 `loops`；Hash Join 先构建 hash 再 probe，关注 build side、batches 与内存；Merge Join 要求两侧有序，排序可能由 index 或显式 Sort 提供。节点名只说明算法，不说明它在本次 cardinality 上是否正确。

## 7.1.2 成本、选择率、行数与路径竞争 {#item-7-1-2}

计划行：

```text
(cost=startup..total rows=N width=W)
```

- startup 是开始输出前的估算成本；
- total 假设节点完整执行；
- rows 是节点**输出**行，不是读取/比较的全部行；
- width 是平均输出 bytes；
- 父节点 cost 包含其子树成本；
- cost 使用由 `seq_page_cost` 等参数构成的相对单位，不是毫秒。

planner 先估 predicate selectivity，再估每个节点 cardinality。一个底层 100 倍误差进入 join 后可能乘成更大误差，改变 join order、algorithm、memory 与 parallelism。因此排查计划常先找“最早出现的大 estimate/actual 偏差”，而不是先看顶层总时间。

路径竞争还受目标影响。带 `LIMIT` 时，低 startup 的路径可能优于完整执行 total 更低的路径；`ORDER BY` 与 index order 匹配时可以省 Sort；参数化 inner path 可让 Nested Loop 每次精确 index lookup。planner 选择的是其估计下的最低 cost，不保证统计错误时仍选到真实最快方案。

禁用 `enable_seqscan` 等 GUC 是诊断对照，不是永久修复。多数 enable flag 只是强烈抬高该路径成本，甚至在没有正确替代时仍会使用并标记 Disabled。对照的价值是回答“如果走另一条路径会怎样”，随后仍要修 SQL、统计、index 或 cost calibration 的根因。

## 7.1.3 计划树的阅读顺序与数据流 {#item-7-1-3}

一套稳定阅读顺序：

1. 先复述 SQL 的结果与参数，不看节点猜业务；
2. 看顶层输出 rows、总时间与是否有 LIMIT/order/aggregate；
3. 从叶子向根追每条数据流；
4. 在每个 node 对比 estimated rows 与 `actual rows × loops`；
5. 找第一处显著偏差与随后放大点；
6. 看 filter/index cond/join filter 分别在哪层生效；
7. 看 buffers、temp、WAL、sort/hash memory 与 worker；
8. 最后结合 wait、客户端时间和并发判断瓶颈。

文本计划的缩进表示 parent/child，不表示实际先后时间。一个节点的 `actual time=a..b` 是每 loop 平均的 first/last row 时间；不能把所有节点时间简单相加，因为父时间包含子时间，pipeline 也会重叠。并行计划的 rows/loops 又可能按 worker 聚合或显示每循环平均，必须回到当前版本字段定义。

以本章参数实验为例，先不评价 Seq/Index：

```text
custom hot:  estimate=90000 actual=90000
custom cold: estimate=10    actual=10
generic:     estimate=100   hot actual=90000 / cold actual=10
```

首先成立的结论是 generic estimate 对 hot 参数错了 900 倍；Seq Scan / Index Scan 的差异是这个 cardinality 与成本模型共同产生的结果。把结论写成“90% 选择率用 Seq Scan、0.01% 用 Index Scan”比“禁止 Seq Scan”更有解释力，但仍只对本表宽度、cache、index 和硬件有效。

计划树最终要翻译成一句因果链：

```text
planner 看见什么统计/参数
  → 估了多少行
  → 为什么认为某路径便宜
  → executor 实际发生什么
  → 哪个可回退改变能验证假设
```

缺少其中任一段，都只是计划描述，不是诊断。

---

[返回本章目录](../) · [下一节：正确使用 EXPLAIN](../02/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
