DBS note3:B+ Trees

目录

1、介绍

2、B+树特征

3、插入

4、删除

5、存储记录

1)方法1:按值存储

2)方法2:按引用存储

3)方法3:按引用列表存储

6、聚类(Clustering)

1)非聚类(Unclustered)

2)Clustered

7、计算 I/O 次数

8、批量加载


1、介绍

在之前我们讨论了不同的文件和记录表示方法,用于数据存储。本文将介绍索引,它是在数据文件之上运行的一种数据结构,有助于加速对特定键的读取。

我们可以将数据文件视为书籍的实际内容,将索引视为用于快速查找的目录。我们使用索引来使查询运行更快,特别是那些经常运行的查询。

考虑一个Web应用程序,在登录过程中根据用户名查找 Users 表中用户的记录。在用户名列上建立索引将通过快速找到尝试登录的用户的行来加速登录过程。

学习B+树(一种特定类型的索引)。以下是B+树的一个示例:

2、B+树特征

  • 数字 d 是 B+ 树的阶数。每个节点(除了根节点)的条目数量必须满足 d ≤ x ≤ 2d,假设没有删除操作发生(如果删除数据,则叶节点最终可能少于 d 个条目)。每个节点内的条目必须排序。
  • 在每个内部节点的条目之间,有一个指向子节点的指针。由于一个节点最多有 2d 个条目,内部节点最多可能有 2d+1 个子指针。这也被称为树的扇出度(tree’s fanout)。
  • 位于一个内部节点条目左侧的子节点的键必须小于该条目,而位于右侧的子节点的键必须大于或等于该条目。
  • 所有叶节点都具有相同的深度,并且具有 d 到 2d 个条目(即至少半满)。

例如,这是一颗阶数 d=2 的树的节点:

请注意,该节点满足阶数要求,也称为(the occupancy invariant )占用不变式(d ≤ x ≤ 2d),因为在这里 d=2,而该节点有 3 个条目,满足 2 ≤ x ≤ 4。

  • 由于排序和子节点的属性,我们可以沿着树向下遍历到叶节点,以找到我们需要的记录。这类似于二叉搜索树(BSTs)。
  • 每个从根到叶子的路径具有相同数量的边 - 这就是树的高度。从这个意义上说,B+树始终是平衡的。换句话说,只有具有根节点的B+树的高度为0。
  • 只有叶节点包含记录(或记录的指针 - 这将在后面解释)。内部节点(非叶节点)不包含实际的记录。

例如,这是一颗阶数 d=1 的树:

3、插入

将条目(节点)插入 B+ 树,可按照以下步骤进行:

a. 找到要插入值的叶节点 L。您可以通过沿树向下遍历来完成。按顺序将键和记录添加到叶节点。

b. 如果 L 溢出(即 L 有超过 2d 个条目)

  • 将其分为 L_1 和 L_2。在 L_1 中保留 d 个条目(这意味着 d+1 个条目将放在 L_2 中)。
  • 如果 L 是叶节点,将 L_2 的第一个条目复制到父节点。如果 L 不是叶节点,则将 L_2 的第一个条目移动到父节点。
  • 调整指针。

c. 如果父节点溢出,那么对其进行递归,即对父节点执行步骤 2。(这是唯一会增加树高度的情况)

注意:我们想要将叶节点数据复制到父节点,这样我们就不会丢失叶节点中的数据。请记住,建立索引的表中的每个键都必须在叶节点中!在内部节点中的键并不意味着该键实际上仍然存在于表中。另一方面,我们可以将内部节点数据移动到父节点,因为内部节点并不包含实际的数据,它们只是在遍历树时引导搜索的引用。

让我们通过一个例子来更好地理解这个过程!我们从以下的阶数为 d=1 的树开始:

让我们将 19 插入到我们的树中。当我们插入 19 时,我们看到在具有 17 的叶节点中有空间:

现在让我们将 21 插入到我们的树中。当我们插入 21 时,它导致一个叶节点溢出。因此,我们将这个叶节点分割成两个如下所示的叶节点:

L_1 是具有 17 的叶节点,而 L_2 是具有 19 和 21 的叶节点。

由于我们分割了一个叶节点,我们将 L_2 的第一个条目复制到父节点,并调整指针。我们还对父节点的条目进行排序,得到:

让我们再进行一次插入。这次我们将插入 36。当我们插入 36 时,叶节点溢出,因此我们将执行与插入 21 时相同的过程,得到:

请注意,现在父节点溢出了,因此我们现在必须进行递归。我们将分割父节点,得到:

L_1 是具有 19 的内部节点,而 L_2 是具有 27 和 32 的内部节点。

