乐于分享
好东西不私藏

从源码看MySQL [1]|一条SQL的完整旅程:从网络包到返回结果

从源码看MySQL [1]|一条SQL的完整旅程:从网络包到返回结果

本系列:《高性能MySQL第三版》源码级伴读,每篇"书本理论+源码分析+实测验证"三位一体

实验环境:MySQL 8.0.46-debug(按 #0-A 编译)

本文是 Phase 1 第 1 篇:建立全局视角


一、场景:一条 SELECT 走完所有模块

先看一个真实的故障:

❓ 场景:DBA同学问我"我的SQL很慢,怎么定位是哪一层的问题?"

回答之前,我们得先知道:一条 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]

关键观察

  1. SQL 层
    有 4 个入口函数(do_command → dispatch_command → dispatch_sql_command → mysql_execute_command
  2. 优化器核心
    在 sql/sql_optimizer.cc(11830 行)+ sql/sql_planner.cc,入口是 Query_block::optimize()
  3. 存储引擎入口
    是 handler 接口(虚函数表),InnoDB 继承实现为 ha_innobase
  4. 真正读数据
    要穿过 4 个 InnoDB 子系统:handler → row(row0sel) → buf(buf0buf) → btr(btr0cur)

📌 注意:8.0 把 5.7 时代的 mysql_parse 重命名为 dispatch_sql_commandSELECT_LEX 改名为 Query_block。如果你看网上老文章里提到 mysql_parse / SELECT_LEX::optimize,在 8.0 源码里是找不到的。


四、实测:观测每一步的真实耗时

我用 performance_schema 跟踪上面这条 SQL 的执行:

SQL
-- 开启详细追踪UPDATEperformance_schema.setup_instruments  SET ENABLED='YES', TIMED='YES'WHERE NAME LIKE'statement/%'OR NAME LIKE'stage/%';

执行查询后,从 events_stages_history_long 看每阶段耗时:

实验准备:先创建测试表并插入数据

SQL
-- 创建测试数据库和表CREATEDATABASEIFNOTEXISTS testdb;USE testdb;CREATETABLE `orders` (   `id` int NOTNULLAUTO_INCREMENT,   `user_id` int NOTNULL,   `amount` decimal(10,2NOTNULL,   `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, statusVALUES (110099.990), (5105199.990), (10110299.990)ON DUPLICATE KEYUPDATE amount=VALUES(amount);

实验输出(在MySQL 8.0.46-debug实例上实测):

SQL
-- 开启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 版本上通常在亚毫秒级。


五、源码核心函数速查表

后续文章会反复引用这些函数,先记一下位置:

阶段
函数
源码位置(8.0.46 核对)
后续篇章
1. 命令分发
do_command()
sql/sql_parse.cc:1308
-
2. 协议解析
dispatch_command()
sql/sql_parse.cc:1688
-
3. SQL 解析
dispatch_sql_command()
sql/sql_parse.cc:5254
#3
 详细
4. 命令执行
mysql_execute_command()
sql/sql_parse.cc:2948
-
5. 查询优化
Query_block::optimize()
sql/sql_select.cc:1986
#19
 详细
6. 查询执行
Query_expression::execute()
sql/sql_union.cc:1821
#21
 详细
7. 引擎入口
handler::ha_index_read_map()
sql/handler.cc:3227
#4
 详细
8. InnoDB 读
row_search_mvcc()
row/row0sel.cc:4420
#9
 详细
9. 页面读取
buf_page_get_gen()
buf/buf0buf.cc:4432
#6
 详细
10. B+树查找
btr_cur_search_to_nth_level()
btr/btr0cur.cc:619
#7
 详细

六、实战启示:怎么用这张图定位性能问题?

1. 卡在"解析"还是"执行"?

SQL
-- 看 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 层入口函数就够了

Bash
"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 看每阶段耗时,定位到具体函数
怎么看 SQL 经过了哪些模块
用 general_log + performance_schema 联合观测
怎么"读源码"
从 5 个关键函数入口读(见上表)
怎么证明 SQL 走了主键
Handler_read_key
 增加 = 用了索引

八、本篇作业 & 下期预告

作业

  1. 在你的 MySQL 实例上执行本文第 4 节的 UPDATE performance_schema.setup_instruments 那段 SQL
  2. 执行一条 SELECT * FROM orders WHERE id=1(先建表)
  3. 查询 SHOW SESSION STATUS LIKE 'Handler_read%',看看哪些值增加了
  4. 打开 /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》第三版,电子工业出版社
  • 未经作者许可,禁止转载或用于商业用途