本系列:《高性能MySQL第三版》源码级伴读,每篇"书本理论+源码分析+实测验证"三位一体
实验环境:MySQL 8.0.46-debug(按 #0-A 编译)
本文是 Phase 1 第 1 篇:建立全局视角
一、场景:一条 SELECT 走完所有模块
先看一个真实的故障:
❓ 场景:DBA同学问我"我的SQL很慢,怎么定位是哪一层的问题?"
回答之前,我们得先知道:一条 SQL 到底走过哪些模块? 知道路径,才能定位"卡在哪一站"。
我们跟踪这一条最普通的查询:
SELECT * FROM orders WHERE id = 5;
它在我们编译的 MySQL 8.0.46-debug 实例上,实际经过了 7 个主要阶段。
二、书上怎么说?——《高性能MySQL》关于查询生命周期的描述
《高性能MySQL第三版》第1章"MySQL架构与历史"中概括描述了查询的执行路径:
「从客户端到服务器,查询要经过连接管理、查询解析、查询优化、查询执行、数据返回等阶段。」
书里也指出:
- MySQL 采用分层架构
:上层是 SQL 层(连接/解析/优化/执行),下层是存储引擎层 - 优化器是核心
:决定 SQL 怎么执行,比单纯执行 SQL 更耗时 - 存储引擎以插件形式存在
:通过 Handler API 与 SQL 层交互
但书没讲的是:
❌ 每一步的真实耗时占比怎么观测 ❌ 具体源码函数是什么 ❌ 怎么用 performance_schema看到每一步的时延
这些是这个系列要补的。
三、源码追踪:一条 SQL 的 7 站旅程
我用 gdb attach + performance_schema 跟踪,得到下面这条真实路径:
[client] ↓ 1. TCP 网络包(SQL文本) [MySQL Server — SQL 层] ↓ 2. do_command() ← sql/sql_parse.cc:1308 ↓ 3. dispatch_command(COM_QUERY) ← sql/sql_parse.cc:1688 ↓ 4. dispatch_sql_command() ← sql/sql_parse.cc:5254 ↓ ├─ 词法分析 (MYSQLlex) ↓ ├─ 语法分析 (Bison → sql_yacc.yy) ↓ └─ 生成解析树 ↓ 5. mysql_execute_command() ← sql/sql_parse.cc:2948 ↓ 6. Query_block::optimize() ← sql/sql_select.cc:1986 ↓ ├─ 成本估算(opt_costmodel.cc) ↓ ├─ 索引选择(sql_planner.cc) ↓ └─ JOIN 顺序优化 ↓ 7. Query_expression::execute() ← sql/sql_union.cc:1821 ↓ └─ handler 接口 → InnoDB [InnoDB — 存储引擎层] ↓ 8. ha_innobase::index_read() ← ha_innodb.cc:10189 ↓ 9. row_search_mvcc() ← row0sel.cc:4420 ↓ 10. buf_page_get_gen() ← buf0buf.cc:4432 ↓ 11. btr_cur_search_to_nth_level() ← btr0cur.cc:619 ↓ 12. 返回结果 [client]关键观察:
- SQL 层
有 4 个入口函数( do_command→dispatch_command→dispatch_sql_command→mysql_execute_command) - 优化器核心
在 sql/sql_optimizer.cc(11830 行)+sql/sql_planner.cc,入口是Query_block::optimize() - 存储引擎入口
是 handler接口(虚函数表),InnoDB 继承实现为ha_innobase - 真正读数据
要穿过 4 个 InnoDB 子系统:handler → row(row0sel) → buf(buf0buf) → btr(btr0cur)
📌 注意:8.0 把 5.7 时代的 mysql_parse 重命名为 dispatch_sql_command,SELECT_LEX 改名为 Query_block。如果你看网上老文章里提到 mysql_parse / SELECT_LEX::optimize,在 8.0 源码里是找不到的。
四、实测:观测每一步的真实耗时
我用 performance_schema 跟踪上面这条 SQL 的执行:
-- 开启详细追踪UPDATEperformance_schema.setup_instruments SET ENABLED='YES', TIMED='YES'WHERE NAME LIKE'statement/%'OR NAME LIKE'stage/%';
执行查询后,从 events_stages_history_long 看每阶段耗时:
实验准备:先创建测试表并插入数据
-- 创建测试数据库和表CREATEDATABASEIFNOTEXISTS testdb;USE testdb;CREATETABLE `orders` ( `id` int NOTNULLAUTO_INCREMENT, `user_id` int NOTNULL, `amount` decimal(10,2) NOTNULL, `status` tinyint DEFAULT'0', `created_at` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARYKEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=10DEFAULTCHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;-- 插入测试数据INSERTINTO orders (id, user_id, amount, status) VALUES (1, 100, 99.99, 0), (5, 105, 199.99, 0), (10, 110, 299.99, 0)ON DUPLICATE KEYUPDATE amount=VALUES(amount);
实验输出(在MySQL 8.0.46-debug实例上实测):
-- 开启performance_schema追踪UPDATEperformance_schema.setup_instruments SET ENABLED='YES', TIMED='YES'WHERE NAME LIKE'statement/%'OR NAME LIKE'stage/%';-- 执行测试查询SELECT * FROM testdb.orders WHERE id=5; /* +----+---------+--------+--------+---------------------+| id | user_id | amount | status | created_at | +----+---------+--------+--------+---------------------+| 5 | 105 | 199.99 | 0 | 2026-06-2716:16:06 | +----+---------+--------+--------+---------------------+*/-- 在同一session中查看Handler_read%状态SHOWSESSIONSTATUSLIKE'Handler_read%'; /* +-----------------------+-------+| Variable_name | Value | +-----------------------+-------+| Handler_read_first | 0 | | Handler_read_key | 1 | ← 用索引读了一次(走主键) | Handler_read_last | 0 | | Handler_read_next | 0 | ← 没用到范围扫描 | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 0 | ← 没用全表扫描 +-----------------------+-------+*/
关键结论:
这条 SQL 走了主键索引( Handler_read_key=1)没有范围扫描( Handler_read_next=0)没有全表扫描( Handler_read_rnd_next=0)
💡 想知道具体耗时,可以用 EXPLAIN ANALYZE 或 performance_schema.events_stages_history_long 看每阶段时延(见下面第六节的 SQL)。对主键点查这类简单 SQL,在 Debug 版本上通常在亚毫秒级。
五、源码核心函数速查表
后续文章会反复引用这些函数,先记一下位置:
do_command() | |||
dispatch_command() | |||
dispatch_sql_command() | #3 | ||
mysql_execute_command() | |||
Query_block::optimize() | #19 | ||
Query_expression::execute() | #21 | ||
handler::ha_index_read_map() | #4 | ||
row_search_mvcc() | #9 | ||
buf_page_get_gen() | #6 | ||
btr_cur_search_to_nth_level() | #7 |
六、实战启示:怎么用这张图定位性能问题?
1. 卡在"解析"还是"执行"?
-- 看 stages 表的耗时分布SELECT EVENT_NAME, SUM(TIMER_WAIT)/1000000AS total_ms, COUNT(*) AS callsFROMperformance_schema.events_stages_history_longWHERE THREAD_ID = (SELECT THREAD_ID FROMperformance_schema.threads WHERE PROCESSLIST_ID = <你的连接ID>)GROUPBY EVENT_NAMEORDERBY total_ms DESCLIMIT10;
stage/sql/Starting占比高 → 网络/权限问题 stage/sql/checking permissions占比高 → 权限或视图问题 stage/sql/optimizing占比高 → 优化器问题(看 #19) stage/sql/statistics占比高 → 统计信息过时(#20) stage/sql/sending data占比高 → 真正的执行慢(看执行计划)
2. 怎么"看源码"?
不需要全部读完。对运维来说,看 4 个 SQL 层入口函数就够了:
"color: #6a737d; font-style: italic;"># 装个 ctags 或用 IDE(如 vscode + C/C++ 插件) cd /root/mysql-8.0.46 grep -n "do_command\|dispatch_command\|dispatch_sql_command\|mysql_execute_command" sql/sql_parse.cc | head -20
3. 这张图怎么用?
把图片保存,遇到性能问题按图索骥:
网络慢?→ 阶段 1-2 解析慢?→ 阶段 3-4 优化器卡?→ 阶段 5-6 存储引擎慢?→ 阶段 7-10
七、本篇对应解决的运维问题
performance_schema.stages 看每阶段耗时,定位到具体函数 | |
general_log + performance_schema 联合观测 | |
Handler_read_key |
八、本篇作业 & 下期预告
作业
在你的 MySQL 实例上执行本文第 4 节的 UPDATE performance_schema.setup_instruments那段 SQL执行一条 SELECT * FROM orders WHERE id=1(先建表)查询 SHOW SESSION STATUS LIKE 'Handler_read%',看看哪些值增加了打开 /root/mysql-8.0.46/sql/sql_parse.cc,搜do_command看 30 行
下期预告
Phase 1 第 2 篇|从源码看连接管理:max_connections 为什么不能设太大——从 SQL 旅程的第 1 站"连接层"展开。我们会实测:1000 个 idle 连接到底吃掉多少内存,以及为什么 max_connections=1000 是个危险值。
如果觉得有帮助,请点赞、在看、转发三连。关注本公众号,不错过后续 30 篇源码级深度解析。
一条简单的 SQL,背后是 12 个函数的协作——下一站,我们进入连接层。
⚖️ 版权声明:
本文为原创技术分析文章,非官方授权翻译或转载 对《高性能MySQL第三版》的引用仅限简短概括,用于评论和教学,符合《著作权法》第24条合理使用 MySQL 源码引用遵循 GPL v2 开源协议,用于教育目的的代码片段分析不构成"分发" "MySQL"为 Oracle Corporation 注册商标,本文使用仅为技术描述 建议购买原书深入学习:《高性能MySQL》第三版,电子工业出版社 未经作者许可,禁止转载或用于商业用途
夜雨聆风