这是《MySQL 8.0 源码与内核实战》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 优化器生成计划后,执行器负责驱动迭代器、调用 handler 接口、计算表达式、执行聚合排序,并处理写入路径。MySQL 8.0 的执行模型相比老版本更明显地转向迭代器式组织。
9.1 执行模型
Query_expression
-> iterator root
|-- table scan iterator
|-- index range iterator
|-- filter iterator
|-- join iterator
|-- aggregate iterator
|-- sort iterator
+-- limit iterator
每个迭代器常见接口:
init()
read()
set_key()
close()
执行时由上层节点驱动下层节点读取行,再应用过滤和投影。
9.2 读路径
Server 层:
Executor iterator
-> handler::ha_index_read / ha_rnd_next
-> ha_innobase
-> row_search_mvcc
| 调用 | 场景 |
|---|---|
ha_rnd_next |
全表或临时表顺序读 |
ha_index_first |
索引第一条 |
ha_index_next |
索引顺序读 |
ha_index_read_map |
按 key 查找 |
ha_index_next_same |
相同前缀继续读 |
9.3 表达式计算
表达式由 Item 树执行:
WHERE a + 1 > 10 AND b = 'x'
-> Item_cond_and
|-- Item_func_gt
| |-- Item_func_plus
+-- Item_func_equal
执行关注:
- NULL 语义;
- 类型转换;
- 字符集排序;
- 短路求值;
- 函数确定性;
- 错误传播;
- 资源消耗。
9.4 过滤与投影
SELECT user_id, amount
FROM orders
WHERE status=1;
执行流程:
read row
-> evaluate status condition
-> if false skip
-> evaluate user_id, amount
-> send to client / upper iterator
投影影响:
- 返回字段;
- 覆盖索引可能性;
- 网络流量;
- 临时表列;
- 结果元数据。
9.5 聚合与分组
SELECT user_id, COUNT(*), SUM(amount)
FROM orders
GROUP BY user_id;
两种路径:
| 路径 | 条件 |
|---|---|
| 流式聚合 | 按分组列顺序读取,索引顺序匹配 |
| 临时表聚合 | 无序数据或内存/磁盘临时表 |
8.0 移除了查询缓存,但临时表引擎和聚合执行仍在持续演进。查看:
EXPLAIN FORMAT=TREE SELECT ...;
SHOW STATUS LIKE 'Created_tmp%';
SHOW STATUS LIKE 'Sort%';
9.6 排序与 LIMIT
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 20;
可能方式:
- 使用索引顺序;
- priority queue top N;
- 内存 sort buffer;
- 文件排序;
- 磁盘临时文件合并。
相关参数:
| 参数 | 说明 |
|---|---|
sort_buffer_size |
每会话排序缓冲 |
max_length_for_sort_data |
影响附加列策略 |
max_sort_length |
字符串排序长度 |
9.7 写入路径
INSERT:
mysql_insert
-> write_row
-> handler::ha_write_row
-> ha_innobase::write_row
-> row_insert_for_mysql
UPDATE:
read row
-> evaluate set expressions
-> handler::ha_update_row
-> InnoDB row update
-> undo / redo / index maintenance
DELETE:
read row
-> handler::ha_delete_row
-> InnoDB delete mark
-> purge later
9.8 多表更新
UPDATE orders o
JOIN users u ON u.id=o.user_id
SET o.level=u.level
WHERE u.status=1;
执行器先按计划读取满足连接条件的行,再逐行计算 SET 表达式并调用更新。触发器、外键和唯一索引都会增加写入路径复杂度。
9.9 执行观测
SELECT event_name, count_star, sum_timer_wait
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC
LIMIT 10;
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id=100;
EXPLAIN ANALYZE 能查看实际行数和每个迭代器耗时,适合与估算计划对比。
9.10 常见问题
| 现象 | 排查 |
|---|---|
| 估算行数差异大 | 统计和 trace |
| 临时表暴涨 | 分组、排序、派生表 |
| 排序落盘 | sort buffer 和索引 |
| 回表过多 | 覆盖索引和投影 |
| 写入慢 | 唯一检查、redo、锁 |
| LIMIT 仍慢 | 前置扫描量 |
本章小结
执行器把计划转换为迭代器驱动和 handler 调用,负责过滤、投影、连接、聚合、排序和写入。源码阅读应从 EXPLAIN FORMAT=TREE 的节点入手,找到对应迭代器,再进入 InnoDB 行操作。
思考题
ha_rnd_next和ha_index_read的使用场景有什么不同?- 聚合什么时候可以流式执行?
- 为什么投影列会影响覆盖索引?
- top N 排序可能使用什么策略?
EXPLAIN ANALYZE与普通EXPLAIN的区别是什么?