SQL语句在MySQL中的执行过程
一条 SQL 从客户端发出到拿到结果,要依次穿过连接器、分析器、优化器、执行器,最终落到存储引擎。搞懂这条链路上每个组件的职责与协作方式,才能在面对慢查询、权限异常、宕机丢数据等问题时,把故障定位到正确的层级。
它解决什么问题
MySQL 在架构上把「理解 SQL」和「存取数据」拆成了两层:Server 层管语法、权限、执行计划,引擎层管数据页、锁与事务日志。很多线上问题之所以排查起来绕弯路,就是因为不知道故障发生在哪一层——权限改了不生效是连接器的事,有索引却走全表是优化器的事,宕机后数据不一致是日志的事。执行链路图就是数据库问题的「分诊台」:先定位层级,再谈手段。
核心原理:Server 层与引擎层的分工
Server 层包含连接器、分析器、优化器、执行器,以及 binlog 模块、内置函数、视图、触发器等跨引擎能力——这些功能与数据如何存储无关,所以收在公共层。引擎层是插件式架构,InnoDB、MyISAM、Memory 各自实现数据的存储与读取;InnoDB 自 MySQL 5.5 起成为默认引擎,并独享 redo log 模块。
一条查询语句的流转顺序如下:
客户端 → 连接器(认证/取权限) → [查询缓存,8.0 已移除] → 分析器(词法/语法) |
深入机制
连接器:认证与权限快照
连接建立在 TCP 之上,连接器校验账号密码通过后,会把该用户的权限一次性读出来缓存住,此后这个连接内的权限判断都使用这份快照。这解释了一个经典现象:管理员中途修改了某用户的权限,已存在的老连接完全不受影响,必须断开后重建才生效。连接本身成本不低,服务端用 wait_timeout 回收空闲连接(默认 8 小时),客户端侧则普遍用连接池做复用;长事务会占着连接不放,是拖垮连接池的常见原因。
查询缓存:一个被删除的组件
8.0 之前,SELECT 及其结果集会以「SQL 原文为 key、结果集为 value」缓存在内存中,命中即直接返回。它失败在失效粒度过粗:表上任何一次写入,都会让这张表的全部查询缓存即刻清空。在读写混合的 OLTP 场景里,命中率低到得不偿失,缓存自身还带全局锁竞争。于是 8.0 把该功能整体移除,将「缓存结果」的职责交还给应用层——Redis 或本地缓存的失效策略可以按业务语义设计,远比一刀切精细。
分析器与优化器
分析器分两步:先做词法分析,把字符串切出关键字、表名、列名;再做语法分析,生成解析树,著名的 You have an error in your SQL syntax 就报在这一关。优化器拿到解析树后决定「怎么执行」:多个索引选哪个、多表 JOIN 谁当驱动表、要不要启用 ICP、MRR 等特性。它基于成本模型做估算,而成本依赖统计信息——统计失真时优化器会选错方案,这正是需要用 EXPLAIN 人工复核的原因。
执行器:按方案驱动引擎
执行前会再针对具体的表做一次权限校验,通过后按执行计划循环调用引擎接口:取满足条件的第一行、再取下一行、逐行判断并累加结果,直到扫描结束。慢查询日志里的 Rows_examined 记录的就是引擎实际返回的行数,这个值往往比 EXPLAIN 里的估算行数更能反映真实代价。
一条 UPDATE 的完整时序
更新语句走同样的链路,但多了日志写入。以 InnoDB 为例:
1. 执行器调引擎接口定位目标行,修改内存中的数据页 |
第 2 到 4 步就是 redo log 的两阶段提交。它要解决的问题是:redo log 属于引擎层、服务崩溃恢复,binlog 属于 Server 层、服务复制与归档,两份日志相互独立;若一前一后写完的间隙宕机,主库按 redo 恢复出的数据和从库按 binlog 重放出的数据就会分叉。拆成 prepare 与 commit 后,崩溃恢复时以「binlog 中是否存在该事务的 XID」为裁判:redo 停在 prepare 但 binlog 完整,则补提交;binlog 缺失,则回滚。两份日志被 XID 严格对齐,主从不会各说各话。
change buffer:把随机读推迟成内存合并
普通二级索引的更新有个尴尬:目标索引页不在内存时,得先从磁盘把页读进 buffer pool 才能修改,一次更新平白搭上一次随机读。change buffer 的思路是「页不在就不读」——先把「对某数据页的修改」记录到 change buffer 中,等将来该页因为其他查询被读入内存时再 merge 应用,后台线程也会择机合并。它只对非唯一的二级索引有效:唯一索引必须立刻读页做唯一性校验,缓存无从谈起。写多读少、写入后不会立刻回读的业务(账单流水、操作日志类)收益最大。
工程实践
- 权限改了不生效:先想到连接器的权限快照,让旧连接断开重建再验证。
- 不要依赖查询缓存:8.0 已移除;即便停留在 5.7,也建议关闭,应用层缓存的失效策略更可控。
- 更新语句也看执行计划:UPDATE/DELETE 同样支持 EXPLAIN,确认更新走索引,避免全表扫描顺带把锁面放大。
- 写密集场景盘点二级索引:每个非唯一索引都能借 change buffer 摊薄成本,但索引本身仍是写放大来源,删掉用不到的索引比调参数更直接。
对比与权衡
| 维度 | Server 层 | 引擎层(InnoDB) |
|---|---|---|
| 核心职责 | 连接、解析、优化、执行调度 | 数据页读写、锁、事务、缓冲池 |
| 日志模块 | binlog(复制/归档) | redo log(崩溃恢复)、undo log(回滚/MVCC) |
| 可替换性 | 不可替换 | 插件式,可换 MyISAM、Memory 等 |
| 典型故障 | 权限不生效、选错索引、语法报错 | 锁等待、刷盘抖动、崩溃恢复 |
面试视角
一条 SELECT 的执行流程是什么? 连接器完成认证并缓存权限;分析器做词法与语法解析;优化器基于成本选索引、定 JOIN 顺序;执行器校验表级权限后按方案循环调引擎接口取行;引擎从 buffer pool 或磁盘读页返回数据。
查询缓存为什么被移除? 失效粒度太粗:表上任何写入都清空该表全部缓存,OLTP 场景命中率极低,还带全局锁竞争。8.0 直接整体删除,缓存职责上移到应用层。
redo log 为什么要两阶段提交? redo 与 binlog 分属引擎层和 Server 层,任一边单独写完宕机都会造成主从或备份恢复的数据分叉。prepare → 写 binlog → commit 的顺序配合 XID 校验,让崩溃恢复时能判定补提交还是回滚,两份日志保持一致。
change buffer 为什么只对非唯一索引有效? 唯一索引必须当场把页读入内存做唯一性检查,延迟合并无从谈起;非唯一索引没有即时校验需求,才能把修改推迟到页再次被读入时应用。
小结
- 执行链路:连接器 → 分析器 → 优化器 → 执行器 → 引擎;Server 层负责理解 SQL,引擎层负责存取数据。
- 查询缓存因失效粒度过粗在 8.0 被移除,缓存应在应用层按业务设计。
- UPDATE 的核心增量是日志:redo prepare → binlog → redo commit,两阶段提交用 XID 对齐两份日志。
- change buffer 用延迟合并消掉二级索引更新的随机读,前提是索引非唯一且读不紧跟写。

