前言
上一篇介绍了 MySQL 的逻辑架构和执行过程,这一篇将介绍索引相关的内容。
索引是用额外的数据结构,来实现快速检索目标数据的。就像字典当中的目录一样,用额外的空间来存储部分内容,从而加快检索速度。
MySQL 的逻辑架构分为 Server 层和存储引擎层,其中索引和数据就位于存储引擎中,而不同的存储引擎可能有不同的实现索引的方式,比如常见的 InnoDB 和 MyISAM 使用的都是 B+Tree,但是实现方式不同。
按照不同的分类方式,索引可以分为以下几类:
- 按「数据结构」分类:B+ 树索引、B 树索引、Hash 索引等。
- 按「物理存储」分类:聚簇索引(主键索引)、非聚簇索引(二级索引)。
- 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引、全文索引、空间索引等。
- 按「字段个数」分类:单列索引、联合索引。
B+树索引
B+ 树是一种多叉平衡搜索树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都指向下一个叶子节点,形成一个链表。所有节点按照索引键大小排序,构成一个双向链表,便于范围查询。
虽然,InnoDB 和 MyISAM 都支持 B+ 树索引,但是它们数据的存储结构实现方式不同:
- InnoDB 存储引擎:B+ 树索引的叶子节点保存数据本身(数据页);
- MyISAM 存储引擎:B+ 树索引的叶子节点保存数据的物理地址;
由于物理空间地址是混乱无序的,MyISAM 只能先取数据,再排序;而 InnoDB 的物理存放顺序和索引顺序一致。
后续如果没有额外说明,则默认说的都是 InnoDB 中的 B+ 树索引的实现。
InnoDB 里的 B+ 树中的每个节点都是一个数据页。
![在这里插入图片描述](https://img-blog.csdnimg.cn/9a0e1016ce3b47bb927fa27f9acf3c2a.png#pic_center)B树和B+树
B 树又名平衡多路查找树,B 树中所有结点的孩子个数的最大值称为 B 树的阶,通常用 m 表示。
B 树和 B+ 树的区别在于,B 树的每个节点都存储了 key 和 data,而 B+ 树的 data 只存储在叶子节点上,这样单个节点可以存更多的索引键,树的高低就越小,查询时磁盘 IO 的次数就越小。且 B 树没有冗余节点,而 B+ 树有冗余节点。
B+ 树的冗余节点:不仅在叶节点中保存了所有的键,且部分键在非叶节点中也存在。
具体区别:
1、单点查询
B 树的查询效率可能更高,但波动较大;B+ 树相对更矮胖,查询底层节点的 I/O 次数更少。
B+ 树的非叶子节点不存放实际的记录数据,仅存放索引值,因此数据量相同的情况下,B+ 树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O 次数会更少。
2、范围查询
B 树的范围查询相当于树的遍历,效率很低;B+ 树的叶子节点间还有双链表连接,利于范围查询。
3、插入删除效率
B 树没有冗余节点,插入和删除都需要涉及树的变形;B+ 树存在冗余节点,插入和删除时不需要涉及树的变形,效率更高。
为什么选择 B+ 树而不是红黑树(或其他平衡二叉搜索树)?
红黑树(或其他平衡二叉搜索树)每个节点只能存储一个数据,所以在数据量大的情况下,树高很大,会导致多次磁盘 IO。
按物理存储分类
聚簇索引
Clustered Index,又称主键索引,叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+ 树的叶子节点里。
因为表的数据都是存放在聚簇索引的叶子节点里,所以 InnoDB 存储引擎一定会为表创建一个聚簇索引,且由于数据在物理上只会保存一份,所以聚簇索引只能有一个。
在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:
- 如果有主键,默认会使用主键作为聚簇索引的索引键;
- 如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键;
- 在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键;
当执行一条查询语句,比如使用主键索引查询 id 号为 5 的商品时,B+ 树会自顶向下逐层进行查找,查询过程是这样的:
- 将 5 与根节点的索引数据(1,10,20)比较,因为 5 在 1 和 10 之间,根据搜索逻辑,会找到第二层的索引数据(1,4,7);
- 在第二层的索引数据(1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6);
- 在叶子节点的索引数据(4,5,6)中进行查找,然后找到了索引值为 5 的行数据,完成查找。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。
以一个整数字段索引为例,每个节点大约能存储 1200 条索引数据。当这棵树高是 4 的时候,就可以存 1200^3≈17 亿条记录。所以在一个 10 亿行的表上根据一个整数字段的索引查找一个值最多只需要访问 3 次磁盘。
通常 B+ 树存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 2-3 次磁盘 I/O。
非聚簇索引
Secondary Index,又称二级索引、辅助索引,主键索引的 B+ 树和二级索引的 B+ 树区别如下:
- 主键索引的 B+ 树的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+ 树的叶子节点里;
- 二级索引的 B+ 树的叶子节点存放的是主键值,而不是实际数据。
通过二级索引查询数据的过程:
这里将商品编号字段设置为二级索引,那么二级索引的 B+ 树如下图,其中非叶子的 key 值是 product_no(橙色部分),叶子节点存储的数据是主键值(绿色部分)。
如果用二级索引查询商品,会先检二级索引中的 B+ 树的索引值,检索过程与上文介绍的一致,找到对应的叶子节点获取主键值,然后再通过主键索引中的 B+ 树查询到对应的叶子节点,获取整行数据。这个过程叫「回表」,也就是说要查两个 B+ 树才能查到数据。如下图:
在索引树上,每次只能根据一个主键 id 查到一行数据,所以回表是一行行搜索主键索引的。
按字段特性分类
主键索引
建立在主键字段上的索引,通常在创建表的时候一起创建,一张表最多只有一个主键索引,索引的值唯一且不能为空值。
唯一索引
建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,允许存在多个空值。
由于索引的唯一性,查询性能是最好的。但是在更新数据时,需要额外的查询来保证索引的唯一性,所以写性能会偏低。
由于每次更新前都需要通过查询来判断唯一性,所以唯一索引没有使用 change buffer,所以写性能会低很多。
普通索引
建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE。
前缀索引
前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。
使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。但是由于前缀索引没有存储一个字段的完整值,所以无法完成排序分组等工作。
全文索引
MySQL5.6 之前,只有 MyISAM 支持,MySQL 5.6 及以后的版本,InnoDB 也支持全文索引。只有字段的数据类型为 char、varchar、text 及其系列才可以建全文索引。
全文索引在做模糊查询的时候性能很高,但是由于会对字段做分词处理,分词结果也会存储在全文索引中,所以会占用很大的空间。
修改字段值后,需要时间分词,所以不会立刻更新全文索引,如果对实时性要求高,则需要手动更新。
一般可以使用 ElasticSearch、Solr、MeiliSearch 等搜索引擎来代替全文索引。
联合索引
联合索引的非叶子节点会按顺序用联合索引的字段作为键值,使用联合索引时,会按照最左匹配原则,也就是按照最左优先的方式进行索引的匹配。
联合索引范围查询
联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配。也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。
select * from t_table where a > 1 and b = 2
在符合 a > 1 条件的二级索引记录的范围里,b 字段的值是无序的,所以联合索引的 b 字段并没有使用到索引。
select * from t_table where a >= 1 and b = 2
和 SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2
都有使用到联合索引。(在边界值时)
联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。
索引下推
ICP,Index Condition Pushdown。
对于联合索引(a, b),在执行 select * from table where a > 1 and b = 2
语句的时候,只有 a 字段能用到索引。
- 在 MySQL5.6 之前,只能从一个个回表,到「主键索引」上找出数据行,再对比 b 字段值。
- 而 MySQL5.6 引入索引下推优化后,可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
覆盖索引
Covering Index,在使用二级索引字段作为条件查询的时候,如果从二级索引中就能查找到需要的数据,而不必再次回表查询,这样就叫做覆盖索引。
示例:
ALTER TABLE `t_user` ADD INDEX `idx1`(`name`,`id`) USING BTREE;
// name 字段为二级索引
select id from t_user where name="木木";
MRR
Multi-Range Read Optimization,是在 MySQL5.6 中引入的性能优化措施,默认开启。通过把「随机磁盘读」转化为「顺序磁盘读」,从而提高了索引查询的性能。
随机磁盘读:在磁盘上随机读取数据,磁盘读取头需要不断移动到不同位置,导致产生大量的磁盘寻道时间,从而降低读取性能。
顺序磁盘读:按照顺序读取数据,磁盘读取头在读取完一个数据块后直接移动到下一个数据块进行读取,读取性能高。
示例:
select * from user where no > 10
如果随着二级索引 no 的值递增顺序查询的话,id 的值就变成随机的,那么回表时就会出现随机磁盘访问,性能相对较差。
因为大多数的数据都是按照主键递增顺序插入得到的,所以我们可以认为,如果按照主键递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。
所以就有了 MRR 优化:
- 根据二级索引 no,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中;
- 将 read_rnd_buffer 中的 id 进行递增排序;
- 排序后的 id 数组,依次到主键索引中查记录,并作为结果返回。
索引跳跃式扫描
Index Skip Scan,是 MySQL8.0 引入的优化机制。当没有使用到联合索引的第一个字段且后续索引字段都使用到时,可能会触发索引跳跃式扫描,会自动对联合索引中的第一个字段的值去重,然后基于去重后的值全部拼接起来查询。
// 有联合索引(A,B,C),执行如下SQL
SELECT * FROM `tb_xx` WHERE B = `xxx` AND C = `xxx`;
// 原本不符合最左前缀原则,不能使用联合索引,但是如果使用了索引跳跃式扫描,则原本的SQL语句会被重构为以下形式:
SELECT * FROM `tb_xx` WHERE B = `xxx` AND C = `xxx`
UNION ALL
SELECT * FROM `tb_xx` WHERE B = `xxx` AND C = `xxx` AND A = "aaa"
......
SELECT * FROM `tb_xx` WHERE B = `xxx` AND C = `xxx` AND A = "zzz";
但是跳跃扫描机制也有很多限制,比如多表联查时无法触发、分组操作时无法触发、使用了 DISTINCT 去重无法触发等。
且只有在唯一性较差的情况下,才能发挥出不错的效果;否则相当于走一次全表扫描。不过最后还是由优化器决定是否使用该策略。
执行计划
使用 Explain 工具可以模拟优化器执行 SQL 查询语句,经常用于分析查询语句是否正确使用索引以及排查性能瓶颈。Explain 只能解释 select 语句。
示例:
EXPLAIN SELECT * FROM `zz_users`;
+----+-------------+----------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------+------+---------------+------+---------+------+------+-------+
| 1 | SIMPLE | zz_users | ALL | NULL | NULL | NULL | NULL | 3 | |
+----+-------------+----------+------+---------------+------+---------+------+------+-------+
参数
- id:执行的顺序;
- select_type:查询类型;
- partitions:分区信息,非分区表为 null;
- type:数据扫描类型;
- possible_keys:可能用到的索引;
- key:实际用的索引,如果这一项为 NULL,说明没有使用索引;
- key_len:索引的长度;
- ref:连接查询的连接条件;
- rows:扫描的数据行数;
- extra:其他信息,有几十种不同的值;
select_type
- simple:表示不需要 union 操作或者不包含子查询的简单查询。
- primary:表示最外层查询。
- subquery:子查询中的第一个查询。
- derived:派生表查询,既 from 字句中的子查询。
- union:union 操作中第二个及之后的查询。
- dependent union:union 操作中第二个及之后的查询,并且该查询依赖于外部查询。
- dependent subquery:子查询中的第一个查询,并且该查询依赖于外部查询。
- materialized:物化查询。
- uncacheable subquery:无法被缓存的子查询,对外部查询的每一行都需要重新进行查询。
- uncacheable union:union 操作中第二个及之后的查询,并且该查询属于 uncacheable subquery。
type
type 字段就是描述了找到所需数据时使用的扫描方式是什么,常见扫描类型的执行效率从低到高的顺序为:
- All(全表扫描);
- index(全索引扫描):对索引表进行全扫描,这样做的好处是不再需要对数据进行排序,但是开销依然很大;
- range(索引范围扫描):一般在 where 子句中使用 < 、>、in、between 等关键词,只检索给定范围的行,属于范围查找;
- ref(非唯一索引扫描):多表查询时,根据非唯一非空索引进行查询的情况;
- eq_ref(唯一索引扫描):多表关联查询时,根据唯一非空索引进行查询的情况;
- const(结果只有一条的主键或唯一索引扫描)。
- NULL:无需访问表或者索引,比如获取一个索引列的最大最小值;
全表扫描和全索引扫描的效率很低,要尽量避免。
const 类型和 eq_ref 都使用了主键或唯一索引。const 是与常量进行比较,查询效率会更快;而 eq_ref 通常用于多表联查中。
extra
- Using filesort :当查询语句中包含 group/order by 操作,且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。
- Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。
- Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,即使用了覆盖索引,避免了回表操作。
索引失效
主要有两种情况会导致索引失效:
- 优化器选择不使用索引。比如当索引扫描的行数超过表行数的 30% 时,就可能会选择放弃索引查询,转而使用全表扫描的方式,因为这样能够使用磁盘顺序 IO,查询效率更高。
- 索引的结构满足不了 SQL 语句。比如联合索引没有遵循最左匹配原则。这种情况是我们可以通过调整索引结构和改写 SQL 语句可以避免的,也是我们在进行索引优化时需要考虑的。
以下几种为常见的索引失效的场景:
- 使用左模糊匹配时,即
like %xx
会造成索引失效; - 在查询条件中对索引列使用计算和函数操作;
- 类型转换操作:比如对字符串字段使用整型作为查询参数(会发生隐式类型转换);
- 联合索引没有遵循最左匹配原则(包括 group/order by);
- order by 时,同时使用升序和降序;
- 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
索引优化
-
主键索引自增:插入记录时,都是追加数据,可以避免移动数据。也能够避免页分裂的情况发生。
-
覆盖索引优化:将所有需要查询的字段都建立联合索引,从而避免回表操作。
-
前缀索引优化:只使用某个字段中字符串的前几个字符建立索引,减小索引字段大小。
局限性:
- order by 无法使用前缀索引
- 无法把前缀索引用作覆盖索引
-
索引设置非空:
- 索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可为 NULL 的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count 会省略值为NULL 的行。
- NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,会导致更多的存储空间占用。因为 InnoDB 默认行存储格式 COMPACT,会用 1 字节空间存储 NULL 值列表。
最后
本文介绍了 MySQL 索引相关的内容,最重要的是 InnoDB 是如何通过 B+ 树实现索引的,以及聚簇索引和非聚簇索引的在实现上的区别。在此基础上,就知道了索引是如何工作的,以及如何进行正确使用和优化,避免索引失效情况的发生。
下一节将介绍 MySQL 的事务。