人人都是 DBA(VII)B 树和 B+ 树

原文:人人都是 DBA(VII)B 树和 B+ 树

B 树(B-Tree)是为磁盘等辅助存取设备设计的一种平衡查找树,它实现了以 O(log n) 时间复杂度执行查找、顺序读取、插入和删除操作。由于 B 树和 B 树的变种在降低磁盘 I/O 操作次数方面表现优异,所以经常用于设计文件系统和数据库。

  • B 树内的节点关系
  • B 树的定义
  • B 树的操作
  • B 树的变种
  • B+ 树的优势
  • B+ 树 C# 代码实现

在 1972 年,在 Boeing Research Labs 工作的 Rudolf BayerEd McCreight 发明了 B 树。当时他们并没有解释 B 树中的 "B" 代表什么涵义,所以猜测的多是 "Balanced", "Broad", "Bushy", "Boeing", "Bayer" 等,不过通常使用 "Balanced" 来描述树是平衡的。Ed McCreight 在 CPM 2013 会议中回答"B" 的起源问题时说:"对 B 的涵义思考的越多,对 B 树的理解则越深。"。

如下图是一棵键值为英语字母的 B 树,带浅阴影的节点是查找字母 R 时要检查的节点。

B 树内的节点关系

B 树中的节点分为内部节点(Internal Node)叶节点(Leaf Node),内部节点也就是非叶节点(Non-Leaf Node)

B 树的内部节点可以包含 2 个以上的子节点,所以在设计时可以预先设定可包含子节点的数量范围,也就是上界(Upper Bound)下界(Lower Bound)。当向节点插入或删除数据时,也就意味着子节点的数量发生变化。为了维持在预先设定的数量范围,内部节点可能会被合并(Join)拆分(Split)。因为子节点的数量有一定的范围,所以 B 树不需要频繁地变化以保持平衡。但同时,由于节点可能没有被完全填充,所以会浪费一些空间。

B 树中每一个内部节点会包含一定数量的键值(Key)。这些键值同时也扮演着分割子节点的角色。例如,假设某内部节点包含 3 个子节点,则实际上必须有 2 个键值:a1 和 a2。其中,a1 的左子树上的所有的值都要小于 a1,在 a1 和 a2 之间的子树中的值都大于 a1 并小于 a2,a2 的右子树上的所有的值都大于 a2

通常,键值的数量被设定在 d 和 2d 之间,其中 d 是可包含键值的最小数量。可知,d + 1 是节点可拥有子节点的最小数量,也就是树的最小的度(Degree)。因数 2 将确保节点可以被合并或拆分。

如果一个内部节点有 2d 个键值,那么添加一个键值给该节点将会导致 2d + 1 的数量大于范围上界,则会拆分 2d + 1 数量的节点为 2 个 d 数量的节点,并有 1 个键值提升至父节点中。

类似地,如果一个内部节点和它的邻居节点(Neighbor)都包含 d 个键值,那么删除一个键值将导致此节点拥有 d - 1 个键值,小于范围下界,则会导致与邻居节点合并。合并后的节点包括 d – 1 的数量加上邻居的 d 的数量和两者的父节点中的 1 个键值,共为 d – 1 + d + 1 = 2d 数量的节点。

深度(Depth)描述树中层(Level)的数量。B 树通过要求所有叶节点保持在相同深度来保持树的平衡。深度通常会随着键值的不断添加而缓慢地增长。

B 树的定义

对于 B 树定义中的一些术语常有混淆,比如对于阶(Order)的定义。Knuth Donald 在 1998 年将阶(Order)定义为节点包含子节点的最大数量。

使用阶来定义 B 树,一棵 m 阶的 B 树,需要满足下列条件:

  1. 每个节点最多包含 m 个子节点。
  2. 除根节点外,每个非叶节点至少包含 m/2 个子节点。
  3. 如果根节点包含子节点,则至少包含 2 个子节点。
  4. 拥有 k 个子节点的非叶节点将包含 k - 1 个键值。
  5. 所有叶节点都在同一层中。

