MySQL执行流程
在使用MySQL时,你是否有疑惑,当我们提交一条SQL给MySQL时它到底是如何执行的?
通过了解MySQL的执行流程一定能解开你的疑惑🤔
总体流程
- 客户端通过连接器连接MySQL
- 查询执行缓存
- 解析器解析SQL
- 执行器执行SQL
- 调用存储引擎API
⚠️:MySQL8.0之后就把查询执行缓存这一步骤去掉了,效率太低
MySQL架构分为两层:Server层和存储引擎层
- Server层负责建立连接、查询缓存、解析SQL、执行SQL。MySQL大部分的核心功能都在这实现。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等)
- 存储引擎负责数据的存储和提取。支持InnoDB、MyISAM、Memory等多个存储引擎,不同存储引擎公用一个Server层。从MySQL5.5版本开始,InnoDB成为了MySQL的默认存储引擎。我们常说的索引数据结构,就是又存储引擎层实现的,不同的存储引擎支持的索引类型也不同,比如InnoDB支持的索引类型是B+树
1.连接器
正常请求下,连接数据库我们会使用如下命令
#连接本机MySQL
mysql -u 用户名 -p
# 连接远程MySQL
mysql -h ip地址 -u 用户名 -p
连接的过程会使用到TCP的三次握手🤝,MySQL的底层就是基于TCP协议进行通信的,如果MySQL服务正常运行的话会对你输入的用户名和密码进行验证,如果不存在当前用户或者当前用户不存在对应的权限就会报错(Access denied权限拒绝)
如果验证通过,连接器会保存当前用户的权限,在之后的操作中会基于当前这个用户的权限进行判断
查看MySQL当前的客户端连接
show processlist
MySQL的连接数有限制吗?
查看最大连接数
show variables like 'max_connections';
可以看到MySQL默认的最大连接数为151,当超过151个客户端同时连接MySQL时,接下来的连接请求就会被拒绝,并报错“Too many connections”
MySQL的连接
MySQL的连接也与Http的连接类似有长连接和短连接之分
// 短连接
连接(三次握手)
执行sql
断开连接(四次挥手)
// 长连接
连接(三次握手)
执行sql
执行sql
执行sql
...
断开连接(四次挥手)
使用长连接的好处就是可以减少建立连接和断开连接的过程,所以一般推荐使用长连接
但是,使用长连接后可能会占用内存增多,因为 MySQL 在执行查询过程中临时使用内存管理连接对象,这些连接对象资源只有在连接断开时才会释放。如果长连接累计很多,将导致 MySQL 服务占用内存太大,有可能会被系统强制杀掉,这样会发生 MySQL 服务异常重启的现象。
怎么解决长连接占用内存的问题?
有两种解决方式。
第一种,定期断开长连接。既然断开连接后就会释放连接占用的内存资源,那么我们可以定期断开长连接。
第二种,客户端主动重置连接。MySQL 5.7 版本实现了 mysql_reset_connection()
函数的接口,注意这是接口函数不是命令,那么当客户端执行了一个很大的操作后,在代码里调用 mysql_reset_connection 函数来重置连接,达到释放内存的效果。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
至此,连接器的工作做完了,简单总结一下:
- 与客户端进行 TCP 三次握手建立连接;
- 校验客户端的用户名和密码,如果用户名或密码不对,则会报错;
- 如果用户名和密码都对了,会读取该用户的权限,然后后面的权限逻辑判断都基于此时读取到的权限;
2.查询缓存
连接器的工作完成后,客户端就可以向MySQL服务发送SQL语句了,MySQL就收到SQL语句后,如果发现是Select查询语句,会去Cache缓存中看看是否有对应的缓存,比如现在我发送给MySQLselect * from t_user
语句,如果MySQL发现缓存中存在
缓存:<select * from t_user, 缓存数据>
命中缓存之后,会直接把缓存返回给客户端
但是查询缓存有个很大的缺点(命中率低、占用内存)
对于更新比较频繁的表,查询缓存的命中率很低的,因为只要一个表有更新操作,那么这个表的查询缓存就会被清空。如果刚缓存了一个查询结果很大的数据,还没被使用的时候,刚好这个表有更新操作,查询缓冲就被清空了,相当于缓存了个寂寞。
所以,MySQL 8.0 版本直接将查询缓存删掉了,也就是说 MySQL 8.0 开始,执行一条 SQL 查询语句,不会再走到查询缓存这个阶段了。
对于 MySQL 8.0 之前的版本,如果想关闭查询缓存,我们可以通过将参数 query_cache_type 设置成 DEMAND。
3.解析SQL
解析器会做如下两件事情。
第一件事情,词法分析。MySQL 会根据你输入的字符串识别出关键字出来,例如,SQL语句 select username from userinfo
,在分析之后,会得到4个Token,其中有2个Keyword,分别为select和from:
关键字 | 非关键字 | 关键字 | 非关键字 |
---|---|---|---|
select | username | from | userinfo |
第二件事情,语法分析。根据词法分析的结果,语法解析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法,如果没问题就会构建出 SQL 语法树,这样方便后面模块获取 SQL 类型、表名、字段名、 where 条件等等。
如果我们输入的 SQL 语句语法不对,就会在解析器这个阶段报错。比如,我下面这条查询语句,把 from 写成了 form,这时 MySQL 解析器就会给报错。
但是注意,表不存在或者字段不存在,并不是在解析器里做的,《MySQL 45 讲》说是在解析器做的,但是经过我和朋友看 MySQL 源码(5.7和8.0)得出结论是解析器只负责检查语法和构建语法树,但是不会去查表或者字段存不存在。
那到底谁来做检测表和字段是否存在的工作呢?别急,接下来就是了。
4.执行SQL
执行SQL分为3小步:
- prepare阶段,也就是预处理阶段;
- optimize,也就是优化阶段;
- execute,也就是执行阶段;
预处理器
预处理器的工作
- 检查SQL查询语句中的表或者字段是否存在;
- 将
select *
中的*
符号,拓展为表上所有的列;
下面这条查询语句,test 这张表是不存在的,这时 MySQL 就会在执行 SQL 查询语句的 prepare 阶段中报错。
mysql> select * from test;
ERROR 1146 (42S02): Table 'mysql.test' doesn't exist
优化器
经过预处理过后的SQL说明没啥毛病了,接下来MySQL就会对这条SQL进行优化,怎么个优化法呢?
第一,假如表里有很多索引的时候,优化器会基于查询成本来考虑使用哪一个索引
以select * from product where id = 1
为栗子,id一般都会设置主键索引,所以这条SQL就会走主键索引,而不是扫描全表
如果查询计划没有找到可以优化的计划,就会走全表扫描
如果现在product有两个索引为id主键索引和作用在name的二级索引
select id from product where id > 1 and name like 'i%'
会怎么进行呢?
是使用主键索引还是二级索引?
如果这条SQL走主键索引,那在定位到id > 1的记录还是得回表查,name是否符合条件比较耗时
但是如果使用作用在name上的二级索引,可以使用到覆盖索引(select查询的字段,刚好在索引上),二级索引叶子节点挂的都是当前记录的id,所以使用name的二级索引不需要回表查,可以直接返回要查询的值
执行器
等优化器决定完这条SQL的执行计划后,执行器就要访问存储引擎,进行数据查询了
主键索引查询
以select * from product where id = 1
为栗子
由于使用到了主键索引天然具有唯一性,并且是等值查询,就注定了查询返回的结果只有一条
执行流程:
- 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为 InnoDB 引擎索引查询的接口,把条件
id = 1
交给存储引擎,让存储引擎定位符合条件的第一条记录。 - 存储引擎通过主键索引的 B+ 树结构定位到 id = 1的第一条记录,如果记录是不存在的,就会向执行器上报记录找不到的错误,然后查询结束。如果记录是存在的,就会将记录返回给执行器;
- 执行器从存储引擎读到记录后,接着判断记录是否符合查询条件,如果符合则发送给客户端,如果不符合则跳过该记录。
- 执行器查询的过程是一个 while 循环,所以还会再查一次,但是这次因为不是第一次查询了,所以会调用 read_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为一个永远返回 - 1 的函数,所以当调用该函数的时候,执行器就退出循环,也就是结束查询了。
索引下推查询
现在建立一个联合索引(name, age)
现在有下面这条查询语句:
select * from t_user where age > 20 and reward = 100000;
当联合索引遇到范围查询(>, <)就会停止匹配,也就是age字段能用到联合索引,但是reward字段则无法使用到联合索引
不使用索引下推的流程:
- Server层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到符合age>20的第一条记录
- 存储引擎根据二级索引B+树快速定位到这条记录后,获取主键值,然后进行回表操作,将完整的记录返回给Server层
- Server层对这条记录进行判断是否符合reward = 100000,不符合就跳过这条记录
- 接着继续向存储引擎要下一条记录,重复上面的操作,直到Server层获取到所有符合条件的结果,接着Server层将结果返回客户端
使用索引下推流程:
-
Server层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到符合age>20的第一条记录
-
存储引擎根据二级索引B+树快速定位到这条记录后,获取主键值,不进行回表操作,先判断当前联合索引中是否reward = 100000,如果条件不成立,则直接跳过该二级索引。如果成立,则执行回表操作,将完成记录返回给 Server 层。
-
Server 层在判断其他的查询条件(本次查询没有其他条件)是否成立,如果成立则将其发送给客户端;否则跳过该记录,然后向存储引擎索要下一条记录。
-
如此往复,直到存储引擎把表中的所有记录读完。
可以看到,使用了索引下推后,虽然 reward 列无法使用到联合索引,但是因为它包含在联合索引(age,reward)里,所以直接在存储引擎过滤出满足 reward = 100000 的记录后,才去执行回表操作获取整个记录。相比于没有使用索引下推,节省了很多回表操作。
当你发现执行计划里的 Extr 部分显示了 “Using index condition”,说明使用了索引下推。
总结
Mysql一条Select语句的执行流程是什么?
- 连接器进行管理连接、鉴权
- 查询缓存
- 解析器进行语法分析,关键词提取等
- 执行器进行预处理、优化SQL执行计划、执行SQL等
- 执行器调用存储引擎接口获取数据返回给客户端
参考链接:https://xiaolincoding.com/mysql/base/how_select.html