B-Tree和B+Tree
在之前的二叉查找树中,可以发现,数据越多,对应树的深度越大,对应的IO也就越大;
同时每个磁盘块(节点/页)保存的数据量太少了,正如之前说的,mysql的一个页默认16k,二用16k去存储这样一个数据,实在太过浪费。
因为无论是二叉查找树还是二叉平衡查找树,都是二叉的。
如果说树的枝杈越多,那么对应着的相同高度的数存储的数据就越多,每个磁盘块存储的数据也就越多。
因此想要减少IO,就要想办法降低树的高度,同时要不浪费,就需要想办法增加树的枝杈
正是在这种思路下,在平衡二叉查找树上,进化了多路平衡查找树,就是B-树。同理B+树也被称为加强版多路平衡查找树。
B树是一种多路平衡的查找树。它的没一个节点最多有m个孩子,m被称为B树的阶
m的大小取决于磁盘页的大小
B-Tree
之前的几种树,每个节点都只有一个元素,而B-树中可以放多个元素,主要就是为了降低树的高度。
一棵m阶的B-Tree有如下特性:
每个节点最多有m个孩子,m称为b树的阶
除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子(Ceil函数返回大于或者等于指定表达式的最小整数)
若根节点不是叶子节点,则至少有2个孩子
所有叶子节点都在同一层,且不包含其它关键字信息
每个非终端节点包含n个关键字(健值)信息
关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
ki(i=1,…n)为关键字,且关键字升序排序
Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)
B-Tree 中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个 3 阶的 B-Tree:
每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的关键字和三个指向子树根节点的指针,指针存储的是子节点所在磁盘块的地址。两个关键词划分成的三个范围域对应三个指针指向的子树的数据的范围域。以根节点为例,关键字为17和35,P1指针指向的子树的数据范围为小于17,P2指针指向的子树的数据范围为17~35,P3指针指向的子树的数据范围为大于35。
模拟查找关键字29的过程:
- 根据根节点找到磁盘块1,读入内存。【磁盘I/O操作第1次】
- 比较关键字29在区间(17,35),找到磁盘块1的指针P2。
- 根据P2指针找到磁盘块3,读入内存。【磁盘I/O操作第2次】
- 比较关键字29在区间(26,30),找到磁盘块3的指针P2。
- 根据P2指针找到磁盘块8,读入内存。【磁盘I/O操作第3次】
- 在磁盘块8中的关键字列表中找到关键字29。
分析上面过程,发现需要3次磁盘I/O操作,和3次内存查找操作。由于内存中的关键字是一个有序表结构,可以利用二分法查找提高效率。而3次磁盘I/O操作是影响整个B-Tree查找效率的决定因素。B-Tree相对于AVLTree缩减了节点个数,使每次磁盘I/O取到内存的数据都发挥了作用,从而提高了查询效率。
B+Tree
B+Tree是平衡多叉树的数据结构,是基于页进行管理数据
B+Tree 是在 B-Tree 基础上的一种优化,使其更适合实现外存储索引结构,InnoDB 存储引擎就是用 B+Tree 实现其索引结构。
从上一节中的B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。
B+Tree的特征
- 每个结点至多有m个子女
- 除根结点外,每个结点至少有[m/2]个子女,根结点至少有两个子女(如果叶节点小于[m/2],例如发生删除操作时,则会发生页合并;如果节点已有m个子女,再插入操作时,将取出中间值,存放在上一层非叶子节点中,如果上一层也满了,就继续分页)
- 有k个子女的结点必有k个关键字
- 父节点中持有访问子节点的指针
- 父节点的关键字在子节点中都存在(如上面的1/20/35在每层都存在),要么是最小值,要么是最
大值,如果节点中关键字是升序的方式,父节点的关键字是子节点的最小值 - 最底层的节点是叶子节点
- 除叶子节点之外,其他节点不保存数据,只保存关键字和指针
- 叶子节点包含了所有数据的关键字以及data,叶子节点之间用链表连接起来,可以非常方便的支
持范围查找
B+Tree相对于B-Tree有几点不同:
- 非叶子节点只存储键值信息;
- 所有叶子节点之间都有一个链指针;
- 数据记录都存放在叶子节点中
将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:
非叶子节点:保存键值(添加索引的字段的值)和指针
指针:指针与数据页是一种映射的关系,通过指针就可以找到对应的数据页
叶子节点:用于保存数据,保存所有记录的值,并经过排序
双向指针(双向链表):用于保存相邻页的指针,提升范围查询效率
通常在B+Tree上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。
可能上面例子中只有22条数据记录,看不出B+Tree的优点,下面做一个推算:
InnoDB存储引擎中页的大小为16KB,一般表的主键类型为INT(占用4个字节)或BIGINT(占用8个字节),指针类型也一般为4或8个字节,也就是说一个页(B+Tree中的一个节点)中大概存储16KB/(8B+8B)=1K个键值(因为是估值,为方便计算,这里的K取值为103)。也就是说一个深度为3的B+Tree索引可以维护103 * 10^3 * 10^3 = 10亿 条记录。
实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2-4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。
B+Tree性质
- 通过上面的分析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。
- 当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。
B+Tree数据存储量计算
InnoDB的页结构
- 在InnoDB中,索引默认使用的数据结构为B+树,而
B+树里的每个节点都是一个页
,默认的页大小为16KB
。 - 非叶子节点存的是索引值以及页的偏移量,而叶子节点上存放的则是完整的每行记录
B+Tree能存多少数据
非叶子节点存储的数据
- 页默认16KB
- File Header、Page Header等一共占102个字节
- Infimum + Supremum分别占13个字节
- 记录头占5个字节
- id占为int,占4个字节
- 页目录的偏移量占4个字节
非叶子节点能存储的记录条数
非叶子节点能存放的索引记录
= (页大小 - File Header - Page Header - ...) / ( 主键 + 页偏移量 + 下一条记录的偏移量)
= (16KB - 128B) / (5B + 4B + 4B)
= 16256 / 13
= 1250 条
也有这样算的:
假设主键ID为bigint类型,长度为8字节,而指针大小在InnoDB源码中设置为6字节,这样一共14字节。
那么一个页中能存放多少这样的组合,就代表有多少指针,
即 16384 / 14 = 1170
我们按照1250估算
叶子节点能存多少数据
叶子节点能存多少条数据记录呢
- 变长列表占1个字节
- null标志位忽略
- 记录头占5个字节
- id占为int,占4个字节
- name为VARCHAR,编码为UTF8,为了好算,所有行记录我都只用两个中文,那就是 2 * 3B = 6个字节
- 事务ID列占6个字节
- 回滚指针列占7个字节
叶子节点能存放的数据记录
= (页大小 - File Header - Page Header - ...) / ( 主键 + 字段 + 下一条记录的偏移量)
= (16KB - 128B) / (1B + 5B + 4B + 6B + 6B + 7B)
= 16256 / 29
= 560 条
按上面的计算过于理想,往往会被高估
磁盘上的一个扇区是512字节,但是文件系统的块大小往往是4k,
而innodb的最小单元--页,大小是16k,
所以一个文件不满4k,也会占用磁盘上4k空间
很多互联网业务数据记录大小通常就是1K左右
照这样来算,
单个页中的记录为
=16k/1k
=16 条
按16来计算
简单计算
假设B+Tree高度为3
- 根节点能放1250条索引记录
- 第二层能放1250 * 1250 = 1,562,500条索引记录
- 叶子节点 1250 * 1250 * 16 = 2500w条数据记录
假设B+Tree高度为4
共有:1250 * 1250 *1250* 16 = 31250000000
所以三层往往能存千万数据,四层上亿数据