跳至内容
7.1 优化器如何选择路径

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 混为一谈。

中间节点转换数据流:

责任常见节点关键观察
joinNested Loop / Hash Join / Merge Joinouter rows、inner loops、hash/sort 输入
orderSort / Incremental Sortkey、method、memory、disk spill
aggregateAggregate / HashAggregate / GroupAggregategroup estimate、batches、memory/disk
reuseMaterialize / Memoize重复读取是否被缓存,命中/溢出
combineAppend / Merge Appendchild/partition 数与裁剪
parallelGather / Gather Mergeplanned/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 计划树的阅读顺序与数据流

一套稳定阅读顺序:

  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:

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 · 查看全书目录 · 查看索引中心

最后更新于