MySQL SQL执行流程深度解析:Server层与引擎层的慢查询定位实践

举报
数据库小学妹 发表于 2026/09/21 10:04:26 2026/09/21
【摘要】 从一次接口超时告警切入,把一条 SQL 在 MySQL 里的完整路径拆成七段(连接器、查询缓存、解析器、预处理器、优化器、执行器、存储引擎),用 optimizer_trace 和 Handler_read 变量判断慢在哪一段,并给出慢查询排查的顺序与三个避坑经验。

大家好,我是数据库小学妹👋 我踩过的坑,你别再踩。

上周三凌晨,一个订单查询接口的 P99 从 40 毫秒掉到了 3 秒。告警响的时候我正在改另一篇稿子。我把慢日志里那条 SQL 抄出来,翻来覆去看了一个小时。有索引,条件不复杂,没子查询,没排序,我找不出毛病。后来我拉了两个数出来看。一个是执行器向存储引擎要数据的次数,那条 SQL 只返回 20 行,引擎层却被问了上百万次。另一个是优化器的决策记录,它选的路径也不是我预期的那条。

那一刻我才明白,我一直在 SQL 文本里找答案。答案不在文本里,在它被执行的这条路上。

一条 SQL 要过七道关

我刚转行那会儿,以为 SQL 发出去,数据库就直接去磁盘翻数据了。不是这么回事。它得先被认出来,被检查,被改写,被规划,最后才由执行器一行一行去向存储引擎要数。

我把它拆成七段。前六段在 Server 层,最后一段在引擎层。这个分层最有用的一点是,你看到的慢,可能压根还没碰到数据。

阶段 干什么 出问题的症状
连接器 握手、认证、授权 连接数爆满、认证慢
查询缓存 按 SQL 文本查缓存(8.0 已移除) 现在是历史包袱
解析器 词法语法分析,生成语法树 报错指向某个 token
预处理器 查表、列、权限,展开视图 表不存在、无权限
优化器 选访问路径、选 JOIN 顺序 执行计划突变、索引不走
执行器 调引擎接口,逐行取数 取数次数爆炸
存储引擎 索引定位、Buffer Pool、回表、加锁 锁等待、刷脏、IO 打满

前面四段:SQL 还没开始跑

连接器是唯一跟 SQL 内容没关系的一段。TCP 握手,验证账号密码,从权限表读权限缓存起来。这个连接从此独占一个线程,直到断开。

权限缓存这件事值得记一笔。你在会话中途改了权限,有时候不生效,得重新连一次才认。长连接还会攒内存。我现在的习惯是让连接池配好最大存活时间,别让一个连接活到天荒地老。

第二段是查询缓存。它在 MySQL 8.0 被整个删掉了。我刚知道的时候有点意外,毕竟听着挺划算。后来想明白了,它按整张表失效。这张表只要有任意一次写入,表上所有缓存全废。对写多读少的业务来说,维护成本比省下来的还高。而且每次查缓存都要加锁。删得对。这件事也提醒我,缓存的失效粒度比缓存本身更值得琢磨。

第三段解析器干两件事。词法分析把一长串字符切成一个个 token。语法分析再按规则把它们拼成语法树。你写错语法时看到的那个 near ‘xxx’,就是它卡住的位置。第四段预处理器拿着语法树去数据字典核对,确认表和列真的存在,你真有权限。

这两段快到几乎零成本。但它们说明一件事,SQL 进数据库后的第一步不是执行,是理解。

第五段:优化器,路真正分岔的地方

前四段是确定性的,同一句 SQL 每次走的路一样。到优化器就变了。它要在好几条能走通的路里挑一条。挑什么?全表扫还是走索引,走哪个索引,多表 JOIN 先连哪两张,用哪种连接算法。挑的依据是代价估算,估算靠统计信息和代价常量。

