myesn

myEsn2E9

hi
github

MySQL 联合索引

介绍#

建立在多列上的索引成为联合索引

比如,将商品表中的 product_no 和 name 字段组合成联合索引 (product_no, name)

CREATE INDEX index_product_no_name ON product(product_no, name);

下图为联合索引(product_no, name)的 B+Tree 示意图(叶子节点画的是单向链表,实际为双向,图片有误):
image

联合索引的非叶子节点用两个字段的值作为 B+Tree 的 key 值。

联合索引查询数据时,先按 product_no 比较,在 product_no 相同的情况下再按 name 列比较。也就是说,联合索引查询的 B+Tree 是先按 product_no 进行排序,然后在 product_no 相同的情况下再按 name 字段排序。

因此,使用联合索引时,存在最左匹配原则:按照最左边字段优先的方式进行索引的匹配,如果不遵守,联合索引就失效。

比如,创建了一个 (a,b,c) 的联合索引,查询条件是以下几种,就可以匹配上联合索引:
- where a=1;
- where a=1 and b=2 and c=3;
- where b=2 and a=1;

需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要

但是,如果是以下几种,因为不符合 [最左匹配原则],所以无法匹配上联合索引:
- where b=2;
- where c=3;
- where b=2 and c=3;

上面这些**查询条件之所以会失效,是因为(a, b, c) 联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。**
所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。

这里举例联合索引(a, b)的 B+ Tree 如下(叶子节点画的是单向链表,实际为双向,图片有误):
image

可以看到,a 是全局有序的(1, 2, 2, 3, 4, 5, 6, 7 ,8),而 b 是全局是无序的(12,7,8,2,3,8,10,5,2)。因此,直接执行 where b = 2 这种查询条件没有办法利用联合索引的,利用索引的前提是索引里的 key 是有序的

只有在 a 相同的情况下,b 才是有序的,比如 a 等于 2 的时候,b 的值为(7,8),这时就是有序的,这个有序状态是局部的,因此,执行 where a = 2 and b = 7 时 a 和 b 字段能用到联合索引,也就是联合索引生效了。

联合索引范围查询#

联合索引有一些特殊情况,并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询,可能存在部分字段用到联合索引的 B+Tree,部分字段没有用到的情况,这种特殊情况就发生在范围查询。

联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引

导致联合索引的最左匹配原则停止匹配的范围查询#

Q1: select * from t_table where a > 1 and b = 2,联合索引 (a, b) 哪个字段用到了联合索引的 B+Tree?#

只有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到。

在符合 a > 1 条件的二级索引级联的范围里,b 字段的值是无序的。因此,无法根据 b = 2 来进一步减少需要扫描的级联数量。

通过执行计划中的 key(使用到的索引) 和 key_len(使用了索引中几个字段) 得知优化器具体使用了多少个字段的搜索条件来形成扫描区间的边界条件。

    EXPLAIN select * from t_table where a > 1 and b = 2

a 和 b 都是 int 且不为 NULL,int 占用 4 字节(如果字段允许为 NULL,就在字段类型占用的字节数上加 1,也就是 5)下图中 key_len 为 4,代表只有 a 字段用到了联合索引:
image

Q2:select * from t_table where a ≥ 1 and b = 2,联合索引 (a, b) 哪个字段用到了联合索引的 B+Tree?#

虽然在符合 a>= 1 条件的二级索引记录的范围里,b 字段的值是「无序」的,但是对于符合 a = 1 的二级索引记录的范围里,b 字段的值是「有序」的(因为对于联合索引,是先按照 a 字段的值排序,然后在 a 字段的值相同的情况下,再按照 b 字段的值进行排序)。也就是说,从符合 a = 1 and b = 2 条件的第一条记录开始扫描,而不需要从第一个 a 字段值为 1 的记录开始扫描

所以,Q2 这条查询语句 a 和 b 字段都用到了联合索引进行索引查询。

通过执行计划中的 key_len 可以得知。

Q3:select * from t_table where a between 2 and 8 and b =2,联合索引 (a, b) 哪个字段用到了联合索引的 B+Tree?#

在 MySQL 中,BETWEEN 包含了 value1 和 value2 边界值,类似于 >= and =<。而有的数据库则不包含 value1 和 value2 边界值(类似于> and <)。

由于 MySQL 的 BETWEEN 包含 value1 和 value2 边界值,所以类似于 Q2 查询语句,因此 Q3 这条查询语句 a 和 b 字段都用到了联合索引进行索引查询

通过执行计划中的 key_len 可以得知。

