Mysql数据库索引的实现——B more tree

身为一名java开发工程师,拥有对于数据库的牢靠掌握是十分重要的,尤其是对于索引的掌握,更为重要,那么索引的底层是如何实现的呢,就来看看这篇违章的Mysql索引底层讲解吧。

此篇文章我们要解决两个问题,第一是数据库索引是如何实现的?第二是底层使用的是什么数据结构和算法?

算法解析

1. 解决问题的前提是定义清除问题

该如何定义清除问题所在?除了对于问题有详细的调研,还有一个办法,那就是通过对于一些模糊的需求进行一定的假设,来限定要解决的问题的范围.

如果你对于数据库的操作十分了解,对索引十分了解,那么我们就可以将这个问题很快解决,但是大多数开发工程师,还是用crud居多,所以我们假设要解决的问题只包含两个常用的需求。

  • 根据某个值查找数据,比如 select * from user where id = 1234;
  • 根据区间值来查找某些数据,比如select * from user where id > 1234 and id < 2345

除了这些比较简单的操作以外,往往会有些安全、性能方面的问题。这次我们针对索引,所以更多说说性能方面的需求。我们主要考虑时间和空间,也就是执行效率和存储空间

在执行效率方面,我们希望通过索引,查询数据的效率尽可能的高,在存储空间方面,我们希望不要消耗太多内存。

2. 尝试用学过的数据结构解决问题

问题的需求大致清楚了,然后我们来看看是否可以从数据结构的角度分析问题,就拿散列表,平衡二叉树和跳表来说。

散列表

散列表的查询性能很好,时间复杂度是O(1)。但是,散列表不支持按照区间内快速查找数据,所以散列表不能满足我们的需求。

平衡二叉树

尽管平衡二叉树的性能也很高,时间复杂度是O(logn)。并且,对树进行中序遍历,我们还可以得到一个从小到大的数据序列,但是这仍然无法实现区间内快速寻找。

跳表

跳表是在链表之上加多层索引构成的。它支持快速的插入、查找、删除数据,对应的时间复杂度是O(logn)。并且,跳表可以实现区间内快速寻找。只需要定位到区间的起点和终点即可,顺序遍历链表,直到区间终点对应的结点为止,这期间遍历得到的数据就是合格的数据。

在这里插入图片描述

这样看来,跳表是可以解决这个问题。但是实际上,数据库索引所用到的数据结构和跳表很相似,就是B+ tree。

而它也是从二叉查找树演变而来的,接下来会从二叉查找树复习一下,看看它是如何演变成为B+树的。

3. 改造二叉查找树来解决这个问题

为了让二叉查找树能够支持按照区间查找数据,那我们就要进行如下的改造:

  1. 树中的节点并不存储数据本身,而是只是作为索引。
  2. 把每个叶子节点串在一条链表上,链表中的数据是从小到大有序的。

修改后如下图所示:

在这里插入图片描述

是不是很像跳表了呢,如果我们要求查找某个区间的数据,就只需要拿到区间的初始值,在树中进行查找,当查找到某个叶子节点之后,就顺着链表往后遍历,直到链表中的结点数据值大于区间的终止值为止。所有遍历到的数据,就是符合区间值的所有数据。

在这里插入图片描述

但是,如果我们的索引个数成千万上亿,又将存储的索引放在内存,该如何解决占用过多的内存这个问题呢?

我们可以借助时间换空间的思路,将索引存储在硬盘内,但是我们又知道硬盘速度太慢了,甚至相差几十万倍。

所以这种思路需要进行改变,二叉查找树,经过改造之后,支持区间查找的功能就实现了。不过为了节省内存,就需要把树存储在硬盘中,那么每个节点的读取(或者访问)都需要对应一次磁盘IO操作树的高度就等于每次查询数据时磁盘IO的操作次数。

我们前面说到,磁盘IO很耗时间,那么我们优化的终点就是尽量减少磁盘IO操作,也就是说尽量降低树的高度。那么该如何降低树的高度呢?

我们来看下,如果我们把二叉索引树构建成为m叉树,高度是不是比二叉树要小?如图所示,给16个数据构建二叉树索引,树的高度是4,查找一个数据,就需要4个磁盘IO操作,如果对16个数据构建五叉树索引,那高度只有2,查找一个数据,也就对应两次磁盘操作。如果是100叉树,对一亿数据构建索引,那树的高度也就是3,最多只要3此磁盘IO就能获取到数据,所以磁盘IO少了,查找的数据效率也就提升了。

在这里插入图片描述

如果我们将m叉树实现B+树索引,用代码实现出来,就是这个样子(案例是给int类型的数据库字段添加索引,所以代码中的keywords是int类型的)