我那个告警的答案就在这儿。我打开 optimizer_trace,看到了它的决策过程。它比了三条访问路径,选了一条跟我预期完全不同的。原因是它算错了这张表要返回多少行。

为什么算错,因为统计信息是随机采样出来的。采样页数有限,数据一倾斜它就看不准。这块内容够单独写一篇,今天先记住一个结论:优化器每次选路都基于估算,不基于事实。它每次都可能改主意。

-- 打开优化器追踪,只对当前会话生效
SET optimizer_trace = 'enabled=on';
SET optimizer_trace_max_mem_size = 1048576;

-- 把要查的 SQL 原样跑一遍
SELECT id, order_no, amount
FROM orders
WHERE user_id = 1024 AND status = 3
ORDER BY created_at DESC
LIMIT 20;

-- 看优化器的完整决策记录
SELECT TRACE FROM information_schema.optimizer_trace\G

第六、七段:逐行的真相

拿到执行计划以后,执行器开始干活。它会再判一次权限。然后按计划里的算子一层层往下走,向存储引擎要数据。

关键在"逐行"两个字。它不是一批一批地拿。它打开接口以后一行一行地取,取够条件才往上交。所以一条只返回 20 行的 SQL,引擎层可能被调了上百万次。大部分行在半路就被丢掉了。

-- 看执行器和引擎之间到底交互了多少次
SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.session_status
WHERE VARIABLE_NAME IN (
  'Handler_read_first', 'Handler_read_key',
  'Handler_read_next', 'Handler_read_rnd_next'
);

第七段进 InnoDB。走主键索引直接定位到页。走二级索引就先拿主键再回表,除非是覆盖索引。Buffer Pool 命中就不用读盘,没命中才从磁盘读页。锁也在这一层加,行锁在引擎层,MDL 在 Server 层。

我以前排障是一上来就翻慢日志。现在中间会插一步,先看一眼 Handler_read 这几个变量。交互次数远远超过返回行数,那问题多半在"取了很多又丢掉",往执行计划的方向查。次数正常但还是很慢,那更可能是锁等待或者 IO,得换条路查。

避坑清单

optimizer_trace 有内存上限,超了就截断,结尾看不到。它还是会话级的,换个连接就没了。如果想在事后复现,需要在问题复现时当场开着。

SHOW PROFILE 这两个词网上教程到处都是。但它在较新的版本里已经不是推荐做法了,performance_schema 才是正经路子。具体到你手上的版本还支不支持,动手前先确认,别照着老文章抄。

最后一条是我自己搞错的。我以前看 EXPLAIN 只看 type 列,看到 ref 或者 range 就觉得稳了,从来不看 rows。后来才知道 rows 是估算值,那个数字本身可能就是优化器算偏的结果。现在我习惯把 rows 和 EXPLAIN ANALYZE 出来的实际行数放一起比。EXPLAIN ANALYZE 会真正执行 SQL,生产环境慎用,建议在测试环境或只读副本上操作。两者差得离谱,就先别急着调 SQL,去治统计信息。

写在最后

这次告警教我一件事,定位 SQL 慢不能只盯着 SQL,得知道它此刻走在哪一段。卡在连接,卡在锁,还是卡在选路上,这三类的排查手法完全不一样。一上来就翻慢日志,等于把七个病人塞进一个诊室。

我不是说慢日志没用。它是入口,不是答案。我现在的顺序是先看慢日志拿到 SQL,再用 EXPLAIN 看计划,用 optimizer_trace 看优化器怎么想的,用 Handler 变量看执行器怎么要数的,最后才去猜引擎层。

一条 SQL 跑得慢,从来不是 SQL 自己的错。它只是老老实实走完了这七段路,把每一段的耗时加总还给你。你要做的,是找出是哪一段。

你上一次排查慢 SQL,卡在哪一段?评论区聊聊。

我是数据库小学妹,帮你少走弯路少踩坑,咱们下篇见👋

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。