查询优化器:从 AST 到执行计划
「SELECT 怎么写」和「数据怎么取」是两回事。查询优化器就是两者之间的翻译官:把声明式的 SQL 变成命令式的执行计划,并在无数种计划里挑一个最快的。
四阶段流水线
SQL 文本
→ 1. 解析(Parser) → AST
→ 2. 绑定(Binder) → 带类型与语义的 AST
→ 3. 逻辑优化(Logical)→ 逻辑计划(关系代数树)
→ 4. 物理优化(Physical)→ 执行计划(带算子与算法)
→ 执行器(Executor)
解析与绑定
- Parser:把 SQL 文本变成语法树,只检查语法
- Binder:解析表名、列名、类型,做语义检查。
SELECT *在这里展开成具体列,类型不匹配在这里报错
sql示例
SELECT u.name, COUNT(o.id)
FROM users u LEFT JOIN orders o ON u.id = o.user_id
WHERE u.age > 18
GROUP BY u.name
逻辑优化:重写等价计划
逻辑优化做的是不依赖数据的等价变换,典型规则:
| 规则 | 动作 | 收益 |
|---|---|---|
| 谓词下推 | WHERE 条件尽量下推到扫描层 | 尽早过滤行 |
| 投影下推 | 只保留需要的列 | 减少行宽与 IO |
| 连接重排 | 小表先连接 | 减少中间结果 |
| 子查询去关联 | 把相关子查询改写为连接 | 消除逐行执行 |
物理优化:用代价选算法
同一份逻辑计划,物理实现有无数种。比如 JOIN 就有三种基本算法:
| 算法 | 复杂度 | 适用场景 |
|---|---|---|
| Nested Loop | O(n·m) | 小表 + 索引驱动 |
| Hash Join | O(n+m) | 大表等值连接 |
| Merge Join | O(n+m) | 已排序输入 |
优化器根据统计信息估算每种方案的代价(磁盘 I/O + CPU),选最小者:
代价模型 ≈ 扫描行数 × 行宽 + 算子成本 + 网络/内存成本
统计信息:优化器的眼睛
优化器是「猜」的,但猜得有依据——依据就是统计信息:
- 行数与页数:表多大,扫全表要多少 I/O
- 直方图:列值分布,估算
WHERE col > 100能过滤多少行 - 基数估算:
JOIN后中间结果多大,决定连接顺序
⚠️ 经典翻车现场: 统计信息过期(比如直方图还是半年前的数据),优化器可能把一个秒级查询变成分钟级——这就是为什么 DBA 常挂在嘴边「
ANALYZE一下」。
常见的执行计划陷阱
- 隐式类型转换:
WHERE phone = 138xxxx对字符串列做数字比较,索引失效 - 函数包裹列:
WHERE DATE(created_at) = '2025-01-01'让索引失效,应改写为范围条件 - SELECT *:把不需要的列带进所有层,放大 IO 与网络
小结
查询优化器是把「正确」翻译成「高效」的工程:逻辑优化用等价变换缩小解空间,物理优化用代价模型逼近最优,统计信息则是所有估算的底气。理解它的四阶段,你就知道「为什么一条 SQL 快、另一条慢」的答案通常不在写法,而在数据分布。