12.B+树和动态哈希
索引结构
- 顺序存储文件的缺点
- 随着文件增多性能衰退
- 需要定期整理整个文件
- B+ 树索引
- 所有商业 DBMS 产品中有序索引的默认实现
- 通过规范的插入删除操作,不会产生上述问题
B+ 树索引
B+树的性质
- B+ 树是一颗平衡树,即所有叶节点深度相同
- 从根到叶子节点的所有路径都具有相同的长度
- 每个节点都有数量在
之间的指针 - 每个叶节点存储数量为
之间的数据值 - n 称为扇出 fanout(指针/子节点的最大数量)
- 值
称为 order 阶数
![image]()
特别的例子
- 如果根不是叶节点,它至少有两个子节点
- 如果根是叶节点(即树中没有其他节点),则它可以具有 0 到( n - 1 )个数据值
B+ 树的节点
B+ 树查询
如果需要查询一个值 a,从根节点开始
- 在当前节点内部寻找 \(p\)\(i\) 以至于 \(k\)\(i-1\) ≤ \(a\) < \(k\)i
- 调到 pi 指向的子节点
- 重复以上步骤直到叶子结点,如果在叶子结点中找不到等于 a 的 key,则该树不包含值为 a 的key
- 例子:搜索值 5、15 和大于等于 24 的值
B+ 树插入
插入键值 K
- 用键值 k 进行查询得到叶节点 L
- 将 k 插入 L 中
- 如果 L 有空位,则已经完成
- 如果 L 没有空位,则将 L 分裂成 L 和 L2
- 将 keys 平分
- 更新 L 的父节点:插入 L2 的最左侧数值
![image]()
- 我们的处理:如果没特别要求,奇数个键值的情况下左侧多分一个
- 如果父节点也因为加入推上来的数值而 space 溢出:
- 由下往上递归地更新节点
- 要分割非叶节点,均匀地重新分配其他键值。此外,将中间键值往父辈节点推进(插入)
![image]()
- 由下往上递归地更新节点
B+ 树删除
删除键值 K
- 用 K 值查询得到其所在叶节点 L
- 将 K 从 L 中移除
- 如果 L 有超过半数 key,则结束
- 否则,从右侧兄弟节点索取一个 key
- 如果右侧节点少于半数 + 1 个 key,则需要合并两个节点
- 发生合并,会导致父节点的一个 key 删除
- 从 L 的父级删除一个键值(指向 L )
- 在极端条件下,删除也可导致连锁反应,一层一层向上反应
- 发生合并,会导致父节点的一个 key 删除
- 如果 L 有超过半数 key,则结束
例子:

B+ 树更新
- 考虑下面的B+树,其阶数为2(这意味着除了根节点外,每个节点必须至少包含两个键值和三个指针)
![image]()



B+ 树文件组织
- 索引退化问题可以用 B+ 树索引解决,得到一直规整的索引
- 数据文件退化问题(碎片化)可以用 B+ 树来组织文件存储
- 要求:叶节点存的是记录(数据)本身,而非指向它们地址的指针
- 由于数据记录一般比指针大,叶节点中可存储的最大记录数小于非叶节点中的指针数
![image]()
动态哈希
静态哈希
- 将搜索键值固定映射到某一个桶 Bucket 中(代表一个存储地址)
- 数据随着时间增长,如果数据持续增长,有由于过多的溢出导致该 index 的性能退化(变慢)
- Bucket 的数量是固定的
- 溢出桶会导致哈希搜索变慢,为什么不增加 Buckets?
- 增加 Buckets 需要对原来的数据进行整理,涉及到读写全部 pages,时间成本昂贵!
![image]()
动态哈希
- Bukcets 数目是可以动态调整的
- 使用指向 Buckets 的目录指针,通过将目录加倍来将存储桶的数目加倍,仅拆分溢出的存储桶!
- 哈希结果跟数据位置中间加一层指针
- 哈希地址不直接存放记录,而是存放记录的指针,增加 Buckets 时,只需要增加记录的指针,从而只需要对指针而不是对记录进行整理,成本小很多
![image]()
例子:

-
目录是一个大小为4的数组,要找到搜索键 r 的存储桶,我们用 2 个 bit 就可以实现这四种选择。
-
对于任意搜索键,我们看它的倒数 2 位( = global deepth ),例如 r = 5,h ( r ) = 5 = 101,则该记录在 01 所指向的桶
-
Globle depth:将一个元素映射到一个桶时需要看多少位,在上图是 2
-
Local depth:桶中所有元素相同尾数序列长度,在上图中都是 2
-
插入 hash value = 20 会导致桶溢出,进而分裂
-
Global depth 的全局深度为 3,现在对一个数进行哈希,需要看后 3 位了
![image]()
-
Local depth:
- A1 桶是 3,决定该元素映射到该桶的位数,其中的元素需要看 3 位才能知道是映射到 A1
- B 桶是 2,001 与 101 都指向 B
-
Local depth = global depth - log2 ( 连接线段数量 )
桶分裂什么时候会导致目录加倍
- 插入之前, local depth of bucket = global depth
- 插入导致 local depth > global depth!
- 然后会把指针目录翻倍( global depth + 1 ),并将溢出的 bucket 分裂,更新指针与 bucket 的指向关系。
动态哈希的例子
假设以下哈希索引,其中哈希函数由最低有效位确定,

- 插入 Insert 18 ( 10010 ), 32 ( 100000 )
![image]()
- 接下来插入 3 ( 11 ), 4 (100 )
![image]()
- 再插入 19 ( 10011 ), 17 ( 10001 )
![image]()
- 再插入 24 ( 11000 )
![image]()
动态哈希:删除
删除:插入过程反过来
- 如果删除导致 bucket 变空的话,将其与其镜像合并(譬如 001 与 101 合并),合并后 local depth 减一
- 当因为一系列删除操作,导致指针目录的任意指针与其镜像指针均指向同一个 bucket,则可以整体将指针目录缩减一半
![image]()
在候选键上进行聚簇 B+ 树的示例


之间的指针
之间的数据值
称为 order 阶数















浙公网安备 33010602011771号