聚簇索引和非聚簇索引的根本区别在于:数据存储方式和物理顺序。
一、核心概念
1. 聚簇索引(Clustered Index)
聚簇索引是指索引的叶子节点直接存储了整行数据。在 InnoDB 中,主键就是聚簇索引。
2. 非聚簇索引(Non-Clustered Index)
非聚簇索引是指索引的叶子节点存储的是主键值,而不是完整数据。需要根据主键值回表查询完整数据。也叫二级索引。
二、结构对比图
1. 聚簇索引结构
聚簇索引(主键索引) ┌─────────────────────────────────┐ │ 根节点 │ │ [1-100] [101-200] [201-300] │ └────────────┬────────────────────┘ │ ┌───────┴───────┐ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ 叶子节点 │ │ 叶子节点 │ │ id=1 完整行 │ │ id=101 完整行│ │ id=2 完整行 │ │ id=102 完整行│ │ id=3 完整行 │ │ id=103 完整行│ │ ... │ │ ... │ └─────────────┘ └─────────────┘ 数据即索引 索引即数据2. 非聚簇索引结构
非聚簇索引(如 name 索引) ┌─────────────────────────────────┐ │ 根节点 │ │ [A-F] [G-M] [N-Z] │ └────────────┬────────────────────┘ │ ┌───────┴───────┐ ▼ ▼ ┌─────────────┐ ┌─────────────┐ │ 叶子节点 │ │ 叶子节点 │ │ 'Alice' → 1 │ │ 'Bob' → 3 │ │ 'Ann' → 2 │ │ 'Ben' → 4 │ └─────────────┘ └─────────────┘ 存储主键值 需要回表查询三、详细区别对比表
| 维度 | 聚簇索引 | 非聚簇索引 |
|---|---|---|
| 数据存储 | 叶子节点存整行数据 | 叶子节点存主键值 |
| 每张表数量 | 只能有一个 | 可以有多个 |
| 物理顺序 | 数据按索引顺序存储 | 数据独立存储 |
| 查询速度 | 极快(一次查找) | 需要回表(两次查找) |
| 占用空间 | 无额外空间(数据本身) | 需要额外存储空间 |
| 主键选择 | 强烈建议使用自增主键 | 任何字段都可以建 |
| 插入性能 | 顺序插入极快 | 随机插入可能慢 |
四、工作原理示例
1. 建表和数据
CREATETABLEusers(idINTPRIMARYKEY,-- 聚簇索引nameVARCHAR(50),ageINT,emailVARCHAR(100),INDEXidx_name(name),-- 非聚簇索引INDEXidx_age(age)-- 非聚簇索引);INSERTINTOusersVALUES(1,'张三',25,' '),(2,'李四',30,' '),(3,'王五',28,' '),(4,'赵六',32,' ');2. 通过聚簇索引查询
-- 通过主键查询(一次查找)SELECT*FROMusersWHEREid=3;-- 执行过程:-- 1. 在聚簇索引树中查找 id=3-- 2. 直接在叶子节点找到完整数据-- 3. 返回结果(不需要回表)-- 性能:极快,O(log n)3. 通过非聚簇索引查询
-- 通过 name 索引查询(两次查找)SELECT*FROMusersWHEREname='王五';-- 执行过程:-- 1. 在 idx_name 索引树中找到 '王五'-- 2. 叶子节点存的是主键值:3-- 3. 拿着 id=3 回聚簇索引查询完整数据-- 4. 返回结果-- 性能:需要两次 B+Tree 查找-- 这叫:回表查询五、回表查询的代价
1. 什么是回表?
-- 场景:查询所有字段EXPLAINSELECT*FROMusersWHEREname='王五';-- Extra 字段可能显示:Using where-- 执行计划显示需要回表2. 如何避免回表?
-- 创建覆盖索引(索引包含所有需要的字段)CREATEINDEXidx_name_ageONusers(name,age);-- 查询只返回索引中的字段SELECTname,ageFROMusersWHEREname='王五';-- Extra: Using index(不需要回表!)-- 这种叫做:覆盖索引查询六、主键选择对性能的影响
1. 使用自增主键(推荐)
CREATETABLEusers_autoinc(idINTPRIMARYKEYAUTO_INCREMENT,-- 顺序插入nameVARCHAR(50));-- 插入数据INSERTINTOusers_autoinc(name)VALUES('张三'),('李四'),('王五');-- 数据物理存储顺序:-- id: 1,2,3,4,5...(连续有序)-- 优点:-- 1. 插入快(只在最后追加)-- 2. 页分裂少-- 3. 空间利用率高2. 使用 UUID 作主键(不推荐)
CREATETABLEusers_uuid(idVARCHAR(36)PRIMARYKEY,-- UUID 无序nameVARCHAR(50));-- 插入数据INSERTINTOusers_uuidVALUES(UUID(),'张三'),(UUID(),'李四');-- 数据物理存储顺序:-- id: 随机分散-- 缺点:-- 1. 插入慢(需要不断调整位置)-- 2. 频繁页分裂-- 3. 空间碎片多-- 4. 索引体积大3. 性能对比
-- 自增主键插入:100万条/分钟-- UUID主键插入:30万条/分钟-- 差距:3-5倍!七、聚簇索引的其他特点
1. 页合并和页分裂
-- 页分裂场景(非顺序插入)-- 当页满时,需要将一部分数据移到新页-- 影响插入性能-- 页合并场景(删除数据)-- 当页数据少于一半时,可能合并-- 优化空间使用2. 辅助索引的叶子节点
-- InnoDB 辅助索引的叶子节点-- 存储的是主键值,不是行指针-- 优点:-- 1. 主键更新时不需要改辅助索引(但很少更新主键)-- 2. 辅助索引大小固定-- 缺点:-- 1. 需要回表查询-- 2. 占用更多空间八、实际优化案例
案例1:查询优化
-- 原查询(需要回表)SELECTid,name,ageFROMusersWHEREageBETWEEN20AND30;-- 创建覆盖索引CREATEINDEXidx_ageONusers(age,name,id);-- 现在查询:SELECTage,name,idFROMusersWHEREageBETWEEN20AND30;-- Extra: Using index(不回表)案例2:分页优化
-- 深分页问题SELECT*FROMusersORDERBYidLIMIT100000,10;-- 需要扫描 100010 行-- 优化:先查主键,再关联SELECT*FROMusers t1INNERJOIN(SELECTidFROMusersORDERBYidLIMIT100000,10)t2ONt1.id=t2.id;-- 二级索引扫描主键,减少回表九、总结
核心区别
| 维度 | 聚簇索引 | 非聚簇索引 |
|---|---|---|
| 数量 | 1个 | N个 |
| 存储内容 | 完整数据 | 主键值 |
| 查询次数 | 1次 | 2次(可能回表) |
| 物理顺序 | 按索引顺序 | 独立存储 |
| 主键影响 | 直接影响性能 | 间接影响 |
选择建议
- 主键一定要用自增:避免页分裂,提高插入性能
- 查询尽量用覆盖索引:减少回表
- 避免 SELECT *:只查需要的字段
- 复合索引设计:考虑查询顺序
一句话理解
聚簇索引就像书的正文,本身已经按页码排好;非聚簇索引就像书的目录,告诉你某个关键词在哪些页码,要看到内容还得翻到对应页。