MySQL中B+树实现原理

本文详细介绍了B+树在数据库中的应用,从索引的用途出发,阐述了B+树的演变过程,包括哈希表、二叉搜索树、平衡二叉树、AVL树、红黑树、B树到B+树的演变。重点讲解了B+树的特征、阶数、插入和查找操作,以及在MySQL中的实际应用和索引原理。通过对B+树的理解,有助于提升数据库查询效率和数据组织的优化。

一、索引用途

       索引的目的在于提高查询效率,比如去图书馆借书,如果你要借某一本书,一定是先找到对应的分类科目,再找到对应的编号,最终找到这本书。索引就是帮我们加快查询速度,快速定位。

        Mysql中B+树索引是B+树在数据库中的一种实现,是最常见也是数据库中使用最为频繁的一种索引。B+树中的B代表平衡(balance),而不是二叉(binary),因为B+树是从最早的平衡二叉树演化而来的。在讲B+树之前必须先了解二叉查找树、平衡二叉树(AVLTree)和平衡多路查找树(B-Tree)。

二、B+树索引演变过程

        ①哈希表

优点:哈希表查询特定的值的时候,特别快,例如查a = 3。
缺点:

  • 哈希表冲突会造成数据散列不均匀,会产生大量的线程查询,比较浪费时间。
  • 不支持范围查询,当进行范围查询的时候,必须要全部遍历,例如查a > 3。
  • 对于内存空间的要求比较高,读入内存需要将整个哈希表读进内存。

那么在mysql中有没有hash索引?

  • Memory存储引挚使用的是hash索引。
  • Innodb支持自适应hash索引(Innodb存储引擎自己决定是用hash索引存还是B+树存)。

 ②二叉搜索树

特性:插入数据的时候必须有序,左子树必须小于根结点,右子树必须大于根结点,使用二分查找来提高查找效率。

        二叉查找树的查询时间复杂度比链表快,链表的查询时间复杂度是O(n),二叉排序树平均是O(logn)。二叉查找树越平衡,越能模拟二分法,所以与二分的思想相似,二叉查找树查询的时间复杂度O(logn)。

        如果插入的结点的值的顺序,是越来越小或者越来越大的,那么BST就会退化为一条链表,那么其查询的时间复杂度就会降为O(n)。如下图所示:

③ 平衡二叉树

性质:通过左旋和右旋让树变平衡,最短子树和最长子树高度只差不超过1,为了保证平衡,在插入数据的时候必须要旋转,旋转消耗性能,插入时性能下降,而查询性能提升。

也就是说,平衡二叉树的前提是它是一棵二叉查找树

平衡因子: 每个结点都有其各自的平衡因子,表示的就是该节点左子树深度减去右子树深度的值称为该结点的平衡因子BF,那么平衡二叉树上的所有节点的平衡因子只能是-1,0,1。只要一棵树中存在平衡因子不是这几个值,则这棵树就是不平衡的,就需要对这棵树进行调整。

        平衡二叉树的查找、插入、删除操作在平均和最坏的情况下都是O(logn),这得益于它时刻维护着二叉树的平衡。

        AVL树的平衡因子的绝对值不会超过1,正因为如此AVL树的形状肯定不会退化成一条链表的,而是“矮胖”型的树。所以能确保AVL的查找、添加、删除的时间复杂度都是O(logn)。

④红黑树

        红黑树是一种自平衡二叉查找树(二叉排序树),与平衡二叉树(avl树)不同的是,红黑树是弱平衡二叉树,即它的左右子树高度差有可能大于1,但不超过一倍。但随着数据的插入,发现数的深度越来越深,意味着IO次数越多,影响数据读取的效率。

⑤B树

  • 问题:我们在平衡二叉树中,每个节点只保存了一个键值和数据,当我们保存海量的数据是,二叉树的高度就会非常高,而且磁盘的利用率也极低,访问磁盘的次数非常多,所以我们必须提高效率,一个节点保存多个键值。

  • 解决:​ B树(blacnce Tree)即平衡树,单节点上能保存多个键值和数据的平衡树。

⑥B+树

        B+ 树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储键值,也会存储数据。之所以这么做是因为在数据库中页的大小是固定的,InnoDB 中页的默认大小是 16KB。如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。

