跳转至

B树与B+树

概述

B树(B-Tree)和B+树(B+ Tree)是**多路平衡查找树**,专为**磁盘存储**设计,能够显著减少磁盘 I/O 次数。它们是现代数据库索引和文件系统的核心数据结构。

核心价值:B树和B+树通过增大节点分支因子(每个节点存储多个关键字),降低树的高度,使得每次查找只需很少的磁盘 I/O 操作。B+树进一步将所有数据存储在叶子节点,并连接成链表,极大优化了范围查询性能。

为什么需要B树?

传统二叉树的问题

二叉搜索树在磁盘存储的问题:

  • 树高度大: n个节点高度约 log₂ n
  • 每层一个节点: 每次查找需要多次磁盘 I/O
  • 节点分散: 无法利用磁盘块预读

示例: 存储 100万 条记录

  • 二叉树高度: log₂(1000000) ≈ 20
  • 查找需要: 20 次磁盘 I/O
  • 假设每次 I/O 10ms: 总时间 200ms

B树的解决方案:

  • 多路分支: 每个节点存储多个关键字
  • 树高度小: 大幅减少磁盘 I/O 次数
  • 节点对齐磁盘块: 一次 I/O 读取整个节点

示例: 100阶 B树存储 100万 条记录

  • B树高度: log₁₀₀(1000000) ≈ 3
  • 查找需要: 3 次磁盘 I/O
  • 总时间: 30ms(提升 6 倍!)

B树特点

B树的定义(m阶B树)

m阶B树的定义:

1. 节点关键字数量:

  • 根节点: 1 ~ m-1 个关键字
  • 非根非叶子节点: ⌈m/2⌉-1 ~ m-1 个关键字
  • 叶子节点: ⌈m/2⌉-1 ~ m-1 个关键字

2. 节点子树数量:

  • 有 k 个关键字的节点有 k+1 棵子树

3. 叶子节点同层:

  • 所有叶子节点都在同一层(完全平衡)

4. 节点内有序:

  • 节点内的关键字按升序排列

5. 子树值域:

  • 节点关键字 K₁, K₂, ..., Kₖ₋₁
  • 子树 P₀ 中的所有值 < K₁
  • Kᵢ < 子树 Pᵢ 中的所有值 < Kᵢ₊₁
  • Kₖ₋₁ < 子树 Pₖ 中的所有值

B树结构示意

Text Only
4阶B树(每个节点最多3个关键字,4个子节点):

                    根节点
                 ┌───────────────┐
                 │  30  │  60   │
                 └───────┬───────┘
              ┌──────────┼──────────┐
              ↓          ↓          ↓
         ┌─────────┐ ┌─────────┐ ┌─────────┐
         │10│20   │ │40│50   │ │70│80│90│
         └────┬────┘ └────┬────┘ └─────────┘
         叶子节点   叶子节点    叶子节点

验证B树性质:
─────────────────────────────────────────────────────────────────
性质                验证                            结果
─────────────────────────────────────────────────────────────────
根节点关键字数       2个 (30, 60), 1 ≤ 2 ≤ 3        ✓
非根节点关键字数     2-3个, 1 ≤ 2,3 ≤ 3             ✓
叶子节点同层         都在第3层                      ✓
节点内有序           [10,20], [40,50], [70,80,90]  ✓
子树值域             左子树值 < 30 < 中子树值 < 60  ✓
                    < 右子树值                      ✓
─────────────────────────────────────────────────────────────────

B树节点结构

Text Only
m = 4 的B树节点结构:

关键字数组: keys[0..m-2] = keys[0..2]
子节点指针: children[0..m-1] = children[0..3]

节点结构:
         ┌─────────────────────────────────────┐
         │  K₁   │   K₂   │   K₃   │           │
         └────────┬────────┬────────┬──────────┘
               ↓        ↓        ↓        ↓
             P₀       P₁       P₂       P₃