由于溢出的是内部节点,我们将 L_2 的第一个条目移动到父节点,并调整指针,得到:

最后,以下是关于插入B+树的一些建议说明:

通常,B+ 树节点具有至少 d 个条目和最多 2d 个条目。换句话说,如果树中的节点在插入之前满足这个不变性(通常会满足),那么插入后,它们将继续满足这个不变性。

当节点包含超过 2d 个条目时,发生插入溢出。

4、删除

要删除一个值,只需找到适当的叶节点,并从该叶节点中删除不需要的值。就是这么简单。(是的,从技术上讲,我们可能会违反一些 B+ 树的不变性。这没关系,因为在实践中,我们进行的插入比删除多得多,因此删除的内容很快就会被替换。)

提醒:我们永远不会删除内部节点的键,因为它们只用于搜索而不是保存数据。

5、存储记录

直到现在,我们还没有讨论过记录实际上是如何存储在叶节点中的。现在让我们来看看。有三种在叶节点中存储记录的方法:

1)方法1:按值存储

在方法1中,叶子页面就是数据页面。与其包含指向记录的指针,不如说叶子页面包含记录本身。

虽然方法 1 是可能最简单的实现方式,但它有一个重要的限制:如果我们只有方法1,就不能支持在同一文件上构建多个索引(在上面的例子中,我们不能在 'name' 上支持二级索引。相反,我们必须复制文件并在该文件上构建一个新的方法 1 索引。

2)方法2:按引用存储

在方法 2 中,叶子页面保存指向相应记录的指针。

符号说明:在上面的图表中,叶节点包含(Key,RecordID)对,其中 RecordID 是 [PageNum,RecordNum]。

按引用进行索引允许我们在同一文件上拥有多个索引,因为实际数据可以以任何顺序存储在磁盘上。

3)方法3:按引用列表存储

在方法 3 中,叶节点保存指向相应记录的指针列表。与方法 2 相比,当同一叶节点条目存在多个记录时,这种方式更紧凑。现在,每个叶节点包含(Key,RecordID列表)对。

6、聚类(Clustering)

现在我们已经讨论了记录如何存储在叶节点中,我们还将讨论数据页上的数据如何组织。 集群/非集群是指数据页的结构方式。 由于叶页是方法1 的实际数据页,并且键在索引叶页上排序,因此方法1 索引默认情况下是聚集的。 因此,去聚类仅适用于方法 2 或 3。

1)非聚类(Unclustered

在非聚类索引中,数据页面是完全混乱的。因此,很可能你需要为每个你想要的记录读取一个单独的页面。例如,考虑这个图示:

在上面的图中,如果我们想要读取键为12和24的记录,那么我们必须分别读取它们指向的每个数据页面,以检索与这些键关联的所有记录。

2)Clustered

在聚类索引中,数据页面按照构建 B+ 树时使用的相同索引进行排序。这并不意味着数据页面是精确排序的,只是键大致按照数据的顺序排列。因此,I/O成本的差异主要来自缓存,其中具有接近键的两个记录可能在同一页,因此可以从缓存的页面中读取第二个记录。因此,通常只需要读取一页即可获取具有公共/相似键的所有记录。例如,考虑这个图示:

在上面的图中,我们可以通过读取 2 页来读取键为 7 和 12 的记录。如果我们按照叶节点值的顺序进行顺序读取,数据页面基本相同。因此,总的来说:

  • 非聚类 = 每条记录 ~ 1 次 I/O。
  • 聚类 = 每页记录 ~ 1 次 I/O。
  • 聚类 vs 非聚类索引:尽管聚类索引在范围搜索方面可能更有效,并在顺序磁盘访问和预取等方面提供潜在的局部性优势,但它们通常比非聚类索引维护成本更高。例如,随着越来越多的插入操作,数据文件可能变得不太聚类,因此需要定期对文件进行排序。

7、计算 I/O 次数

以下是一般的步骤。这是值得记录的:

1. 读取适当的从根到叶子的路径。

2. 读取适当的数据页面。如果我们需要读取多个页面,我们将为每个页面分配一次读取 I/O。此外,我们需要考虑 Alt. 2 或 3 的聚类情况(见下文)。
3. 如果要修改数据页面,则写入数据页面。同样,如果我们要执行跨多个数据页面的写入,我们需要为每个页面分配写入 I/O。
4. 更新索引页面。

让我们看一个例子。请参考以下的 Alternative 2 非聚类 B+ 树:

我们想要从数据库中删除唯一的 11 岁。这将需要多少次I/O?