三、B+树中阶数m

①m阶的B+树特征

  • 根结点最少可以只有1个关键字。
  • 对于任意的节点有最多 m 个子字节。
  • 阶数越大,树的高度越小,IO次数越少。
  • 每个非根节点所包含的关键字个数 j 满足:m/2 - 1 <= j <= m - 1.(表示向上取整)。
  • 所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。
  • 每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。

每个结点中存储了关键字(key)和关键字对应的数据(data),以及孩子结点的指针。我们将一个key和其对应的data称为一行记录。

②B+树的插入

B+树插入要记住这几个步骤:

  • B+树插入都是在叶子结点进行的,就是插入前,需要先找到要插入的叶子结点。
  • 如果被插入关键字的叶子节点,当前含有的关键字数量是小于阶数m,则直接插入。
  • 如果插入关键字后,叶子节点当前含有的关键字数目等于阶数m,则插,该节点开始 「分裂」为两个新的节点,一个节点包含m/2 个关键字,另外一个关键字包含m/2个关键值。(m/2表示向下取整,m/2表示向上取整,如3/2=2)。
  • 分裂后,需要将第m/2的关键字上移到父结点。如果这时候父结点中包含的关键字个数小于m,则插入操作完成。
  • 分裂后,需要将m/2的关键字上移到父结点。如果父结点中包含的关键字个数等于m,则继续分裂父结点。

以一颗4阶的B+树为例子吧,4阶的话,关键值最多3(m-1)个。假设插入以下数据43,48,36,32,37,49,28。

Ⅰ、在空树中插入43

 

这时候根结点就一个关键值,此时它是根结点也是叶子结点。

Ⅱ、依次插入48,36

 

这时候跟节点拥有3个关键字,已经满了

Ⅲ、继续插入 32,发现当前节点关键字已经不小于阶数4了,于是分裂 第4/2=2(下标0,1,2)个,也即43上移到父节点。

 

Ⅳ、继续插入37,49,前节点关键字都是还没满的,直接插入,如下:

 

Ⅴ、最后插入28,发现当前节点关键字也是不小于阶数4了,于是分裂,于是分裂, 第 4/2=2个,也就是36上移到父节点,因父子节点只有2个关键值,还是小于4的,所以不用继续分裂,插入完成。

 

③、B+树的查找

假设我们要查的值为32。

Ⅰ、第一次磁盘 I/O,查找磁盘块1,即根节点(36,43),因为32小于36,因此访问根节点的左边第一个孩子节点。

 

Ⅱ、第二次磁盘 I/O, 查找磁盘块2,即根节点的第一个孩子节点,获得区间(28,32),遍历即可得32。

 

动态图如下:

 

④、B+树的删除

B+树删除关键字,分这几种情况

  • 找到包含关键值的结点,如果关键字个数大于m/2-1,直接删除即可。
  • 找到包含关键值的结点,如果关键字个数大于m/2-1,并且关键值是当前节点的最大(小)值,并且该关键值存在父子节点中,那么删除该关键字,同时需要相应调整父节点的值。
  • 找到包含关键值的结点,如果删除该关键字后,关键字个数小于m/2,并且其兄弟结点有多余的关键字,则从其兄弟结点借用关键字。
  • 找到包含关键值的结点,如果删除该关键字后,关键字个数小于m/2,并且其兄弟结点没有多余的关键字,则与兄弟结点合并。

以下这颗5阶的B+树为例:

Ⅰ、如果删除22,因为关键字个数为3 > 5/2-1=2, 直接删除(表示向上取整的意思)

 

Ⅱ、如果关键字个数大于m/2-1,并且删除的关键字存在于父子节点中,那么需要相应调整父子节点的值。

 

如果删除20,因为关键字个数为3 > 5/2-1=2,并且20是当前节点的边界值,且存在父子节点中,所以删除后,其父子节点也要响应调整。

 

Ⅲ、如果删除该关键字后,关键字个数小于m/2-1,兄弟节点可以借用。

 

 

Ⅳ、如果删除15,删除关键字的结点只剩1个关键字,小于5/2-1=2,不满足B+树特点,但是其兄弟节点拥有3个元素(7,8,9),可以借用9过来。