关键字关系:
- K₁ < K₂ < K₃
- P₀ 中所有 key < K₁
- K₁ < P₁ 中所有 key < K₂
- K₂ < P₂ 中所有 key < K₃
- K₃ < P₃ 中所有 key

实际存储示例:
         ┌─────────────────────────────────────┐
         │  20   │   40   │   60   │           │
         └────────┬────────┬────────┬──────────┘
               ↓        ↓        ↓        ↓
            [1-19]   [21-39]  [41-59]  [61-100]

B+树特点

B+树与B树的区别

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    B树 vs B+树                                       │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  B树:                                                               │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ - 数据存储在所有节点                                          │   │
│  │ - 查找可能在非叶子节点终止                                     │   │
│  │ - 叶子节点之间无连接                                          │   │
│  │ - 范围查询需要中序遍历                                        │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│              [30|60]           ← 数据可能在这里                    │
│              /  |  \                                             │
│          [10|20] [40|50] [70|80]  ← 叶子节点                     │
│                                                                     │
│  B+树:                                                              │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ - 数据只存储在叶子节点                                         │   │
│  │ - 查找必须到达叶子节点                                        │   │
│  │ - 叶子节点连接成链表                                          │   │
│  │ - 范围查询只需遍历叶子链表                                    │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│              [30|60]           ← 只有索引,无数据                  │
│              /  |  \                                             │
│          [10|20]←→[40|50]←→[70|80]  ← 叶子节点存储数据            │
│           叶子链表连接                                             │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

B+树节点结构

Text Only
B+树内部节点(只存索引):
         ┌─────────────────────────────────────┐
         │  20   │   40   │   60   │           │   ← 索引值
         └────────┬────────┬────────┬──────────┘
               ↓        ↓        ↓        ↓
             指向      指向     指向     指向
            叶子节点   叶子节点  叶子节点  叶子节点

B+树叶子节点(存数据+指针):
┌─────────────────────────────────────────────────────────────────┐
│ prev │  10  │data│  20  │data│  30  │data│ ... │ next │        │
└─────────────────────────────────────────────────────────────────┘
   ↑                                                      ↑
 槽向前一个                                        指向下一个
 叶子节点                                          叶子节点

关键特点:
- 内部节点只存储索引(复制的键值)
- 叶子节点存储实际数据
- 叶子节点通过指针连接成双向链表

原理详解

B树的查找

Text Only
B树查找算法:

目标: 查找关键字 key

步骤:
1. 从根节点开始
2. 在当前节点中找第一个 ≥ key 的关键字位置 i
3. 如果 keys[i] == key,找到,返回
4. 如果是叶子节点,未找到,返回失败
5. 否则,进入 children[i] 子树继续查找

示例: 在 4 阶 B树中查找 50

                    [30|60]
                    /  |  \
               [10|20] [40|50] [70|80|90]

步骤1: 从根节点 [30|60] 开始
       50 > 30, 50 < 60, 进入中间子树

步骤2: 进入节点 [40|50]
       50 > 40, 50 == 50, 找到!

查找路径: [30|60] → [40|50]
磁盘 I/O: 2 次

B树的插入

Text Only
B树插入算法:

步骤:
1. 找到应该插入的叶子节点
2. 如果叶子节点未满,直接插入
3. 如果叶子节点已满,分裂节点:
   a. 将中间关键字上移到父节点
   b. 将节点分裂为两个节点
   c. 如果父节点也满,递归分裂

示例: 向 4 阶 B树插入 25

初始状态:
              [30|60]
              /  |  \
         [10|20] [40|50] [70|80|90]

步骤1: 找到插入位置 - 叶子节点 [10|20]
步骤2: 插入 25,节点变为 [10|20|25]
       未超过最大关键字数 3,直接插入

结果:
              [30|60]
              /  |  \
         [10|20|25] [40|50] [70|80|90]

─────────────────────────────────────────────────────────────────

示例: 继续插入 5,导致分裂

