数据结构 - B, B+树原理及其在MySQL中的应用
该文章介绍B,B+树的原理及其在MySQL中的应用,包含以下内容,
- 背景
- B树简介
- B树的查找,增加,删除
- B+树简介
- B+树的查找,增加,删除
- B+树在MySQL中的应用
背景
B树和B+树一般应用于文件系统或数据库系统,其能够在很大程度上提高文件系统或数据库系统的查询性能。这里有一个疑问,文件系统或数据库为什么要使用B,B+树,而不使用AVL,红黑树等呢?其实,单从查询的逻辑上来说,AVL和红黑树的效率要高于B,B+树,因为前者都是二叉查找树,而B,B+树是m(一般情况m>2)叉查找树。因为在某个节点内,B,B+树存在顺序查找的可能,因此,其查询性能要低于AVL和红黑树。但在文件系统或数据库系统中,总的查询时间还包括从磁盘读取数据到内存等这些操作的时间,而这些时间相对于在内存中比较数据的时间来说要高很多数量级,因此在文件系统或数据库系统中,真正的瓶颈是磁盘IO操作。
对数据库系统而言,通常使用索引来提高查询效率,索引一般以文件的形式存储到磁盘,由于索引文件可能很大(可达到几十上百G甚至更大),一般不会一次全部加载到内存,只有在使用的时候才按需加载。每加载一次索引块就需要一次磁盘IO,所以,需要尽量减少磁盘IO的次数。在操作系统中,一次IO至少加载一页,大小通常为4K。但根据局部性原理磁盘一般会预读更多的数据,因为当一个数据被用到时,其附近的数据也通常会马上被使用,所以MySQL数据库一页的大小是16K。如果把树中的某个节点信息都存放到一页,由于AVL和红黑树的深度远远大于B,B+树,所以查询数据时要检查更多的节点,就需要更多的磁盘IO,其性能也就不如B,B+树了。这就是为什么在文件系统或数据库应用中采用B,B+树,而不是AVL和红黑树的原因。
B树简介
B树也称B-树或B-Tree,是一种多路平衡查找树。AVL和红黑树都是二叉树,而B树是m叉树,m称为B树的阶,所以这里叫多路。平衡是指任何节点的所有子树都一样高,其平衡性比AVL树还好。为什么B树能保持这么好的平衡性呢?这与其增加和删除元素的方式有关,后面会提到。最后,B树是查找树,其最终目的是快速查找数据,这点和AVL及红黑树一样。
B树有以下特点:
- 一个节点可以存储2个及以上的元素,可以拥有2个及以上的子节点。
- 拥有二叉查找树的基本特点。
- 平衡性很好,高度很矮。
m(m>=2)阶B树的性质:
- 根节点元素个数范围:
。
- 非根节点元素个数范围:
,其中,
是向上取整的值。
- 根节点的子节点个数范围:
。
- 非根节点的子节点个数范围:
,注意:叶子节点无子节点。
例如,5阶B树,根节点元素个数介于[1, 4]之间,非根节点元素个数介于[2,4]之间。
5阶B树又称为(3, 5)树,5代表阶,3代表非根节点至少包含3个子树。
同理,3阶B树可称(2, 3)树,4阶B树可称(2, 4)树,6阶B树可称(3, 6)树,7阶B树可称(4, 7)树,m阶B树可称为树。
下图是一个4阶B树,
在数据库应用中,m一般是200或300左右。
B树和二叉查找树在逻辑上是一样的,二叉查找树通过父子合并即可得到B树。
B树的查找,增加,删除
查找
B树的查找与二叉查找树的查找基本类似,只是B树在判断当前节点时需要依次判断当前节点内的所有元素,如果命中则返回,否则到对应的子节点中查找。例如,查找值为15的元素,查找路径为:
增加
增加操作一定会把元素增加到叶子节点,增加后,如果叶子节点中元素的个数没有超过最大限制则返回,否则称叶子节点上溢(overflow),解决上溢的步骤:
- 求出叶子节点中间元素的位置k。
- 将位置k的元素与父节点所有元素合并。
- 将叶子节点分裂成两个子节点 [0, k-1] 和 [K+1, m-1],叶子节点的父节点增加指针指向两个新的叶子节点。
- 将步骤1-3依次应用到所有的父节点并一直到root,因为父节点增加一个元素后也有可能会上溢,如果root节点也上溢,则会生成一个新的root节点,此时树的高度会增加1。
下图是一个4阶B树增加元素后发生溢出的示例:
删除
删除操作最终删除的一定是叶子节点中的元素,分两种情况,
- 如果删除的是叶子节点元素,则直接删除对应的元素。
- 如果删除的是非叶子节点元素,需要找到待删除节点的前驱或后继节点元素,交换待删除节点元素和其前驱或后继节点的元素值,然后删除前驱或后继节点的元素。
我们知道,非根节点中元素个数至少为,上面两种情况在删除元素后,如果节点中元素的个数小于该值(一定是
)则称为下溢(underflow),解决下溢的步骤:
- 如果下溢节点的兄弟节点个数大于等于
,则向其兄弟节点借一个元素。此时兄弟节点元素,父节点元素,下溢节点会进行一次旋转操作。注意,兄弟节点借出一个元素后依然满足元素个数最低要求。
- 如果无法借用兄弟节点元素,则将父节点中对应的元素挪下来,与下溢节点,下溢节点的兄弟节点一起合并成一个新的节点。注意,合并后的新节点元素依然满足最大元素个数要求。
- 如果步骤2发生,则将步骤1-2应用到所有父节点并一直到root,因为父节点挪走了一个元素后有可能产生下溢。如果root也产生下溢,则树的高度会减1。
下图是一个4阶B树删除元素后产生下溢但可以向兄弟节点借一个元素的示例:
下图是一个4阶B树删除元素后产生下溢并无法借用兄弟节点元素的示例:
为什么B树是平衡的?
只有增加操作且root节点产生了上溢才会使B树的高度增加,root节点产生上溢会分裂成一个新的root节点和两个子节点,此时root的两个子节点的高度都会同时加一,所以增加操作不会破坏B树的平衡性。
只有删除操作且root节点产生了下溢才会使B树的高度减少,root节点产生下溢会删除原root节点并生成一个新的root节点,此时新root节点的所有子节点高度都会减一,所以删除操作不会破坏B树的平衡性。
综上所述,B树一直是保持平衡的,且任何节点的所有子树都一样高,是绝对的平衡。
B+树简介
B+树是B树的一种变形,比B树具有更广泛的应用,其在满足B树的一般性质下(元素和节点的个数范围要求),还具备以下不同点,
- B+树的非叶子节点不存放关键字对应的真实数据,只存放关键字本身,只有叶子节点才存放关键字对应的真实数据。而B树的非叶子节点会存放关键字对应的真实数据。
- 所有叶子节点都有一个指向下一个叶子节点的指针,利用该指针我们可以方便的查询到当前元素的下一个元素的位置,对于DB中的范围查找有很大的帮助。B树没有该指针。
- B+树在增加元素后如果出现上溢,对叶子节点而言,分裂后的其中一个节点依然保留了上溢节点的中间元素,即新的父节点元素。B树分裂后上溢节点的中间元素只存在于父节点中,子节点不会保存。
- B+树在删除元素后如果出现下溢且无法向兄弟节点借用元素时,对叶子节点而言,合并后的节点不包含父节点中对应的元素,对非叶子节点而言,会合并父节点对应的元素。B树会始终合并父节点中对应的元素。
下图是一颗4阶B+树,
B+树的查找,增加,删除
查找
由于真实数据都存放于叶子节点,所以B+树的查找会从根节点开始一直查找到某个叶子节点,即便某个非叶子节点包含了待查找的元素,例如:
增加
与B树一样,增加元素最终也是增加到叶子节点,增加后叶子节点可能出现上溢,解决上溢的步骤:
- 计算出上溢节点中间元素的位置k。
- 将位置k的元素与父节点所有元素合并。
- 如果上溢节点是叶子节点,则将该叶子节点分裂成两个子节点[0, k - 1]和[k, m - 1],叶子节点的父节点增加指针指向两个新的叶子节点。
- 如果上溢节点是非叶子节点,则将该节点分裂成两个子节点[0, k - 1]和[k - 1, m - 1],叶子节点的父节点增加指针指向两个新的叶子节点。
- 将步骤1-2应用到所有父节点并一直到root,因为父节点增加元素后也可能出现上溢,如果root节点也上溢,则生成新的root节点,此时树的高度增加1。
下图是一个4阶B+树增加元素后产生上溢的示例:
删除
删除操作一定是删除叶子节点中的元素,B+树不用像B树一样需要寻找前驱或后继元素,直接删除叶子节点中的元素即可。删除元素后可能出现下溢,解决下溢的步骤:
- 如果下溢节点的兄弟节点的元素个数大于等于最低要求
,则向其借用一个元素。
- 如果下溢节点是叶子节点,则将兄弟节点元素挪至下溢节点元素的位置,并用兄弟节点元素的下一个元素覆盖父节点相应的位置。
- 如果下溢节点不是叶子节点,则进行一次旋转,此时的处理逻辑与B树类似。
- 如果无法向兄弟节点借用元素则需要合并节点。
- 如果下溢节点是叶子节点,则将下溢节点,兄弟节点,父节点对应元素合并成一个新的节点。注意:新节点不包含父节点的元素。
- 如果下溢节点不是叶子节点,则将下溢节点,兄弟节点,父节点元素合并成一个新的节点。注意:新节点包含父节点的元素。
- 如果步骤2发生,则将步骤1-2应用到所有父节点并一直到root,因为父节点减少一个元素后也可能发生下溢,如果root发生下溢,则生成新的root节点,此时树的高度减一。
下图是一个4阶B+树删除节点后产生下溢但可以借用兄弟节点元素的示例:
下图是一个4阶B+树删除节点后产生下溢且无法借用兄弟节点元素的示例:
为什么B+树是平衡的?
这点与B树类似,因为只有root节点产生上溢后树的高度才增加,只有root节点下溢后树的高度才减少,所以B+树是一直平衡的。
B+树在MySQL中的应用
MySQL底层使用的是B+树,相对于B树,B+树有以下优点:
- B+树的非叶子节点不存放真实数据,所以其非叶子节点就能存放更多的元素,这样能提高B+树的阶,最终减少B+树的高度,减少磁盘IO,提高查询性能。
- B+树的叶子节点有一个指向下一个叶子节点的指针,在范围查询时效率更高。
MySQL有两个重要的存储引擎,MyISAM 和 InnoDB,下面分别介绍。
MyISAM存储引擎
MyISAM的数据库表对应三个文件,table.frm,table.MYI,table.MYD,
表类型 | 作用 |
---|---|
table.frm | 数据库表本身的定义,包含哪些字段,字段类型,长度,约束,主键等等。 |
table.MYI | 存放表的所有索引。 |
table.MYD | 存放表的真实数据。 |
可以看出,索引和真实数据存放在不同的文件。假设有以下表数据:
该表有三列,左侧的0x07,0x56,0x6A等是对应行的指针,可以简单理解成物理地址。通过该指针,MySQL能够非常快速的定位到对应行的物理磁盘位置,从而查询到该行数据。
我们以列Col1列作为主键建立索引,对应的B+树储存形式如下图所示:
若以Col2列建立索引,得到的辅助索引结构和上面的主键索引的结构类似:
所以,MyISAM中的索引都是等价的,相互之间没有依赖关系。
InnoDB存储引擎
InnoDB数据库表对应两个文件,table.frm,table.ibd,
表类型 | 作用 |
---|---|
table.frm | 数据库表本身的定义,包含哪些字段,字段类型,长度,约束,主键等等。 |
table.ibd | 存放表的所有索引和真实数据。 |
InnoDB表有以下特点:
- 表数据文件包含数据本身和索引。
- 表数据文件本身就是按B+树组织的一个索引结构文件,该索引称为聚集索引。
- 聚集索引中叶节点包含了完整的真实数据记录。
- InnoDB表必须要有主键,并且推荐使用整型自增主键,通过主键可以得到一个主索引。
如果我们在设计表结构时没有显式指定索引列的话,MySQL会从表中选择数据不重复的列建立索引,如果没有符合的列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,并且这个字段长度为6个字节,类型为整型。
还是以上面的数据为例,假设列Col1是主键,其对应的B+树存储结构为:
可以看出,通过主索引就可以直接查询到真实的数据。
若以列Col3建立辅助索引,对应的B+树存储结构为:
此时,叶子节点存放的值是主索引的值,如果要查询真实数据,还需要借助主索引的B+树结构,因为真实数据全部都存放在主索引的B+树结构中。
InnoDB为什么推荐使用整型自增主键而不是UUID呢?
- UUID虽然是唯一的,但其是字符串,会比整型消耗更多的存储空间。
- 在B+树中进行查找时需要跟经过的节点值比较大小,整型数据的比较运算比字符串更快。
- 自增的整型索引在磁盘中会连续存储,在读取一页数据时也是连续,UUID是随机产生的,读取的上下两行数据存储是分散的,不适合执行where id > 5 && id < 20的条件查询语句。
- 在插入或删除数据时,整型自增主键会在叶子结点的末尾建立新的叶子节点,不会破坏左侧子树的结构。UUID主键很容易出现这样的情况,B+树为了维持自身的特性,有可能会进行结构的重构,消耗更多的时间。
所以,对于InnoDB存储引擎,最好使用自增的整型字段作为主键,而不是采用其它字段,因为维护主索引有一定的开销,且其它辅助索引都依赖于主索引。