【保姆级 :图解MySQL 执行全链路讲解】主键索引扫描,全局扫描,索引下推还是分不清楚?这一篇就够啦

前言

如果把 MySQL 比作一家高效运转的餐厅,那么你提交的一条 SQL 语句就是一份"点菜单"。

为了让这份菜单变成餐桌上的佳肴,餐厅内部需要精密协作:门口的接待员(连接器)核验身份,前台(解析器)解读需求,经验丰富的大厨(优化器)制定最省时省力的烹饪路线,而最终在后厨火热开工的则是各种厨具(存储引擎)。

很多同学在写代码时更关注业务逻辑,却忽略了这套精密机器内部的运作流程。结果就是在遇到线上死锁、查询变慢或者 Binlog 报错时手足无措。今天,我们就脱离复杂的代码逻辑,用一张全景图透视 MySQL 的执行全过程,看看这套成熟的数据库系统是如何保证在每秒万级并发下,依然能精准无误地处理每一条指令的。

MySQL执行流程

我们先从图解来直观了解一下,MySQL中到底包含了写什么东西。

我们会发现:MySQL的架构被分为了两层:Service层与存储引擎。

  • Service层:负责建立连接,分析和执行SQL。MySQL的多数核心功能模块都在这里实现;主要包括了:连接器,查询缓存,解析器,预处理器,优化器,执行器。另外还有内置的函数和跨存储引擎功能。
  • 存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎。不同的存储引擎公用一个Service层。从MySQL5.5版本之后,我们最常见的存储引擎就成了InnoDB。我们常说的索引数据结构,就是由存储引擎层实现的,不同的存储引擎支持的索引类型也不同,像InnoDB支持的索引类型就是B+树。

到这里大家对Server 层和存储引擎层有了一个简单认识,接下来我们就具体讲讲他们是怎么实现的吧。

1.连接器

顾名思义,我们要使用MySQL,第一步就是要与MySQL建立连接,才能够执行sql语句。

连接的过程是我们熟悉的TCP三次握手,因为MySQL是基于TCP协议进行传输的。我们在这不做MySQL连接错误的异常处理,默认大家成功连接上了MySQL。

我们知道MySQL是多线程模型,那这里就迎来了我们第一个问题

如何查看 MySQL 服务被多少个客户端连接了?

答:我们可以执行show processlist 命令来进行查看。

空闲连接会一直占着吗?

答:并不会,MySQL 定义了空闲连接的最大空闲时长,由 wait_timeout 参数控制的,默认值是 8 小时(28880秒),如果空闲连接超过了这个时间,连接器就会自动将它断开。

我们也可以进行手动断开:使用kill connection + id 的命令。

MySQL的连接数有限制吗?

答:有的。MySQL 服务支持的最大连接数由 max_connections 参数控制。我们可以执行:

show variables like 'max_connections';命令来查看

MySQL 的连接也跟 HTTP 一样,有短连接和长连接,它们的区别如下:

java 复制代码
// 短连接
连接 mysql 服务(TCP 三次握手)
执行sql
断开 mysql 服务(TCP 四次挥手)

// 长连接
连接 mysql 服务(TCP 三次握手)
执行sql
执行sql
执行sql
....
断开 mysql 服务(TCP 四次挥手)

长连接的好处是减少了与MySQL建立与断开连接的过程,一般推荐使用长连接。

为什么是一般呢,应为使用长连接会导致内存占用增多

因为:MySQL在执行查询过程中临时使用内存管理来连接对象,这些资源只有在断开连接时才会得到释放。

怎么解决长连接占用内存问题?

有两种方案:

  1. 定期断开长连接
  2. 客户端主动重置连接
    在MySQL5.7版本后,实现了mysql_reset_connection() 函数接口。当客户端执行了一个很大操作后,在代码里调用mysql_reset_connection() 函数来重置连接,达到释放内存效果,这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

连接器小结

这一部分主要进行三步主要操作

  • 与客户端进行TCP三次握手
  • 校验客户端的用户名和密码
  • 如果校验通过,则会读取用户权限,通过权限来判断用户可以进行哪些操作

2.缓存查询

完成上一步连接器操作,客户端就可以正式的向MySQL发送sql语句了。当MySQL接收到一条SQL语句时,他会先解析SQL语句的第一个字段:看看你到底是什么语句。
查询语句(select) :MySQL会先去缓存查询里查找缓存数据,看看之前有没有执行过这一条语句,这个查询结果是以kv形式保存至内存中,key=SQL查询语句,value=语句查询结果。

命中则返回value给客户端,没命中,继续往下执行,等待执行完毕,再添加到查询缓存中。

有的小伙伴就说,这不就是Redis,这操作肯定很强吧,肯定能抗住亿万级的高并发吧

结果恰恰相反,这个查询缓存很鸡肋。甚至在8.0版本之后就被删除了! 这是为什么呢?

MySQL中的表更新是非常频繁的,一旦有一个表有更新操作,那么这个表的查询缓存就会被清空,这就导致了查询缓存的命中率极低。

3.解析SQL

SQL语句到这里还没有被执行,而是先进行解析操作。解析SQL语句,就需要用到解析器。

解析器

解析器只要对SQL执行两件事:

  1. 词法分析
    顾名思义,解析器会先将SQL语句中的关键词与非关键词分离出来。
    例如:select username from userinfo
    在分析之后,会得到4个token,其中两个为Keyword(select 与 from)
  2. 语法分析
    根据词法分析的结果,语法解析器会根据语法规则,先判断语法是否正确,正确则会构造出SQL语法树,方便后面模块获取SQL类型,表名,字段名,where条件等等。反之则会返回错误信息。