Q4:select * from t_user where name like ‘j%’ and age = 22,联合索引 (name, age) 哪个字段用到了联合索引的 B+Tree?#

a 字段可以在联合索引的 B+Tree 中进行索引查询,形成的扫描区间是 ['j','k')。注意, j 是闭区间(即≥=j and <k 的区间)。如下图:
image

虽然在符合前缀为 ‘j’ 的 name 字段的二级索引记录的范围里,age 字段的值是「无序」的,但是对于符合 name = j 的二级索引记录的范围里,age 字段的值是「有序」的(因为对于联合索引,是先按照 name 字段的值排序,然后在 name 字段的值相同的情况下,再按照 age 字段的值进行排序)。

也就是说,从符合 name = 'j' and age = 22 条件的第一条记录时开始扫描,而不需要从第一个 name = 'j' 的记录开始扫描 。如下图的右边:
image

所以,Q4 这条查询语句 a 和 b 字段都用到了联合索引进行索引查询

通过执行计划中的 key_len 可以知道:

  • name 字段类型是 varchar (30) 且不为 NULL,数据库表使用了 utf8mb4 字符集,一个字符是 4 个字节,因此 name 最多占用 120 字节(30*4),又因为 name 是变长类型的字段,需要再加 2 字节(用于存储该字段实际数据的长度值),最终得知 name 的 ken_len 为 122

  • age 字段类型是 int 且不为 NULL,ken_len 为 4

    TIP

    对于「因为 name 是变长类型的字段,需要再加 2 字节」。之前这篇 **文章 (opens new window)** 说「如果变长字段允许存储的最大字节数小于等于 255 字节,就会用 1 字节表示变长字段的长度」,而这里为什么是 2 字节?

    key_len 的显示比较特殊,行格式是由 innodb 存 储引擎实现的,而执行计划是在 server 层生成的,所以它不会去问 innodb 存储引擎可变字段的长度占用多少字节,而是不管三七二十一都使用 2 字节表示可变字段的长度。

    毕竟 key_len 的目的只是为了告诉你索引查询中用了哪些索引字段,而不是为了准确告诉这个字段占用多少字节空间。

Q4 查询语句的执行计划如下,可以看到 key_len 为 126 字节,name 的 key_len 为 122,age 的 key_len 为 4,说明优化器使用了 2 个字段的查询条件来形成扫描区间的边界条件,也就是 name 和 age 字段都用到了联合索引进行索引查询。
image

综上所述,联合索引的最左匹配原则,在遇到范围查询(如 >、<)时停止匹配,即范围查询的字段可以用到联合索引,但其后的字段无法用到。注意,对于 ≥、≤、between、”like 前缀匹配” 的范围查询并不会停止匹配。

索引下推#

对于联合索引 (a, b),在执行 select * from table where a > 1 and b = 2; 语句时,只有 a 字段能用到索引,在联合索引的 B+Tree 找到第一个满足条件的主键值(ID 为 2)后,还需要判断其他条件是否满足(即 b = 2),这时是在联合索引里判断?还是回主键索引去判断?

  • MySQL 5.6 之前,只能从 ID2(主键值)开始一个个回表,到 [主键索引] 上找出数据行,再对比 b 字段值
  • MySQL 5.6 引入的索引下推优化 (index condition pushdown),可以在联合索引遍历过程中,在联合索引中对包含的字段(该字段在联合索引的列中)先做判断(b=2),直接过滤掉不满足条件的记录,按照过滤后的数据再一一进行回表扫描,以此减少回表次数

当查询语句的执行计划里,出现了 Extra 为 Using index condition,就说明使用了索引下推的优化。

索引区分度#

建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到

区分度就是某个字段 column 不同值的个数「除以」表的总行数,计算公式如下:
image

比如,性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 UUID 这类字段就比较适合做索引或排在联合索引列的靠前的位置。

因为如果索引的区分度很小,假设字段的值分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比(惯用的百分比界线是 "30%")很高的时候,它一般会忽略索引,进行全表扫描。

联合索引进行排序#

select * from order where status = 1 order by create_time asc

针对以上 SQL,建立 (status, create_time) 联合索引,可避免 MySQL 数据库发生文件排序(filesort)

在查询时,如果只用到 status 的索引,但是这条语句还要对 create_time 排序,这时就要用文件排序 filesort,也就是在 SQL 执行计划中,Extra 列会出现 Using filesort。

所以,要利用索引的有序性,在 status 和 create_time 列建立联合索引,这样根据 status 筛选后的数据就是按照 create_time 排好序的,避免再文件排序,提高了查询效率。

总结一句话:避免 using filesort 就使用覆盖索引

参考#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。