这是《MySQL 8.0 源码与内核实战》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 优化器决定 SQL 的物理执行方式:使用哪个索引、按什么顺序连接表、是否物化子查询、使用哪种聚合策略。它的目标是基于代价模型估计总成本,而不是保证绝对最优。
8.1 输入输出
Query_block
-> logical transformations
-> candidate access methods
-> join order search
-> access method selection
-> physical plan
核心决策:
- 单表访问路径;
- 连接顺序;
- 连接算法;
- 是否使用索引条件下推;
- 是否使用覆盖索引;
- 是否物化派生表;
- 是否使用临时表;
- 排序是否可由索引完成。
8.2 单表访问路径
| 路径 | 说明 |
|---|---|
| table scan | 全表扫描 |
| index scan | 扫描整个索引 |
| range scan | 范围或多个区间 |
| ref / eq_ref | 使用索引等值或前缀匹配 |
| full text | 全文索引 |
| index merge | 合并多个索引 |
| skip scan | 联合索引跳过前导列,受版本和条件限制 |
示例:
CREATE TABLE orders(
id BIGINT PRIMARY KEY,
user_id BIGINT,
status TINYINT,
created_at DATETIME,
KEY idx_user_created(user_id, created_at)
) ENGINE=InnoDB;
EXPLAIN FORMAT=TREE
SELECT * FROM orders
WHERE user_id=100 AND created_at>='2026-01-01';
如果查询只返回少量行,二级索引加回表可能优于全表扫描。
8.3 连接算法
| 算法 | 过程 |
|---|---|
| Nested Loop | 外层每行到内层查找 |
| Block Nested Loop | 外层分块后比较 |
| Hash Join | 构建小表哈希表并探测 |
8.0 逐步增强 hash join,用于无可用索引的等值连接。执行计划中可通过 EXPLAIN FORMAT=TREE 观察 join 节点。
影响连接代价:
- 外层行数;
- 内层访问方式;
- 缓冲区;
- 过滤条件;
- join buffer;
- 回表次数;
- 内存排序。
8.4 连接顺序搜索
多表连接的顺序组合会快速增长:
table count N -> permutations N!
优化器会做剪枝,并受 optimizer_search_depth、optimizer_prune_level 等参数影响。
常见策略:
- 先估计单表过滤后的行数;
- 优先低代价访问路径;
- 依赖连接条件传播;
- 限制搜索深度;
- 保留候选计划;
- 读取系统统计和引擎统计。
8.5 统计信息
InnoDB 统计包括:
- 表行数估计;
- 索引基数;
- 不同值数量;
- 索引层级;
- 聚簇索引大小;
- 二级索引大小。
查看:
SHOW INDEX FROM orders;
ANALYZE TABLE orders;
SELECT * FROM information_schema.innodb_tablestats;
控制方式:
SET GLOBAL innodb_stats_persistent_sample_pages=64;
统计过旧或抽样偏差会导致行数估计错误,进而影响连接顺序。
8.6 条件处理
可传递条件:
t1.id = t2.id AND t1.id = 100
=> t2.id = 100
不可直接使用索引的常见形式:
| 写法 | 影响 |
|---|---|
col+1=10 |
需要表达式重写或无法使用 |
LOWER(col)='a' |
取决于函数和排序规则 |
col LIKE '%x' |
前导通配无法普通 range |
| 隐式类型转换 | 可能无法使用索引 |
| OR 非索引列 | 可能退化为扫描 |
8.7 子查询
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders WHERE status=1);
策略:
- semijoin;
- materialization;
- first match;
- loose scan;
- duplicate weedout;
- correlated subquery execution。
EXPLAIN FORMAT=TREE 和 optimizer trace 能看到选择。8.0 对子查询策略有多轮增强,版本差异明显。
8.8 Optimizer Trace
SET optimizer_trace='enabled=on,one_line=off';
SELECT * FROM orders WHERE user_id=100;
SELECT trace
FROM information_schema.optimizer_trace\G
重点看:
- range 分析;
- 每个索引估计行数;
- 单表访问代价;
- join 顺序候选;
- 最终计划原因;
- 是否物化;
- 是否下推条件。
8.9 Hint
常见 hint:
SELECT /*+ INDEX(orders idx_user_created) */ *
FROM orders WHERE user_id=100;
SELECT /*+ JOIN_ORDER(orders, users) */ *
FROM orders JOIN users ON users.id=orders.user_id;
SELECT /*+ NO_HASH_JOIN(orders, users) */ ...
使用原则:
- 先确认默认计划不合理的原因;
- 用最小范围加 hint;
- 记录版本和数据分布;
- 上线后监控;
- 数据变化后重新评估;
- 不把 hint 当作长期掩盖统计问题的手段。
8.10 源码入口
常见文件:
| 文件 | 职责 |
|---|---|
sql/sql_optimizer.cc |
优化主流程 |
sql/sql_select.cc |
SELECT 逻辑和计划 |
sql/range_optimizer/* |
range 分析 |
sql/join_optimizer/* |
连接优化 |
sql/sql_executor.cc |
计划执行 |
sql/opt_hints.cc |
hint 解析 |
调试断点:
b JOIN::optimize
b mysql_select
b calculate_scan_cost
本章小结
优化器基于统计信息和代价模型,在候选访问路径与连接顺序中做搜索。读源码时要结合 EXPLAIN、optimizer trace 和统计信息,理解行数估计、过滤条件、连接策略和版本差异。
思考题
- 为什么优化器不能保证最优计划?
- 统计信息如何影响连接顺序?
- range、ref、index scan 的代价差异是什么?
- 子查询 semijoin 和物化的适用条件有什么不同?
- optimizer trace 应重点查看哪些字段?