1. MySQL 执行流程是怎样的?
MySQL 的架构共分为两层:Server 层和存储引擎层
- Server 层负责建立连接、分析和执行 SQL。MySQL 大多数的核心功能模块都在这实现,主要包括连接器,查询缓存、解析器、预处理器、优化器、执行器等。另外,所有的内置函数(如日期、时间、数学和加密函数等)和所有跨存储引擎的功能(如存储过程、触发器、视图等。)都在 Server 层实现。
- 存储引擎层负责数据的存储和提取。支持 InnoDB、MylSAM、Memory 等多个存储引擎,不同的存储引擎共用一个 Server 层。现在最常用的存储引擎是 InnoDB。我们常说的索引数据结构,就是由存储引擎层实现的,不同的存储引擎支持的索引类型也不相同,比如 InnoDB 支持索引类型是 B+ 树,且是默认使用,也就是说在数据表中创建的主键索引和二级索引默认使用的是 B+ 树索引。
第一步:连接器
如果在 Linux 操作系统里要使用 MySQL,首先要连接 MySQL 服务,然后才能执行 SQL 语句:
# -h 指定 MySQL 服务得 IP 地址,如果是连接本地的 MySQL 服务,可以不用这个参数;
# -u 指定用户名,管理员角色名为 root;
# -p 指定密码,如果命令行中不填写密码,就需要在交互对话里面输入密码
mysql -h$ip -u$user -p
连接的过程需要先经过 TCP 三次握手,因为 MySQL 是基于 TCP 协议进行传输的。
如果 MySQL 服务正常运行,完成 TCP 连接的建立后,连接器就要开始验证你的用户名密码,如果都没有问题,连接器就会获取该用户的权限,然后保存起来,后续该用户在此连接里的任何操作,都会基于连接开始时读到的权限进行权限逻辑的判断。
所以,如果一个用户已经建立了连接,即使管理员中途修改了该用户的权限,也不会影响已经存在连接的权限。
如何查看 MySQL 服务被多少个客户端连接了
执行 show processlist
命令查看:
上图显示的结果中,id 为 6 的用户的 Command 列的状态为Sleep
,这意味着该用户连接完 MySQL 服务就没有再执行过任何命令,也就是一个空闲的连接。
空闲连接
MySQL 定义了空闲连接的最大空闲时长,由 wait_timeout
参数控制的,默认值是 8 小时,如果空闲连接超过了这个时间,连接器就会自动将它断开。
当然,我们也可以手动断开空闲的连接,使用的是 kill connection + id 的命令。
mysql> kill connection +6;
Query OK, 0 rows affected (0.00 sec)
一个处于空闲状态的连接被服务端主动断开后,客户端并不会马上知道,等到客户端在发起下一个请求的时候,才会收到报错。
MySQL 的连接数有限制吗?
最大连接数由 max_connections
参数控制
解决长连接占用内存的问题
MySQL 的连接也跟 HTTP 一样,有短连接和长连接的概念,一般推荐使用长连接,但长连接后可能会占用内存增多,因此可以:
- 定期断开长连接
- 客户端主动重置连接,这是一个接口函数而不是命令,当客户端执行了一个很大的操作后,在代码里调用 mysql_reset_connection 函数来重置连接,达到释放内存的效果。
第二步:查询缓存
连接器的工作完成后,客户端就可以向 MySQL 服务发送 SQL 语句了,MySQL 服务收到 SQL 语句后,就会解析出 SQL 语句的第一个字段,看看是什么类型的语句。SQL 的查询缓存是以 key-value 形式保存在内存中,key 为 SQL 查询语句,value 为 SQL 语句查询的结果。由于对于更新频繁的表,查询缓存的命中率很多,所以 MySQL 8.0 版本直接将查询缓存删掉了。
这里说的查询缓存是 server 层的,而不是 Innodb 存储引擎中的 buffer pool。
第三步:解析 SQL
解析器
在正式执行 SQL 查询语句之前,MySQL 会先对 SQL 语句做解析,这个工作交由【解析器】来完成。
第一件事,词法分析。MySQL 会根据输入的字符串识别出关键词出来,例如,SQL 语句 select username from userinfo,在分析之后,会得到 4 个 Token,其中有 2 个 Keyword,分别是 select 和 from;
第二件事,语法分析。根据词法分析的结果,语法解析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法,如果没问题就会构建出 SQL 语法树,这样方便后面模块获取 SQL 类型、表名、字段名、where 条件等。
如果输入的 SQL 语句语法不对,就会在解析器这个阶段报错。但表不存在或者字段不存在不是在解析器里做的。
第四步:执行 SQL
经过解析器后,接着就要进入执行 SQL 查询语句的流程,每条 SELECT
查询语句流程主要可以分为下面这三个阶段:
- prepare 阶段,也就是预处理阶段;
- optimize 阶段,也就是优化阶段;
- execute 阶段,也就是执行阶段;
预处理器
预处理阶段首先检查 SQL 查询语句中的表或者字段是否存在;将 select *
中的 *
符号,扩展为表上的所有列。
优化器
经过预处理阶段后,还需要为 SQL 查询语句先制定一个执行计划,这个工作交由【优化器】来完成。
优化器主要负责将 SQL 查询语句的执行方案确定下来,比如在表里面有多个索引的时候,优化器会基于查询成本的考虑,来决定选择使用哪个索引。
要想知道优化器选择了哪个索引,可以在查询语句最前面加个 ``explain 命令,这样就会输出这条 SQL 语句的执行计划,执行计划中的 key 就表示执行过程中使用了哪个索引,如果 key 为 PRIMARY
就是使用了主键索引,如果为 null
说明没有使用索引,那么就会全表扫描。
执行器
优化器确定了执行方案,接下来由【执行器】开始执行语句,在执行的过程中,执行器和存储引擎交互,交互是以记录为单位的。
下面是不同情况执行的例子;
- 主键索引查询
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,意味着优化器预计只需要访问数据一次,因为数据是不变的,所以当 read_record 函数指针指向的函数被调用时,返回 1,这个值满足 while 循环的终止条件,循环结束。 ==
- 全表扫描
select * from product where name = 'iphone'
这条查询语句的查询条件没有用到索引,所以优化器决定选用访问类型为 ALL 进行查询,也就是全表扫描的方式查询:
- 执行器第一次查询,会调用 read_first_record 函数指针指向的函数,因为优化器选择访问的类型为 all,这个函数指针被指向为 InnoDB 引擎全扫描的接口,让存储引擎读取表中的第一条记录;
- 执行器会判断读到的这条记录的 name 是不是 ipone,如果不是则跳过;如果是则将记录发给客户端(客户端显示的时候是所有记录一起显示是因为客户端是等查询语句查询完成后才会统一显示)
- 执行器查询的过程是一个 while 循环,所以还会再查一次,会调用 read_record 函数指针指向的函数,因为优化器选择 all,指向的还是 InnoDB 引擎全扫描的接口,接着向存储引擎层要求读刚才那条记录的下一条记录,然后存储引擎取出后将其返回给执行器,执行器继续判断条件。
- 一直重复上述过程,直到存储引擎把表中的所有记录读完,然后向执行层返回了读取完毕的信息;
- 执行器收到存储引擎报告的查询完毕的信息,退出循环,停止查询。
- 索引下推
select * from t_user where age > 20 and reward = 100000
索引下推能够减少二级索引在查询时的回表操作,提高查询的效率,因为它将 Server 层部分负责的事情,交给存储引擎层去处理了。相当于索引下推是在索引扫描过程中应用过滤条件,在 Server 层读取数据之前就排除不符合条件的记录
比如上面的 SQL 查询语句,t_user 表对 age 和 reward 字段建立了联合索引(age,reward),联合索引当遇到范围查询(>、<)就会停止匹配,也就是age 字段能用到联合索引,但是 reward 字段无法利用到索引,这是因为 B+ 树索引的有序性,在范围查询的前缀条件下,后序列的有序性无法保证,索引也就失去了优化查询的能力。
不使用索引下推时,执行器与存储引擎的执行过程为:
- Server 层首先调用存储引擎的接口定位到满足查询条件的第一条二级索引记录,也就是定位到 age > 20 的第一条记录;
- 存储引擎根据二级索引的 B+ 树快速定位到这条记录后,获取主键值,然后进行回表操作(查找到索引键值后还需要获取其他列的值,这时候回到表中找到实际的数据行),将完整的记录返回给 Server 层;
- Server 层在判断该记录的 reward 是否等于 100000,如果成立则将其发送给客户端;否则跳过该记录;
- 接着,继续向存储引擎索要下一条记录,存储引擎在二级索引定位到记录后,获取主键值,然后回表操作,将完整的记录返回给 Server 层;
- 如此往复,直到存储引擎把表中的所有记录读完。
在没有索引下推的时候,每查询到一条二级索引记录都要进行回表操作,使用索引下推后,存储引擎定位到二级索引后,先不执行回表操作,而是先判断一下该索引包含的列(reward 列)的条件是否成立,如果条件不成立,则直接跳过该二级索引,如果成立,则执行回表操作,将完成记录返回给 Server 层。