大家好,我是十三!欢迎来到十三Tech。

去年压测一个慢查询——explain 看执行计划用的是 idx_create_time(很合理,create_time 排序),结果实际跑下来用的是 idx_status(错的离谱,status 才两个值)。用 force index(idx_create_time) 强制指定,查询时间从 3 秒降到 30 毫秒。

这件事让我开始认真研究 Server 层流水线的中间三节——分析器、优化器、执行器。explain 给出的执行计划是优化器的"建议",但执行器实际跑的不一定跟它一致——尤其当统计信息过期、有 hint 干预、或并发场景下 buffer pool 命中情况变化时。

这一篇就聊聊这三个组件怎么协作,以及优化器为什么会"选错"。

分析器、优化器、执行器:Server 层流水线的中间三节

一、分析器——SQL 怎么变成"可执行的结构"

分析器的工作分两步:词法分析语法分析

词法分析把 SQL 拆成一个一个 token——SELECT*FROMusersWHEREid=1。语法分析把这些 token 拼成一棵"解析树"(parse tree),验证 SQL 语法对不对——少个引号、关键字拼错都在这一步报错。

SELECT * FROM users WHERE id = 1;

这一行 SQL 经过分析器后,会变成 MySQL 内部的 AST(抽象语法树),后续的优化器和执行器都基于这棵树工作——不再关心 SQL 原文长什么样。

分析器几乎不会成为性能瓶颈——除非你写的 SQL 极其复杂(嵌套十几层子查询)。99% 的"SQL 慢"跟它无关。

写分析器相关的代码很少需要关心,但有一个细节值得知道:MySQL 不缓存解析结果。同一句 SQL 每次进来都要重新分析一遍——除非你用了 prepared statement。这就是为什么 ORM 框架(MyBatis、Hibernate)都强烈推荐用 ? 占位符 + prepared statement,不仅是防 SQL 注入,也是省分析开销。

二、优化器——执行计划到底谁说了算

分析器把 SQL 变成 AST 后,优化器负责生成"怎么执行"的计划

优化器考虑的事情:

  • 选哪个索引:表上有多个索引时,挑成本最低的
  • join 顺序:多表 join 时,决定先 join 谁、后 join 谁
  • join 算法:Nested Loop、Block Nested Loop、Hash Join(8.0.18+)
  • 是否走临时表:order by、group by 是否需要 Using temporary

优化器的判断基于成本估算——它扫一遍索引统计信息(cardinality、cluster factor),算出每种执行方式的预估行数和 IO 成本,选最低的那一个。

估算就是估算。优化器不知道实际数据分布,依赖统计信息——而统计信息可能过期、可能采样不准、可能在数据倾斜场景下完全失真。开头那个 idx_status 被选中的故事——status 只有两个值(active / inactive),统计信息说"这个索引过滤性很好",但实际上 99% 的数据都是 active,索引根本过滤不掉。

优化器怎么生成执行计划

优化器选错时的应对手段

  • force index(idx_xxx):强制用指定索引
  • ignore index(idx_xxx):强制不用指定索引
  • analyze table:刷新统计信息
  • straight_join:强制 join 顺序

这些是兜底,不是日常手段。让优化器自己选对,比强行干预更可持续

三、执行器——跑计划 + 调 handler

执行器拿到优化器生成的计划后,按计划的步骤逐步执行,每一步通过 handler API 调用存储引擎。

比如执行器看到"用 idx_create_time 扫 1000 行"——它就调 handler 的 index_firstindex_next 接口一行行取数据。每取一行,存储引擎把数据返回给执行器,执行器再决定是否需要过滤、聚合、排序。

这一节的关键细节:执行器是"逐行处理"的,不是"一把梭"。WHERE 条件分两层——存储引擎层能下的(索引条件下推 ICP)就下;不能下的留给执行器做行过滤。ICP 在第 15 篇会详细讲

执行器跑出来的实际行为,跟 explain 显示的计划通常一致,但有几个例外:

  • 统计信息变化(数据被并发写入了)
  • LIMIT 提前结束(explain 显示"扫 1000 行",实际可能扫了 100 行就够 LIMIT 了)
  • buffer pool 命中情况影响 IO 成本(但不影响计划本身)

explain 跟实际为什么对不上

回到开头那个故事——explain 显示 idx_create_time,实际跑 idx_status,原因是:第一次 explain 时统计信息还准,但同一会话内数据被改了,第二次实际执行时优化器重新估算选了别的

更常见的情况是反过来——explain 选了 idx_status,你想验证 idx_create_time 是不是更好,加 force index 强制指定,结果真的更快。这种情况说明优化器被陈旧的统计信息骗了,跑一次 analyze table 通常能修复。

explain 和实际执行对不上的几种情况

执行计划真正教会我的,不是"看懂 explain 三列",而是优化器的判断是估算而非事实——估算可能错,你必须知道什么时候信、什么时候验证、什么时候干预。

下一篇进入日志系统——开始讲一条更新语句怎么不丢数据。


关于十三Tech 资深服务端研发,AI 实践者,专注分享真实可落地的技术经验。 相信 AI 是程序员的最佳搭档。

联系方式:569893882@qq.com GitHub:@TriTechAI