1、每个 2 个相关的索引页面(索引页面是内部节点或叶节点)需要 1 次I/O。
2、读取 11 岁记录所在的数据页面需要 1 次 I/O。一旦它在内存中,我们可以从页面中删除记录。
3、将修改后的数据页面写回磁盘需要 1 次 I/O。
4、现在我们的数据库中没有 11 岁了,我们应该从 B+ 树的叶页面中删除键 "11",这是在步骤 1 中已经读取的。我们这样做,然后将修改后的叶页面写回磁盘需要 1 次 I/O。

因此,删除记录的总成本是 5 次 I/O。

8、批量加载

我们之前讨论的插入过程适用于对现有 B+ 树进行添加操作。然而,如果我们要从头开始构建一个 B+ 树,我们可以做得更好。这是因为如果我们使用插入过程,每次插入新内容时都必须遍历整棵树;对随机数据页面的常规插入也会导致缓存效率低和叶节点的利用率低,因为它们通常是半空的。相反,我们将使用批量加载:

1、对将建立索引的键进行排序。
2、填充叶节点,直到达到某个填充因子 f。注意,填充因子仅适用于叶节点。对于内部节点,我们仍然遵循相同的规则去插入,直到它们被填满。
3、从父节点到叶节点添加指针。如果父节点溢出,我们将执行与插入类似的过程。我们将父节点分成两个节点:
    - 在 L_1 中保留 d 个条目(这意味着 d+1 个条目将进入 L_2)。
    - 由于父节点溢出,我们将 L_2 的第一个条目移动到父节点。
    - 调整指针。

让我们看一个例子。假设我们的填充因子是 3/4,我们想要将 1 到 20 插入到一个顺序 d=2 的树中。我们将从填充叶节点开始,直到达到填充因子:

我们已经将一个叶节点填充到填充因子的 3/4,并从父节点添加了一个指向叶节点的指针。让我们继续填充:

在上面的图中,我们看到父节点已经溢出。我们将父节点分成两个节点,并创建一个新的父节点:

从上面的例子中可以看出,通过批量加载构建的索引始终以聚类方式开始,因为底层数据是根据键排序的。为了保持聚类,我们可以根据未来的插入模式选择填充因子。

以上,DBS note2:DIsks and Files

祝好。

