一、一条Select语句的执行流程

上图展示的是执行一条sql会经历的流程:如select * from t
的执行大致过程如下:
- 客户端发送一条查询语句到服务端。
- 服务端接收到查询语句后,会先检查查询缓存是否有这个语句。
- 如果有,就直接返回缓存中的结果。
- 如果没有,就会先分析查询语句,然后优化查询语句。
- 优化完成后,就会调用执行器执行查询语句。
- 执行器会调用存储引擎的 API 来执行查询语句。
- 存储引擎会根据查询语句的条件,从磁盘中读取数据。
- 读取完成后,存储引擎会将数据返回给执行器。
- 执行器会将数据返回给客户端。
下面是每个组件的详细介绍:
客户端
连接工具(Navacat、SQLyog、JDBC)都归纳为MySQL客户端(Client),主要用于发送执行sql语句的请求。
服务端总览
- Server层:跨存储引擎实现
- 连接器
- 查询缓存
- 分析器
- 优化器
- 执行器
- 存储引擎层:负责数据的存储和检索
- InnoDB
- MyISAM
- Memory
- 其他存储引擎
Server 层
- 负责处理 SQL 语句、解析、优化、缓存等。
- 负责权限管理、用户认证等。
- 提供了各种 SQL 函数和存储过程。
- 提供了复制、备份、恢复等高级功能。
- Server 层有自己的日志系统,称为 binlog(归档日志)。binlog 记录了所有修改数据库数据的 SQL 语句(如 INSERT、UPDATE、DELETE 等)的信息,但不包括 SELECT 和 SHOW 这类查询语句。binlog 主要用于复制和恢复操作。
存储引擎层
- 负责数据的存储和检索。
- MySQL 支持多种存储引擎,如 InnoDB、MyISAM、Memory 等,每种引擎都有其特点和适用场景。
- InnoDB 是 MySQL 的默认存储引擎,它支持事务、行级锁定和外键约束。InnoDB 有自己的日志系统,称为 redo log(重做日志) 和 undo log(撤销日志)。redo log 用于保证事务的持久性,在数据库崩溃后可以用来恢复数据;undo log 用于支持事务的原子性和多版本并发控制(MVCC)。
连接器
第一步,你会先连接到这个数据库上,这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。连接命令一般是这么写的:
bash
mysql -h$ip -P$port -u$user -p
连接命令中的 mysql 是客户端工具,用来跟服务端建立连接。在完成经典的 TCP 握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码。
- 如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行。
- 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。
- 如果链接长时间不活动,连接器会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。
Plain
# 查看数据库的连接状态
show processlist;
#查看当前的wait_timeout参数值
SHOW VARIABLES LIKE 'wait_timeout';
分析器
词法分析:
- 词法分析器将 SQL 语句分解为词法单元(tokens),如关键字、标识符、运算符、字符串等。
- 词法分析器会忽略 SQL 语句中的空格、换行符等空白字符。
语法分析:
- 语法分析器会根据 SQL 语句的语法规则,将词法单元组织成语法树。
- 语法分析器会检查 SQL 语句是否符合语法规则(例如查information_schema是否有未定义的表或列),如果不符合,会返回语法错误。
优化器
经过了分析器,若语句正确,就会进入优化器。优化器的作用是在基于同一个查询语句的多个查询方案中找出效率最高的。比如,在表里面有多个索引的时候,决定使用哪个索引,或者决定是否使用索引。
- 优化器的作用是根据语法树,生成执行计划。
- 优化器会考虑多种因素,如索引、表连接、排序等,以确定最优的执行计划。
- 优化器会根据执行计划,生成执行器可以理解的指令。
执行器
执行器负责执行优化器生成的执行计划。执行器会根据执行计划,调用存储引擎的 API 执行具体的操作。执行器会处理查询结果、更新结果、返回结果等。
开始执行的时候,要先判断一下你对这个表有没有执行查询的权限,如果没有,就会返回没有权限的错误。如果有权限,就打开表继续执行。这是一种安全机制,确保只有被授权的用户才能访问和操作数据。
注意:
- 如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。
- 在语法分析过程中,解析器会进行一些初步的权限检查 precheck,例如验证用户是否有权访问指定的数据库和表。
- 有些时候,SQL语句要操作的表不只是SQL字面上那些。SQL执行过程中可能会有触发器这种在运行时才能确定的过程,precheck是不能对这种运行时涉及到的表进行权限校验的,所以需要在执行器阶段进行权限检查。
打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口:
- 调用引擎接口取这个表的第一行,判断是否满足条件,如果不是则跳过,如果是则将这行存在结果集中
- 调用引擎接口取下一行,重复相同的判断逻辑,直到取到这个表的最后一行
- 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端 至此,这个语句就执行完成了。
注意:结果集也是属于Server层范畴的,而不是存储引擎层。
二、一条Update语句的执行流程
在解释Update语句的执行流程之前,先介绍一下 MySQL 中的两大日志系统 redo log 和 binglog
物理日志 redo log
背景 在 MySQL 里如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,INNODB 的设计者就用了WAL技术的思路来提升更新效率。WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。
redolog 物理模型是一个环形数组:

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要写入磁盘的位置,也是往后推移并且循环的,如果write pos 追上checkpoint 需要等待checkpoint位置更新到数据文件后才能继续写入。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
redo log 保证的是单机MySQL的故障恢复能力,但是在主从复制场景下,由于主库和从库的 redo log 是不同的,所以从库在恢复数据时需要依赖主库的 binlog 来进行恢复。
此外为了保证 MySQL 异常重启之后数据不丢失。可以设置 innodb_flush_log_at_trx_commit = 1
,表示每次事务的 redo log 都直接持久化到磁盘。
innodb_flush_log_at_trx_commit={0|1|2} 指定何时将事务日志刷到磁盘,默认为1。 0表示每秒将"log buffer"同步到"os buffer"且从"os buffer"刷到磁盘日志文件中。 1表示每事务提交都将"log buffer"同步到"os buffer"且从"os buffer"刷到磁盘日志文件中。 2表示每事务提交都将"log buffer"同步到"os buffer"但每秒才从"os buffer"刷到磁盘日志文件中。
redo log存储格式: redo log的写盘时间会直接影响系统吞吐,显而易见,redo log的数据量要尽量少。
其次,系统崩溃总是发生在始料未及的时候,当重启重放redo log时,系统并不知道哪些redo log对应的Page已经落盘,因此redo log的重放必须可重入,即redo log操作要保证幂等。
最后,为了便于通过并发重放的方式加快重启恢复速度,redo log应该是基于Page的,即一条记录只涉及一个Page的修改。
数据量小是Logical Logging的优点,而幂等以及基于Page正是Physical Logging的优点,因此InnoDB采取了一种称为Physiological Logging的方式,来兼得二者的优势。
所谓Physiological Logging,就是以Page为单位,但在Page内以逻辑的方式记录。
举个例子,MLOG_REC_UPDATE_IN_PLACE类型的REDO中记录了对Page中一个Record的修改,方法如下: (Page ID,Record Offset,(Filed 1, Value 1) ... (Filed i, Value i) ... ) 其中,PageID指定要操作的Page页,Record Offset记录了Record在Page内的偏移位置,后面的Field数组,记录了需要修改的Field以及修改后的Value。
逻辑日志 binlog
上面提到的 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。
这两种日志有以下三点不同:
- redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志,记录的是"在某个数据页上做了什么修改";binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如"给 ID=2 这一行的 c 字段加 1 "。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。"追加写"是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
binlog存储格式: binlog的存储格式有三种:Statement、Row、Mixed。
- Statement:基于语句的复制,每一条会修改数据的 SQL 语句都会记录在 binlog 中。
- Row:基于行的复制,会记录每一行数据的修改,所以 binlog 中会包含很多内容。
- Mixed:混合模式,默认是 Statement,但是如果语句中包含了需要使用 Row 模式的语句,就会切换到 Row 模式。
update 执行流程
有了对这两个日志的概念性理解,再来看执行器和 InnoDB 引擎在执行update t set c=c+1 where id=2
这个一条简单 update 语句时的内部流程。
- 执行器先调用引擎的
get()
方法,取到 id=2 这一行。如果 id=2 这一行所在的数据页不在内存中,就会先从磁盘读入内存。 - 更新这一行的数据 c = c + 1,但后在调用innodb接口写入这一行新数据。
- 引擎将这行新数据更新到buffer pool中,同时将这个更新操作记录到 redo log buffer里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成 binlog 并写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

