联合索引在 B + 树上的存储结构及数据查找方式
联合索引在 B + 树上的存储结构及数据查找方式
引言
上一篇文章《MySQL 索引那些事》主要讲了 MySQL 索引的底层原理,且对比了 B+Tree 作为索引底层数据结构相对于其他数据结构(二叉树、红黑树、B 树)的优势,最后还通过图示的方式描述了索引的存储结构。但都是基于单值索引,由于文章篇幅原因也只是在文末略提了一下联合索引,并没有大篇幅的展开讨论,所以这篇文章就单独去讲一下联合索引在 B + 树上的存储结构。
本文主要讲解的内容有:
- 联合索引在 B + 树上的存储结构
- 联合索引的查找方式
- 为什么会有最左前缀匹配原则
在分享这篇文章之前,我在网上查了关于 MySQL 联合索引在 B + 树上的存储结构这个问题,翻阅了很多博客和技术文章,其中有几篇讲述的与事实相悖。庆幸的是看到搜索引擎列出的有一条是来自思否社区的问答,有答主回答了这个问题,贴出一篇文章和一张图以及一句简单的描述。PS:贴出的文章链接已经打不开了。
所以在这样的条件下这篇文章就诞生了。
联合索引的存储结构
下面就引用思否社区的这个问答来展开我们今天要讨论的联合索引的存储结构的问题。
来自思否的提问,联合索引的存储结构 (https://segmentfault.com/q/1010000017579884) 有码友回答如下:

联合索引 bcd , 在索引树中的样子如图 , 在比较的过程中 ,先判断 b 再判断 c 然后是 d ,
由于回答只有一张图一句话,可能会让你有点看不懂,所以我们就借助前人的肩膀用这个例子来更加细致的讲探寻一下联合索引在 B + 树上的存储结构吧。
首先,表 T1 有字段 a,b,c,d,e,其中 a 是主键,除 e 为 varchar 其余为 int 类型,并创建了一个联合索引 idx_t1_bcd(b,c,d),然后 b、c、d 三列作为联合索引,在 B + 树上的结构正如上图所示。联合索引的所有索引列都出现在索引数上,并依次比较三列的大小。上图树高只有两层不容易理解,下面是假设的表数据以及我对其联合索引在 B + 树上的结构图的改进。PS:基于 InnoDB 存储引擎。

bcd 联合索引在 B + 树上的结构图

T1 表
通过这俩图我们心里对联合索引在 B + 树上的存储结构就有了个大概的认识。下面用我的语言为大家解释一下吧。
我们先看 T1 表,他的主键暂且我们将它设为整型自增的(PS:至于为什么是整型自增上篇文章有详细介绍这里不再多说),InnoDB 会使用主键索引在 B + 树维护索引和数据文件,然后我们创建了一个联合索引(b,c,d)也会生成一个索引树,同样是 B + 树的结构,只不过它的 data 部分存储的是联合索引所在行的主键值(上图叶子节点紫色背景部分),至于为什么辅助索引 data 部分存储主键值上篇文章也有介绍,感兴趣或还不知道的可以去看一下。
好了大致情况都介绍完了。下面我们结合这俩图来解释一下。
对于联合索引来说只不过比单值索引多了几列,而这些索引列全都出现在索引树上。对于联合索引,存储引擎会首先根据第一个索引列排序,如上图我们可以单看第一个索引列,如,1 1 5 12 13.... 他是单调递增的;如果第一列相等则再根据第二列排序,依次类推就构成了上图的索引树,上图中的 1 1 4 ,1 1 5 以及 13 12 4,13 16 1,13 16 5 就可以说明这种情况。
联合索引的查找方式
当我们的 SQL 语言可以应用到索引的时候,比如 select * from T1 where b = 12 and c = 14 and d = 3; 也就是 T1 表中 a 列为 4 的这条记录。存储引擎首先从根节点(一般常驻内存)开始查找,第一个索引的第一个索引列为 1,12 大于 1,第二个索引的第一个索引列为 56,12 小于 56,于是从这俩索引的中间读到下一个节点的磁盘文件地址,从磁盘上 Load 这个节点,通常伴随一次磁盘 IO,然后在内存里去查找。当 Load 叶子节点的第二个节点时又是一次磁盘 IO,比较第一个元素,b=12,c=14,d=3 完全符合,于是找到该索引下的 data 元素即 ID 值,再从主键索引树上找到最终数据。

最左前缀匹配原则
之所以会有最左前缀匹配原则和联合索引的索引构建方式及存储结构是有关系的。
首先我们创建的 idx_t1_bcd(b,c,d) 索引,相当于创建了 (b)、(b、c)(b、c、d)三个索引,看完下面你就知道为什么相当于创建了三个索引。
我们看,联合索引是首先使用多列索引的第一列构建的索引树,用上面 idx_t1_bcd(b,c,d) 的例子就是优先使用 b 列构建,当 b 列值相等时再以 c 列排序,若 c 列的值也相等则以 d 列排序。我们可以取出索引树的叶子节点看一下。

索引的第一列也就是 b 列可以说是从左到右单调递增的,但我们看 c 列和 d 列并没有这个特性,它们只能在 b 列值相等的情况下这个小范围内递增,如第一叶子节点的第 1、2 个元素和第二个叶子节点的后三个元素。
由于联合索引是上述那样的索引构建方式及存储结构,所以联合索引只能从多列索引的第一列开始查找。所以如果你的查找条件不包含 b 列如(c,d)、(c)、(d) 是无法应用缓存的,以及跨列也是无法完全用到索引如 (b,d),只会用到 b 列索引。
这就像我们的电话本一样,有名和姓以及电话,名和姓就是联合索引。在姓可以以姓的首字母排序,姓的首字母相同的情况下,再以名的首字母排序。
如:
M
毛 不易 178****
马 化腾 183****
马 云 188****
Z
张 杰 189****
张 靓颖 138****
张 艺兴 176****
我们知道名和姓是很快就能够从姓的首字母索引定位到姓,然后定位到名,进而找到电话号码,因为所有的姓从上到下按照既定的规则(首字母排序)是有序的,而名是在姓的首字母一定的条件下也是按照名的首字母排序的,但是整体来看,所有的名放在一起是无序的,所以如果只知道名查找起来就比较慢,因为无法用已排好的结构快速查找。
到这里大家是否明白了为啥会有最左前缀匹配原则了吧。
实践
如下列举一些 SQL 的索引使用情况
[
](javascript:void(0)😉
1 select * from T1 where b = 12 and c = 14 and d = 3;-- 全值索引匹配 三列都用到
3 select * from T1 where b = 12 and c = 14 and e = 'xml';-- 应用到两列索引
5 select * from T1 where b = 12 and e = 'xml';-- 应用到一列索引
7 select * from T1 where b = 12 and c >= 14 and e = 'xml';-- 应用到一列索引及索引条件下推优化
9 select * from T1 where b = 12 and d = 3;-- 应用到一列索引 因为不能跨列使用索引 没有c列 连不上
11 select * from T1 where c = 14 and d = 3;-- 无法应用索引,违背最左匹配原则
[
](javascript:void(0)😉

浙公网安备 33010602011771号