查询优化与执行
PostgreSQL 的优化器是代价优化器:为每条 SQL 枚举可能的执行路径,按估算代价选最便宜的。理解代价模型,才知道执行计划为什么长这样,以及怎么调。
代价模型
每条路径都有估算代价(cost 单位,不是毫秒):
- 顺序扫描:
seq_page_cost(默认 1.0)乘页数 - 索引扫描:
random_page_cost(默认 4.0)乘页数加索引遍历 - CPU 代价:
cpu_tuple_cost等
代价估算的输入是统计信息:ANALYZE 收集的每列分布、相关性、NULL 比例。统计信息不准,代价模型再精细也是错的方向。“先查统计,再查计划”是 PG 优化的第一原则。default_statistics_target 控制采样精度,关键列可调大。
执行计划怎么读
EXPLAIN (ANALYZE, BUFFERS) SELECT ...阅读顺序:
- 从叶子(扫描节点)往根读
- 对比 estimated rows 和 actual rows,找第一处大的估算偏差。偏差会沿连接逐层放大,改变连接顺序和算法
- 看每个节点的 loops(嵌套循环内层会执行多次,实际行数要乘 loops)
- 看 BUFFERS 的命中(hit)和读取(read)判断是否吃缓存
常见节点:
| 节点 | 含义 | 关注点 |
|---|---|---|
| Seq Scan | 顺序扫描 | 小表或低选择性时合理 |
| Index Scan | 索引扫描 | 是否用了对的索引 |
| Index Only Scan | 仅索引扫描 | 依赖可见性映射 |
| Bitmap Scan | 位图扫描 | 多个索引组合 |
| Nested Loop | 嵌套循环 | 内层 loops 次数 |
| Hash Join | 哈希连接 | hash 内存是否落盘 |
| Merge Join | 归并连接 | 两侧是否有序 |
三种连接算法
- Nested Loop:外层每行去内层找,适合小表驱动大表、内层走索引
- Hash Join:小表建哈希,大表探测,适合无索引的大连接
- Merge Join:两侧按连接键排序后归并,适合已有序或等值连接
优化器根据行数估算选,work_mem 不足时 Hash Join 会落盘(临时文件),是常见性能瓶颈。
并行查询
PG 支持查询级并行:计划中出现 Gather/Gather Merge 节点,多个 worker 并行扫描。收益最大的场景是“访问大量数据、返回少量行”的分析查询。三个相关参数:max_parallel_workers_per_gather(单查询并行度)、max_parallel_workers(全局)、max_worker_processes(总进程上限)。OLTP 小查询不建议并行,启动 worker 的开销大于收益。
面试追问
- 代价优化器怎么工作? 枚举执行路径按估算代价选最便宜。代价基于页数和 CPU 成本的加权,输入是统计信息
- EXPLAIN ANALYZE 看什么? 对比估算行数和实际行数找偏差,偏差沿连接放大。先修统计信息再调 SQL
- 三种连接算法怎么选? 小表驱动大表用 Nested Loop,无索引大连接用 Hash Join,两侧有序用 Merge Join
- 统计信息不准怎么办? ANALYZE 更新,调大 default_statistics_target,对关键列做扩展统计。统计错了优化器选什么计划都是错的
- 并行查询什么时候有效? 大表扫描返回少量行。小查询并行开销大于收益,OLTP 不要开