B+树:MySQL数据库索引是如何实现?

------ 本文是学习算法的笔记,《数据结构与算法之美》,极客时间的课程 ------

我们都知道,数据库使用索引,会提升效率。那数据库索引是如何实现的呢?底层使用的是什么数据结构和算法?

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

假设我们有这两个基本的需求:

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

对于常用的数据结构,能否解决上面的两个需求呢?

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

再看平衡二叉查找树。尽管平衡二叉查找树查询性能也很高,时间复杂度是O(logn)。而且,对树进行中序遍历,我们还可能得到一个从小到大有序的数据序列,但这仍然不足以支持按照区间快速查找数据。

我们再看跳表。跳表是在链表之上加上多层索引结构。它支持快速插入、查找、删除数据,对应的时间复杂度是O(logn)。并且,跳表也支持按区间快速地查找数。我们只需要定位到区间起点值对应在链表的结点,然后从这个结点开始,顺序遍历链表,直到区间终点对应的结点为止,这期间遍历得到的数据就是满足区间值的数据。
在这里插入图片描述

这样看来,跳表是可能解决这个问题。实际上,数据库索引所用的数据结构跟跳表非常相似,叫作 B+树。不过,这是通过二叉查找树演化过来的,而不是跳表。我们从二叉查找树讲起,看它如何一步一步被改造成 B+树的。

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

为了让二叉查找树支持按照区间来查找数据,我们对其进行改造:树中的节点并不存储数据本身,而只是作为索引。除此之外,我们把每个叶子节点串在一条链表上,链表中的数据是从小到大有序的。经过改造之后的二叉树,就像图上这样,看越来是不是很像跳表呢?
在这里插入图片描述

改造之后,如果我们要求区间的数据。我们只需要拿区间的起始值,在树中查找,当查找到某个叶子节点之后,我们再顺着链表往后遍历,直到链表中的结点数据值大于区间的终止值为止。所有遍历到的数据,就是符合区间值的所有数据。
在这里插入图片描述

但是,要对上千万、上亿的数据构建索引,如果将索引存储在内存中,尽管内存访问的速度非常快,查询的效率非常高,但是,占用的内存会非常多。

比如,给一亿个数据构建二叉树查找树索引,那索引中会包含大约1亿个节点,每个节点假设占用16个字节,那需要大约1GB的内存。给一张表建立索引,我们需要1GB的内存空间。如果我们要给10张表建立索引,那对内存的需要是无法满足的。如果解决这个索引占用太多内存的问题呢?

借助时间换空间的思路,把索引存储在硬盘中,而非内存中。我们都知道,硬盘是一个非常慢的存储设备。通常内存的访问速度是纳秒级别的,而磁盘访问的速度是毫秒级别的。读取同样大小的数据,从磁盘中读取花费的时间,是从内存中读取所花费时间的上万倍,甚至几十万倍。

这种方案,需要读取磁盘中的索引,因此数据查询效率就相应降低很多。

二叉查找树,经过改造之后,支持区间查找的功能。可为了节省内存,把树存储在硬盘中,每个节点的读取(或访问),都对应一次磁盘IO操作。树的高度就等于每次查询数据时磁盘IO操作的次数。

如果我们把索引构建成 m 叉树,树的高度就会降低。如果对16个数据构建五叉树索引,那高度只有2,查找一个数据,对应只需要2次磁盘操作。如果 m=1000,那对一亿个数据构建索引,树的高度也只是3,最多只要3次磁盘IO就能获取到数据。磁盘IO变少了,查找数据的效率也就提高了。
在这里插入图片描述

在这里插入图片描述