两阶段提交
上面的流程将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。这是为了让两份日志之间的逻辑一致。可以这么理解一下:
单机MySQL下,其实也没必要这么复杂,甚至你都可以把binlog关掉,MySQL是支持binlog关闭的。但如果是主从的MySQL,我们知道主从间数据的同步通过binlog,主服务器通过发binlog给从服务器来保证数据的同步
为什么不用redo log?因为redo log 是 InnoDB 自己的,并非所有存储引擎都有redo log,但都会有binlog。这也是历史原因,MySQL一开始就有binlog,而redo log是在InnoDB引入的。因此多机下就就需要binlog。
- 如果我们的binlog与redo log不同步:比如redo log中有的数据在binlog不存在时,redo log同步更新到磁盘的话,那么由于binlog中不存在这部分数据,最终MySQL崩溃恢复后会导致主从不一致(根据redo log恢复)。
- 如果binlog与redo log是一致的,那恢复的时候直接用binlog不就可以了,为什么用redo log来恢复? 因为实际干活的是存储引擎,到底这个数据有没有同步到磁盘,binlog是不知道的。拿上述两阶段提交举例子:先是记录redo状态prepare,然后记录binlog,再是将redo改为commit。如果以binlog作为恢复的判断依据,那如果在记录完binlog但未将redo改为commit时发生crash,此时表面上看这条记录已经执行成功了,但实际redo根本还没刷盘,执行成功是存储引擎告诉它的成功,到底是不是真的成功只有存储引擎自己知道(如InnoDB可能并不会真的将redo刷盘,只是放到pool就返回成功了),因此binlog不能作为数据恢复的依据。