插入前:
         [10|20|25]  ← 已满 (3个关键字)

步骤1: 插入 5,节点变为 [5|10|20|25] (临时,超过容量)

步骤2: 分裂
       - 中间关键字: 10 (第2个)
       - 左节点: [5]
       - 右节点: [20|25]
       - 10 上移到父节点

分裂后:
              [10|30|60]      ← 10 上移
              /  |  |  \
            [5] [20|25] [40|50] [70|80|90]

B树的分裂

Text Only
节点分裂过程(m=4,最多3个关键字):

分裂前(节点已满):
         ┌─────────────────────────────────────┐
         │  10  │  20  │  30  │               │  ← 3个关键字(已满)
         └────────┬────────┬────────┬──────────┘
               ↓        ↓        ↓        ↓
              P₀       P₁       P₂       P₃

插入新关键字 25 后(临时状态):
         ┌───────────────────────────────────────────┐
         │  10  │  20  │  25  │  30  │               │  ← 4个关键字(超容量)
         └────────┬────────┬────────┬────────┬───────┘
               ↓        ↓        ↓        ↓       ↓
              P₀       P₁       P₂       P₃      P₄

分裂过程:
1. 找中间关键字: mid = keys[m/2] = keys[2] = 20
2. 创建新节点
3. 左节点保留 keys[0..mid-1] = [10]
4. 右节点保留 keys[mid+1..] = [25, 30]
5. 中间关键字上移到父节点

分裂后:
    父节点: [... | 20 | ...]  ← 中间关键字上移
              /         \
    左节点: [10]      右节点: [25|30]
     /    \            /   \    \
    P₀    P₁          P₂   P₃   P₄

B+树的范围查询

Text Only
B+树范围查询优势:

查询: 查找 [40, 80] 范围内的所有数据

B树方式:
- 需要中序遍历整棵树
- 访问多个非叶子节点
- 效率较低

              [30|60]
              /  |  \
         [10|20] [40|50] [70|80|90]
         ↑       ↑        ↑
       访问    访问     访问

B+树方式:
- 找到范围起点叶子节点
- 沿着叶子链表遍历
- 只访问叶子节点
- 效率极高

              [30|60]
              /  |  \
         [10|20]←→[40|50]←→[70|80|90]
                 ↑ 沿链表遍历 →
              起点            终点

步骤:
1. 查找起点 40: 到达叶子节点 [40|50]
2. 输出 40, 50
3. 沿 next 指针到 [70|80|90]
4. 输出 70, 80
5. 遇到 90 > 80,停止

结果: [40, 50, 70, 80]

可视化演示

B树完整操作演示

Text Only
4阶B树操作演示(最多3个关键字)

═══════════════════════════════════════════════════════════════
初始状态: 空树
═══════════════════════════════════════════════════════════════

NULL

═══════════════════════════════════════════════════════════════
插入 10, 20, 30
═══════════════════════════════════════════════════════════════

插入 10: 根节点 [10]
插入 20: 根节点 [10|20]
插入 30: 根节点 [10|20|30]  ← 已满

状态:
         [10|20|30]

═══════════════════════════════════════════════════════════════
插入 40: 触发分裂
═══════════════════════════════════════════════════════════════

临时状态: [10|20|30|40] (超容量)

分裂:
- 中间关键字 20 上移
- 左节点: [10]
- 右节点: [30|40]

分裂后:
              [20]
              /  \
           [10]  [30|40]

═══════════════════════════════════════════════════════════════
插入 5, 15
═══════════════════════════════════════════════════════════════

插入 5: 到左节点 [10],变为 [5|10]
插入 15: 到左节点 [5|10],变为 [5|10|15]

状态:
              [20]
              /  \
         [5|10|15]  [30|40]

═══════════════════════════════════════════════════════════════
插入 25: 到右节点 [30|40],变为 [25|30|40]
═══════════════════════════════════════════════════════════════

状态:
              [20]
              /  \
         [5|10|15]  [25|30|40]