下面是一棵高度(Height)为 3 的 B 树。

B 树上大部分操作所需的磁盘存取次数与 B 树的高度成正比。

使用 h 代表 B 树的高度;使用 n 代表整个树中包含键值的数量 n > 0;m 为内部节点可包含子节点的最大数量,则当树满时 n = mh – 1;每个内部节点最多包含 m - 1 个键值;使用 d 代表内部节点可包含最少子节点的数量,即最小度数(Degree)有 d = ⌈m/2⌉。

B 树的最优条件下的 h 为:

B 树的最差条件下的 h 为:

合理的选取最小度数 d 的值,可以大大地降低树的高度,则可降低查找任意键值时所需的磁盘存取次数。与自平衡二叉树相比,高度都以 O(log n) 的速度增长,对 B 树来说对数的底要大很多倍。对于大多数树操作来说,B 树要比自平衡二叉树节省大约 lg d 因子的节点检查次数。因为在树中查找任意一个节点通常都需要一次磁盘存取,所以磁盘存取的次数大大地减少了。

B 树的操作

查询操作

在 B 树中查询键值与在二叉树中的键值查询方式是类似的。从根节点开始查询,通过递归进行自顶向下的遍历。在每一层上,将查询键值与内部节点中的键值比较,以确定向哪个子树中进行遍历。

二叉树相关的查询可参考如下文章:

插入操作

当要插入一个新的键值时,首先在树中找到该键值应当被插入的叶节点的位置:

  • 如果叶节点包含键值的数量在设定的范围上界和下界内,则直接插入新键值,并保持键值在节点中顺序。
  • 否则,节点已满,将节点分割为 2 个节点:
    1. 选择中间值(Median)作为分割点;
    2. 小于中间值的键值放入新的左节点中,大于中间值的键值放入新的右节点中;
    3. 将中间值插入到父节点中。此时可能导致父节点满,采用同样方式分割。如果父节点不存在,比如是根节点,则创建一个新的父节点,也就导致树的高度增长。

删除操作

在 B 树中删除键值可以通过不同的策略来实现,这里介绍常见的定位删除策略:定位键值后删除,然后重构整个树至平衡。平衡指的是仍然保持 B 树的性质。

  1. 搜索要被删除键值的位置。
  2. 如果键值在叶节点中,则直接删除。
  3. 如果键值在内部节点中,由于其正扮演分割子节点的角色,所以删除后需要找一个替代键值继续保持两个子节点的分割。此时,可以选择左子节点中最大的键值,或者右子节点中最小的键值。将选中的键值从子节点中删除,然后插入到被替换的位置。
  4. 如果删除键值后的节点已经不满足对最少键值数量的要求,则需要重平衡整棵树,平衡操作包括旋转(Rotation)、组合(Join)等。

B 树的变种

"B 树" 这个术语在实际应用中还代表着多种 B 树的变种,它们有着相似的结构,却各有特点和优势:

  • B 树在其内部节点中存储的键值不会再在叶节点中存储,内部节点不仅存储键值也会存储键值关联附属数据,或者存储指向关联附属数据的指针。同时,B 树会保持内部节点的 1/2 填充。
  • B+ 树是 B 树的一个变种,在内部节点中存储的键值同样也会出现在叶节点中,但内部节点中不存储关联附属数据或指针。在叶节点中的不仅存储键值,还存储关联附属数据或指针。此外,叶节点还增加了一个指向下一个顺序关联叶节点的指针,以改进顺序读取的速度。
  • B* 树也是 B 树的一个变种,要求除根节点外的内部节点要至少 2/3 填充,而不是 1/2 填充。为了维持这样的结构,当一个节点填满后不会立即分割节点,而是将它的键值与下一个节点共享,当两个节点都填满之后,再将 2 个节点分割成 3 个节点。

B+ 树的优势