数据结构算法与应用-C++语言描述 目 录 译者序 前言 第一部分 预备知识 第1章 C++程序设计 1 1.1 引言 1 1.2 函数与参数 2 1.2.1 传值参数 2 1.2.2 模板函数 3 1.2.3 引用参数 3 1.2.4 常量引用参数 4 1.2.5 返回值 4 1.2.6 递归函数 5 1.3 动态存储分配 9 1.3.1 操作符new 9 1.3.2 一维数组 9 1.3.3 异常处理 10 1.3.4 操作符delete 10 1.3.5 二维数组 10 1.4 类 13 1.4.1 类Currency 13 1.4.2 使用不同的描述方法 18 1.4.3 操作符重载 20 1.4.4 引发异常 22 1.4.5 友元和保护类成员 23 1.4.6 增加#ifndef, #define和#endif语句 24 1.5 测试与调试 24 1.5.1 什么是测试 24 1.5.2 设计测试数据 26 1.5.3 调试 28 1.6 参考及推荐读物 29 第2章 程序性能 30 2.1 引言 30 2.2 空间复杂性 31 2.2.1 空间复杂性的组成 31 2.2.2 举例 35 2.3 时间复杂性 37 2.3.1 时间复杂性的组成 37 2.3.2 操作计数 37 2.3.3 执行步数 44 2.4 渐进符号(O、 健?、 o) 55 2.4.1 大写O符号 56 2.4.2 椒?58 2.4.3 符号 59 2.4.4 小写o符号 60 2.4.5 特性 60 2.4.6 复杂性分析举例 61 2.5 实际复杂性 66 2.6 性能测量 68 2.6.1 选择实例的大小 69 2.6.2 设计测试数据 69 2.6.3 进行实验 69 2.7 参考及推荐读物 74 第二部分 数据结构 第3章 数据描述 75 3.1 引言 75 3.2 线性表 76 3.3 公式化描述 77 3.3.1 基本概念 77 3.3.2 异常类NoMem 79 3.3.3 操作 79 3.3.4 评价 83 3.4 链表描述 86 3.4.1 类ChainNode 和Chain 86 3.4.2 操作 88 3.4.3 扩充类Chain 91 3.4.4 链表遍历器类 92 3.4.5 循环链表 93 3.4.6 与公式化描述方法的比较 94 3.4.7 双向链表 95 3.4.8 小结 96 3.5 间接寻址 99 3.5.1 基本概念 99 3.5.2 操作 100 3.6 模拟指针 102 3.6.1 SimSpace的操作 103 3.6.2 采用模拟指针的链表 106 3.7 描述方法的比较 110 3.8 应用 111 3.8.1 箱子排序 111 3.8.2 基数排序 116 3.8.3 等价类 117 3.8.4 凸包 122 3.9 参考及推荐读物 127 第4章 数组和矩阵 128 4.1 数组 128 4.1.1 抽象数据类型 128 4.1.2 C++数组 129 4.1.3 行主映射和列主映射 129 4.1.4 类Array1D 131 4.1.5 类Array2D 133 4.2 矩阵 137 4.2.1 定义和操作 137 4.2.2 类Matrix 138 4.3 特殊矩阵 141 4.3.1 定义和应用 141 4.3.2 对角矩阵 143 4.3.3 三对角矩阵 144 4.3.4 三角矩阵 145 4.3.5 对称矩阵 146 4.4 稀疏矩阵 149 4.4.1 基本概念 149 4.4.2 数组描述 149 4.4.3 链表描述 154 第5章 堆栈 161 5.1 抽象数据类型 161 5.2 派生类和继承 162 5.3 公式化描述 163 5.3.1 Stack的效率 164 5.3.2 自定义Stack 164 5.4 链表描述 166 5.5 应用 169 5.5.1 括号匹配 169 5.5.2 汉诺塔 170 5.5.3 火车车厢重排 172 5.5.4 开关盒布线 176 5.5.5 离线等价类问题 178 5.5.6 迷宫老鼠 180 5.6 参考及推荐读物 188 第6章 队列 189 6.1 抽象数据类型 189 6.2 公式化描述 190 6.3 链表描述 194 6.4 应用 197 6.4.1 火车车厢重排 197 6.4.2 电路布线 201 6.4.3 识别图元 204 6.4.4 工厂仿真 206 6.5 参考及推荐读物 217 第7章 跳表和散列 218 7.1 字典 218 7.2 线性表描述 219 7.3 跳表描述 222 7.3.1 理想情况 222 7.3.2 插入和删除 223 7.3.3 级的分配 224 7.3.4 类SkipNode 224 7.3.5 类SkipList 225 7.3.6 复杂性 229 7.4 散列表描述 229 7.4.1 理想散列 229 7.4.2 线性开型寻址散列 230 7.4.3 链表散列 234 7.5 应用——文本压缩 238 7.5.1 LZW压缩 239 7.5.2 LZW压缩的实现 239 7.5.3 LZW解压缩 243 7.5.4 LZW解压缩的实现 243 7.6 参考及推荐读物 247 第8章 二叉树和其他树 248 8.1 树 248 8.2 二叉树 251 8.3 二叉树的特性 252 8.4 二叉树描述 253 8.4.1 公式化描述 253 8.4.2 链表描述 254 8.5 二叉树常用操作 256 8.6 二叉树遍历 256 8.7 抽象数据类型BinaryTree 259 8.8 类BinaryTree 260 8.9 抽象数据类型及类的扩充 263 8.9.1 输出 263 8.9.2 删除 264 8.9.3 计算高度 264 8.9.4 统计节点数 265 8.10 应用 265 8.10.1 设置信号放大器 265 8.10.2 在线等价类 268 8.11 参考及推荐读物 275 第9章 优先队列 276 9.1 引言 276 9.2 线性表 277 9.3 堆 278 9.3.1 定义 278 9.3.2 最大堆的插入 279 9.3.3 最大堆的删除 279 9.3.4 最大堆的初始化 280 9.3.5 类MaxHeap 281 9.4 左高树 285 9.4.1 高度与宽度优先的最大及最小 左高树 285 9.4.2 最大HBLT的插入 287 9.4.3 最大HBLT的删除 287 9.4.4 合并两棵最大HBLT 287 9.4.5 初始化最大HBLT 289 9.4.6 类MaxHBLT 289 9.5 应用 293 9.5.1 堆排序 293 9.5.2 机器调度 294 9.5.3 霍夫曼编码 297 9.6 参考及推荐读物 302 第10章 竞?303 10.1 引言 303 10.2 抽象数据类型WinnerTree 306 10.3 类WinnerTree 307 10.3.1 定义 307 10.3.2 类定义 307 10.3.3 构造函数、析构函数及Winner 函数 308 10.3.4 初始化赢者树 308 10.3.5 重新组织比赛 310 10.4 输者树 311 10.5 应用 312 10.5.1 用最先匹配法求解箱子装载 问题 312 10.5.2 用相邻匹配法求解箱子装载 问题 316 第11章 搜索树 319 11.1 二叉搜索树 320 11.1.1 基本概念 320 11.1.2 抽象数据类型BSTree和 IndexedBSTree 321 11.1.3 类BSTree 322 11.1.4 搜索 322 11.1.5 插入 323 11.1.6 删除 324 11.1.7 类DBSTree 326 11.1.8 二叉搜索树的高度 327 11.2 AVL树 328 11.2.1 基本概念 328 11.2.2 AVL树的高度 328 11.2.3 AVL树的描述 329 11.2.4 AVL搜索树的搜索 329 11.2.5 AVL搜索树的插入 329 11.2.6 AVL搜索树的删除 332 11.3 红-黑树 334 11.3.1 基本概念 334 11.3.2 红-黑树的描述 336 11.3.3 红-黑树的搜索 336 11.3.4 红-黑树的插入 336 11.3.5 红-黑树的删除 339 11.3.6 实现细节的考虑及复杂性分析 343 11.4 B-树 344 11.4.1 索引顺序访问方法 344 11.4.2 m 叉搜索树 345 11.4.3 m 序B-树 346 11.4.4 B-树的高度 347 11.4.5 B-树的搜索 348 11.4.6 B-树的插入 348 11.4.7 B-树的删除 350 11.4.8 节点结构 353 11.5 应用 354 11.5.1 直方图 354 11.5.2 用最优匹配法求解箱子装载 问题 357 11.5.3 交叉分布 359 11.6 参考及推荐读物 363 第12章 图 365 12.1 基本概念 365 12.2 应用 366 12.3 特性 368 12.4 抽象数据类型Graph和Digraph 370 12.5 无向图和有向图的描述 371 12.5.1 邻接矩阵 371 12.5.2 邻接压缩表 373 12.5.3 邻接链表 374 12.6 网络描述 375 12.7 类定义 376 12.7.1 不同的类 376 12.7.2 邻接矩阵类 377 12.7.3 扩充Chain类 380 12.7.4 类LinkedBase 381 12.7.5 链接类 382 12.8 图的遍历 386 12.8.1 基本概念 386 12.8.2 邻接矩阵的遍历函数 387 12.8.3 邻接链表的遍历函数 388 12.9 语言特性 389 12.9.1 虚函数和多态性 389 12.9.2 纯虚函数和抽象类 391 12.9.3 虚基类 391 12.9.4 抽象类和抽象数据类型 393 12.10 图的搜索算法 394 12.10.1 宽度优先搜索 394 12.10.2 类Network 395 12.10.3 BFS的实现 395 12.10.4 BFS的复杂性分析 396 12.10.5 深度优先搜索 397 12.11 应用 399 12.11.1 寻找路径 399 12.11.2 连通图及其构件 400 12.11.3 生成树 402 第三部分 算法设计方法 第13章 贪婪算法 405 13.1 最优化问题 405 13.2 算法思想 406 13.3 应用 409 13.3.1 货箱装船 409 13.3.2 0/1背包问题 410 13.3.3 拓扑排序 412 13.3.4 二分覆盖 415 13.3.5 单源最短路径 421 13.3.6 最小耗费生成树 424 13.4 参考及推荐读物 433 第14章 分而治之算法 434 14.1 算法思想 434 14.2 应用 440 14.2.1 残缺棋盘 440 14.2.2 归并排序 443 14.2.3 快速排序 447 14.2.4 选择 452 14.2.5 距离最近的点对 454 14.3 解递归方程 462 14.4 复杂性的下限 463 14.4.1 最小最大问题的下限 464 14.4.2 排序算法的下限 465 第15章 动态规划 467 15.1 算法思想 467 15.2 应用 469 15.2.1 0/1背包问题 469 15.2.2 图像压缩 471 15.2.3 矩阵乘法链 476 15.2.4 最短路径 480 15.2.5 网络的无交叉子集 483 15.2.6 元件折叠 486 15.3 参考及推荐读物 491 第16章 回溯 492 16.1 算法思想 492 16.2 应用 496 16.2.1 货箱装船 496 16.2.2 0/1背包问题 503 16.2.3 最大完备子图 506 16.2.4 旅行商问题 508 16.2.5 电路板排列 510 第17章 分枝定界 516 17.1 算法思想 516 17.2 应用 519 17.2.1 货箱装船 519 17.2.2 0/1背包问题 526 17.2.3 最大完备子图 528 17.2.4 旅行商问题 529 17.2.5 电路板排列 532
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吕飞雨的头发不能秃

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值