═══════════════════════════════════════════════════════════════
插入 35: 触发右节点分裂
═══════════════════════════════════════════════════════════════

临时状态: 右节点 [25|30|35|40] (超容量)

分裂:
- 中间关键字 35 上移
- 左节点: [25|30]
- 右节点: [40]

父节点变为 [20|35]

状态:
              [20|35]
              /  |  \
         [5|10|15] [25|30] [40]

═══════════════════════════════════════════════════════════════
插入 50, 60, 70
═══════════════════════════════════════════════════════════════

插入 50: [40|50]
插入 60: [40|50|60]
插入 70: 触发分裂

分裂 [40|50|60|70]:
- 中间关键字 60 上移
- 左节点: [40|50]
- 右节点: [70]

父节点变为 [20|35|60]  ← 已满

状态:
              [20|35|60]
              /   |   |   \
         [5|10|15] [25|30] [40|50] [70]

═══════════════════════════════════════════════════════════════
插入 80: 触发多层分裂
═══════════════════════════════════════════════════════════════

插入 80 到 [70],变为 [70|80]

状态:
              [20|35|60]
              /   |   |   \
         [5|10|15] [25|30] [40|50] [70|80]

═══════════════════════════════════════════════════════════════
最终B树
═══════════════════════════════════════════════════════════════

              [20|35|60]
              /   |   |   \
         [5|10|15] [25|30] [40|50] [70|80]

B+树结构演示

Text Only
B+树完整结构(数据只在叶子节点)

═══════════════════════════════════════════════════════════════
B+树结构
═══════════════════════════════════════════════════════════════

第1层(根节点,只有索引):
              [30|60]
              /  |  \

第2层(中间节点,只有索引):
         [10|20]   [40|50]   [70|80]
          /  \      /  \      /  \

第3层(叶子节点,存储数据):
    [1|5|10]←→[15|20]←→[30|35|40]←→[45|50]←→[60|65|70]←→[75|80]
       ↑ 叶子节点双向链表连接 ↑

关键特点:
- 内部节点只存储索引(复制的键值)
- 叶子节点存储实际数据
- 叶子节点通过指针连接

范围查询示例: 查找 [20, 50]

1. 从根节点开始,20 < 30,进入左子树
2. 在 [10|20] 节点,20 == 20,进入右子树
3. 到达叶子节点 [15|20]
4. 输出 20
5. 沿 next 指针遍历: [30|35|40] → [45|50]
6. 输出 30, 35, 40, 45, 50
7. 遇到 > 50 的值,停止

结果: [20, 30, 35, 40, 45, 50]

代码实现

B树节点定义

C
#define M 4  // B树阶数

typedef struct BTreeNode {
    int keys[M - 1];            // 关键字数组(最多M-1个)
    struct BTreeNode *children[M];  // 子节点指针数组(最多M个)
    int keyCount;               // 当前关键字数量
    int isLeaf;                 // 是否为叶子节点
} BTreeNode;

// 创建新节点
BTreeNode* createNode(int isLeaf) {
    BTreeNode *node = (BTreeNode*)malloc(sizeof(BTreeNode));
    node->keyCount = 0;
    node->isLeaf = isLeaf;
    for (int i = 0; i < M; i++) {
        node->children[i] = NULL;
    }
    return node;
}

B树查找操作

C
// 在节点中查找关键字位置
int findKey(BTreeNode *node, int key) {
    int idx = 0;
    while (idx < node->keyCount && node->keys[idx] < key) {
        idx++;
    }
    return idx;
}

// B树查找
BTreeNode* search(BTreeNode *root, int key, int *pos) {
    if (root == NULL) return NULL;
    
    // 在当前节点找关键字位置
    int idx = findKey(root, key);
    
    // 如果找到关键字
    if (idx < root->keyCount && root->keys[idx] == key) {
        *pos = idx;
        return root;
    }
    
    // 如果是叶子节点,查找失败
    if (root->isLeaf) return NULL;
    
    // 递归查找子树
    return search(root->children[idx], key, pos);
}

