7.1 优化器如何选择路径
SQL 描述结果关系,planner 则要在等价实现中选择一棵可执行树。选择发生在当前 catalog、statistics、parameter visibility、planner GUC 和 cost constants 下;环境改变,最便宜路径也可能改变。
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 成本、选择率、行数与路径竞争
计划行:
(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 计划树的阅读顺序与数据流
一套稳定阅读顺序:
- 先复述 SQL 的结果与参数,不看节点猜业务;
- 看顶层输出 rows、总时间与是否有 LIMIT/order/aggregate;
- 从叶子向根追每条数据流;
- 在每个 node 对比 estimated rows 与
actual rows × loops; - 找第一处显著偏差与随后放大点;
- 看 filter/index cond/join filter 分别在哪层生效;
- 看 buffers、temp、WAL、sort/hash memory 与 worker;
- 最后结合 wait、客户端时间和并发判断瓶颈。
文本计划的缩进表示 parent/child,不表示实际先后时间。一个节点的 actual time=a..b 是每 loop 平均的 first/last row 时间;不能把所有节点时间简单相加,因为父时间包含子时间,pipeline 也会重叠。并行计划的 rows/loops 又可能按 worker 聚合或显示每循环平均,必须回到当前版本字段定义。
以本章参数实验为例,先不评价 Seq/Index:
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 和硬件有效。
计划树最终要翻译成一句因果链:
planner 看见什么统计/参数
→ 估了多少行
→ 为什么认为某路径便宜
→ executor 实际发生什么
→ 哪个可回退改变能验证假设缺少其中任一段,都只是计划描述,不是诊断。
返回本章目录 · 下一节:正确使用 EXPLAIN · 查看全书目录 · 查看索引中心