一条 SQL 发出去之后,MySQL 到底做了什么?

我们前面讲 redo log、binlog 的时候,反复提到 MySQL 分成 Server 层和存储引擎层,但一直没把这条链路本身展开。这篇我们换个角度看同一件事:一条 SQL 从客户端发出去,到结果返回,中间走了哪些环节。

我们先记住一个大框架:一条语句要穿过两层。

层 管什么 有哪些东西
Server 层 SQL 的语法语义、权限、决定怎么执行 连接器、分析器、优化器、执行器,还有 binlog
存储引擎层 数据存在哪、怎么读写 InnoDB(默认)、MyISAM、Memory,redo log 和 undo log 在这一层

真正执行一条语句,是这两层配合的结果。下面从客户端连上来开始走。


连接器

客户端连 MySQL,第一步是连接器,它管三件事:

text 复制代码
1. TCP 握手,建立起连接
2. 认证身份:客户端发来用户名和一段密码摘要
3. 认证通过后,读取这个账号的权限

密码不是明文传的,客户端发的是密码的加密摘要,连接器拿去和存储的密码比对。权限在这里一次性读出来,之后这条连接上的操作都用这份权限判断------所以一个已经连上的会话,中途改了它的权限不会立刻生效,要重连。

认证通过后连接就保持住了,空闲时状态是 Sleep,用 show processlist 能看到。连接不是用完就断,默认会挂着(wait_timeout 默认 8 小时)。这里有两种用法:

  • 短连接:每次操作新建连接,用完断开
  • 长连接:连一次一直用

长连接省了建连开销,但有个坑:MySQL 执行过程中用的临时内存是挂在连接对象上的,要到连接断开才释放。一条长连接如果跑过几个大查询,内存会一直占着不还。8.0 的解法是执行 mysql_reset_connection 把连接状态重置掉,或者定期主动断开重连。


查询缓存

8.0 之前的 MySQL 在连接器和分析器之间还有一层查询缓存:把 SQL 语句当 key,结果当 value 存起来,下次来一模一样的语句直接返回。

听起来很美,但它的失效粒度太粗------只要表上有一条记录被更新,这张表的所有查询缓存全部清空。写多读少的库里,缓存刚存进去就被清掉,维护它反而成了负担。所以 8.0 直接把查询缓存整个删掉了。知道这层曾经存在、以及为什么被删就够了,现在不用管它。


分析器

到了分析器,MySQL 才真正"读"这条 SQL,分两步:

  • 词法分析 :把一串字符拆开,认出 select 是查询关键字、T 是表名、ID 是列名
  • 语法分析:按 MySQL 的语法规则,判断这句话是否合法

语法不对,报错就出在这一步,报错信息里会指出它在哪儿卡住:

text 复制代码
You have an error in your SQL syntax; check the manual that corresponds to
your MySQL server version for the right syntax to use near 'where id = 1' at line 1

分析器的产物是"这句话说得通,而且说的是对表 T 按 ID 查",此时它还不知道该怎么执行。比如表上到底用哪个索引、多表 join 谁先谁后,是下一步的事。


优化器

同一条 SQL 往往有好几种执行方式,优化器负责挑一条代价最小的。它主要决定两件事:

  1. 单表查询:走哪个索引,还是干脆全表扫描
  2. 多表 join:以哪张表为驱动表,连接的顺序怎么定

我们拿一个最直观的例子看它的作用。假设要查两张表:

sql 复制代码
select * from t1 join t2 on t1.a = t2.b where t1.c = 1;

可以先扫 t1 用 c=1 过滤,再拿结果去 t2 上匹配;也可以反过来先扫 t2。两条路的代价可能差很远------如果 t1 的 c=1 只剩几行,先扫 t1 就快得多。优化器就是算这个账的。

选完以后,它输出一份执行计划 ,用 EXPLAIN 可以看:

sql 复制代码
explain select * from t1 join t2 on t1.a = t2.b where t1.c = 1;

计划里会写明用不用索引(key 列)、扫描方式(type 列)、谁做驱动表。优化器的判断不一定永远最优------它靠的是统计信息和代价估算,估错了就可能选一条差的。判断"索引为什么没走"这件事,看的就是 EXPLAIN 的结果,可以接着看 MySQL 索引失效的场景总结。


执行器

优化器给完计划,执行器开始照着执行。顺序是:

text 复制代码
1. 先判断当前连接对要操作的表有没有权限,没有就报错返回
2. 有权限就打开表,调用存储引擎提供的接口,一行一行把数据取出来
3. 把满足条件的结果集返回给客户端

关键在于第 2 步"怎么取"。有没有索引,执行器调用引擎的方式是不一样的。

没有索引

以 select * from T where k = 5 为例,假设 k 上没有索引:

text 复制代码
执行器调用引擎接口「取表的第一行」
    引擎返回第一行 → 执行器判断 k 是不是 5
        不是 → 跳过,再调「取下一行」
        是   → 放进结果集,再调「取下一行」
    重复到整张表扫完

每一行都要取出来、由执行器判断一遍,这就是全表扫描。判断"k 是不是 5"这个动作是执行器做的,引擎只负责把行给它。

有索引

如果 k 上建了索引,执行器换一个接口:

text 复制代码
执行器调用引擎接口「取满足 k = 5 的第一行」
    引擎用 k 索引直接定位到第一条 k = 5 的记录,返回
执行器调用「取满足 k = 5 的下一行」
    引擎沿着索引往下找,返回第二条......
    直到没有满足条件的记录

这次引擎返回的每一行都已经满足 k = 5,执行器不用再逐行判断,而且要读的行数也从"整张表"缩到了"命中的那几行"。这就是索引的价值所在,也是为什么"索引失效了"会那么让人头疼。


存储引擎层

执行器往下调的接口,进到的是存储引擎层。数据真正待的地方在这一层,以 InnoDB 为例:

  • 数据在磁盘上按页 组织,一页 16KB。读一行,实际上是把它所在的整页读进内存(buffer pool)
  • 页不在 buffer pool 里,就从磁盘读,这是一次随机 IO,比顺序读慢一两个数量级
  • 有索引时走 B+ 树定位到目标页,没索引时只能顺着页一页页扫

第 1 步里执行器拿到的行,也不是磁盘上的原始数据,而是 InnoDB 按 MVCC 规则判断出来、对当前事务可见的那个版本,可以回顾一下 MVCC 的 ReadView。

写入路径同样落在这层:修改要先写 undo log、再改内存里的页、同时写 redo log,提交时配合 Server 层的 binlog 做两阶段提交。这部分在MySQL 日志全景图和 Redo Log 和 Binlog 为什么需要两阶段提交?里展开,这里不重复。

再补充一个和"取行"相关的概念:回表 。普通索引的叶子节点只存了索引列和主键值,如果要查的列不在索引里,InnoDB 得先拿到主键,再回主键索引里查一次完整行。所以 select * 常常比只查需要的列更慢------查的列多了,索引覆盖不住,每一行都要回表。这个也是索引失效那篇里反复出现的一个成本点。


为什么要分两层

讲到这里,我们可以回答一个"为什么不合成一层"的问题。

分成 Server 层和引擎层,最大的好处是存储引擎可以换 。Server 层把 SQL 的语法、语义、权限、执行计划都处理完,再通过一套统一的接口(handler API)去调引擎;InnoDB、MyISAM、Memory 只要实现这套接口,就能被 Server 层调用。所以改存储引擎是改配置的事,SQL 不用动。

另一层意义在日志的归属上:binlog 属于 Server 层,redo log / undo log 属于引擎层。两个日志在不同的层、由不同的代码写,没法用一次原子操作保证同时成功------这正是它们之间需要两阶段提交的根本原因。这也是分清这两层之后,能顺手解释掉的一个问题。

相关推荐
March.s4 小时前
MySQL 数据库原理与实战:从数据管理到 LAMP 建站全解
数据库·mysql
悟道子HD5 小时前
网络安全基本功——MySQL基础
android·mysql·web安全
晚风叙码5 小时前
MySQL 增删改查(CRUD)完全指南:从入门到面试
数据库·mysql·面试
刘胡子大叔15 小时前
SQL 脚本的导入顺序
数据库·sql
程序员Sunday17 小时前
MySQL 为什么使用 B+ 树索引?把范围查询、回表和覆盖索引连起来
数据库·mysql
Yyyyyy~18 小时前
[ Mysql ] 库的操作
mysql
小米里的大麦18 小时前
16 MySQL 事务
数据库·mysql
Titan202418 小时前
MySQL访问个人学习笔记
笔记·学习·mysql
for_ever_love__19 小时前
MySQL 全文索引实战:FULLTEXT、ngram 中文分词与 MATCH AGAINST 到底该怎么用
java·python·mysql·全文检索·分词·索引·ngram