B树和 B+树是两种广泛用于数据库和文件系统的平衡树数据结构,主要用于索引和存储大规模数据。它们的核心目标是提高磁盘 I/O 效率,从而加快查询和更新操作。
B树(B-Tree)
B树是一种 自平衡的多路搜索树,它的主要特点是:
- 每个节点可以存储多个键值,并且按升序排序,节点中的键值用于分割子节点的范围。
- 所有叶子节点的深度相同,保证了查询的时间复杂度始终为 (O(\log n))。
- 节点的键值个数范围 :假设 B 树的阶(order)为 (m),则:
- 一个节点最多有 (m) 个子节点(即最多 (m-1) 个键)。
- 一个节点至少有 (\lceil m/2 \rceil) 个子节点(根节点除外)。
- 插入与删除操作会自动调整树的结构,保证树的平衡性。
B树示例(阶数 = 3)
[10 | 20]
/ | \
[5] [15] [25, 30]
- 叶子节点可以存储数据,也可以仅存储索引。
- 搜索操作在 非叶子节点 可能会提前找到数据。
B+树(B+ Tree)
B+树是 B 树的变体,针对数据库和文件系统的查询优化做了改进:
- 所有数据存储在叶子节点,非叶子节点仅存储键值用于索引。
- 叶子节点通过指针连接,形成有序链表,支持范围查询和顺序扫描。
- 节点的键值个数范围:与 B 树类似,但非叶子节点的子节点数量更大,提高了索引效率。
- 查询时,总是查找到叶子节点,查询性能更加稳定。
B+树示例(阶数 = 3)
[10 | 20]
/ | \
[5, 7] [15, 17] [25, 30]
- 叶子节点存储所有数据,并有指针相连。
- 适用于 范围查询(如 SQL 中的
BETWEEN
查询),因为叶子节点是链表结构,可以顺序遍历。
B树 vs B+树 区别总结
特点 | B 树 | B+ 树 |
---|---|---|
非叶子节点是否存数据 | 可能存数据 | 仅存索引,不存数据 |
搜索是否可能提前结束 | 可能在中间节点结束 | 必须到叶子节点才返回数据 |
范围查询 | 需要遍历多个节点 | 叶子节点链表可顺序扫描,效率高 |
叶子节点结构 | 可能不形成链表 | 叶子节点按顺序连接 |
B+树的优势
- 查询更稳定:所有查询都到达叶子节点,避免 B 树在非叶子节点提前结束查询导致的不均衡访问。
- 范围查询更高效 :B+树的叶子节点形成 有序链表,可以高效地遍历数据。
- 磁盘 I/O 友好:非叶子节点只存索引,使得每个节点能存储更多索引,减少磁盘读取次数。
B树的优势
- 适用于小规模索引,因为它在非叶子节点上可以存储数据,减少叶子节点的存储需求。
应用场景
- B树 适用于键值存储系统,比如小规模索引、部分 NoSQL 存储引擎。
- B+树 广泛用于数据库(如 MySQL InnoDB)、文件系统(如 NTFS、Ext4)等,需要高效范围查询的场景。
索引与 B+ 树的关系
-
主键索引(Clustered Index)
- 表的数据存储在 B+ 树的叶子节点,叶子节点直接存储整行数据。
- 在 MySQL InnoDB 存储引擎中,主键索引是 聚簇索引(Clustered Index),它决定了数据在磁盘上的物理存储顺序。
- 由于数据直接存储在叶子节点中,一个表只能有一个主键索引。
-
二级索引(Secondary Index)
- 也叫 非聚簇索引(Non-Clustered Index),它会额外创建一个 B+ 树,用于加速查询。
- 叶子节点存储的是 主键的值,而不是整行数据。
- 通过 二级索引 → 主键索引 的方式获取完整数据(回表查询)。
示例
假设有一个 users
表:
sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
email VARCHAR(100),
INDEX idx_name (name),
INDEX idx_age (age)
);
B+ 树索引的情况
-
主键索引(id):
- 以
id
作为键,叶子节点存储完整行数据。
- 以
-
二级索引 idx_name(name):
- 以
name
作为键,叶子节点存储id
(主键),查询完整数据时需要回表查询。
- 以
-
二级索引 idx_age(age):
- 以
age
作为键,叶子节点存储id
(主键),查询完整数据时需要回表查询。
- 以
多个索引对应多个 B+ 树
索引类型 | B+ 树键值 | 叶子节点存储 |
---|---|---|
主键索引 (id ) |
id |
完整数据行 |
二级索引 (name ) |
name |
id (需要回表查询) |
二级索引 (age ) |
age |
id (需要回表查询) |
示例:
- 查询
id = 3
,直接在主键 B+ 树中找到完整数据,速度快。 - 查询
name = 'Alice'
:- 在
idx_name
这棵 B+ 树中找到name = 'Alice'
对应的id
。 - 再去主键索引 B+ 树中查找
id
对应的完整行数据(回表查询)。
- 在
- 查询
age > 25
(范围查询):- 在
idx_age
这棵 B+ 树中遍历符合条件的id
。 - 通过
id
在主键 B+ 树中获取完整行数据(回表查询)。
- 在
总结
✅ 一个表的每个索引都会对应一棵 B+ 树 ,但主键索引和二级索引的存储方式不同。
✅ 二级索引的叶子节点存的是主键 id ,查询完整数据需要 回表 。
✅ 多个索引不会影响彼此的 B+ 树结构 ,但过多索引可能会 影响写入性能,因为插入、更新、删除时,每棵 B+ 树都要同步更新。
叶子节点存储结构
1. 主键索引(聚簇索引,Clustered Index)
- 键(Key) :主键
id
- 叶子节点存储:完整数据行
- 特点 :
- 这是 唯一存储完整数据的 B+ 树 ,因为 InnoDB 采用 聚簇索引。
- 查询主键时效率最高,无需额外的索引访问。
2. 二级索引(辅助索引,Secondary Index)
- 键(Key) :索引列(如
age
) - 叶子节点存储 :主键
id
(不存储完整行数据) - 特点 :
- 叶子节点 不会存储完整行 ,只存 主键值,可以减少索引体积。
- 查询完整数据时,需要通过 主键回表 到 聚簇索引 读取数据。
示例
假设有一个 users
表:
sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
email VARCHAR(100),
INDEX idx_age (age)
);
表中数据:
id | name | age | email
-------------------------
1 | Alice | 25 | [email protected]
2 | Bob | 30 | [email protected]
3 | Carol | 22 | [email protected]
4 | David | 35 | [email protected]
B+ 树结构
主键索引(id 的 B+ 树)
[ 2 ]
/ \
[1] [3 | 4]
叶子节点存储完整行数据:
[1 → {Alice, 25, [email protected]}]
[2 → {Bob, 30, [email protected]}]
[3 → {Carol, 22, [email protected]}]
[4 → {David, 35, [email protected]}]
二级索引(age 的 B+ 树)
[ 25 | 30 ]
/ | \
[ 22 ] [ 25 ] [ 30 | 35 ]
叶子节点存储的是 主键 id
:
[22 → 3] → [25 → 1] → [30 → 2] → [35 → 4]
✅ 查询 age=25
,找到 id=1
,然后去主键索引 B+ 树获取完整数据 (回表查询)。
查询时的"回表查询"
查询 age = 25
sql
SELECT * FROM users WHERE age = 25;
查询步骤:
- 在
age
的二级索引 B+ 树 中找到age = 25
对应的id=1
。 - 回表查询 :用
id=1
到主键索引(聚簇索引)中查找完整数据{Alice, 25, [email protected]}
。
✅ 缺点:如果索引列不包含所有查询需要的字段,就必须回表,导致额外的磁盘 I/O。
如何优化索引,减少回表?
1. 覆盖索引(Covering Index)
定义 :如果索引已经包含查询所需的所有列,则可以直接在索引 B+ 树中获取数据,避免回表。
✅ 优化方式
sql
CREATE INDEX idx_age_email ON users(age, email);
这样,idx_age_email
叶子节点存储:
[22 → (3, [email protected])] → [25 → (1, [email protected])] → [30 → (2, [email protected])] → [35 → (4, [email protected])]
🔹 查询 age=25
且 email
时,不需要回表,因为索引叶子节点已经包含 email
。
2. 使用 id
作为主键,避免二级索引存储过大字段
✅ 选择短的 id
作为主键 ,因为 二级索引的叶子节点存储主键 ,如果主键是长字符串(如 UUID
),会导致 二级索引体积变大,影响查询性能。
总结
✅ 主键索引(Clustered Index) 叶子节点存储 完整数据 。
✅ 二级索引(Secondary Index) 叶子节点只存 主键 id
,查完整数据需要 回表查询 。
✅ 回表查询可能影响性能,优化方式包括覆盖索引、选择短主键等。
✅ B+ 树的叶子节点有序且双向链接,使得范围查询(BETWEEN
、>
等)更高效。