代码实现就是正面的样子(假设我们给 int 类型数据库字段加索引,所以代码中的 keywords是int类型的):

    /**
     * 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 class BPlusTreeNode {
		public static final int m = 5; // 5叉树
		public int[] keywords = new int[m - 1]; // 键值,用来划分数据区间
		public BPlusTreeNode[] children = new BPlusTreeNode[m]; // 保存子节点指针
	}
	
	/**
	 * 这是 B+ 树中叶子节点的定义
	 * 
	 * B+ 树中叶子节点跟内部结点是不一样的,
	 * 叶子节点存储的是值,而非区间。
	 * 这个定义里,每个叶子节点存储3个数据行的键值及地址信息
	 * 
	 * k 值是事先计算得到的,计算依据是让所有信息的大小正好等于页的大小;
	 * PAGE_SIZE = k*4[keywords大小] + k*8[dataAddress 大小] + 8[prev大小] + 8[next大小]
	 */
	public class PBlusTreeLeafNode{
		public static final int k = 3;
		public int[] keywords = new int[k]; // 数据的键值
		public long[] dataAddress = new long[k]; // 数据的地址
		
		public PBlusTreeLeafNode prev; // 这个结点在链表中的前驱结点
		public PBlusTreeLeafNode next; // 这个结点在链表中的后继结点
	}

对于相同个数的数据构建 m 叉树索引,m 越大,树的高度越小,那 m 多大才最合适呢?

不管内存中的数据,还是磁盘中的数据,操作系统都是按页(一页大小通常是4KB)来读取的,一次会读一页的数据。如果需要读取的数据量超过一页的大小,就会触发多次IO操作。所以,在选择m大小时,尽量让每个节点的大小等于一页的大小,读取一个节点,只需要一次磁盘IO操作。
在这里插入图片描述

索引也不是“银弹”,它会让写入数据的效率下降。因为在数据写入过程,会涉及索引的更新,这是索引导致写入变慢的主要原因。

对于一个B+树来说,m 值是根据页面的大小事先计算好的,也就是说,每个节点最多只能有 m 个子节点。在数据写入的过程中,这样就可能使索引中某些节点的子节点个数超过 m,这个节点的大小超过了一个页的大小,读取一个节点,就会导致多次磁盘IO操作。

实际上,解决这个问题并不复杂。只需要将这个节点分裂成两个节点。但是节点分裂之后,其上层父节点的子节点就可能超过 m 个。不过也没关系,可以用同样的方法,将这个父节点也分裂成两个节点。这种级联反应会从下往上,一直影响到要节点。这个分裂过程,结合图来看,就容易理解些(图中的 B+ 树是一个三叉树。我们限定叶子节点中,,数据的个数超过2个就分裂节点;非叶子节点个数超过3个就分裂节点)。在这里插入图片描述

正是因为要时刻保证 B+ 树索引是一个 m 叉树,所有,索引的存在会导致数据库写入的速度降低。实际上,删除数据也会变慢。

因为在删除某个数据的时候,也要对应更新索引节点。频繁的数据删除,就会导致某些节点中,子节点的个数变得非常少,长此以往,如果每个节点的子节点都比较少,势必会影响索引的效率。

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

数据库索引以及B+树的由来,就讲完了。有没有发现,B+ 树的结构和操作,跟跳表非常类似。理论上讲,对跳表稍加改造,也可以替代 B+ 树,作为数据库的索引实现。

总结引申

B+ 对的特点大概有以个几个

  • 每个节点中子节点的个数不能超过 m,也不能小于 m/2;
  • 根节点的子节点个数可以不超过 m/2,这是一个例外;
  • m 叉树只存储索引,并不真正存储数据,这个有点儿类似跳表;
  • 通过链表将叶子节点串联在一起,这样可以方便的按区间查找;
  • 一般情况,根节点会被存储在内存中,其他节点存储在磁盘中。

除了 B+ 树,你可能还听过 B树、B-树。实际上B-树就是B树,其英文翻译都是 B-Tree,这里的 “-”只是一个连接符。

而B树实际上是低版本的B+树,或者说 B+树是B树的改进版。B树跟B+不同点主要集中在这几个地方:

  • B+树中的节点不存储数据,只是索引,而B树中的节点存储数据;
  • B树中的叶子节点并不需要链表来串联

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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值