Skip to content

Btree 树


B-tree(B树)是一种常见的 多路平衡查找树(balanced search tree),广泛应用于 数据库索引(如MySQL、PostgreSQL)文件系统(如NTFS、HFS+) 中,用于高效地存储和检索大量数据。

在理解 B树之前,首先要明白计算机存储的层次结构:

  • 内存:访问速度快,但容量小、价格贵、断电后数据丢失。
  • 磁盘:访问速度慢(相对于内存),但容量大、价格便宜、数据持久化。

数据库的表数据通常远远大于可用内存,因此必须存储在磁盘上。磁盘I/O(即从磁盘读取数据到内存)是数据库操作中最耗时的部分

B树的核心设计目标就是最大限度地减少磁盘I/O次数

  • 二叉搜索树的缺陷:一个节点只存一个键和两个指针。如果数据量巨大,树会变得非常高。每次比较都可能需要一次新的磁盘I/O,效率极低。
  • B树的优势:B树的一个节点可以存储大量键值指针。这意味着树的高度非常低(通常只有3-4层),从而使得在十亿级别的数据中查找一个记录,也仅需3-4次磁盘I/O

一棵 m 阶 B 树(m-way B-tree) 具有以下性质:

  1. 节点类型

    • 内部节点:存储若干键(key)对应的数据记录或数据指针,以及指向子节点的指针
      (与 B+ 树不同,B 树的内部节点也可以存放实际数据。)

    • 叶子节点:存储对应的数据(或指向数据的指针)
      所有叶子节点都位于同一层,体现了树的平衡性。

  2. 键的数量

    • 除根节点外,每个节点至少包含 ⌈m/2⌉ − 1 个键,最多包含 m − 1 个键。

    • 根节点至少包含 1 个键(若它不是叶节点)。

  3. 子节点数量

    • 若一个内部节点有 k 个键,则它恰好有 k + 1 个子节点。

    • 每个子节点也是一棵 B 树。

  4. 键的排序与子树范围

    • 每个节点中的键按照**非降序(通常为升序)**排列。

    • 对于一个节点中键 K₁, K₂, ..., Kₖ

      • 第 1 个子树中所有键 < K₁

      • 第 2 个子树中所有键 ∈ (K₁, K₂)

      • 第 (k+1) 个子树中所有键 > Kₖ

3. B树的操作(以查找、插入为例)

Section titled “3. B树的操作(以查找、插入为例)”

B 树的查找操作与二叉搜索树类似,但在每个节点中是多路分支查找,即每个节点可以包含多个键值和多个子指针。
与 B+ 树不同的是,B 树的键和值(数据或数据指针)都可能存在于内部节点或叶子节点中

查找过程:

  1. 从根节点开始。
    如果根节点为空,则查找失败。

  2. 在当前节点中查找目标键:

    • 在节点的键集合中进行顺序查找二分查找

    • 如果在当前节点中找到目标键,则查找成功(可直接返回该节点中的数据或数据指针);

    • 如果未找到目标键,则确定目标键位于哪两个键之间,从而选取对应的子节点指针。

  3. 访问子节点。
    根据指针找到对应的子节点(通常对应磁盘页),并将其读入内存。

  4. 重复步骤 2 和 3,
    直到:

    • 找到目标键(查找成功),或

    • 到达叶子节点仍未找到(查找失败)。

示例:在下图的 3 阶 B 树中查找键 15

[10, 20]
/ | \
[5, 8] [13, 15, 18] [25, 30]

查找过程:

  1. 在根节点 [10, 20] 中查找 15,发现它位于 1020 之间;

  2. 沿第二个子指针进入节点 [13, 15, 18]

  3. 在该节点中找到键 15

  4. 查找成功,返回数据。

插入操作比查找复杂,因为它可能破坏B树的定义(如节点键数超过m-1),因此可能需要分裂节点

过程

  1. 查找插入位置:找到键应该被插入的叶子节点。
  2. 插入:将新键插入到该叶子节点中(保持有序)。
  3. 检查节点是否已满:如果插入后,叶子节点中的键数 等于 m(即超过了m-1),则需要分裂。
  4. 分裂节点
    • 将该节点的 m 个键分成两部分:
      • 左半部分:前 ⌈m/2⌉ - 1 个键。
      • 中间键:第 ⌈m/2⌉ 个键。
      • 右半部分:后 ⌊m/2⌋ 个键。
    • 创建一个新的兄弟节点,将右半部分的键放入新节点。
    • 将中间键提升到父节点中,并在父节点中建立指向新节点的指针。
  5. 递归向上:如果父节点也因为这次提升而变满,则重复分裂过程。这个过程可能会一直向上传递到根节点。
  6. 创建新根:如果根节点需要分裂,就创建一个新的根节点,它只有一个键和两个子节点。这是B树长高的唯一方式。

示例:向一个 3 阶 B 树(m=3,每个节点最多 2 个键)中插入 25

初始状态:

[10]
/ \
[5] [20, 30] <-- 要插入到这里
  1. 在根 [10] 中查找插入位置,25 > 10 → 进入右子节点 [20, 30]
  2. 插入后节点变为 [20, 25, 30](已满,键数 = 3 = m)。
  3. 分裂
    • 中间键:25
    • 左节点:[20]
    • 右节点:[30]
    • 将中间键 25 提升到父节点 [10]
  4. 父节点变为 [10, 25],键数 = 2,未满,停止。

最终状态:

[10, 25]
/ | \
[5] [20] [30]
  1. 矮胖的树形:由于每个节点存储大量键,树的高度非常低,减少了访问任一记录所需的磁盘I/O次数。
  2. 平衡:所有叶子节点都在同一层,保证了最坏情况下的性能也是稳定的。
  3. 高扇出:一个节点有很多子节点,这直接导致了树的低高度。
  4. 自平衡:插入和删除算法通过分裂和合并节点来维持平衡,无需复杂的旋转操作。

在实际的数据库系统中(如MySQL的InnoDB引擎),更常用的是B树的变种——B+树。它与B树的主要区别在于:

特性B树B+树
数据存储内部节点和叶子节点都存储数据指针只有叶子节点存储数据指针(或完整的数据记录)。内部节点只存储键和子节点指针。
叶子节点链接叶子节点之间没有链接。所有叶子节点通过一个链表串联起来。
查询性能可能在内部节点就找到数据,提前返回。任何查找都必须到达叶子节点。
范围查询效率较低,需要回溯。效率极高。一旦在叶子节点找到起点,顺着链表扫描即可。
内部节点扇出由于存储数据指针,每个节点能存的键相对较少。不存储数据指针,所以每个节点能存更多的键,树更矮胖。

B+树是现代关系型数据库索引的绝对主流实现方式。

B树是一种为磁盘存储量身定做的、多路的、自平衡的搜索树。它通过最大化每个节点的容量最小化树的高度,从而将昂贵的磁盘I/O次数降到最低,是数据库和文件系统能够高效管理海量数据的基石。其变种B+树则进一步优化了范围查询和顺序访问的性能,成为了数据库索引的实际标准。