B树分裂节点

C
// 分裂已满的子节点
void splitChild(BTreeNode *parent, int index) {
    BTreeNode *fullChild = parent->children[index];
    BTreeNode *newNode = createNode(fullChild->isLeaf);
    
    int mid = M / 2;  // 中间位置
    newNode->keyCount = mid - 1;
    
    // 复制后半部分关键字到新节点
    for (int i = 0; i < mid - 1; i++) {
        newNode->keys[i] = fullChild->keys[mid + 1 + i];
    }
    
    // 如果不是叶子节点,复制子节点指针
    if (!fullChild->isLeaf) {
        for (int i = 0; i < mid; i++) {
            newNode->children[i] = fullChild->children[mid + 1 + i];
        }
    }
    
    // 更新原节点关键字数量
    fullChild->keyCount = mid - 1;
    
    // 在父节点中插入新子节点指针
    for (int i = parent->keyCount; i > index; i--) {
        parent->children[i + 1] = parent->children[i];
    }
    parent->children[index + 1] = newNode;
    
    // 在父节点中插入中间关键字
    for (int i = parent->keyCount - 1; i >= index; i--) {
        parent->keys[i + 1] = parent->keys[i];
    }
    parent->keys[index] = fullChild->keys[mid];
    parent->keyCount++;
}

B树插入操作

C
// 向非满节点插入关键字
void insertNonFull(BTreeNode *node, int key) {
    int i = node->keyCount - 1;
    
    if (node->isLeaf) {
        // 叶子节点:直接插入
        while (i >= 0 && node->keys[i] > key) {
            node->keys[i + 1] = node->keys[i];
            i--;
        }
        node->keys[i + 1] = key;
        node->keyCount++;
    } else {
        // 非叶子节点:找到子树位置
        while (i >= 0 && node->keys[i] > key) {
            i--;
        }
        i++;
        
        // 如果子节点已满,先分裂
        if (node->children[i]->keyCount == M - 1) {
            splitChild(node, i);
            if (node->keys[i] < key) {
                i++;
            }
        }
        insertNonFull(node->children[i], key);
    }
}

// B树插入
BTreeNode* insert(BTreeNode *root, int key) {
    if (root == NULL) {
        root = createNode(1);
        root->keys[0] = key;
        root->keyCount = 1;
        return root;
    }
    
    // 如果根节点已满,需要分裂
    if (root->keyCount == M - 1) {
        BTreeNode *newRoot = createNode(0);
        newRoot->children[0] = root;
        splitChild(newRoot, 0);
        insertNonFull(newRoot, key);
        return newRoot;
    }
    
    insertNonFull(root, key);
    return root;
}

B树遍历

C
// 中序遍历B树
void traverse(BTreeNode *root) {
    if (root == NULL) return;
    
    int i;
    for (i = 0; i < root->keyCount; i++) {
        if (!root->isLeaf) {
            traverse(root->children[i]);
        }
        printf("%d ", root->keys[i]);
    }
    
    if (!root->isLeaf) {
        traverse(root->children[i]);
    }
}

B+树节点定义

C
typedef struct BPlusTreeNode {
    int keys[M];                        // 关键字数组
    struct BPlusTreeNode *children[M + 1];  // 子节点指针
    struct BPlusTreeNode *next;         // 叶子节点链表指针
    int keyCount;                       // 当前关键字数量
    int isLeaf;                         // 是否为叶子节点
} BPlusTreeNode;

// 创建B+树节点
BPlusTreeNode* createBPlusNode(int isLeaf) {
    BPlusTreeNode *node = (BPlusTreeNode*)malloc(sizeof(BPlusTreeNode));
    node->keyCount = 0;
    node->isLeaf = isLeaf;
    node->next = NULL;
    for (int i = 0; i <= M; i++) {
        node->children[i] = NULL;
    }
    return node;
}

