Skip to content
横幅:非聚簇索引:为什么查一行数据,数据库要跑两趟?

非聚簇索引:为什么查一行数据,数据库要跑两趟? ​

最近在学 MySQL 索引,碰到一个想不通的问题。

给 users 表的 name 字段建了索引,查一个用户:

sql
SELECT * FROM users WHERE name = 'Tom';

EXPLAIN 显示走了索引,type 是 ref,看起来没问题。但执行时间还是比预期长。仔细看 Extra,只有 Using where,没有 Using index。

后来才搞明白,问题出在四个字上:回表。

而回表的根源,在于我用的是非聚簇索引。

非聚簇索引是什么 ​

非聚簇索引,是一种索引结构与数据行物理存储顺序分离的索引。

它的叶子节点不存放完整数据行,只存放指向数据行的引用——在 InnoDB 里是主键值,在 MyISAM 里是行地址。

查询的时候,先查索引拿到引用,再拿着这个引用去查完整数据行。这个过程,就是回表。

一句话:

索引是索引,数据是数据,两者分开存。

核心特点 ​

  • 一个表可以有多个非聚簇索引
  • 索引和数据物理存储顺序不一致
  • 叶子节点不存完整行,只存索引列 + 引用
  • 查询可能需要回表
  • 在 InnoDB 里,二级索引就是非聚簇索引
  • 在 MyISAM 里,所有索引都是非聚簇索引

InnoDB 里的结构 ​

InnoDB 里,非聚簇索引通常叫二级索引或辅助索引。

它的 B+ 树长这样:

text
二级索引(name)
        [Alice | Bob | Tom]
       /        |        \
   <Alice   Alice~Bob   >Tom
   /   \     /   \      /   \
[name,id] [name,id] [name,id] ...

几个关键点:

  • 非叶子节点:存放索引列的值
  • 叶子节点:存放索引列 + 主键值
  • 叶子节点之间:按索引列顺序用链表连接

现在看那个查询:

sql
SELECT * FROM users WHERE name = 'Tom';

它实际经历了这些步骤:

  1. 在 name 二级索引里找到 Tom
  2. 拿到对应的主键 id
  3. 用这个 id 去聚簇索引里查完整行
  4. 返回数据

第 3 步,就是回表。

一次查询,跑了两棵 B+ 树。

MyISAM 的区别 ​

MyISAM 没有聚簇索引,所有索引都是非聚簇索引。

区别在于:它的叶子节点存放的是数据行的物理地址,不是主键值。

查询过程:

  1. 在索引中找到目标值
  2. 拿到数据行地址
  3. 直接去数据文件读取整行

MyISAM 不一定叫“回表”,但本质一样——先查索引,再查数据。

回表为什么慢 ​

回表的问题在于随机 I/O。

索引里的主键值是顺序的,但对应的数据行在磁盘上可能东一个西一个。查一次数据,可能就多一次磁盘寻道。

这就是为什么有些查询“明明走了索引,还是慢”。

用覆盖索引避免回表 ​

如果查询需要的列全部都在非聚簇索引的叶子节点里,就不需要回表。

sql
SELECT id, name FROM users WHERE name = 'Tom';

因为 id 和 name 都在 name 索引的叶子节点里,直接就能返回,不用再去查聚簇索引。

这叫覆盖索引。

判断方法:EXPLAIN 的 Extra 里出现 Using index,说明用上了覆盖索引,没有回表。

优点 ​

  • 一个表可以建多个,满足不同查询需求
  • 加快查询,避免全表扫描
  • 支持覆盖索引,设计得好可以完全避免回表
  • 不影响数据物理顺序,插入数据时数据行按主键组织,索引单独维护
  • 灵活,可以为任意列或列组合建索引
  • 支持排序和分组,索引顺序和 ORDER BY / GROUP BY 一致时可以省掉额外排序

缺点 ​

  • 可能回表,查询列不在索引中时需要额外查一次聚簇索引
  • 占空间,每个非聚簇索引都要单独存储
  • 维护成本高,增删改都要同步维护所有相关索引
  • 更新索引列代价高,修改索引列会移动索引结构
  • 过多索引拖慢写入,写操作要同步更新多个索引
  • 查询性能不如聚簇索引直接

和聚簇索引对比 ​

特性聚簇索引非聚簇索引
数量一个表只能有一个一个表可以有多个
数据存储叶子节点存完整行叶子节点存索引列 + 引用
物理顺序与索引顺序一致与索引顺序无关
查询完整行一次查找通常需要回表
主键查询极快走主键索引则快,走二级索引需回表
范围查询很快可能回表,取决于是否覆盖
插入影响依赖主键顺序单独维护,影响写入
典型代表InnoDB 主键索引InnoDB 二级索引、MyISAM 所有索引

设计建议 ​

  1. 为高频查询条件建非聚簇索引
  2. 尽量使用覆盖索引,减少回表
  3. 不要建太多索引,避免拖慢写入
  4. 联合索引注意最左前缀原则
  5. 选择区分度高的列建索引
  6. 主键尽量短,因为 InnoDB 二级索引叶子节点会存主键值
  7. 定期检查无用索引并删除

总结 ​

非聚簇索引是独立于数据行物理顺序的索引,叶子节点存放索引列和指向数据行的引用。

一个表可以有多个,能加快查询,但查询完整行时可能需要回表。InnoDB 的二级索引就是非聚簇索引,MyISAM 的所有索引也都是非聚簇索引。

下次发现查询慢了,先看 EXPLAIN 里有没有 Using index。如果没有,大概率就是在回表。想想能不能用覆盖索引解决——少跑一趟,就是快。

Released under the MIT License.