引言
数据库索引是提升查询效率的核心组件,而B树因其高效的磁盘I/O特性和平衡的树结构,成为数据库索引的基石。本文将深入讲解B树的原理及其在数据库索引中的应用,并通过C语言实现一个支持插入和查询的B树索引系统,帮助读者理解底层机制。
一、数据库索引基础
1.1 为什么需要索引?
-
加速查询:避免全表扫描,快速定位数据。
-
支持范围查询:B树索引天然支持有序遍历。
-
唯一性约束:通过唯一索引保证数据唯一性。
1.2 哈希表 vs B树
特性 | 哈希表 | B树 |
---|---|---|
查询速度 | O(1) | O(log n) |
范围查询 | 不支持 | 支持 |
磁盘友好性 | 差(随机访问) | 优(顺序块访问) |
适用场景 | 精确查找(如缓存) | 数据库索引、文件系统 |
二、B树核心原理
2.1 B树的定义
B树是一种平衡多路搜索树,具有以下特性:
-
每个节点最多包含
M-1
个键和M
个子节点(M
为阶数)。 -
根节点至少包含1个键(除非为空树)。
-
非根节点至少包含
ceil(M/2)-1
个键。 -
所有叶子节点位于同一层。
2.2 B树的操作
2.2.1 查找
从根节点开始,逐层比较键值,直到找到目标或到达叶子节点。
2.2.2 插入
-
查找插入位置。
-
若节点未满,直接插入。
-
若节点已满,分裂节点并向上递归处理。
2.2.3 删除
需处理节点合并和键重分配,较为复杂(本文暂不实现)。
三、C语言实现B树索引
3.1 定义B树结构
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define M 3 // B树的阶数(每个节点最多M-1个键)
typedef struct BTreeNode {
int keys[M - 1]; // 键数组
struct BTreeNode* children[M]; // 子节点指针数组
int num_keys; // 当前键的数量
bool is_leaf; // 是否为叶子节点
} BTreeNode;
// 创建新节点
BTreeNode* create_node(bool is_leaf) {
BTreeNode* node = (BTreeNode*)malloc(sizeof(BTreeNode));
node->num_keys = 0;
node->is_leaf = is_leaf;
for (int i = 0; i < M; i++) {
node->children[i] = NULL;
}
return node;
}
3.2 插入操作实现
// 分裂子节点
void split_child(BTreeNode* parent, int index) {
BTreeNode* child = parent->children[index];
BTreeNode* new_node = create_node(child->is_leaf);
new_node->num_keys = (M - 1) / 2;
// 移动右半部分键到新节点
for (int i = 0; i < new_node->num_keys; i++) {
new_node->keys[i] = child->keys[i + (M / 2)];
}
// 移动子节点指针(若非叶子节点)
if (!child->is_leaf) {
for (int i = 0; i <= new_node->num_keys; i++) {
new_node->children[i] = child->children[i + (M / 2)];
}
}
child->num_keys = (M - 1) / 2 - 1;
// 调整父节点
for (int i = parent->num_keys; i > index; i--) {
parent->children[i + 1] = parent->children[i];
}
parent->children[index + 1] = new_node;
parent->keys[index] = child->keys[(M - 1) / 2 - 1];
parent->num_keys++;
}
// 插入键到未满节点
void insert_non_full(BTreeNode* node, int key) {
int i = node->num_keys - 1;
if (node->is_leaf) {
while (i >= 0 && key < node->keys[i]) {
node->keys[i + 1] = node->keys[i];
i--;
}
node->keys[i + 1] = key;
node->num_keys++;
} else {
while (i >= 0 && key < node->keys[i]) {
i--;
}
i++;
if (node->children[i]->num_keys == M - 1) {
split_child(node, i);
if (key > node->keys[i]) {
i++;
}
}
insert_non_full(node->children[i], key);
}
}
// 插入键(入口函数)
void insert(BTreeNode** root, int key) {
BTreeNode* root_node = *root;
if (root_node->num_keys == M - 1) {
BTreeNode* new_root = create_node(false);
new_root->children[0] = root_node;
*root = new_root;
split_child(new_root, 0);
insert_non_full(new_root, key);
} else {
insert_non_full(root_node, key);
}
}
3.3 查找操作实现
// 查找键
bool search(BTreeNode* node, int key) {
int i = 0;
while (i < node->num_keys && key > node->keys[i]) {
i++;
}
if (i < node->num_keys && key == node->keys[i]) {
return true;
} else if (node->is_leaf) {
return false;
} else {
return search(node->children[i], key);
}
}
3.4 测试代码
int main() {
BTreeNode* root = create_node(true); // 初始为叶子节点
// 插入测试数据
insert(&root, 10);
insert(&root, 20);
insert(&root, 5);
insert(&root, 15);
insert(&root, 30);
// 查找测试
printf("Search 15: %s\n", search(root, 15) ? "Found" : "Not Found"); // Found
printf("Search 25: %s\n", search(root, 25) ? "Found" : "Not Found"); // Not Found
return 0;
}
四、B树在数据库中的应用
4.1 磁盘页对齐
-
B树节点大小通常与磁盘页(如4KB)对齐,减少I/O次数。
-
每个节点存储多个键,充分利用块设备特性。
4.2 范围查询优化
-
中序遍历B树可直接获取有序数据。
-
支持
WHERE age BETWEEN 20 AND 30
类查询。
五、总结
-
B树优势:平衡性、高扇出、磁盘友好,适合数据库索引。
-
核心操作:插入时分裂节点保证平衡,查找效率O(log n)。
-
扩展方向:实现删除操作、支持持久化存储、优化为B+树。