B+树范围查询

C
// B+树范围查询
void rangeQuery(BPlusTreeNode *root, int low, int high) {
    BPlusTreeNode *leaf = root;
    
    // 找到包含 low 的叶子节点
    while (!leaf->isLeaf) {
        int i = 0;
        while (i < leaf->keyCount && leaf->keys[i] <= low) {
            i++;
        }
        leaf = leaf->children[i];
    }
    
    // 沿叶子链表遍历
    while (leaf != NULL) {
        for (int i = 0; i < leaf->keyCount; i++) {
            if (leaf->keys[i] >= low && leaf->keys[i] <= high) {
                printf("%d ", leaf->keys[i]);
            } else if (leaf->keys[i] > high) {
                return;  // 超出范围,停止
            }
        }
        leaf = leaf->next;  // 移到下一个叶子节点
    }
}

复杂度分析

时间复杂度

操作 时间复杂度 磁盘I/O 说明
查找 O(log n) O(log_m n) 从根到叶子的路径
插入 O(log n) O(log_m n) 查找 + 分裂
删除 O(log n) O(log_m n) 查找 + 合并
范围查询(B+) O(k + log n) O(log_m n + k) k为结果数量
Text Only
磁盘I/O分析:

B树高度:
h = log_m(n) 其中 m 是阶数,n 是记录数

示例: 
- n = 100万条记录
- m = 100(节点大小约4KB,每个关键字8字节)
- h = log₁₀₀(1,000,000) ≈ 3

每次查找需要 3 次磁盘 I/O!

对比二叉树:
- h = log₂(1,000,000) ≈ 20
- 需要 20 次磁盘 I/O

B树优势: 减少 85% 的磁盘 I/O!

空间复杂度

  • O(n):存储 n 个记录
  • B+树由于索引复用,空间利用率更高

B树 vs B+树详细对比

Text Only
┌─────────────────────────────────────────────────────────────────────┐
│                    B树 vs B+树 详细对比                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  数据存储位置:                                                      │
│  ┌─────────────────────┬─────────────────────┐                     │
│  │       B树           │       B+树          │                     │
│  ├─────────────────────┼─────────────────────┤                     │
│  │ 所有节点都可能存储   │ 只有叶子节点存储     │                     │
│  │ 数据               │ 数据                │                     │
│  └─────────────────────┴─────────────────────┘                     │
│                                                                     │
│  查找路径长度:                                                      │
│  ┌─────────────────────┬─────────────────────┐                     │
│  │       B树           │       B+树          │                     │
│  ├─────────────────────┼─────────────────────┤                     │
│  │ 可能在非叶子节点终止 │ 必须到达叶子节点     │                     │
│  │ 平均稍短            │ 稍长但稳定          │                     │
│  └─────────────────────┴─────────────────────┘                     │
│                                                                     │
│  范围查询效率:                                                      │
│  ┌─────────────────────┬─────────────────────┐                     │
│  │       B树           │       B+树          │                     │
│  ├─────────────────────┼─────────────────────┤                     │
│  │ 需要中序遍历        │ 叶子链表遍历        │                     │
│  │ 效率较低            │ 效率极高            │                     │
│  │ O(n) 访问内部节点   │ 只访问叶子节点      │                     │
│  └─────────────────────┴─────────────────────┘                     │
│                                                                     │
│  节点空间利用率:                                                    │
│  ┌─────────────────────┬─────────────────────┐                     │
│  │       B树           │       B+树          │                     │
│  ├─────────────────────┼─────────────────────┤                     │
│  │ 内部节点存储数据    │ 内部节点只存索引     │                     │
│  │ 分支因子较小        │ 分支因子较大        │                     │
│  │ 树较高              │ 树更矮              │                     │
│  └─────────────────────┴─────────────────────┘                     │
│                                                                     │
│  典型应用:                                                          │
│  ┌─────────────────────┬─────────────────────┐                     │
│  │       B树           │       B+树          │                     │
│  ├─────────────────────┼─────────────────────┤                     │
│  │ 文件系统元数据      │ 数据库索引          │                     │
│  │ MongoDB             │ MySQL InnoDB        │                     │
│  │                     │ PostgreSQL          │                     │
│  └─────────────────────┴─────────────────────┘                     │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
特性 B树 B+树
数据存储 所有节点 仅叶子节点
查找终止位置 可能在内部节点 必须到叶子节点
范围查询 中序遍历 叶子链表遍历
节点利用率 较低(内部存数据) 较高(内部只存索引)
树高度 较高 较低
空间开销 较大 较小
查找稳定性 不稳定 稳定
适用场景 随机查找、文件系统 范围查询、数据库

