Skip to content

MySQL B+Tree、B-Tree 与数据库索引家族

很多资料会把这个问题讲得很乱:

MySQL 用 B+Tree,Oracle、PostgreSQL、SQL Server 用 B-Tree。

这句话容易误导初学者。更准确的说法是:MySQL InnoDB 的索引按 B+Tree 理解最合适;Oracle、PostgreSQL、SQL Server 文档里常说 B-tree index,但这里的 B-tree 往往是数据库索引家族名,工程实现通常是适合页式存储、范围扫描和高并发的 B-Tree/B+Tree/B*Tree 变体

这一页把 B+Tree 为什么适合 MySQL、B-Tree 和 B+Tree 的区别、其他数据库为什么叫 B-Tree 讲清楚。

这一页解决这些问题

问题要掌握到什么程度
B+Tree 是什么知道根页、内部页、叶子页、叶子链表
为什么不用 B-Tree知道内部节点存数据会降低扇出,范围扫描不如叶子链表
为什么不用 Hash、红黑树知道数据库索引核心目标是减少页 IO
聚簇索引和二级索引怎么组织知道主键叶子是整行,二级索引叶子是索引列加主键
Oracle/PG/SQL Server 为什么叫 B-Tree知道这是数据库索引家族名,不要死扣教材定义
商业项目怎么用能解释联合索引、范围查询、分页、页分裂和主键设计

数据库索引首先要围绕“页”理解

InnoDB 不是一行一行从磁盘读取数据。它以页为基本单位管理数据,默认页大小通常是 16KB。

mermaid
flowchart TD
    A["表空间 Tablespace"] --> B["段 Segment"]
    B --> C["区 Extent"]
    C --> D["页 Page,默认 16KB"]
    D --> E["行 Record"]

为什么这点重要?

事实对索引设计的影响
磁盘和 Buffer Pool 以页为单位读写索引节点最好能放进页里
一页能放的 key 越多树的分叉越多,高度越低
树越矮查询需要的随机 IO 越少
叶子页有序范围查询和排序可以顺序扫描

所以数据库索引最核心的问题不是“比较次数少不少”,而是:一次查询要读多少页,随机 IO 多不多,能不能顺序扫描

B+Tree 长什么样

mermaid
flowchart TD
    A["根页<br/>key + 子页指针"] --> B["内部页 1<br/>key + 子页指针"]
    A --> C["内部页 2<br/>key + 子页指针"]
    B --> D["叶子页 1<br/>有序 key + 数据"]
    B --> E["叶子页 2<br/>有序 key + 数据"]
    C --> F["叶子页 3<br/>有序 key + 数据"]
    C --> G["叶子页 4<br/>有序 key + 数据"]
    D <--> E
    E <--> F
    F <--> G

B+Tree 的关键特征:

特征解释
根节点和内部节点像目录主要保存 key 和子页指针
真实数据在叶子层InnoDB 聚簇索引叶子保存整行数据
叶子节点按 key 有序支持范围、排序、分页
叶子节点之间有链表找到范围起点后可以继续向后扫
高扇出一个页能放很多目录项,树高通常很低

为什么 B+Tree 查询 IO 少

假设一个索引页能放几百到上千个目录项,树高为 3 或 4 时就能管理大量数据。查询一条记录大致是:

mermaid
flowchart TD
    A["查 id=1001"] --> B["读根页"]
    B --> C["读内部页"]
    C --> D["读叶子页"]
    D --> E["找到记录"]

如果根页和部分内部页常驻 Buffer Pool,实际磁盘 IO 可能更少。

这也是为什么数据库不用普通二叉树。二叉树每个节点最多两个分支,数据量大时树高很高。树高越高,随机 IO 越多。

数据结构分叉数量是否适合磁盘页范围查询适合作为 InnoDB 主力索引吗
二叉搜索树2不适合一般不适合
红黑树2不适合一般不适合
Hash很高部分场景不支持不适合作主力
B-Tree 家族适合支持适合
B+Tree很适合很强InnoDB 主力

B-Tree 和 B+Tree 的核心区别

教材里的 B-Tree 和 B+Tree 可以这样区分:

对比B-TreeB+Tree
数据保存位置内部节点和叶子节点都可能保存数据数据统一在叶子节点
内部节点容量因为保存数据,能放的 key 和指针更少只做目录,能放更多 key 和指针
树高相对可能更高通常更矮
单点查询可能在内部节点提前命中一般走到叶子节点
范围查询需要中序遍历或复杂跳转找到起点后沿叶子链表顺序扫描
适合数据库页式存储可以,但不是最优表达更适合 OLTP 常见范围查询

B-Tree 示意:

mermaid
flowchart TD
    A["根节点<br/>key + data + pointer"] --> B["内部节点<br/>key + data + pointer"]
    A --> C["内部节点<br/>key + data + pointer"]
    B --> D["叶子节点<br/>key + data"]
    B --> E["叶子节点<br/>key + data"]

B+Tree 示意:

mermaid
flowchart TD
    A["根节点<br/>key + pointer"] --> B["内部节点<br/>key + pointer"]
    A --> C["内部节点<br/>key + pointer"]
    B --> D["叶子节点<br/>key + data"]
    B --> E["叶子节点<br/>key + data"]
    D <--> E

为什么 InnoDB 更适合 B+Tree

InnoDB 的典型业务查询很多都不是单点查询,而是范围、排序、分页组合:

sql
select id, order_no, amount
from orders
where user_id = 1001
  and created_at >= '2026-07-01'
  and created_at < '2026-08-01'
order by created_at desc
limit 20;

如果有联合索引:

sql
create index idx_user_time on orders(user_id, created_at);

B+Tree 可以这样执行:

mermaid
flowchart TD
    A["联合索引 user_id, created_at"] --> B["先定位 user_id=1001"]
    B --> C["再定位 created_at 范围起点"]
    C --> D["沿叶子页链表顺序扫描"]
    D --> E["按索引顺序返回前 20 条"]

这比扫描整张表再排序要稳定得多。

InnoDB 使用 B+Tree 的收益:

收益原理
单点查询快从根到叶少量页访问
范围查询快叶子页有序并链表连接
排序友好索引顺序可以避免额外 filesort
分页友好where + order by + limit 可沿索引取前 N
Buffer Pool 友好热点根页、内部页、叶子页可缓存
写读平衡插入、删除、页分裂、页合并都有成熟机制

聚簇索引和二级索引里的 B+Tree

InnoDB 中主键索引是聚簇索引。叶子节点保存整行数据。

mermaid
flowchart TD
    A["主键 B+Tree"] --> B["叶子页"]
    B --> C["整行数据<br/>id、user_id、amount、status、created_at"]

二级索引叶子节点保存的是索引列和主键值。

mermaid
flowchart TD
    A["二级索引 idx_user_time"] --> B["叶子页"]
    B --> C["user_id、created_at、主键 id"]
    C --> D["如果还需要其他列,回到主键 B+Tree"]

这解释了回表:

sql
select amount
from orders
where user_id = 1001
order by created_at desc
limit 20;

如果 amount 不在 idx_user_time 中,二级索引先找到主键 id,再回聚簇索引读取 amount。如果查询字段都在索引中,就形成覆盖索引,不需要回表。

页分裂为什么影响写入

B+Tree 叶子页容量有限。插入新 key 时,如果目标页已满,就要分裂。

mermaid
flowchart TD
    A["叶子页已满"] --> B["插入新 key"]
    B --> C{"是否追加到最右侧"}
    C -- "是" --> D["更可能顺序写入"]
    C -- "否" --> E["拆分页面"]
    E --> F["移动部分记录"]
    F --> G["更新父节点指针"]
    G --> H["产生碎片和额外 IO"]

这就是为什么 InnoDB 常推荐主键短、稳定、趋势递增。

主键类型对 B+Tree 的影响
自增 bigint大多追加到右侧,页分裂较少
雪花 ID趋势递增,分布式友好
随机 UUID插入位置随机,页分裂和碎片更多
很长字符串主键聚簇索引和所有二级索引都变大

不是说 UUID 一定不能用,而是要知道代价。如果必须用 UUID,商业项目常见做法是另设自增或趋势递增主键,把 UUID 做唯一业务键。

为什么不用 Hash 做所有索引

Hash 等值查询很快:

sql
where id = 1001

但商业系统大量查询不是单纯等值:

sql
where created_at >= '2026-07-01'
order by created_at

where name like 'tom%'

where user_id = 1001
order by created_at desc
limit 20

Hash 的问题:

问题后果
无序不能天然支持范围查询
无序不能利用索引顺序排序
不支持最左前缀联合条件能力弱
Hash 冲突还要处理冲突链

所以 Hash 可以作为某些内存结构或特定引擎的补充,但不适合作 InnoDB 主力索引。

Oracle、PostgreSQL、SQL Server 为什么说 B-Tree

很多数据库文档会把默认索引叫 B-tree index。这里不要机械理解成“它们一定使用教材里的 B-Tree,而 MySQL 才用 B+Tree”。

更准确的理解:

mermaid
flowchart TD
    A["数据库工程里的 B-Tree family"] --> B["B-Tree"]
    A --> C["B+Tree"]
    A --> D["B*Tree"]
    A --> E["各数据库工程变体"]
    E --> F["高扇出"]
    E --> G["页式存储"]
    E --> H["有序扫描"]
    E --> I["并发控制和日志恢复"]

不同数据库的叫法和实现:

数据库文档常见叫法工程上怎么理解
MySQL InnoDBB+Tree聚簇索引叶子保存整行,二级索引叶子保存主键
PostgreSQLB-tree index默认通用索引类型,支持等值、范围、排序,是 B-Tree 家族工程实现
OracleB-tree indexroot/branch/leaf block,leaf 保存 key 和 ROWID,适合范围扫描
SQL ServerB-tree structureroot/intermediate/leaf level,聚集索引叶子是数据行,非聚集索引叶子是行定位器
SQLiteB-tree表和索引都基于页式 B-tree 结构

