所以,需要注意,哈希表后的链表并不是有序的,区间查询的话需要扫描链表,所以哈希表这种结构适用于只有等值查询的场景,比如Memcached及其他一些NoSQL引擎。
另外一个大家比较熟悉的数组结构,有序数组在等值查询和范围查询场景中的性能都非常优秀。
如果仅仅看查询效率,有序数组是非常棒的数据结构。但是,在需要更新数据的时候就麻烦了,你往中间插入一个记录就必须得挪动后面所有的记录,成本太高。
所以,有序数组索引只适用于静态存储引擎,比如你要保存的是2017年某个城市的所有人口信息,这类不会再修改的数据。
这两种都不是最主要的索引,常见的索引使用的数据结构是树结构,树是数据结构里相对复杂一些的数据结构,我们来一步步认识索引的树结构。
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
查找方法:首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
上面提到的有序数组的等值查询和比较查询效率非常高,但是更新数据存在问题。
为了支持频繁的修改,比如插入数据,我们需要采用链表。链表的话,如果是单链表,它的查找效率还是不够高。
所以,有没有可以使用二分查找的链表呢?
为了解决这个问题,BST(Binary Search Tree)也就是我们所说的二叉查找树诞生了。
二叉树具有以下性质:左子树的键值小于根的键值,右子树的键值大于根的键值。
如下图所示就是一棵二叉查找树
在这种比较平衡的状态下查找时间复杂度是O(log(n))。
但是二叉查找树存在一个问题:在某些极端情况下会退化成链表。
同样是2,3,4,6,7,8这六个数字,如果我们插入的数据刚好是有序的,那它就变成这样👇
这个时候,二叉查找树查找的时间复杂度就和链表一样,是O(n)。
造成它“叉劈”的原因是什么呢? 因为左右子树深度差太大,这棵树的左子树根本没有节点——也就是它不够平衡。
所以,我们有没有左右子树深度相差不是那么大,更加平衡的树呢? ——那就就是平衡二叉树,叫做Balanced binary search trees,或者AVL树。
AVL Trees (Balanced binary search trees) 平衡二叉树的定义:左右子树深度差绝对值不能超过 1。
例如左子树的深度是 2,右子树的深度只能是 1 或者 3。 这个时候我们再按顺序插入 2,3,4,6,7,8,就不会“叉劈”👇
AVL树的平衡是怎么做到的呢?主要用到了两个操作左旋、右旋。
(1)插入1、2、3。
当我们插入了1、2之后,如果按照二叉查找树的定义,3肯定是要在2的右边的,这个时候根节点1的右节点深度会变成2,但是左节点的深度是 0,因为它没有子节点,所以就会违反平衡二叉树的定义。
那应该怎么办呢?因为它是右节点下面接一个右节点,右–右型,所以这个时候我们要把2提上去,这个操作叫做左旋。
(2)同样的,如果我们插入3、2、1,这个时候会变成左左型,就会发生右旋操作,把2提上去。
既然平衡二叉树能保持平衡,不会退化,那么我们用平衡二叉树存储索引可以吗?——可以的。
当我们用树的结构来存储索引的时候,访问一个节点就要跟磁盘之间发生一次IO。 InnoDB操作磁盘的最小的单位是一页(或者叫一个磁盘块)。与主存不同,磁盘I/O存在机械运动耗费,因此磁盘I/O的时间消耗是巨大的。
所以如果每个节点存储的数据太少,从索引中找到我们需要的数据,就要访问更多的节点,意味着跟磁盘交互次数就会过多。
那么解决方案是什么?
(1)让每个节点存储更多的数据。
(2)让节点上有更多的关键字。
节点上的关键字的数量越多,我们的指针数也越多,也就是意味着可以有更多的分叉(我们把它叫做“路数”)。
因为分叉数越多,树的深度就会减少(根节点是 0)。 这样,树就从瘦高变成了矮胖。
这个时候,我们的树就不再是二叉了,而是多叉,或者叫做多路。
接下来看一下多路平衡查找树,也就是B树。
B树是一种多叉平衡查找树,如下图主要特点:
(1)B树的节点中存储着多个元素,每个内节点有多个分叉。
(2)节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点都储存数据。
(3)父节点当中的元素不会出现在子节点中。
(4)所有的叶子结点都位于同一层,叶节点具有相同的深度,叶节点之间没有指针连接。
以上图为例,我们来简单看几个查询:
(1)如果查找key<17,就走左边子节点;
(2)如果查找17<key<35,就走中间子节点;
(3)如果查找key>35,就走右边子节点;
(4)如果查找key=17,直接命中;
(5)如果查找key=35,直接命中;
(6)B树看起来很完美,到这就结束了吗?并没有。
B树不支持范围查询的快速查找,你想想这么一个情况如果我们想要查找10和35之间的数据,查找到15之后,需要回到根节点重新遍历查找,需要从根节点进行多次遍历,查询效率有待提高。
如果data存储的是行记录,行的大小随着列数的增多,所占空间会变大。这时,一个页中可存储的数据量就会变少,树相应就会变高,磁盘IO次数就会变大
所以接下来就引入我们的终极数据结构——B+树。
B+树,作为B树的升级版,在B树基础上,MySQL在B树的基础上继续改造,使用B+树构建索引。B+树和B树最主要的区别在于非叶子节点是否存储数据的问题
B树:非叶子节点和叶子节点都会存储数据。 B+树:只有叶子节点才会存储数据,非叶子节点至存储键值。叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。 来看一下InnoDB里的B+树的具体存储结构:
来说一下这张图的重点:
(1)最外面的方块,的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(粉色所示)和指针(黄色/灰色所示),如根节点磁盘包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、4、5……、65。非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。
(2)叶子节点之间使用双向指针连接,最底层的叶子节点形成了一个双向有序链表。
举个例子:假设一条记录是1K,一个叶子节点(一页)可以存储16条记录。非叶子节点可以存储多少个指针?
假设索引字段是bigint类型,长度为8字节。指针大小在InnoDB源码中设置为6字节,这样一共14字节。非叶子节点(一页)可以存储16384/14=1170个这样的 单元(键值+指针),代表有1170个指针。
树深度为2的时候,有1170^2个叶子节点,可以存储的数据为1170117016=21902400。
在查找数据时一次页的查找代表一次IO,也就是说,一张2000万左右的表,查询数据最多需要访问3次磁盘。
所以在InnoDB中B+树深度一般为1-3层,它就能满足千万级的数据存储。
我们来看一下B+Tree的数据搜寻过程:
(1)例如我们要查找 35,在根节点就找到了键值,但是因为它不是页子节点,所以会继续往下搜寻,25 是[17,35)的左闭右开的区间的临界值,所以会走中间的子节点,然 后继续搜索,它又是[28,34)的左闭右开的区间的临界值,所以会走左边的子节点,最后在叶子节点上找到了需要的数据。
(2)如果是范围查询,比如要查询从 22 到 60 的数据,当找到 22 之后,只需要顺着节点和指针顺序遍历就可以一次性访问到所有的数据节点,这样就极大地提高 了区间查询效率(不需要返回上层父节点重复遍历查找)。
(3)添加了指向相邻叶节点的指针,形成了带有顺序访问指针的B+Tree,这样做是为了提高区间查找的效率,只要找到第一个值那么就可以顺序的查找后面的值。
总结一下,InnoDB中的B+Tree的特点:
(1)它是B Tree 的变种,B Tree能解决的问题,它都能解决。B Tree解决的两大问题是什么?(每个节点存储更多关键字;路数更多)
(2)扫库、扫表能力更强(如果我们要对表进行全表扫描,只需要遍历叶子节点就可以了,不需要遍历整棵 B+Tree拿到所有的数据)
(3)B+Tree的磁盘读写能力相对于B Tree来说更强(根节点和枝节点不保存数据区, 所以一个节点可以保存更多的关键字,一次磁盘加载的关键字更多)
(4)排序能力更强(因为叶子节点上有下一个数据区的指针,数据形成了链表)
(5)效率更加稳定(B+Tree 永远是在叶子节点拿到数据,所以 IO 次数是稳定的)
聚簇索引的主键索引的叶子结点存储的是键值对应的数据本身,辅助索引的叶子结点存储的是键值对应的数据的主键键值。因此主键的值长度越小越好,类型越简单越好。
聚簇索引的数据和主键索引存储在一起。
从上图中可以看到辅助索引的叶子节点的data存储的是主键的值,主键索引的叶子节点的data存储的是数据本身,也就是说数据和索引存储在一起,并且索引查询到的地方就是数据(data)本身,那么索引的顺序和数据本身的顺序就是相同的。
因为聚簇辅助索引存储的是主键的键值,因此可以在数据行移动或者页分裂的时候降低成本,因为这时不用维护辅助索引。但是由于主键索引存储的是数据本身,因此聚簇索引会占用更多的空间。
聚簇索引在插入新数据的时候比非聚簇索引慢很多,因为插入新数据时需要检测主键是否重复,这需要遍历主索引的所有叶节点,而非聚簇索引的叶节点保存的是数据地址,占用空间少,因此分布集中,查询的时候I/O更少,但聚簇索引的主索引中存储的是数据本身,数据占用空间大,分布范围更大,可能占用好多的扇区,因此需要更多次I/O才能遍历完毕。