B树和B+树,聚簇索引和非聚簇索引
B树与B+树的区别
B树和B+树都是多路平衡查找树,是数据库索引的常用数据结构,核心区别体现在结构设计和查询特性上:
对比维度 | B树 | B+树 |
---|---|---|
节点存储内容 | 非叶子节点既存储键值(索引值),也存储对应的数据记录。 | 非叶子节点仅存储键值(作为索引指引),叶子节点存储键值和完整数据记录。 |
叶子节点连接 | 叶子节点之间无关联,是独立的。 | 叶子节点之间通过双向链表连接(形成有序链表)。 |
查询效率 | 查找数据时,可能在非叶子节点命中并返回,查询速度不稳定(取决于数据所在层级)。 | 所有查询最终都需到叶子节点获取数据,查询路径长度固定,效率更稳定。 |
范围查询支持 | 需通过回溯父节点遍历范围,效率低。 | 可直接通过叶子节点的链表顺序遍历,无需回溯,范围查询(如BETWEEN )效率极高。 |
空间利用率 | 非叶子节点存储数据,导致单个节点能容纳的键值数量少,树的高度更高。 | 非叶子节点仅存键值,单个节点可容纳更多键值,树的高度更低(IO次数更少)。 |
总结:B+树通过“非叶子节点仅存索引、叶子节点链表连接”的设计,在范围查询、查询稳定性和空间利用率上优于B树,因此成为主流数据库(如MySQL、PostgreSQL)的默认索引结构。
聚簇索引与非聚簇索引的区别
聚簇索引(Clustered Index)和非聚簇索引(Non-Clustered Index)的核心区别在于索引与数据的物理存储关系:
对比维度 | 聚簇索引 | 非聚簇索引 |
---|---|---|
数据存储方式 | 索引的叶子节点直接存储完整的数据记录,数据的物理存储顺序与索引的逻辑顺序一致。 | 索引的叶子节点仅存储指向数据记录的物理地址(如行指针),数据与索引分开存储,物理顺序与索引顺序无关。 |
数量限制 | 一个表只能有1个聚簇索引(因为数据物理顺序只能有一种)。 | 一个表可以有多个非聚簇索引(多个索引指向同一批数据)。 |
默认关联 | 通常与主键绑定(数据库会默认对主键创建聚簇索引)。 | 可基于任意列创建,需显式定义。 |
查询性能 | 按索引列查询时,直接从叶子节点获取数据,无需额外查找,效率极高。 | 按索引列查询时,需先通过索引找到数据地址,再去原表读取数据(称为“回表”),效率低于聚簇索引(除非是覆盖索引)。 |
更新代价 | 若更新索引列(如主键),会导致数据物理位置移动,代价较高。 | 更新索引列时,仅需更新索引中的指针,代价较低。 |
举例理解:
- 聚簇索引类似“电话簿按姓名排序”:姓名的顺序(索引顺序)就是电话记录的物理排列顺序,按姓名查电话时直接翻到对应位置即可。
- 非聚簇索引类似“电话簿的按电话号码排序索引”:索引中记录“电话号码→姓名位置”,查号时先查索引找到姓名位置,再翻到对应页找详细信息。
总结:聚簇索引适合高频按主键查询的场景,非聚簇索引适合多条件查询场景,两者需配合使用以平衡查询效率和更新代价。
下面通过一个实际的业务场景(电商订单表),举例说明聚簇索引和非聚簇索引的创建与应用。
场景说明
假设有一个电商平台的订单表 order_info
,用于存储用户的订单数据,表结构如下:
字段名 | 类型 | 说明 |
---|---|---|
order_id | BIGINT | 订单ID(唯一标识) |
user_id | BIGINT | 下单用户ID |
order_time | DATETIME | 下单时间 |
total_amount | DECIMAL(10,2) | 订单总金额 |
status | TINYINT | 订单状态(0-待支付,1-已支付等) |
1. 聚簇索引(Clustered Index)
特点:表中数据的物理存储顺序与索引顺序一致,一个表只能有1个聚簇索引,通常与主键绑定。
实际创建(隐式)
在MySQL中,当我们定义主键时,数据库会自动为该主键创建聚簇索引。
创建表的SQL语句:
CREATE TABLE order_info (order_id BIGINT NOT NULL COMMENT '订单ID',user_id BIGINT NOT NULL COMMENT '用户ID',order_time DATETIME NOT NULL COMMENT '下单时间',total_amount DECIMAL(10,2) NOT NULL COMMENT '总金额',status TINYINT NOT NULL COMMENT '订单状态',-- 定义主键,自动创建聚簇索引PRIMARY KEY (order_id)
) ENGINE=InnoDB COMMENT '订单表';
聚簇索引的作用
- 聚簇索引的叶子节点直接存储完整的订单记录(包含所有字段),且数据物理存储顺序与
order_id
的顺序一致。 - 当需要通过
order_id
查询订单详情时(如“查询订单ID=10086的订单信息”),聚簇索引能直接定位到数据,无需额外查找:SELECT * FROM order_info WHERE order_id = 10086; -- 聚簇索引加速,效率极高
2. 非聚簇索引(Non-Clustered Index)
特点:索引与数据分开存储,叶子节点存储“索引值+指向数据物理位置的指针”,一个表可以有多个非聚簇索引。
实际创建(显式)
根据业务查询需求,我们通常会为高频查询的非主键字段创建非聚簇索引。
例1:为 user_id
创建非聚簇索引
业务场景:用户经常需要查询“自己的所有订单”(按 user_id
过滤)。
创建索引:
-- 为user_id创建非聚簇索引
CREATE INDEX idx_order_user_id ON order_info(user_id);
该索引的结构:
- 非叶子节点:按
user_id
有序存储索引值; - 叶子节点:存储
user_id
的值 + 对应订单记录的物理地址(指向聚簇索引中order_id
对应的位置)。
查询时的流程:
SELECT * FROM order_info WHERE user_id = 1001; -- 使用idx_order_user_id索引
- 先通过非聚簇索引
idx_order_user_id
找到user_id=1001
对应的所有物理地址; - 再根据物理地址到聚簇索引中查询完整的订单记录(称为“回表”)。
例2:为 order_time
创建联合非聚簇索引
业务场景:运营需要查询“某时间段内的订单”(如“2023-10-01至2023-10-07的订单”),且常需按时间排序。
创建索引:
-- 联合索引(order_time, status):优化范围查询+状态过滤
CREATE INDEX idx_order_time_status ON order_info(order_time, status);
该索引的作用:
- 索引按
order_time
排序(先),order_time
相同再按status
排序; - 支持高效的范围查询(如
order_time BETWEEN ...
)和多条件过滤(如order_time >= ... AND status = 1
); - 若查询的字段仅包含索引列(如
SELECT order_id, order_time, status FROM ...
),则无需回表(覆盖索引),效率更高。
总结
索引类型 | 在订单表中的实例 | 核心作用 |
---|---|---|
聚簇索引 | 主键 order_id 自动创建 | 加速按订单ID的精确查询,数据物理顺序与索引一致 |
非聚簇索引 | idx_order_user_id (user_id) | 加速按用户ID的查询(需回表) |
非聚簇索引 | idx_order_time_status (联合) | 加速按时间范围+状态的查询,支持覆盖索引 |
通过合理设计聚簇索引(绑定主键)和非聚簇索引(针对高频查询字段),可大幅提升订单表的查询效率。
在物理存储层面,聚簇索引和数据是存储在同一个位置的,这是聚簇索引最核心的特性;而非聚簇索引与数据则是分开存储的。
具体解释:
-
聚簇索引:其B+树结构的叶子节点直接包含完整的数据记录。也就是说,索引的物理存储结构中,叶子节点本身就是数据行的物理存储位置。
例如,InnoDB引擎中,主键聚簇索引的叶子节点会直接存储整行数据(所有字段的值),数据的物理排列顺序与索引的逻辑顺序(按主键排序)完全一致。此时,“索引的叶子节点”和“数据行”在物理上是同一块存储空间,无法分割。 -
非聚簇索引:其B+树结构的叶子节点仅存储“索引值+指向数据行物理位置的指针”(如数据行在磁盘上的地址),而数据行本身存储在其他独立的物理位置。
例如,MyISAM引擎的所有索引都是非聚簇索引:索引文件(.MYI)和数据文件(.MYD)是分开的,索引叶子节点的指针指向数据文件中对应行的位置,两者物理上完全分离。
举例类比:
- 聚簇索引类似“一本按章节排序的书”:目录(索引)的最后一级(叶子节点)直接是章节内容(数据),目录和内容在同一本书里,物理上不可分割。
- 非聚簇索引类似“图书馆的索引卡片”:卡片(索引)上记录了“书名+书架位置”,而书籍(数据)实际放在卡片指示的书架上,卡片和书籍是分开存储的。
这种存储方式导致聚簇索引查询时无需“回表”(直接从索引叶子节点获取数据),而非聚簇索引通常需要根据指针到数据存储位置读取完整记录,这也是聚簇索引查询效率更高的底层原因。
聚簇索引的非叶子节点与叶子节点(包含数据)存储在同一个物理文件中,共同构成完整的聚簇索引结构(通常是B+树)。
具体来说,聚簇索引的B+树是一个整体,所有节点(包括非叶子节点和叶子节点)都存储在数据库为表分配的数据文件中(例如InnoDB引擎的.ibd
文件,既包含数据也包含聚簇索引)。
- 非叶子节点:仅存储“索引键值”和“指向子节点的指针”,用于导航查询路径(类似B+树的“目录层级”)。例如,订单表聚簇索引(以
order_id
为主键)的非叶子节点,会存储若干order_id
的范围值和指向子节点的地址,帮助快速定位到包含目标order_id
的叶子节点。 - 叶子节点:存储完整的数据行(所有字段值),且物理顺序与索引键值(如
order_id
)的逻辑顺序一致。
两者在物理上属于同一文件的不同数据块,非叶子节点作为“上层导航结构”,与叶子节点(数据)共同组成聚簇索引的完整B+树,不存在“非叶子节点单独存储”的情况。
这种设计保证了聚簇索引的查询效率:从非叶子节点导航到叶子节点的过程,始终在同一个物理文件中完成,无需跨文件查找,最终直接从叶子节点获取数据。
在MySQL中,聚簇索引和非聚簇索引的存储位置是否相同,取决于所使用的存储引擎(主要是InnoDB和MyISAM,两者是最常用的引擎)。这两种引擎对索引的存储设计有本质区别:
1. InnoDB引擎(MySQL默认引擎)
InnoDB是聚簇索引设计的典型代表,其聚簇索引和非聚簇索引(二级索引)的存储位置相同,都位于同一个表空间文件中(通常是 .ibd
文件,即“表独立表空间文件”)。
-
聚簇索引:
InnoDB会以主键作为聚簇索引(若未显式定义主键,会自动选择唯一非空列作为聚簇索引,若也没有则隐式生成一个行ID作为聚簇索引)。
聚簇索引的B+树结构中,叶子节点直接存储完整的数据行,非叶子节点存储索引键值和指向子节点的指针。这些节点(包括非叶子和叶子节点)与数据行一起,共同存储在.ibd
文件中。 -
非聚簇索引(二级索引):
基于非主键列创建的索引(如user_id
、order_time
等)称为二级索引,也是B+树结构。
其叶子节点不存储完整数据行,而是存储“索引键值 + 对应的聚簇索引键值(通常是主键)”。这些二级索引的所有节点(非叶子+叶子)同样存储在同一个.ibd
文件中,与聚簇索引共享表空间。例如:订单表
order_info
的.ibd
文件中,既包含以order_id
为主键的聚簇索引(含完整数据),也包含以user_id
为键的二级索引(含user_id
和对应的order_id
)。
2. MyISAM引擎
MyISAM引擎不支持聚簇索引,所有索引都是非聚簇索引,且索引与数据的存储位置完全分离。
-
数据存储:所有数据行单独存储在
.MYD
文件(Data File)中,数据的物理顺序与索引无关。 -
索引存储:所有索引(包括基于主键的索引)单独存储在
.MYI
文件(Index File)中,其B+树的叶子节点存储“索引键值 + 数据行在.MYD
文件中的物理地址”。例如:订单表
order_info
的数据存在order_info.MYD
,所有索引(包括主键索引)存在order_info.MYI
,两者是完全独立的文件。
总结
存储引擎 | 聚簇索引是否存在 | 聚簇索引存储位置 | 非聚簇索引存储位置 | 两者是否同位置 |
---|---|---|---|---|
InnoDB | 存在(主键默认) | 与数据一起在 .ibd 文件 | 与数据、聚簇索引在 .ibd 文件 | 是 |
MyISAM | 不存在 | - | 单独在 .MYI 文件 | -(无聚簇索引) |
核心区别:InnoDB通过“聚簇索引+共享表空间”将所有索引(聚簇和非聚簇)与数据存储在同一文件中,而MyISAM将所有索引(均为非聚簇)与数据分离存储。这也是InnoDB查询效率通常更高(尤其按主键查询)的原因之一——聚簇索引的叶子节点直接包含数据,无需跨文件查找。
创建聚簇索引时,对索引列的核心要求是具备可排序性(即支持“大小比较”或“顺序定义”),这是由聚簇索引的本质决定的——聚簇索引的B+树结构需要按索引列的值有序排列,且数据的物理存储顺序会与索引的逻辑顺序保持一致。如果索引列的值无法定义顺序(例如某些非结构化数据类型),则无法构建聚簇索引。
具体来说,聚簇索引对索引列的要求包括:
-
可排序性:索引列的值必须支持比较操作(如
>
、<
、=
等),能明确确定先后顺序。
例如:数字(INT
、BIGINT
)、日期(DATETIME
)、字符串(按字典序)、UUID(字符串类型,按字典序)等均满足此要求;而二进制大对象(BLOB
)、文本(TEXT
)等非结构化类型通常不支持有序比较,无法作为聚簇索引列。 -
唯一性:聚簇索引列通常需要具备唯一性(或通过“隐藏列”保证唯一性)。
因为聚簇索引的叶子节点直接对应数据行,若索引列存在重复值,会导致数据物理存储顺序无法唯一确定。数据库通常会自动处理这种情况(例如InnoDB会为重复值添加隐藏的“行ID”作为补充,确保索引键唯一),但最好显式使用唯一列作为聚簇索引(如主键)。
关于“UUID作为主键能否创建聚簇索引”?
可以创建,但不推荐(存在性能隐患)。
原因:
-
UUID满足聚簇索引的基本要求:
UUID是字符串类型(如CHAR(36)
),支持按字典序排序(例如'a1b2...' < 'c3d4...'
),且主键本身具有唯一性,因此完全可以作为聚簇索引的索引列。在InnoDB中,若将UUID设为主键,数据库会默认基于该UUID创建聚簇索引。 -
但UUID作为聚簇索引会导致严重的性能问题:
聚簇索引的性能依赖于“数据插入顺序与索引顺序一致”(即顺序插入)。而UUID是随机生成的(无递增规律),插入时会导致:- 频繁的页分裂:由于UUID值随机,新插入的记录可能需要插入到已有数据页的中间位置,导致数据页拆分,浪费存储空间且降低写入效率。
- 数据存储碎片化:物理存储顺序混乱,查询时磁盘IO效率低(连续的逻辑查询可能需要访问分散的物理页)。
对比示例:
-
推荐方案:用自增
BIGINT
作为主键(聚簇索引),UUID作为业务唯一标识(另存为普通列,可建非聚簇索引)。
自增ID是顺序插入的,不会导致页分裂,聚簇索引的物理存储连续,写入和查询效率都更高。 -
不推荐方案:直接用UUID作为主键(聚簇索引),适用于分布式场景(需全局唯一ID)但可接受一定性能损耗的情况,此时需通过分区表、定期优化等手段缓解碎片化问题。
结论:
聚簇索引要求索引列可排序且最好唯一,UUID满足这两个条件,因此可以基于UUID主键创建聚簇索引;但由于UUID的随机性,会导致聚簇索引的存储效率和写入性能下降,实际业务中需权衡使用。