B+树索引
为什么 MySQL 会使用 B+树作为索引结构?
1)高效的查找性能
B+树是一种自平衡树,每个叶子节点到根节点的路径长度相同,B+ 树在插入和删除节点时会进行分裂和并操作,以保证树的平衡,但它又会有一定的冗余节点。
查找、插入、删除等操作的时间复杂度为 O(log n),能保证在大数据量情况下也能有较快的响应时间。
2)树的高度增长不会过快,使得查询磁盘的 I/O 次数减少
B+树不像红黑树,数据越多树的高度增长就越快。它是多叉树,非叶子节点仅保存主键或索引值和页面指针,使得每一页能容纳更多的记录,因此内存中就能存放更多索引,容易命中缓存,使得查询磁盘的 I/O 次数减少。
3)范围查询能力强
B+树特别适合范围查询。因为叶子节点通过链表链接,从根节点定位到叶子查找到范围的起点之后,只需要顺序扫描链表即可遍历后续的数据,非常高效。
详细描述 MySQL 的 B+树中查询数据的全过程。
1)数据从根节点找起,根据比较数据键值与节点存储的索引键值,确定数据落在哪个区间,从而确定分支,从上到下最终定位到叶子节点
2)叶子节点存储实际的数据行记录,但是一页有 16KB 大小,存储的数据行不止一条
3)叶子节点中数据行以组的形式划分,利用页目录结构,通过二分查找可以定位到对应的组
4)定位组后,利用链表遍历就可以找到对应的数据行
MySQL 三层 B+ 树能存多少数据?
参数:
- 每个节点页大小为 16KB(即16384字节)
- 假设每个数据记录的主键和数据大小为 1 KB
- 每个内部节点(非叶子节点)存储的是指向节点的指针和索引键
三层 B+ 树的存储计算:
- 叶子节点:第三层为叶子节点,每个叶子节点页可以存储 16条数据记录
- 第二层(中间层):假设每个指针 6字节 和索引键的大小为8字节,那么每个中间节点页可以执行 1170 个叶子节点。(16*1024/(6+8))
- 第一层(根节点):根节点可以指向 1170 个中间节点。
因此,三层 B+ 树大致能存储的数据总量为:1170 * 1170 * 16 = 21902400,一颗三层的 B+ 树在 MySQL 中可以存储大约 2000万条记录。
B+树 Java 实现:
/**
* B+树节点抽象类
*/
abstract class BPlusNode {
List<Integer> keys = new ArrayList<>(); // 存储键值
// 抽象方法定义
abstract Object get(int key); // 查询方法
abstract void insert(int key, Object value); // 插入方法
abstract boolean isOverflow(int order); // 判断是否溢出
abstract SplitResult split(); // 分裂节点
abstract int getFirstKey(); // 获取第一个键(用于分裂后提升)
abstract boolean isLeaf(); // 判断是否为叶子节点
}
/**
* 内部节点类(非叶子节点)
*/
class InternalNode extends BPlusNode {
List<BPlusNode> children = new ArrayList<>(); // 子节点指针
@Override
Object get(int key) {
// 根据键值找到合适的子节点
int i = 0;
while (i < keys.size() && key >= keys.get(i)) i++;
return children.get(i).get(key); // 递归查询
}
@Override
void insert(int key, Object value) {
// 路由到合适的子节点
int i = 0;
while (i < keys.size() && key >= keys.get(i)) i++;
children.get(i).insert(key, value);
}
@Override
boolean isOverflow(int order) {
// 内部节点最多允许order个子节点
return children.size() > order;
}
@Override
SplitResult split() {
// 分裂内部节点(提升中间键)
int mid = keys.size() / 2;
int promoteKey = keys.get(mid); // 中间键提升到父节点
// 创建右兄弟节点
InternalNode right = new InternalNode();
right.keys.addAll(keys.subList(mid + 1, keys.size()));
right.children.addAll(children.subList(mid + 1, children.size()));
// 清理已移动的数据
keys.subList(mid, keys.size()).clear();
children.subList(mid + 1, children.size()).clear();
return new SplitResult(promoteKey, this, right);
}
@Override
int getFirstKey() {
// 内部节点的第一个键是其最左子节点的第一个键
return children.get(0).getFirstKey();
}
@Override
boolean isLeaf() {
return false;
}
}
/**
* 叶子节点类
*/
class LeafNode extends BPlusNode {
List<Object> values = new ArrayList<>(); // 存储实际数据
LeafNode next; // 指向下一个叶子节点(形成链表)
@Override
Object get(int key) {
// 在叶子节点中直接查找数据
int idx = keys.indexOf(key);
return idx != -1 ? values.get(idx) : null;
}
@Override
void insert(int key, Object value) {
// 保持有序插入
int i = 0;
while (i < keys.size() && keys.get(i) < key) i++;
keys.add(i, key);
values.add(i, value);
}
@Override
boolean isOverflow(int order) {
// 叶子节点最多允许order-1个键
return keys.size() > order - 1;
}
@Override
SplitResult split() {
// 分裂叶子节点(右兄弟的第一个键需要提升)
LeafNode right = new LeafNode();
int mid = keys.size() / 2;
// 移动后半部分数据到右兄弟
right.keys.addAll(keys.subList(mid, keys.size()));
right.values.addAll(values.subList(mid, values.size()));
keys.subList(mid, keys.size()).clear();
values.subList(mid, values.size()).clear();
// 维护叶子链表
right.next = next;
next = right;
// 返回分裂结果(右兄弟的第一个键需要提升)
return new SplitResult(right.keys.get(0), this, right);
}
@Override
int getFirstKey() {
return keys.get(0);
}
@Override
boolean isLeaf() {
return true;
}
}
/**
* 分裂结果包装类
*/
class SplitResult {
int key; // 需要提升到父节点的键
BPlusNode left; // 分裂后的左节点
BPlusNode right; // 分裂后的右节点
SplitResult(int key, BPlusNode left, BPlusNode right) {
this.key = key;
this.left = left;
this.right = right;
}
}
/**
* B+树主类
*/
public class BPlusTree {
private BPlusNode root; // 根节点
private LeafNode head; // 叶子链表头节点(用于范围查询)
private final int order; // 树阶数
public BPlusTree(int order) {
this.order = order;
root = new LeafNode();
head = (LeafNode) root;
}
/**
* 插入键值对
*/
public void insert(int key, Object value) {
// 1. 查找插入路径
List<BPlusNode> path = new ArrayList<>();
BPlusNode node = root;
while (!node.isLeaf()) {
InternalNode internal = (InternalNode) node;
path.add(node);
int i = 0;
while (i < internal.keys.size() && key >= internal.keys.get(i)) i++;
node = internal.children.get(i);
}
// 2. 插入叶子节点
LeafNode leaf = (LeafNode) node;
leaf.insert(key, value);
// 3. 处理节点分裂(从下往上)
if (leaf.isOverflow(order)) {
SplitResult result = leaf.split();
if (path.isEmpty()) {
// 根节点分裂需要创建新根
InternalNode newRoot = new InternalNode();
newRoot.keys.add(result.key);
newRoot.children.add(result.left);
newRoot.children.add(result.right);
root = newRoot;
} else {
insertIntoParent(path, result);
}
}
}
/**
* 向上传播分裂结果
*/
private void insertIntoParent(List<BPlusNode> path, SplitResult result) {
BPlusNode child = result.right;
int promoteKey = result.key;
for (int i = path.size() - 1; i >= 0; i--) {
InternalNode parent = (InternalNode) path.get(i);
int idx = parent.children.indexOf(result.left);
// 插入提升的键和新的子节点
parent.keys.add(idx, promoteKey);
parent.children.add(idx + 1, child);
if (!parent.isOverflow(order)) return;
// 继续向上分裂
SplitResult splitResult = parent.split();
promoteKey = splitResult.key;
child = splitResult.right;
if (i == 0) {
// 分裂到根节点
InternalNode newRoot = new InternalNode();
newRoot.keys.add(promoteKey);
newRoot.children.add(splitResult.left);
newRoot.children.add(child);
root = newRoot;
return;
} else {
result.left = splitResult.left;
}
}
}
/**
* 精确查询
*/
public Object get(int key) {
return root.get(key);
}
/**
* 范围查询(左闭右闭)
*/
public List<Object> rangeQuery(int start, int end) {
List<Object> result = new ArrayList<>();
LeafNode node = head;
while (node != null) {
for (int i = 0; i < node.keys.size(); i++) {
int key = node.keys.get(i);
if (key > end) return result; // 超过范围直接返回
if (key >= start) result.add(node.values.get(i));
}
node = node.next; // 通过链表访问下一个叶子节点
}
return result;
}
public static void main(String[] args) {
// 创建5阶B+树
BPlusTree tree = new BPlusTree(5);
// 批量插入测试数据
int[] keys = {5, 8, 10, 15, 20, 25, 30, 35, 40, 45, 50};
String[] values = {"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"};
for (int i = 0; i < keys.length; i++) {
tree.insert(keys[i], values[i]);
}
// 查询测试
System.out.println("精确查询:");
System.out.println("key=25 -> " + tree.get(25)); // 输出 F
System.out.println("\n范围查询[15,35]:");
System.out.println(tree.rangeQuery(15, 35)); // 输出 [D, E, F, G, H]
}
}