Ⅴ、在删除关键字后,如果导致其结点中关键字个数不足,并且兄弟结点没有得借用的话,需要合并兄弟结点。

如果删除关键字7后,只剩一个8的关键字,不满足B+树特点(m/2-1<=关键字<=m-1)。并且没有兄弟结点关键字借用,因此8与前面的兄弟结点结合。被删关键字结点的父节点,7索引也被删掉了,只剩一个9,并且其右兄弟结点(18,20)只有两个关键字,也是没得借,因此在此合并。被删关键字结点的父子节点,也和其兄弟结点合并后,只剩一个子树分支,因此根节点(16)也下移了。

 

⑤、mysql的B+树索引有多少阶?

        对于这个问题,我们需要先了解下磁盘相关知识。磁盘的最小存储单位是扇区(512字节)磁盘的读取是以块为基本单位,一块大小为8个扇区(即4kb)。B+树的每一个节点占用的空间就是一个块大小。

        由于B+树节点中只存储元素和指针。如果有n个元素,那么就有n+1个指针。假设现在索引存储int类型的值,一个int(32位)占用4个字节,一个指针占用8个字节,那么一个块最多能存4096/(4n+8(n+1))即340个元素,那么索引即为341阶(m阶数最多包含m-1个元素)。

⑥、mysql的B+树索引能存多少数据?

        以innodb引擎的索引数据结构为例,它的存储单元为一页,每页大小默认为16kb,该值可以通过参数调整,如下图:

        假设每个节点中索引元素占8个字节,指针占用6个字节,那么每页可存(16*1024)/(8+6)=1170个索引元素,假设B+树的高度为3,一条数据大小为1k,那么,第一层可以存1170个元素;第二层可以存1170*1170=1368900个元素;第三层属于叶子结点,可以存的数据条数为页大小16k/每条数据大小1k,即16条,那么总共可以存储的数据条数即为16*1368900=21902400。

四、B+树使用场景

B+树主要用在磁盘文件组织、数据索引和数据库索引。

  • B+tree的磁盘读写代价更低

        B+tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。

  • B+tree的查询效率更加稳定

        由于非叶子结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

五、B+树索引原理

假设有一个表table_demo,表中有2个INT类型的列,1个CHAR(1)类型的列,c1列为主键:

CREATE TABLE table_demo(c1 INT,c2 INT,c3 CHAR(1),PRIMARY KEY(c1)) ;

table_demo表的简化的行格式示意图如下:

我们只在示意图里展示记录的这几个部分:

  • record_type:表示记录的类型, 0是普通记录、 2是最小记录、 3 是最大记录、1是B+树非叶子节点记录。

  • next_record:表示下一条记录的相对位置,我们用箭头来表明下一条记录。

  • 各个列的值:这里只记录在 index_demo 表中的三个列,分别是 c1 、 c2 和 c3 。

  • 其他信息:除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息。

其他信息项暂时去掉并把它竖起来的效果就是这样:

把一些记录放到页里的示意图就是(这里一页就是一个磁盘块,代表一次IO):

MySQL InnoDB默认的页大小是16KB,因此数据存储在磁盘中,可能会占用多个数据页。如果各个页中的记录没有规律,我们就不得不依次遍历所有的数据页。如果我们想快速的定位到需要查找的记录在哪些数据页中,我们可以这样做 :

  • 下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值

  • 给所有的页建立目录项

    以页28为例,它对应目录项2 ,这个目录项中包含着该页的页号28以及该页中用户记录的最小主键值5。我们只需要把几个目录项在物理存储器上连续存储(比如:数组),就可以实现根据主键值快速查找某条记录的功能了。比如:查找主键值为 20 的记录,具体查找过程分两步:

  • 先从目录项中根据二分法快速确定出主键值为20的记录在目录项3中(因为 12 ≤ 20 < 209 ),对应页9。

  • 再到页9中根据二分法快速定位到主键值为 20 的用户记录。

至此,针对数据页做的简易目录就搞定了。这个目录有一个别名,称为索引 ,存储数据结构就是运用的B+树

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值