
在数据库领域,提到索引优化几乎绕不开B+树——MySQL的InnoDB引擎、Oracle等主流数据库均采用B+树作为索引结构;在文件系统中(如NTFS),B+树也承担着文件目录管理的核心角色。为什么B+树能成为存储领域的“宠儿”?它和B树、红黑树的本质区别是什么?本文将从底层原理到实战实现,把B+树的逻辑掰开揉碎,让你既能理解理论,又能落地应用。
B+树是多路平衡查找树的变种,其设计完全围绕“磁盘IO优化”展开(机械硬盘的单次IO耗时约10ms,内存访问仅ns级,减少IO次数是关键)。先明确阶数(m)的定义:
k个关键字和k+1个子节点,其中⌈m/2⌉ ≤ k+1 ≤ m(子节点数范围);i个关键字对应第i+1个子节点的最小值;k个关键字和对应数据(或指针),其中⌈m/2⌉-1 ≤ k ≤ m-1;用一张图直观理解阶数3的B+树结构:
[3,5] <-- 非叶子节点(仅存索引)
├─>[1,2] <-- 叶子节点(存数据+链表连接)
├─>[3,4] <-- 叶子节点
└─>[5,6] <-- 叶子节点
很多人会把B树(B-树)和B+树混为一谈,二者的核心差异直接决定了应用场景:
特性 | B树 | B+树 |
|---|---|---|
非叶子节点存储内容 | 关键字+数据(或指针) | 仅关键字(索引) |
叶子节点位置 | 分散在不同层 | 全部在同一层 |
叶子节点连接方式 | 无 | 双向链表 |
查找效率 | 不稳定(可能在非叶子节点命中) | 稳定(必须到叶子节点) |
范围查询效率 | 低(需遍历多个分支) | 高(叶子节点链表直接遍历) |
磁盘IO效率 | 低(节点存储数据,单次IO加载少) | 高(节点仅存索引,单次IO加载多) |
权威结论:B树更适合内存中的数据查找,而B+树因IO效率和范围查询优势,成为磁盘存储的首选(来源:《数据库系统实现》第10章)。
以阶数3的B+树为例(m=3,非叶子节点最多3个子节点,叶子节点最多2个关键字),逐一拆解操作逻辑。
规则:从根节点开始,通过关键字比较定位子节点,直到叶子节点;若叶子节点存在目标值则返回,否则返回不存在。
示例:查找值4
根节点[3,5] → 比较4与3、5 → 进入第二个子节点[3,4] → 叶子节点中找到4 → 返回结果
流程图:

核心规则:插入到叶子节点,若节点满则分裂(分裂点为⌈m/2⌉),中间关键字提升到父节点。
示例:依次插入1、2、3、4(阶数3)
插入1 → 叶子节点[1]
插入2 → 叶子节点[1,2](未满)
插入3 → 叶子节点[1,2,3](满,分裂为[1,2]和[3],中间关键字2提升到根节点)
[2]
├─>[1,2]
└─>[3]
插入4 → 叶子节点[3,4](未满),最终结构:
[2]
├─>[1,2]
└─>[3,4]
核心规则:从叶子节点删除,若节点关键字数低于下限,则向兄弟节点借关键字或与兄弟节点合并,并更新父节点索引。
示例:删除上述结构中的2(阶数3)
删除叶子节点[1,2]中的2 → 节点变为[1](≥下限⌈3/2⌉-1=1,无需调整)
父节点的关键字2需更新为3(因右子节点最小值为3),最终结构:
[3]
├─>[1]
└─>[3,4]
InnoDB的索引体系完全基于B+树构建,分为聚簇索引和二级索引,二者的差异直接影响SQL性能。
-- 创建带主键和二级索引的表
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
username VARCHAR(50) NOT NULL COMMENT '用户名',
age INT COMMENT '年龄',
INDEX idx_age (age) COMMENT '年龄二级索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入测试数据
INSERT INTO user (username, age) VALUES
('Alice', 20), ('Bob', 25), ('Charlie', 30), ('David', 25);
-- 分析查询:使用二级索引idx_age
EXPLAIN SELECT * FROM user WHERE age=25;
EXPLAIN结果解读:
type=ref:非主键索引的等值查询;key=idx_age:命中二级索引;rows=2:预计扫描2行(叶子节点中age=25的记录);Extra=Using index condition:使用索引条件过滤,需回表(因查询*包含主键外的字段)。优化回表:使用覆盖索引
EXPLAIN SELECT id, age FROM user WHERE age=25;
此时Extra=Using index,无需回表,直接从二级索引获取数据。
以下实现阶数3的B+树(支持插入、查找),严格遵循《阿里巴巴Java开发手册》,使用Spring工具类、Lombok、Guava等组件。
<dependencies>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Spring核心工具类 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.1.2</version>
</dependency>
<!-- Guava集合工具 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
package com.jam.demo.bplus;
import lombok.Getter;
import lombok.Setter;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* B+树节点抽象类
* @author ken
*/
@Getter
@Setter
public abstract class BPlusNode<K extends Comparable<K>, V> {
/** 关键字列表 */
protected List<K> keys;
/** 父节点 */
protected BPlusInternalNode<K, V> parent;
/** 阶数 */
protected final int order;
public BPlusNode(int order) {
this.order = order;
this.keys = Lists.newArrayList();
}
/**
* 判断节点是否已满
*/
public abstract boolean isFull();
/**
* 获取节点的最小关键字数
*/
public int getMinKeyCount() {
return (int) Math.ceil(order / 2.0) - 1;
}
/**
* 获取节点的最大关键字数
*/
public int getMaxKeyCount() {
return order - 1;
}
/**
* 判断节点是否需要合并
*/
public boolean needMerge() {
return CollectionUtils.isEmpty(keys) || keys.size() < getMinKeyCount();
}
}
package com.jam.demo.bplus;
import com.google.common.collect.Lists;
import lombok.Getter;
import lombok.Setter;
import org.springframework.util.ObjectUtils;
import java.util.List;
/**
* B+树非叶子节点(仅存索引)
* @author ken
*/
@Getter
@Setter
public class BPlusInternalNode<K extends Comparable<K>, V> extends BPlusNode<K, V> {
/** 子节点列表 */
private List<BPlusNode<K, V>> children;
public BPlusInternalNode(int order) {
super(order);
this.children = Lists.newArrayList();
}
@Override
public boolean isFull() {
return children.size() > order;
}
/**
* 添加子节点(按关键字升序)
*/
public void addChild(BPlusNode<K, V> child, K key) {
int index = 0;
while (index < keys.size() && key.compareTo(keys.get(index)) > 0) {
index++;
}
keys.add(index, key);
children.add(index + 1, child);
child.setParent(this);
}
/**
* 获取关键字对应的子节点
*/
public BPlusNode<K, V> getChild(K key) {
for (int i = 0; i < keys.size(); i++) {
if (key.compareTo(keys.get(i)) < 0) {
return children.get(i);
}
}
return children.get(children.size() - 1);
}
/**
* 分裂非叶子节点
*/
public BPlusInternalNode<K, V> split() {
int mid = getMaxKeyCount() / 2;
K midKey = keys.get(mid);
// 创建新节点
BPlusInternalNode<K, V> newNode = new BPlusInternalNode<>(order);
newNode.setKeys(Lists.newArrayList(keys.subList(mid + 1, keys.size())));
newNode.setChildren(Lists.newArrayList(children.subList(mid + 1, children.size())));
newNode.setParent(parent);
// 更新当前节点
keys = Lists.newArrayList(keys.subList(0, mid));
children = Lists.newArrayList(children.subList(0, mid + 1));
// 更新子节点的父节点
for (BPlusNode<K, V> child : newNode.getChildren()) {
child.setParent(newNode);
}
// 返回新节点和中间关键字
parent.addChild(newNode, midKey);
return newNode;
}
}
package com.jam.demo.bplus;
import com.google.common.collect.Lists;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* B+树叶子节点(存数据+链表连接)
* @author ken
*/
@Getter
@Setter
public class BPlusLeafNode<K extends Comparable<K>, V> extends BPlusNode<K, V> {
/** 数据列表(与关键字一一对应) */
private List<V> values;
/** 前驱节点 */
private BPlusLeafNode<K, V> prev;
/** 后继节点 */
private BPlusLeafNode<K, V> next;
public BPlusLeafNode(int order) {
super(order);
this.values = Lists.newArrayList();
}
@Override
public boolean isFull() {
return keys.size() >= getMaxKeyCount();
}
/**
* 插入键值对(按关键字升序)
*/
public void insert(K key, V value) {
int index = 0;
while (index < keys.size() && key.compareTo(keys.get(index)) > 0) {
index++;
}
keys.add(index, key);
values.add(index, value);
}
/**
* 查找关键字对应的值
*/
public V find(K key) {
int index = keys.indexOf(key);
return index != -1 ? values.get(index) : null;
}
/**
* 分裂叶子节点
*/
public BPlusLeafNode<K, V> split() {
int mid = getMaxKeyCount() / 2;
K midKey = keys.get(mid);
// 创建新节点
BPlusLeafNode<K, V> newNode = new BPlusLeafNode<>(order);
newNode.setKeys(Lists.newArrayList(keys.subList(mid, keys.size())));
newNode.setValues(Lists.newArrayList(values.subList(mid, values.size())));
newNode.setParent(parent);
newNode.setPrev(this);
newNode.setNext(next);
// 更新当前节点
keys = Lists.newArrayList(keys.subList(0, mid));
values = Lists.newArrayList(values.subList(0, mid));
if (next != null) {
next.setPrev(newNode);
}
next = newNode;
// 父节点添加新节点的索引
parent.addChild(newNode, midKey);
return newNode;
}
}
package com.jam.demo.bplus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
/**
* B+树实现类(阶数3)
* @author ken
*/
@Slf4j
public class BPlusTree<K extends Comparable<K>, V> {
/** 阶数(默认3) */
private final int order = 3;
/** 根节点 */
private BPlusNode<K, V> root;
/** 头叶子节点(用于范围遍历) */
private BPlusLeafNode<K, V> headLeaf;
public BPlusTree() {
// 初始化根节点为叶子节点
this.root = new BPlusLeafNode<>(order);
this.headLeaf = (BPlusLeafNode<K, V>) root;
}
/**
* 插入键值对
*/
public void insert(K key, V value) {
BPlusLeafNode<K, V> leaf = findLeafNode(key);
leaf.insert(key, value);
// 叶子节点满则分裂
if (leaf.isFull()) {
splitLeafNode(leaf);
}
}
/**
* 查找关键字对应的值
*/
public V find(K key) {
BPlusLeafNode<K, V> leaf = findLeafNode(key);
return leaf.find(key);
}
/**
* 定位关键字所在的叶子节点
*/
private BPlusLeafNode<K, V> findLeafNode(K key) {
BPlusNode<K, V> current = root;
while (!(current instanceof BPlusLeafNode)) {
current = ((BPlusInternalNode<K, V>) current).getChild(key);
}
return (BPlusLeafNode<K, V>) current;
}
/**
* 分裂叶子节点
*/
private void splitLeafNode(BPlusLeafNode<K, V> leaf) {
BPlusLeafNode<K, V> newLeaf = leaf.split();
// 若分裂的是根节点,则创建新的根节点
if (ObjectUtils.isEmpty(leaf.getParent())) {
BPlusInternalNode<K, V> newRoot = new BPlusInternalNode<>(order);
newRoot.addChild(leaf, leaf.getKeys().get(0));
newRoot.addChild(newLeaf, newLeaf.getKeys().get(0));
root = newRoot;
leaf.setParent(newRoot);
newLeaf.setParent(newRoot);
}
// 更新头节点
if (headLeaf == leaf) {
headLeaf = leaf;
}
}
}
package com.jam.demo.bplus;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* B+树测试类
* @author ken
*/
public class BPlusTreeTest {
@Test
public void testInsertAndFind() {
BPlusTree<Integer, String> tree = new BPlusTree<>();
// 插入测试数据
tree.insert(1, "Alice");
tree.insert(2, "Bob");
tree.insert(3, "Charlie");
tree.insert(4, "David");
// 验证查找结果
assertEquals("Alice", tree.find(1));
assertEquals("Bob", tree.find(2));
assertEquals("Charlie", tree.find(3));
assertEquals("David", tree.find(4));
assertNull(tree.find(5));
}
}
测试结果:所有断言通过,B+树的插入和查找逻辑正确。
SELECT * FROM user WHERE age BETWEEN 20 AND 30)。适用场景:
B+树的设计是“磁盘友好型”数据结构的典范——它通过“多路平衡”减少IO次数,通过“叶子节点链表”优化范围查询,通过“索引与数据分离”提升节点利用率。理解B+树不仅能帮助你优化数据库索引(如避免回表、设计覆盖索引),更能掌握“存储系统设计需贴合硬件特性”的核心思想。