public class BmoreTreeDemo {
    /**
     * 这是 B+树非叶子节点的定义
     *
     * 假设keywords=[3,5,8,10]
     * 4个键值将数据分为5个区间:(-INF,3),[3,5),[5,8),[8,10),[10,INF)
     * 5个区间分别对应:children[0]...children[4]
     *
     * m值是事先计算得到的,计算的依据是让所有信息的大小正好等于页的大小:
     * PAGE_SIZE = (m-1)*4[keywords大小]+m*8[children大小]
     */

    public static int m=5;//5叉树
    public int[] keywords = new int[m-1];//键值,用来划分数据区间
    public BmoreTreeDemo[] children = new BmoreTreeDemo[m];//保存子节点指针

}

/**
 * 这是B+树中叶子节点的定义
 *
 * B+树中的叶子节点跟内部节点是不一样的,
 * 叶子节点存储的是值,而不是区间。
 * 这个定义里,每个叶子节点存储3个数据行的键值及地址信息
 *
 * k值是事先计算得到的,计算的依据是让所有信息的大小正好等于页的大小:
 * PAGE_SIZE = k*4[keywords]+k*8[dataAddress大小]+8[prev大小]+8[next大小]
 */

class BPlusTreeLeafNode{
    public static int k =3;
    public int[] keywords = new int[k];//数据的键值
    public long[] dataAddress = new long[k];//数据地址

    public BPlusTreeLeafNode prev; //这个结点在链表中的前驱节点
    public BPlusTreeLeafNode next; //这个结点在链表中的后继节点
}

稍微说一下这段代码,对于相同个数的数据构建m叉索引,分的叉越多,磁盘IO访问也就越少,那我们就分一层,可行不??

当然不可行,要那样哪还叫树,不管是内存中的数据,还是磁盘中的数据,操作系统都是按照页(一页大小通常是4kb)来读取的,一次会读取一页的数据。如果要读取的数据量超过一页的大小,就会触发多次磁盘IO操作,所以我们在选择m大小的时候,要尽量让每个节点的大小等于一个页的大小。读取一个节点,只需要一次磁盘IO操作。

在这里插入图片描述

尽管索引可以提高数据库的查询效率,但是应该知道,索引有利也有弊,他也会让写入数据的效率下降。这是为什么呢?

数据的写入,会涉及到索引的更新,这是索引导致写入变慢的主要原因

对于一个B+树而言,m值是根据页的大小事先计算好的,也就是说,每个节点最多只能有m个子节点,往数据库中写入数据的过程中,这样就有可能让索引中某些节点的子节点个数超过m,这个节点的大小超过了一个页的大小,读取这样的一个节点,就会导致多次磁盘IO操作。我们该如何解决这个问题呢?

实际上,处理思路并不复杂,我们只需要将这个节点分裂成两个节点,但是分裂之后,其上层父节点的子节点个数有可能会超过m个。不过这没有关系,将父节点也分裂成两个节点,这种级联反应会从下往上,一直影响到根节点。这个分裂过程,你可以结合着下面这个图一块看,会更容易理解。

图中的B+树是一个三叉树,我们限定叶子节点中,数据的个数超过2个就分裂节点,非叶子节点中,子节点的个数超过三个就分裂节点。

在这里插入图片描述

正式因为要时刻保持B+树索引是一个m叉树,所以,索引的存在会导致数据库写入的速度降低。实际上,不光写入数据会变慢,删除数据也能变慢。这是为什么呢?

我们在删除某个数据的时候,也要对应的更新索引节点。这个处理思路有点类似跳表中删除数据的处理思路。频繁的数据删除,就会导致某些结点中,子节点的个数变得非常少,长此以往,如果每个节点的子节点都比较少,势必会影响到索引的效率。

我们可以设置一个阈值,在B+树中,这个阈值等于m/2.如果节点中的子节点少于m/2,我们就把它跟相邻的兄弟节点合并。不过,在合并之后的结点的子节点个数有可能就会超过m,针对这种情况,我们可以借助插入数据时候的处理方法,再分裂节点。

举一个删除操作的例子:

在这里插入图片描述

总结一下B+树的特点:

  1. 每个节点中子节点不能超过m,也不能小于m/2
  2. 根节点的子节点个数可以不超过m/2,这是一个例外
  3. m叉树只存储索引,而不存储数据
  4. 通过链表将叶子节点串联在一起,这样可以方便按照区间查找
  5. 一般情况,根节点会被存储在内存中,其他节点存储在磁盘中。

然后再顺便说一个B树和B-树:

B树就是B-树,B树是B+树的低级版,他们之间有几个不同的地方:

  1. B+树的结点不存储数据,B树存储
  2. B树的叶子节点不需要链表来串联

也就是说B树只是一个每个节点的子节点个数不能小于m/2的m叉树。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值