48.讲B+树:MySQL数据库索引是如何实现的


数据库索引是如何实现的呢?底层使用的是什么数据结构和算法呢?

1. 算法解析

1.1 解决问题的前提是定义清楚问题

如何定义清楚问题呢?除了对问题进行详细的调研,还可以对一些模糊的需求进行假设,来限定要解决的问题的范围

这里假设要解决两位基本sql需求:

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

另外这里只考虑性能问题:包括执行效率和存储空间,我们希望索引执行效率尽可能高,同时存储尽可能小。

1.2 尝试用学过的数据结构解决这个问题

支持快速查询、插入等操作的动态数据结构,已经学习过散列表、平衡二叉查找树、跳表。

1.2.1 散列表

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

1.2.2 平衡二叉查找树

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

1.2.3跳表

跳表是在链表之上加上多层索引构成的。它支持快速地插入、查找、删除数据,对应的时间复杂度是O(logn)。并且,跳表也支持按照区间快速地查找数据。我们只需要定位到区间起点值对应在链表中的结点,然后从这个结点开始,顺序遍历链表,直到区间终点对应的结点为止,这期间遍历得到的数据就是满足区间值的数据。
在这里插入图片描述
跳表可以解决问题,实际上用的是B+树,由二叉查找树演化而来。

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

1.3.1 如何改造

在这里插入图片描述
在这里插入图片描述

1.3.2 索引存储问题

比如给1亿条数据建二叉查找树索引,大约1亿个节点,每个节点假设占用16字节,大约需要1G,如果给10张表,20张表建索引,那么内存的占用将会很大,索引放内存显然是不可行的。

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

那么如何降低树的高度? 用多叉树。

  • 16个数据构建二叉树索引,树的高度是4,查找一个数据,就需要4个磁盘IO操作
  • 构建五叉树索引,那高度只有2,查找一个数据,对应只需要2次磁盘操作
  • m叉树中的m是100,那对一亿个数据构建索引,树的高度也只是3,最多只要3次磁盘IO就能获取到数据。磁盘IO变少了,查找数据的效率也就提高了。
    在这里插入图片描述

1.3.3 B+树

如果我们将m叉树实现B+树索引,用代码实现出来,就是下面这个样子(假设我们给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[keywordss大小]+m*8[children大小]
 */
public class BPlusTreeNode {
  public static 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[keyw..大小]+k*8[dataAd..大小]+8[prev大小]+8[next大小]
 */
public 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; // 这个结点在链表中的后继结点
}

不管是内存中的数据,还是磁盘中的数据,操作系统都是按页(一页大小通常是4KB,这个值可以通过getconfig PAGE_SIZE命令查看)来读取的,一次会读一页的数据。如果要读取的数据量超过一页的大小,就会触发多次IO操作。所以,我们在选择m大小的时候,要尽量让每个节点的大小等于一个页的大小。读取一个节点,只需要一次磁盘IO操作

在这里插入图片描述

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

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

在这里插入图片描述

索引的存在会导致数据库写入的速度降低。实际上,不光写入数据会变慢,删除数据也会变慢。这是为什么呢?

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

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

在这里插入图片描述

2. 总结引申

今天,我们讲解了数据库索引实现,依赖的底层数据结构,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树中的叶子节点并不需要链表来串联。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值