跳转至

聚簇索引与覆盖索引

核心问题:为什么二级索引查询需要"回表"?如何避免回表提升性能?


它解决了什么问题?

理解聚簇索引和二级索引的区别,能帮你: - 解释为什么 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 是随机值,插入时会导致频繁页分裂,索引碎片多,性能差。自增主键顺序插入,页分裂少,索引紧凑,查询性能好。