聚簇索引与覆盖索引¶
核心问题:为什么二级索引查询需要"回表"?如何避免回表提升性能?
它解决了什么问题?¶
理解聚簇索引和二级索引的区别,能帮你:
- 解释为什么 SELECT * 比 SELECT id, name 慢
- 设计出能避免回表的覆盖索引
- 理解为什么主键推荐用自增整数而不是 UUID
聚簇索引 vs 二级索引¶
flowchart LR
subgraph "聚簇索引(主键索引)"
CI_Root["根节点"] --> CI_Leaf["叶子节点\n存储完整行数据"]
end
subgraph "二级索引(普通索引)"
SI_Root["根节点"] --> SI_Leaf["叶子节点\n存储索引值 + 主键值"]
SI_Leaf -->|回表查询| CI_Leaf
end
| 对比项 | 聚簇索引 | 二级索引 |
|---|---|---|
| 叶子节点存储 | 完整行数据 | 索引列值 + 主键值 |
| 数量 | 每表只有一个 | 可以有多个 |
| 查询 | 直接获取数据 | 需要回表查询(除非覆盖索引) |
什么是回表?¶
通过二级索引查询时,先在二级索引 B+ 树中找到主键值,再拿主键去聚簇索引中查完整数据,这个二次查询的过程叫做回表。
-- 假设 name 字段有普通索引
SELECT * FROM user WHERE name = 'Tom';
-- 执行过程:
-- 1. 在 name 索引树中找到 name='Tom' 对应的主键 id=5
-- 2. 拿 id=5 去主键索引树中查完整行数据(回表)
-- 3. 返回结果
为什么二级索引存主键而不是行地址:如果存行地址,当数据行移动(如页分裂)时,所有二级索引都要更新,维护成本极高。存主键后,数据移动只需更新聚簇索引,二级索引不受影响。
覆盖索引:避免回表的利器¶
覆盖索引:查询的列全部在索引中,无需回表。EXPLAIN 中 Extra 列显示 Using index。
-- 建立联合索引:INDEX(name, age)
-- ✅ 覆盖索引,无需回表
SELECT name, age FROM user WHERE name = 'Tom';
-- 查询列 name、age 都在索引中,直接从索引返回
-- ❌ 需要回表
SELECT * FROM user WHERE name = 'Tom';
-- SELECT * 包含了索引之外的列,必须回表
flowchart LR
subgraph "覆盖索引(无回表)"
Q1["SELECT name, age\nWHERE name='Tom'"] --> IDX["name+age 联合索引\n直接返回结果 ✅"]
end
subgraph "普通查询(需回表)"
Q2["SELECT *\nWHERE name='Tom'"] --> IDX2["name 索引\n找到主键 id"] --> PK["主键索引\n查完整数据 ⚠️"]
end
主键设计建议¶
| 主键类型 | 优点 | 缺点 |
|---|---|---|
| 自增整数(推荐) | 顺序插入,页分裂少,索引紧凑 | 可能被猜测到数量 |
| UUID | 全局唯一,分布式友好 | 随机插入,大量页分裂,索引膨胀,性能差 |
| 雪花 ID | 全局唯一,趋势递增 | 需要额外组件生成 |
为什么 UUID 作为主键性能差:UUID 是随机值,每次插入都可能插到 B+ 树的中间位置,导致频繁的页分裂(将一个满页拆成两个页),产生大量碎片,索引文件膨胀,查询性能下降。
InnoDB vs MyISAM¶
| 对比项 | InnoDB | MyISAM |
|---|---|---|
| 事务支持 | ✅ 支持 | ❌ 不支持 |
| 锁粒度 | 行锁 | 表锁 |
| 外键 | ✅ 支持 | ❌ 不支持 |
| 崩溃恢复 | ✅ redo log 自动恢复 | ❌ 需手动修复 |
| 适用场景 | 高并发写入、事务场景 | 读多写少(已逐渐淘汰) |
结论:现代 MySQL 项目几乎都应使用 InnoDB,MyISAM 已逐渐被淘汰。
面试高频问题¶
Q:聚簇索引和二级索引的区别?什么是回表?如何避免回表?
聚簇索引叶子节点存完整行数据,二级索引叶子节点存索引值+主键。通过二级索引查询时,先找到主键,再去聚簇索引查完整数据,这就是回表。避免回表:使用覆盖索引(查询列全在索引中)。
Q:为什么推荐用自增主键而不是 UUID?
UUID 是随机值,插入时会导致频繁页分裂,索引碎片多,性能差。自增主键顺序插入,页分裂少,索引紧凑,查询性能好。