首先是mysql索引的要求:
要尽少在磁盘做 I/O 操作
mysql是存储在磁盘上嘛,所以最后还是要在磁盘上去找。
要能尽快的按照区间高效地范围查找
索引是用来查询的,查询肯定不是单查一个人,肯定还有范围查询。
接来下就是找用什么样的数据结构了,
Hash?
hash的话,首先数据多了肯定就hash碰撞,那就要加个链表,单个数据查询还好,那区间呢?hash怎么做区间查找?gg
跳表?
跳表似乎对于我们来说是一个比较陌生的数据结构,但是在Redis中却是比较常用的数据结构之一。跳表底层实质就是可以进行二分查找的有序链表。而且在链表基础加上索引层。即能支持插入、删除等动态操作,也支持按区间高效查询。而且不管是查找、插入、删除对应的时间复杂度都是 O(logn)。
要理解跳表,先来看链表,假设链表存储是有序的数据,我们要想查询某一个数据,在最差的情况下要从头全遍历整个链表,时间复杂度是 O(n)。
如下图所示,跳表是在链表基础上加了索引层。可以起到支持区间查询的作用。
从上图所示,我们如果要查询一个 26 的节点,跳表就可以先从索引层遍历,当遍历到在索引层的 21 节点,会发现下一个索引层的节点是 36 节点时,很明显要找的 26 的节点就在这区间。此时我们只要再通过索引层指向原始链表的指针往下移到原始链这一层遍历,只要遍历 2 个节点即可找到 26 了。如果用原来的链表需要遍历 10 个节点,现在只要遍历 8 个节点。
没有满足 要尽少在磁盘做 I/O 操作条件,随着数据的增多,io次数也会增加。gg。
树
普通的树就是根节点,叶子节点。
二叉查找树
二叉查找树不同于普通二叉查找树,是将小于根节点的元素放在左子树,而右子树正好相反是放大于根节点的元素。(说白了就是根节点是左子树和右子树的中位数,左边放小于中位数的,右边放大于中位数,这不就是二分查找算法的奥义)
但是二叉树也有明显弊端,在极端情况下,如果每次插入的数据都是最小或者都是最大的元素,那么树结构会退化成一条链表。查询数据是的时间复杂度就会是O(n)
为什么不用[二叉查找树]作为数据库索引
二叉查找树,查找到指定数据,效率其实很高logn。但是数据库索引文件有可能很大,关系型数据存储了上亿条数据,索引文件大则上G,不可能全部放入内存中, 而是需要的时候换入内存,方式是磁盘页。一般来说树的一个节点就是一个磁盘页。如果使用二叉查找树,那么每个节点存储一个元素,查找到指定元素,需要进行大量的磁盘IO,效率很低,而且他的效率不稳定。 而B树解决了这个问题,通过单一节点包含多个data,大大降低了树的高度,大大减少了磁盘IO次数。
平衡二叉树
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
红黑树也是平衡二叉树的一种,但是不管自平衡树是平衡二叉查找树还是红黑树,都会随着插入的元素增多,而导致树的高度变高,这同样意味着磁盘 I/O 操作次数多,影响到整体查询的效率。
B树
我们平时看到 B+树 还有 B-树,不免就会将 B-树 读成 "B减树" ,但 B-树 其 - 横线只是连接符,所以 B-树 就是称为 B树。
自平衡二叉树虽然查找的时间复杂度在O(logn),前面也说过它本身是一个二叉树,每个节点只能有2个子节点,那么随着数据量增大的时候,节点个数越多,树高度也会增高(也就是树的深度越深),增加磁盘I/O次数,影响查询效率。
那么你如果从树形结构的二叉树这一路的进阶过程中可以看到,二叉树每一次为了解决一个新的问题都会创造出新的 bug (或者创造一个又个的痛点)。
看到这就不难猜到,B树的出现可以解决树高度的问题。之所以是B树,而并不是名称中"xxx二叉树",就是它不再限制一个父节点中只能有两个子节点,而是允许 M 个子节点(M > 2)。不仅如此,B树的一个节点可以存储多个元素,相比较于前面的那些二叉树数据结构又将整体的树高度降低了。
B 树的节点可以包含有多个字节点,所以 B树是一棵多叉树,它的每一个节点包含的最多子节点数量的称为B树的阶。如下图是一颗3阶的B树。
上图中每一个节点称为页,在mysql中数据读取的基本单位是页,而页就是我们上面所说的磁盘块。磁盘块中的p节点是指向子节点的指针。指针在树结构中都有,在前面的二叉树中也都是有的。
那我们来看一下上图所示,当一颗3阶的B树查找 90 这个的元素时的流程是怎么样的?
先从根节点出发,也就是 磁盘块1,判断 90 在17 ~ 35之间,通过磁盘块1中的指针 p3 找到磁盘块4。还是按照原来的步骤,在磁盘块4中的65 ~ 87之间相比较,最后磁盘4的指针p3找到磁盘块11。也就找到有匹配90的键值。
可以发现一颗3阶的B树在查找叶子节点时,由于树高度只有 3,所以查找过程最多只需要3次的磁盘I/O操作。
数据量不大时可能不太真切。但当数据量大时,节点也会随着增多;此时如果还是前面的自平衡二叉树的场景下,由于二叉树只能最多2个叶子节点的约束,也只能纵向去的去扩展子节点,树的高度会很高,意味着需要更多的操作磁盘I/O次数。而B树则可以通过横向扩展节点从而降低树的高度,所以效率自然要比二叉树效率更高。(直白说就是变矮胖了)
看到这,相信你也知道如果B树这么适合,也就没有接下来B+树的什么事了。
接着,那为什么不用B树,而用了B+树呢?
你看啊,B树其实已经满足了我们最前面所要满足的条件,减少磁盘I/O操作,同时支持按区间查找。但注意,虽然B树支持按区间查找,但并不高效。例如上面的例子中,B树能高效的通过等值查询 90 这个值,但不方便查询出一个期间内3 ~ 10区间内所有数的结果。因为当B树做范围查询时需要使用中序遍历,那么父节点和子节点也就需要不断的来回切换涉及了多个节点会给磁盘I/O带来很多负担。
B+树
B+树从 + 的符号可以看出是B树的升级版,MySQL 中innoDB引擎中的索引底层数据结构采用的正是 B+树。
B+树相比于B树,做了这样的升级:B+树中的非叶子节点都不存储数据,而是只作为索引。由叶子节点存放整棵树的所有数据。而叶子节点之间构成一个从小到大有序的链表互相指向相邻的叶子节点,也就是叶子节点之间形成了有序的双向链表。如下图B+树的结构。
定义如下
m阶B+树满足以下条件:
(1) 每个结点至多有m个孩子。
(2) 除根节点和叶结点外,每个结点至少有(m+1)/2个孩子。
(3) 如果根节点不为空,根结点至少有两个孩子。
(4) 所有叶子结点增加一个链指针,所有关键字都在叶子结点出现。
(5) 除了叶节点,结点的孩子数目等于关键字数目。 注意,B+树中非叶子结点存储的不是关键字数据的地址,而是指向叶子结点中关键字的索引。(所以任何关键字的查找必须走一条从根结点到叶子结点的路)
(B+树是不是有点像前面的跳表,数据底层是数据,上层都是按底层区间构成的索引层,只不过它不像跳表是纵向扩展,而是横向扩展的“跳表”。这么做的好处即减少磁盘的IO操作又提高了范围查找的效率。)
接着再来看B+树的插入和删除,B+树做了大量冗余节点,从上面可以发现父节点的所有元素都会在子节点中出现,这样当删除一个节点时,可以直接从叶子节点中删除,这样效率更快。
B树相比于B+树,B树没有冗余节点,删除节点时会发生复杂的树变形,而B+树有冗余节点,不会涉及到复杂的树变形。而且B+树的插入也是如此,最多只涉及树的一条分支路径。B+树也不用更多复杂算法,可以类似黑红树的旋转去自动平衡。
优点
- B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;
- B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
- B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
- B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。