type
status
date
slug
summary
tags
category
icon
password
在前两篇文章中,我们讨论了关于索引的基础和失效问题。
我们也随之对索引有了较为深刻的了解,但索引是否是万能的?它又有何优缺点和机制呢?
索引的优点
- 使用索引可以大幅度减少所需要的扫描的数据。
- 当 SQL 语句中使用
GROUP BY
和ORDER BY
时,可以依靠索引来减少分组或排序的时间。
- 连表查询时,在主外键字段上建立索引,可以大幅加快连表速度。
- 可以将随机 IO 变成顺序 IO 。
由此可见,索引作为一个高效的优化手段,它所带来的好处可以说是非常明显,然而 “凡事有利必有弊”,MySQL 的索引机制也有它的弊端。
索引的缺点
- 对于较小的表,使用索引不如直接使用全表扫描。
- 写入数据时,索引的维护需要额外的时间开销,“写” 相关的操作都会有性能损耗。
- 区分度不大的索引,不恰当的联合索引都会导致资源浪费。
因此,为了更好的使用索引,理解它的一些重要机制可以说是必要的。
索引的重要机制
索引覆盖
在MySQL索引(一) — 理解索引中讲到二级索引时提到过索引覆盖:如果当前索引包含了所有所需要查询的列,那么此时就无需回表查找,这个情况被称为 “索引覆盖”。
例如下面这个例子:
在 user 表中,有联合索引 idx_name_age,有下面 SQL 语句
可以看出,如果使用 idx_name_age,就能够直接拿到所有需要的字段,而无需再回表去查找,这就是索引覆盖的能力,能够大大减少回表次数,优化性能。
如果查询时使用 *,基本上大部分索引都不会涵盖表中的所有列,所以基本都会触发回表操作,因此不推荐在业务中使用 * 来作为查找列,会导致索引覆盖失效。
索引下推
索引下推是 MySQL5.6 版本以后推出的一种优化机制,用于优化查找时的额外回表问题。
对于如下 user 表:
id | name | age | address |
1 | 张三 | 18 | 浙江 |
2 | 李四 | 25 | 北京 |
3 | 王五 | 37 | 上海 |
4 | 张四 | 22 | 山东 |
5 | 赵六 | 23 | 浙江 |
它有联合索引 idx_name_address,执行如下 SQL:
在 5.6 版本之前,MySQL 的查询流程如下:
- 利用联合索引 idx_name_address,找到 “张三”、“张四” 两个索引节点。
- 返回索引节点存储的 id(1、4)给 Server 层,之后逐一进行回表查找。
- 在 Serevr 层中根据 address = '浙江' 这个条件逐条判断,最终确定 id = 1 的数据是真正的数据。
很显然,这是不够聪明的做法,毕竟联合索引中已经涵盖了 address 的数据,那么在引擎查找的时候,就应该可以筛选出 address != ‘浙江’ 的数据,从而减少回表次数。
所以在 5.6 版本之后,加入了 “索引下推” 机制,MySQL 的查询流程如下:
- 利用联合索引 idx_name_address,找到 “张三”、“张四” 两个索引节点。
- 根据 address = '浙江' 这个条件在索引节点中逐条判断,筛选出符合的节点 id = 1.
- 返回索引节点存储的 id(1)给 Server 层,之后进行回表查找出所需要的数据。
由此可见,索引下推能够优化查找的次数,增加查询效率。
MMR(Multi-Range Read) 机制
Multi-Range Read 简称为 MRR 机制,这也是和索引下推一同在 MySQL5.6 版本中引入的性能优化措施,那什么叫做
MRR
优化呢?在大多数业务情况中,我们都会通过调整索引来尽量避免回表产生的磁盘 IO ,以免降低数据查找的性能。但大多数时候,回表是不可避免的,并且更严重的是,它有可能会产生大量的离散 IO,大大增加查找时间。
例如这句 SQL
这条语句很清晰,是在 user 表中查找年龄在 18 岁到 45 岁的用户信息,但假设目前在 age 字段上有一个普通索引 idx_age,那么这条 SQL 语句的执行流程就会变成这样:
- 先在 age 字段的索引上找到 18 岁的节点,然后拿着 id 回表得到 18 岁的用户信息。
- 再次回到 idx_age 索引,继续找到所有 19 岁的节点,继续回表得到 19 岁的用户信息。
- 再次回到 idx_age 索引,继续找到所有 20 岁的节点......
- 周而复始,不断重复这个过程,直到将 18 ~ 45 岁的所有用户信息全部拿到为止。
那此时假设此时年龄 18~28 岁的表数据,位于磁盘空间的page_01 页上,而成绩为 28~38 岁的数据,位于磁盘空间的 page_02 页上,成绩为 38~45 岁的数据,又位于磁盘空间的 page_01 页上。此时回表查询时就会导致在 page_01、page_02 两页空间上来回切换,但 18~28 和 38~45 岁的数据完全可以合并,然后读一次 page_01 就可以了,既能减少 IO 次数,同时还避免了离散 IO 。
而 MRR 机制就主要是解决这个问题的,针对于辅助索引的回表查询,减少离散 IO,并且将随机 IO 转换为顺序 IO,从而提高查询效率。
而 MRR 是如何做到这一点的呢?
在 MRR 机制中,对于二级索引中查询出的主键 id,会将其放到缓冲区的 read_rnd_buffer 中,等到所有的索引检查工作完成后,或者缓冲区中的数据打到 read_rnd_buffer_size 的大小时,MySQL 会对缓冲区中的数据进行排序,从而得到一个有序的 id 集合 —— rest_sort,最终在根据顺序 IO 去主键索引中回表查询数据。
Index Skip Scan 索引跳跃式扫描
在讲联合索引时,提到过最左前缀匹配原则,也就是 SQL 的查询条件中必须要包含联合索引的第一个字段,这样才能命中联合索引查询。
实际上这条规则也并不是 100% 遵循的。因为在 MySQL8.x 版本中加入了一个新的优化机制,也就是索引跳跃式扫描,这种机制使得即使查询条件中,没有使用联合索引的第一个字段,也依旧可以使用联合索引,看起来就像跳过了联合索引中的第一个字段一样,这也是跳跃扫描的名称由来。
比如此时通过 (A、B、C) 三个列建立了一个联合索引,此时有如下一条 SQL:
按理来说,这条 SQL 既不符合最左前缀原则,也不具备使用索引覆盖的条件,因此绝对是不会走联合索引查询的,但思考一个问题,这条 SQL 中都已经使用了联合索引中的两个字段,结果还不能使用索引,这似乎有点亏啊对不?因此 MySQL8.x 推出了跳跃扫描机制,但跳跃扫描并不是真正的“跳过了”第一个字段,而是优化器为你重构了 SQL ,比如上述这条 SQL 则会重构成如下情况:
其实也就是 MySQL 优化器会自动对联合索引中的第一个字段的值去重,然后基于去重后的值全部拼接起来查一遍,一句话来概述就是:虽然你没用第一个字段,但我给你加上去,今天这个联合索引你就得用,不用也得给我用。
参考资料:
- 《高性能 MySQL 第三版》
- 作者:Tuwilt
- 链接:https:/blog.tuwilt.top/article/859b3125-7de6-4462-aae2-fb23ce2f110e
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。