B+ 树是 B 树的一个变种,在内部节点中存储的键值同样也会出现在叶节点中,但内部节点中不存储关联附属数据或指针。在叶节点中的不仅存储键值,还存储关联附属数据或指针。这样,所有的附属数据都保存在了叶节点中,只将键值和子女指针保存在了内节点中,因此最大化了内节点的分支能力。

此外,叶节点还增加了一个指向下一个顺序关联叶节点的指针,以改进顺序读取的速度。

常见的文件系统和数据库均使用 B+ 树实现,例如:

  • 文件系统:NTFS, ReiserFS, NSS, XFS, JFS, ReFS, BFS, Ext4;
  • 关系型数据库:DB2, Informix, SQL Server, Oracle, Sybase ASE, SQLite;
  • NoSQL 数据库:CouchDB, Tokyo Cabinet;

B+ 树的优势在于:

  • 由于内部节点不存储键值关联的附属数据,所以内部节点节省的空间可以存放更多的键值。也就意味着从磁盘存取一页时可获得更多的键值信息。
  • 叶节点形成了一个链,所以对树的全扫描就是对所有叶节点的线性遍历。

B+ 树 C# 代码实现

在 GitHub 上的项目 BPlusTreePractice 使用 C# 语言实现了简单的 B+ 树,功能包括插入键值对、搜索键、删除键、存储至磁盘块文件等,但未实现节点链的双向链表和扫描功能。

下面是测试代码示例。

 1 using System;
 2 using System.IO;
 3
 4 namespace BPlusTreePractice
 5 {
 6   class Program
 7   {
 8     static void Main(string[] args)
 9     {
10       // 指定磁盘文件位置
11       string treeFileName =
12         @"E:\BPlusTree_" + DateTime.Now.ToString(@"yyyyMMddHHmmssffffff") + ".data";
13       Stream treeFileStream =
14         new FileStream(treeFileName, FileMode.CreateNew, FileAccess.ReadWrite);
15
16       // 初始化 B+ 树,固定长度字符串为键,映射至长整形
17       int keyLength = 64;
18       int nodeCapacity = 2;
19       BPlusTree tree =
20         BPlusTree.InitializeInStream(treeFileStream, 0L, keyLength, nodeCapacity);
21
22       // 插入 0 到 7 共 8 个键值对
23       for (int i = 0; i < 8; i++)
24       {
25         tree.Set(i.ToString(), (long)(i * 1000)); // Key 是字符串,Value 是 long 类型
26       }
27
28       // 将 B+ 树输出到命令行
29       Console.WriteLine(tree.ToText());
30
31       // 获取指定的键值对
32       Console.WriteLine();
33       Console.WriteLine(string.Format("Tree's first key is {0}.", tree.FirstKey()));
34       Console.WriteLine(string.Format("Check key {0} exists {1}.",
35         "3", tree.ContainsKey("3")));
36       Console.WriteLine(string.Format("{0}'s next key is {1}.", "6", tree.NextKey("6")));
37       Console.WriteLine(string.Format("Get key {0} with value {1}.", "2", tree.Get("2")));
38       Console.WriteLine(string.Format("Index key {0} with value {1}.", "4", tree["4"]));
39       Console.WriteLine();
40
41       // 删除键值对
42       tree.RemoveKey("6");
43       Console.WriteLine(tree.ToText());
44
45       Console.ReadKey();
46     }
47   }
48 }

本篇文章《B 树和 B+ 树》由 Dennis Gao 发表自博客园个人技术博客,未经作者本人同意禁止任何形式的转载,任何自动或人为的爬虫转载或抄袭行为均为耍流氓。

时间: 2024-08-01 14:51:17

人人都是 DBA(VII)B 树和 B+ 树的相关文章

人人都是 DBA(V)SQL Server 数据库文件

原文:人人都是 DBA(V)SQL Server 数据库文件 SQL Server 数据库安装后会包含 4 个默认系统数据库:master, model, msdb, tempdb. SELECT [name] ,database_id ,suser_sname(owner_sid) AS [owner] ,create_date ,user_access_desc ,state_desc FROM sys.databases WHERE database_id <= 4; master mas