它们共同选择 B-Tree 家族,是因为:

  1. 高扇出降低树高,减少随机 IO。
  2. key 有序,支持范围、排序、分页、min/max
  3. 页式结构适合 Buffer Cache、预读和刷盘。
  4. 插入、删除、分裂、合并有成熟算法。
  5. 能和锁、MVCC、日志恢复结合。

“使用 B-Tree 的数据库有哪些”怎么答

面试可以这样答:

text
严格说,很多关系型数据库文档会把默认索引称为 B-tree index,例如 PostgreSQL、Oracle、SQL Server、SQLite。MySQL InnoDB 通常明确按 B+Tree 理解。这里的 B-tree 不一定是教材里内部节点也保存完整数据的纯 B-Tree,而是数据库工程里的 B-Tree family,包括 B+Tree、B*Tree 和各种页式存储变体。它们共同特点是高扇出、树高低、key 有序、适合范围查询和排序,也方便数据库按 page/block 做缓存、预读、并发控制和崩溃恢复。

不要这样答:

text
Oracle、PostgreSQL、SQL Server 用 B-Tree,所以它们范围查询不如 MySQL。

这个说法不严谨。它们的 B-tree index 同样支持范围扫描,只是页格式、行定位、聚簇组织、MVCC、锁和优化器实现不同。

商业场景:订单表索引怎么设计

订单列表常见查询:

sql
select id, order_no, amount, status, created_at
from orders
where user_id = 1001
  and status in (1, 2)
order by created_at desc
limit 20;

可能索引:

sql
create index idx_user_status_time
on orders(user_id, status, created_at);

为什么这样设计:

条件作用
user_id等值条件,先缩小到某个用户
status继续过滤订单状态
created_at服务排序和范围

但要注意:status in (1,2) 可能让排序利用变复杂,真实是否合适要看数据分布和 EXPLAIN。如果查询更多是“用户最近订单”,也可能设计:

sql
create index idx_user_time
on orders(user_id, created_at);

索引设计不是背公式,而是从真实 SQL、选择性、排序、分页、回表成本一起判断。

可运行 Demo:观察 B+Tree 索引效果

建表:

sql
create table btree_demo_order (
  id bigint primary key auto_increment,
  user_id bigint not null,
  status tinyint not null,
  amount decimal(10, 2) not null,
  created_at datetime not null,
  key idx_user_time (user_id, created_at),
  key idx_status_time (status, created_at)
) engine = InnoDB default charset = utf8mb4;

查询用户最近订单:

sql
explain
select id, amount, created_at
from btree_demo_order
where user_id = 1001
order by created_at desc
limit 20;

观察:

字段期望
key使用 idx_user_time
typerefrange
rows明显小于全表
Extra尽量避免大范围 Using filesort

再观察覆盖索引:

sql
explain
select user_id, created_at
from btree_demo_order
where user_id = 1001
order by created_at desc
limit 20;

如果查询字段都在 idx_user_time 里,Extra 可能出现 Using index,表示覆盖索引,不需要回表。

常见坑

为什么错正确理解
B+Tree 查询快只是因为二分查找核心是减少页 IO,不只是 CPU 比较围绕页、树高、顺序扫描理解
B-Tree 一定比 B+Tree 差工程实现有很多变体比较时要看数据库具体实现
Oracle/PG/SQL Server 用纯 B-Tree文档叫法常是家族名不要死扣教材定义
随机 UUID 主键没问题会导致页分裂、二级索引膨胀尽量短、稳定、趋势递增
覆盖索引万能只能减少回表,不能减少大范围扫描还要控制扫描范围和排序

面试标准回答

text
InnoDB 使用 B+Tree,核心原因是数据库以页为单位读写,索引要尽量减少随机 IO。B+Tree 的内部节点只保存 key 和页指针,一页能放更多目录项,树高更低;真实数据在叶子层,叶子页按 key 有序并通过链表连接,范围查询、排序和分页都更高效。B-Tree 的内部节点也可能保存数据,扇出相对更低,范围扫描通常不如 B+Tree 的叶子链表直接。Hash 等值查询快,但不支持范围、排序和最左前缀;红黑树分叉太少,树高太高,不适合磁盘页式索引。

Oracle、PostgreSQL、SQL Server 文档常说 B-tree index,但这里的 B-tree 更像数据库索引家族名,不一定是教材里的纯 B-Tree。它们的实现也是围绕页、块、高扇出、有序扫描、并发控制和日志恢复设计的工程变体。所以更准确的表达是:现代关系型数据库主流索引都属于 B-Tree family,MySQL InnoDB 按 B+Tree 理解最清楚。

关联知识点

知识点说明
存储结构InnoDB 页、行、表空间、聚簇索引
索引知识点最左前缀、覆盖索引、索引失效
EXPLAIN 执行计划验证索引是否被优化器选择
大表覆盖索引仍然慢覆盖索引解决回表,不解决扫描大
SQL 执行全过程查询如何通过 B+Tree 和 Buffer Pool 读页