4.执行SQL

经过前面那么多流程,我们的SQL语句总算是来到了最终执行这一步了。

在执行过程中,执行器会和存储引擎交互,交互是以记录为单位的。

执行器与存储引擎的交互过程,有三种方式:

  • 主键索引查询
  • 全表扫描
  • 索引下推

主键索引扫描

这条查询语句的查询条件用到了主键索引,而且是等值查询,同时主键 id 是唯一,不会有 id 相同的记录,所以优化器决定选用访问类型为 const 进行查询,也就是使用主键索引查询一条记录,那么执行器与存储引擎的执行流程是这样的:

  • 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为 InnoDB 引擎索引查询的接口,把条件 id = 1 交给存储引擎,让存储引擎定位符合条件的第一条记录
  • 存储引擎通过主键索引的 B+ 树结构定位到 id = 1 的第一条记录,如果记录是不存在的,就会向执行器上报记录找不到的错误,然后查询结束。如果记录是存在的,就会将记录返回给执行器;
  • 执行器从存储引擎读到记录后,接着判断记录是否符合查询条件,如果符合则发送给客户端,如果不符合则跳过该记录。
  • 执行器查询的过程是一个 while 循环,所以还会再查一次,但是这次因为不是第一次查询了,所以会调用 read_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为一个永远返回 -1 的函数,所以当调用该函数的时候,执行器就退出循环,也就是结束查询了。

至此,这个语句就执行完成了。

全局扫描

我们以为例:

sql 复制代码
select * from product where name = 'iphone';

这条查询语句没有用到索引,所以优化器选择用访问类型为ALL来查询(全表扫描)

具体流程:

  • 执行器第一次查询,调用read_first_record 函数指针指向的函数,因为访问类型为ALL,这个函数指针被指像 InnoDB 引擎全扫描的接口,让存储引擎读取表中的第一条记录
  • 执行器会判断读到这条这条记录的name是不是iphone,不是则跳过;是则将记录发送给客户
  • 执行器查询过程是一个while循环,所以还会再查一次,流程如上。
  • 一直重复上述过程,直到存储引擎把表中的所有记录读完,然后向执行器(Server层) 返回了读取完毕的信息。
  • 执行器收到存储引擎报告的查询完毕的信息,退出循环,停止查询。

索引下推

索引下推能够减少二级索引 在查询时的回表操作,提高查询的效率。因为他将Server 层部分负责的事情,交给存储引擎层去处理了。

我们以为例:

sql 复制代码
select * from t_user  where age > 20 and reward = 100000;

联合索引当遇到范围查询 (>、<) 就会停止匹配,也就是age 字段能用到联合索引,但是 reward 字段则无法利用到索引。(最左前缀法则)

在使用索引下推后,判断记录的reward 是否等于100000的工作交给了存储引擎层,流程如下:

  • Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录(age > 20 的第一条记录)。
  • 存储引擎定位到二级索引后,先不执行回表 操作,而是先判断一下该索引中包含的列(reward 列)的条件(reward 是否等于 100000)是否成立。条件不成立,则跳****过该二级索引。成立,则执行回表操作,将完成记录返回给 Server 层。
  • Server 层在判断其他的查询条件(本次查询没有其他条件)是否成立,如果成立则将其发送给客户端;否则跳过该记录,然后向存储引擎索要有一条记录。
  • 如此往复,直到存储引擎把表中的所有记录读完。

分析流程我们可以发现:使用索引下推后,虽然 reward 列无法使用到联合索引,但是因为它包含在联合索引里,省去了很多回表的操作(存储索引过滤了满足reward = 100000 的记录)

总结

在本篇的结尾,我们对执行一条SQL语句期间发生的流程再做一个小览:

  • 连接器:建立连接,管理连接、校验用户身份;
  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
  • 解析 SQL:通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
  • 执行 SQL :执行 SQL 共有三个阶段:
    • 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
    • 优化阶段:基于查询成本的考虑,选择查询成本最小的执行计划;
    • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

希望这篇文章能够帮到你更快理解sql语句在MySQL中的执行流程,我会持续更新MySQL底层知识,想要学习MySQL知识的小伙伴可以专注我哦。

相关推荐
丸辣,我代码炸了2 小时前
用 PostgreSQL 一库模拟 MySQL / MongoDB / Redis / Elasticsearch(附 ts_rank 详解)
mysql·mongodb·postgresql
Trouvaille ~2 小时前
【MySQL篇】表的约束:保证数据完整性
数据库·mysql·约束·数据完整性·实体完整性·域完整性·参照完整性
薿夜10 小时前
SpringSecurity(三)
android
计算机毕设vx_bysj686915 小时前
【免费领源码】77196基于java的手机银行app管理系统的设计与实现 计算机毕业设计项目推荐上万套实战教程JAVA,node.js,C++、python、大屏数据可视化
java·mysql·智能手机·课程设计
吴声子夜歌15 小时前
ES6——正则的扩展详解
前端·mysql·es6
xixingzhe215 小时前
Mysql统计空间增量
数据库·mysql
程序员萌萌15 小时前
Java之mysql实战讲解(三):聚簇索引与非聚簇索引
java·mysql·聚簇索引
zh_xuan16 小时前
Android Hilt实现依赖注入
android·hilt
freshman_y16 小时前
Qtcreator怎么新建安卓项目?编写一个五子棋游戏APP?
android·qt