数据库索引应用

MySQL InnoDB 索引结构

Text Only
InnoDB B+树索引:

聚簇索引(主键索引):
┌─────────────────────────────────────────────────────────────────────┐
│  - 叶子节点存储完整的行数据                                          │
│  - 数据按主键顺序存储                                               │
│  - 每张表只有一个聚簇索引                                            │
└─────────────────────────────────────────────────────────────────────┘

              [主键: 30|60]
              /      |      \
         [10|行数据] [40|行数据] [70|行数据]
          叶子节点    叶子节点    叶子节点

二级索引(辅助索引):
┌─────────────────────────────────────────────────────────────────────┐
│  - 叶子节点存储索引列值 + 主键值                                     │
│  - 查找需要回表(用主键查聚簇索引)                                   │
│  - 每张表可以有多个二级索引                                          │
└─────────────────────────────────────────────────────────────────────┘

              [索引列: 30|60]
              /      |      \
         [10|主键=5] [40|主键=8] [70|主键=9]
          叶子节点    叶子节点    叶子节点

查询流程:
SELECT * FROM table WHERE index_col = 40;

1. 在二级索引 B+树中查找 40
2. 获取主键值 8
3. 在聚簇索引 B+树中查找主键 8
4. 返回完整行数据(回表操作)

索引优化示例

SQL
-- 创建索引
CREATE INDEX idx_name ON employees(name);
CREATE INDEX idx_dept_salary ON employees(dept_id, salary);  -- 联合索引

-- 利用索引的查询
SELECT * FROM employees WHERE name = 'John';  -- 使用 idx_name

-- 联合索引(最左前缀原则)
SELECT * FROM employees WHERE dept_id = 10;  -- 使用 idx_dept_salary
SELECT * FROM employees WHERE dept_id = 10 AND salary > 5000;  -- 使用 idx_dept_salary
SELECT * FROM employees WHERE salary > 5000;  -- 不使用 idx_dept_salary(违反最左前缀)

-- 范围查询(B+树叶子链表遍历)
SELECT * FROM employees WHERE salary BETWEEN 5000 AND 10000;

联合索引结构

Text Only
联合索引 (dept_id, salary):

B+树结构(按 (dept_id, salary) 排序):

                    [(10, 5000) | (20, 3000)]
                    /                    \
    [(5, 1000) | (8, 2000) | (10, 3000)]   [(15, 4000) | (20, 5000)]
     叶子节点                             叶子节点

排序规则:
先按 dept_id 排序,dept_id 相同再按 salary 排序

最左前缀原则:
- WHERE dept_id = 10: 使用索引 ✓
- WHERE dept_id = 10 AND salary > 5000: 使用索引 ✓
- WHERE salary > 5000: 不使用索引 ✗
- WHERE dept_id > 10 AND salary > 5000: 部分使用(只用于 dept_id)

性能优化

节点大小选择

Text Only
节点大小设计原则:

目标: 一次磁盘 I/O 读取整个节点

计算:
- 磁盘块大小: 通常 4KB 或 8KB
- 每个关键字大小: 假设 8 字节(64位整数)
- 每个子节点指针: 8 字节

