非聚簇索引:为什么查一行数据,数据库要跑两趟?
最近在学 MySQL 索引,碰到一个想不通的问题。
给 users 表的 name 字段建了索引,查一个用户:
SELECT * FROM users WHERE name = 'Tom';EXPLAIN 显示走了索引,type 是 ref,看起来没问题。但执行时间还是比预期长。仔细看 Extra,只有 Using where,没有 Using index。
后来才搞明白,问题出在四个字上:回表。
而回表的根源,在于我用的是非聚簇索引。
非聚簇索引是什么
非聚簇索引,是一种索引结构与数据行物理存储顺序分离的索引。
它的叶子节点不存放完整数据行,只存放指向数据行的引用——在 InnoDB 里是主键值,在 MyISAM 里是行地址。
查询的时候,先查索引拿到引用,再拿着这个引用去查完整数据行。这个过程,就是回表。
一句话:
索引是索引,数据是数据,两者分开存。
核心特点
- 一个表可以有多个非聚簇索引
- 索引和数据物理存储顺序不一致
- 叶子节点不存完整行,只存索引列 + 引用
- 查询可能需要回表
- 在 InnoDB 里,二级索引就是非聚簇索引
- 在 MyISAM 里,所有索引都是非聚簇索引
InnoDB 里的结构
InnoDB 里,非聚簇索引通常叫二级索引或辅助索引。
它的 B+ 树长这样:
二级索引(name)
[Alice | Bob | Tom]
/ | \
<Alice Alice~Bob >Tom
/ \ / \ / \
[name,id] [name,id] [name,id] ...几个关键点:
- 非叶子节点:存放索引列的值
- 叶子节点:存放索引列 + 主键值
- 叶子节点之间:按索引列顺序用链表连接
现在看那个查询:
SELECT * FROM users WHERE name = 'Tom';它实际经历了这些步骤:
- 在
name二级索引里找到Tom - 拿到对应的主键
id - 用这个
id去聚簇索引里查完整行 - 返回数据
第 3 步,就是回表。
一次查询,跑了两棵 B+ 树。
MyISAM 的区别
MyISAM 没有聚簇索引,所有索引都是非聚簇索引。
区别在于:它的叶子节点存放的是数据行的物理地址,不是主键值。
查询过程:
- 在索引中找到目标值
- 拿到数据行地址
- 直接去数据文件读取整行
MyISAM 不一定叫“回表”,但本质一样——先查索引,再查数据。
回表为什么慢
回表的问题在于随机 I/O。
索引里的主键值是顺序的,但对应的数据行在磁盘上可能东一个西一个。查一次数据,可能就多一次磁盘寻道。
这就是为什么有些查询“明明走了索引,还是慢”。
用覆盖索引避免回表
如果查询需要的列全部都在非聚簇索引的叶子节点里,就不需要回表。
SELECT id, name FROM users WHERE name = 'Tom';因为 id 和 name 都在 name 索引的叶子节点里,直接就能返回,不用再去查聚簇索引。
这叫覆盖索引。
判断方法:EXPLAIN 的 Extra 里出现 Using index,说明用上了覆盖索引,没有回表。
优点
- 一个表可以建多个,满足不同查询需求
- 加快查询,避免全表扫描
- 支持覆盖索引,设计得好可以完全避免回表
- 不影响数据物理顺序,插入数据时数据行按主键组织,索引单独维护
- 灵活,可以为任意列或列组合建索引
- 支持排序和分组,索引顺序和
ORDER BY/GROUP BY一致时可以省掉额外排序
缺点
- 可能回表,查询列不在索引中时需要额外查一次聚簇索引
- 占空间,每个非聚簇索引都要单独存储
- 维护成本高,增删改都要同步维护所有相关索引
- 更新索引列代价高,修改索引列会移动索引结构
- 过多索引拖慢写入,写操作要同步更新多个索引
- 查询性能不如聚簇索引直接
和聚簇索引对比
| 特性 | 聚簇索引 | 非聚簇索引 |
|---|---|---|
| 数量 | 一个表只能有一个 | 一个表可以有多个 |
| 数据存储 | 叶子节点存完整行 | 叶子节点存索引列 + 引用 |
| 物理顺序 | 与索引顺序一致 | 与索引顺序无关 |
| 查询完整行 | 一次查找 | 通常需要回表 |
| 主键查询 | 极快 | 走主键索引则快,走二级索引需回表 |
| 范围查询 | 很快 | 可能回表,取决于是否覆盖 |
| 插入影响 | 依赖主键顺序 | 单独维护,影响写入 |
| 典型代表 | InnoDB 主键索引 | InnoDB 二级索引、MyISAM 所有索引 |
设计建议
- 为高频查询条件建非聚簇索引
- 尽量使用覆盖索引,减少回表
- 不要建太多索引,避免拖慢写入
- 联合索引注意最左前缀原则
- 选择区分度高的列建索引
- 主键尽量短,因为 InnoDB 二级索引叶子节点会存主键值
- 定期检查无用索引并删除
总结
非聚簇索引是独立于数据行物理顺序的索引,叶子节点存放索引列和指向数据行的引用。
一个表可以有多个,能加快查询,但查询完整行时可能需要回表。InnoDB 的二级索引就是非聚簇索引,MyISAM 的所有索引也都是非聚簇索引。
下次发现查询慢了,先看 EXPLAIN 里有没有 Using index。如果没有,大概率就是在回表。想想能不能用覆盖索引解决——少跑一趟,就是快。