人人都是 DBA(X)资源信息收集脚本汇编

原文:人人都是 DBA(X)资源信息收集脚本汇编 什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA". 索引 获取数据库的 CPU 使用率 过去一段时间里 CPU 利用率的历史情况 谁用 CPU 工作的时间最长 服务器上安装了多大的 Memory SQL Server 进程用了多少 Memory 是否申请新的 Memory 无法得到 SQL Ser

人人都是 DBA(XI)I/O 信息收集脚本汇编

原文:人人都是 DBA(XI)I/O 信息收集脚本汇编 什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA". 索引 数据文件和日志文件位置和大小 查看指定数据库文件的大小和可用空间 服务器 Disk 容量和挂载信息 查看 Disk 剩余空间 查询数据库设置的 Recovery Model 查看最近的 Full Backup 信息 获取所有数据库的

人人都是 DBA(IV)SQL Server 内存管理

原文:人人都是 DBA(IV)SQL Server 内存管理 SQL Server 的内存管理是一个庞大的主题,涉及特别多的概念和技术,例如常见的 Plan Cache.Buffer Pool.Memory Clerks 等.本文仅是管中窥豹,描述常见的内存管理相关概念. 在了解内存管理之前,通过 sys.dm_os_memory_clerks 视图可以查询内存的使用职责(Memory Clerks),也就是内存的消耗者. SELECT [type], SUM(pages_kb) AS tota

人人都是 DBA(XV)锁信息收集脚本汇编

原文:人人都是 DBA(XV)锁信息收集脚本汇编 什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA". 索引 查看 Session 对应的 Thread 和当前 Command 侦测 Deadlocking 或阻塞问题 查看 Task 执行中哪个 Wait Type 最慢 查看当前 Task 的运行情况 查看 Lock Waits 状态 查看 La

人人都是 DBA(XII)查询信息收集脚本汇编

原文:人人都是 DBA(XII)查询信息收集脚本汇编 什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA". 索引 按页编号查看数据表信息 获取查询 SELECT 语句的执行次数排名 看看哪些 Ad-hoc Query 在浪费资源 查看当前处于等待状态的 Task 在等什么 查询谁在占着 Session 连接 查询程序占用的 SPID 信息 查询所有

人人都是 DBA(XIII)索引信息收集脚本汇编

原文:人人都是 DBA(XIII)索引信息收集脚本汇编 什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA". 索引 找出哪些表的 Index 需要改进 在指定数据库中查找哪些表的 Index 需要改进 根据缓存的查询计划判断 SP 是否需要优化 发现那些 Index 的写远多于读的表  查看 Index 的 Statistics 最后更新时间 查看

人人都是 DBA(VI)SQL Server 事务日志

原文:人人都是 DBA(VI)SQL Server 事务日志 SQL Server 的数据库引擎通过事务服务(Transaction Services)提供事务的 ACID 属性支持.ACID 属性包括: 原子性(Atomicity) 一致性(Consistency) 隔离性(Isolation) 持久性(Durability) 事务日志(Transaction Log) 事务日志(Transaction Log)存储的是对数据库所做的更改信息,让 SQL Server 有机会恢复数据库.而恢复

人人都是 DBA(IX)服务器信息收集脚本汇编

原文:人人都是 DBA(IX)服务器信息收集脚本汇编 什么?有个 SQL 执行了 8 秒! 哪里出了问题?臣妾不知道啊,得找 DBA 啊. DBA 人呢?离职了!!擦!!! 程序员在无处寻求帮助时,就得想办法自救,努力让自己变成 "伪 DBA". 索引 SQL Server 安装的是什么版本 Windows 操作系统是什么版本 SQL Server 是什么时候安装的 服务器主机名是什么 硬件服务器是谁制造的 服务器硬件是什么配置 服务器的 CPU 有几个核 服务器的 CPU 是什么型号