节点大小 ≈ (关键字数量 × 关键字大小) + (子节点数量 × 指针大小)
         = (m-1) × 8 + m × 8
         = 16m - 8 字节

如果磁盘块 4KB:
16m - 8 ≤ 4096
m ≤ 256

所以阶数 m 可以设为 200-256,每个节点存储约 200 个关键字!

这意味着:
- 树高度极低: log₂₅₆(1,000,000) ≈ 2.5
- 查找只需 2-3 次磁盘 I/O

缓冲区管理

C
typedef struct {
    BTreeNode *node;       // 节点指针
    int isDirty;           // 是否被修改
    time_t lastAccess;     // 最后访问时间
    int nodeId;            // 节点ID(磁盘位置)
} BufferSlot;

BufferSlot buffer[BUFFER_SIZE];  // 缓冲池

// 读取节点(带缓冲)
BTreeNode* readNode(int nodeId) {
    // 检查缓冲池
    if (inBuffer(nodeId)) {
        updateAccessTime(nodeId);  // 更新访问时间
        return getFromBuffer(nodeId);
    }
    
    // 缓冲池满,淘汰
    if (bufferFull()) {
        evictLRU();  // 淘汰最近最少使用
    }
    
    // 从磁盘读取
    BTreeNode *node = readFromDisk(nodeId);
    addToBuffer(nodeId, node);
    return node;
}

// LRU 淘汰策略
void evictLRU() {
    int oldest = findOldestAccess();
    if (buffer[oldest].isDirty) {
        writeToDisk(buffer[oldest].node);  // 写回脏页
    }
    removeFromBuffer(oldest);
}

应用场景

1. 数据库索引

Text Only
主流数据库 B+树索引:

MySQL InnoDB:
- 聚簇索引: 叶子存储行数据
- 二级索引: 叶子存储主键值

PostgreSQL:
- B-tree 索引: 标准 B+树实现
- 支持唯一索引、部分索引

Oracle:
- B-tree 索引: 类似 B+树
- 位图索引: 用于低基数列

SQL Server:
- 聚集索引: 类似 InnoDB 聚簇索引
- 非聚集索引: 二级索引

2. 文件系统

Text Only
文件系统 B树应用:

NTFS (Windows):
- MFT (Master File Table) 使用 B+树
- 目录索引使用 B+树

ext4 (Linux):
- Htree 目录索引
- 使用类似 B树的结构

HFS+ (macOS):
- 目录文件使用 B树
- 属性文件使用 B树

作用:
- 快速查找文件
- 高效目录遍历
- 支持大目录

3. 键值存储

Text Only
键值存储引擎:

LevelDB / RocksDB:
- 使用 LSM-Tree (Log-Structured Merge Tree)
- 但内部排序使用类似 B树的机制

Berkeley DB:
- Btree 访问方法
- 支持事务、并发

 WiredTiger (MongoDB 存储引擎):
- B+树作为底层索引结构
- 支持压缩、检查点

4. 搜索引擎

Text Only
搜索引擎倒排索引:

倒排索引结构:
词项 → 文档ID列表

使用 B+树存储词项索引:
- 词项按字典序排列
- 叶子节点存储词项 + 倒排列表指针
- 范围查询: 前缀匹配、范围词项

示例:
B+树叶子节点:
┌─────────────────────────────────────────────────────────────────┐
│ "apple" → [doc1, doc5, doc10]                                   │
│ "apply" → [doc2, doc8]                                          │
│ "banana" → [doc3, doc7]                                         │
└─────────────────────────────────────────────────────────────────┘

前缀查询 "app%":
1. 查找 "app" 位置
2. 沿叶子链表遍历
3. 返回所有 "app" 开头的词项

参考资料

  • 《算法导论》第18章 - B树
  • 《数据库系统概念》第11章 - 索引与散列
  • MySQL 技术内幕:InnoDB 存储引擎
  • PostgreSQL B-tree 索引实现文档
  • "The Ubiquitous B-Tree" - Comer (1979)