执行一条 select 语句,期间发生了什么?
// 在 product 表中,查询 id = 1 的记录
select * from product where id = 1;
MySQL 执行流程是怎样的?
先来一个上帝视角图,下面就是 MySQL 执行一条 SQL 查询语句的流程,也从图中可以看到 MySQL 内部架构里的各个功能模块。(这个图原作者做的太好啦,一目了然!)
可以看到, MySQL 的架构共分为两层:Server 层和存储引擎层,
- Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等。)都在 Server 层实现。
- 存储引擎层负责数据的存储和提取。支持 InnoDB、MyISAM、Memory 等多个存储引擎,不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是 InnoDB,从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。我们常说的索引数据结构,就是由存储引擎层实现的,不同的存储引擎支持的索引类型也不相同,比如 InnoDB 支持索引类型是 B+树 ,且是默认使用,也就是说在数据表中创建的主键索引和二级索引默认使用的是 B+ 树索引。
好了,现在我们对 Server 层和存储引擎层有了一个简单认识,接下来,就详细说一条 SQL 查询语句的执行流程,依次看看每一个功能模块的作用。
第一步:连接器
在 MySQL 中,连接器(Connection Manager)是负责处理客户端与服务器之间连接的组件。当客户端应用程序尝试连接到 MySQL 服务器时,连接器负责接受连接请求、建立连接,并处理连接的身份验证和权限验证等过程。以下是 MySQL 内部连接器的主要功能和流程:
- 接受连接请求: 当客户端应用程序尝试连接到 MySQL 服务器时,连接器首先接受连接请求。连接请求可以是来自 TCP/IP 连接、Unix 套接字连接或其他支持的连接方式。
- 建立连接: 连接器根据客户端请求建立连接,包括分配连接资源、为连接分配标识符等操作。
- 身份验证: 连接器在建立连接后,会进行客户端身份验证。这包括验证客户端提供的用户名和密码,以确定客户端是否有权限连接到服务器。
- 权限验证: 在身份验证成功后,连接器会对客户端进行权限验证。它会检查客户端对数据库、表和其他资源的操作权限,以确保客户端只能执行其具有权限的操作。
- 维护连接池: 连接器通常会维护一个连接池,以便在需要时重复使用已建立的连接,从而减少连接建立的开销和资源消耗。
- 处理查询请求: 一旦连接建立成功并通过身份验证和权限验证,连接器就会接收客户端发送的 SQL 查询请求,并将其传递给查询处理器进行处理。
详细讲一讲;
对于mysql连接,需要先经过tcp的三次握手,若密码都没有问题,会验证用户权限,然后保存起来,后续该用户在此连接里的任何操作,都会基于连接开始时读到的权限进行权限逻辑的判断。
所以,如果一个用户已经建立了连接,即使管理员中途修改了该用户的权限,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
一个mysql服务端是支持多个客户端进行连接的,可用show processlist查看,
sleep标识长时间未执行过命令,并且空闲时间是736秒,MySQL 定义了空闲连接的最大空闲时长,由 wait_timeout
参数控制的,默认值是 8 小时(28880秒),如果空闲连接超过了这个时间,连接器就会自动将它断开。
并且mysql也设置有最大连接数:
可以查看,基本都是151个
连接方式:和http一样有长连接和短连接之分
// 短连接
连接 mysql 服务(TCP 三次握手)
执行sql
断开 mysql 服务(TCP 四次挥手)// 长连接
连接 mysql 服务(TCP 三次握手)
执行sql
执行sql
执行sql
....
断开 mysql 服务(TCP 四次挥手)
通常会使用长连接,但是,使用长连接后可能会占用内存增多,因为 MySQL 在执行查询过程中临时使用内存管理连接对象,这些连接对象资源只有在连接断开时才会释放。如果长连接累计很多,将导致 MySQL 服务占用内存太大,有可能会被系统强制杀掉,这样会发生 MySQL 服务异常重启的现象。
相信以前使用小服务器部署大量客户访问的网站时最清楚不过,数据库老是自动关闭,难受得很;
当时也是有解决方法,先说下我当时使用情况,编写一个重启脚本,每天晚上12点定时关闭服务器,并经过一分钟后重启,可以有效地减少服务器关闭概率;
通常来说。有两种解决方式。
第一种,定期断开长连接。既然断开连接后就会释放连接占用的内存资源,那么我们可以定期断开长连接。
第二种,客户端主动重置连接。MySQL 5.7 版本实现了 mysql_reset_connection()
函数的接口,注意这是接口函数不是命令,那么当客户端执行了一个很大的操作后,在代码里调用 mysql_reset_connection 函数来重置连接,达到释放内存的效果。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
至此,连接器的工作做完了,简单总结一下:
- 与客户端进行 TCP 三次握手建立连接;
- 校验客户端的用户名和密码,如果用户名或密码不对,则会报错;
- 如果用户名和密码都对了,会读取该用户的权限,然后后面的权限逻辑判断都基于此时读取到的权限;
第二步:查询缓存
Mysql8.0之后已经移除;
连接器得工作完成后,客户端就可以向 MySQL 服务发送 SQL 语句了,MySQL 服务收到 SQL 语句后,就会解析出 SQL 语句的第一个字段,看看是什么类型的语句。
如果 SQL 是查询语句(select 语句),MySQL 就会先去查询缓存( Query Cache )里查找缓存数据,看看之前有没有执行过这一条命令,这个查询缓存是以 key-value 形式保存在内存中的,key 为 SQL 查询语句,value 为 SQL 语句查询的结果。
如果查询的语句命中查询缓存,那么就会直接返回 value 给客户端。如果查询的语句没有命中查询缓存中,那么就要往下继续执行,等执行完后,查询的结果就会被存入查询缓存中。
这么看,查询缓存还挺有用,但是其实查询缓存挺鸡肋的。
对于更新比较频繁的表,查询缓存的命中率很低的,因为只要一个表有更新操作,那么这个表的查询缓存就会被清空。如果刚缓存了一个查询结果很大的数据,还没被使用的时候,刚好这个表有更新操作,查询缓冲就被清空了,相当于缓存了个寂寞。
所以,MySQL 8.0 版本直接将查询缓存删掉了,也就是说 MySQL 8.0 开始,执行一条 SQL 查询语句,不会再走到查询缓存这个阶段了。
对于 MySQL 8.0 之前的版本,如果想关闭查询缓存,我们可以通过将参数 query_cache_type 设置成 DEMAND。
只对于长时间不更新和频繁查询的静态数据有良好的效果,其他时候甚至起到反作用;
第三步:解析SQL
解析器
解析器会做如下两件事情。
第一件事情,词法分析。MySQL 会根据你输入的字符串识别出关键字出来,例如,SQL语句 select username from userinfo,在分析之后,会得到4个Token,其中有2个Keyword,分别为select和from:
关键字 | 非关键字 | 关键字 | 非关键字 |
---|---|---|---|
select | username | from | userinfo |
第二件事情,语法分析。根据词法分析的结果,语法解析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法,如果没问题就会构建出 SQL 语法树,这样方便后面模块获取 SQL 类型、表名、字段名、 where 条件等等。
如果我们输入的 SQL 语句语法不对,就会在解析器这个阶段报错。但是不是什么错误都归解析器管,他只负责检查语法和构建语法树,对于数据是否错误或者存在是在之后进行的
第四步:执行 SQL
经过解析器后,接着就要进入执行 SQL 查询语句的流程了,每条SELECT
查询语句流程主要可以分为下面这三个阶段:
- prepare 阶段,也就是预处理阶段;
- optimize 阶段,也就是优化阶段;
- execute 阶段,也就是执行阶段;
即预处理器,优化器,执行器三个阶段
预处理器
我们先来说说预处理阶段做了什么事情。
- 检查 SQL 查询语句中的表或者字段是否存在;
- 将
select *
中的*
符号,扩展为表上的所有列;
我下面这条查询语句,test 这张表是不存在的,这时 MySQL 就会在执行 SQL 查询语句的 prepare 阶段中报错。
mysql> select * from test;
ERROR 1146 (42S02): Table 'mysql.test' doesn't exist
优化器
经过预处理阶段后,还需要为 SQL 查询语句先制定一个执行计划,这个工作交由「优化器」来完成的。
优化器主要负责将 SQL 查询语句的执行方案确定下来,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引。
当然,我们本次的查询语句(select * from product where id = 1)很简单,就是选择使用主键索引。
要想知道优化器选择了哪个索引,我们可以在查询语句最前面加个 explain
命令,这样就会输出这条 SQL 语句的执行计划,然后执行计划中的 key 就表示执行过程中使用了哪个索引,比如下图的 key 为 PRIMARY
就是使用了主键索引。
当出现多个索引时,优化器会帮忙找效率最高的索引执行;
执行器
经历完优化器后,就确定了执行方案,接下来 MySQL 就真正开始执行语句了,这个工作是由「执行器」完成的。在执行的过程中,执行器就会和存储引擎交互了,交互是以记录为单位的。
接下来,用三种方式执行过程,跟大家说一下执行器和存储引擎的交互过程(PS :为了写好这一部分,特地去看 MySQL 源码,也是第一次看哈哈)。
- 主键索引查询
- 全表扫描
- 索引下推
select * from product where id = 1;
主键索引查询
这条查询语句的查询条件用到了主键索引,而且是等值查询,同时主键 id 是唯一,不会有 id 相同的记录,所以优化器决定选用访问类型为 const 进行查询,也就是使用主键索引查询一条记录,那么执行器与存储引擎的执行流程是这样的:
- 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为 InnoDB 引擎索引查询的接口,把条件
id = 1
交给存储引擎,让存储引擎定位符合条件的第一条记录。 - 存储引擎通过主键索引的 B+ 树结构定位到 id = 1的第一条记录,如果记录是不存在的,就会向执行器上报记录找不到的错误,然后查询结束。如果记录是存在的,就会将记录返回给执行器;
- 执行器从存储引擎读到记录后,接着判断记录是否符合查询条件,如果符合则发送给客户端,如果不符合则跳过该记录。
- 执行器查询的过程是一个 while 循环,所以还会再查一次,但是这次因为不是第一次查询了,所以会调用 read_record 函数指针指向的函数,因为优化器选择的访问类型为 const,这个函数指针被指向为一个永远返回 - 1 的函数,所以当调用该函数的时候,执行器就退出循环,也就是结束查询了。
const类型就是常量类型,即结果为常量,只用查询一次就结束了;
- const(常数访问): 表示查询条件是一个常数,优化器可以确定查询结果是一个确定的常数值,因此直接返回该常数值而无需执行实际的查询操作(优化器会使用主键索引直接定位到符合条件的数据行,而不必扫描整个表或者使用其他索引。这样可以在常数时间内获取到查询结果,而不必执行实际的查询操作。)const 访问类型并不意味着完全不执行查询操作,而是表示查询条件是一个常数,并且可以通过主键索引快速定位到唯一的一行数据。
- all(全表扫描): 表示优化器选择了对整个表进行扫描的方式来执行查询操作。这意味着数据库系统将遍历整个表的每一行数据,然后根据查询条件过滤数据,返回符合条件的结果。全表扫描通常在没有合适的索引可用时被选择,或者对大部分数据都需要进行查询的情况下使用。
- range(范围访问): 表示优化器选择了对索引进行范围查询的方式来执行查询操作。这种访问类型通常用于根据范围条件(例如 BETWEEN、<、> 等)对索引进行查询,获取符合范围条件的数据行。
- index(索引访问): 表示优化器选择了对索引进行直接访问的方式来执行查询操作。这种访问类型通常用于利用索引快速定位到符合查询条件的数据行,而无需扫描整个表。索引访问可以是全覆盖索引或部分索引,取决于索引是否包含了查询所需的所有列。
- ref(引用访问): 表示优化器选择了对索引进行引用查询的方式来执行查询操作。这种访问类型通常用于基于索引键值进行查询,并且索引键值与其他表之间存在外键关联关系的情况。引用访问通过索引键值快速定位到符合条件的数据行,然后根据外键关联关系获取关联表中的数据行。