MySQL 单点详解(03)

MySQL 单点详解(03):聚簇索引与二级索引,回表为什么贵、覆盖索引怎么省掉它

写在前面

上一期讲清了 B+ 树为什么矮、为什么有序,这一期往下走一步:树上的叶子到底放了什么。

面试官的常见问法:

  • 什么是回表?为什么说它慢?
  • 覆盖索引是什么?怎么判断用上了?
  • 为什么建议少写 select *?

所有答案都指向同一句:

主键索引的叶子里放的是整行,二级索引的叶子里放的是主键。

所以按二级索引查,拿到主键之后还得再查一次主键索引。这第二次查找就叫回表。

回表不是多一次内存查找,是多下一次树、多一次随机读盘。代价全在这里。

一、一张表,两种索引

InnoDB 把数据直接挂在主键索引上,这种表叫索引组织表。

意思是:没有独立的数据文件,数据就是主键索引的叶子。

维度 聚簇索引(主键) 二级索引(普通、唯一)
叶子存什么 整行数据 索引列 + 主键值
一个表有几个 只有一个 可以有多个
能否直接拿到行 能 不能,必须回表
叶子上的顺序 按主键有序 按索引列有序
树的数量 一棵 每个索引一棵

所以一张表有几个索引,就有几棵 B+ 树,但只有一棵树的叶子上有完整数据。

一条查询走哪棵树,取决于 where 里用的是哪个索引。

用主键查,走聚簇索引,一次到位;用普通列查,先走那棵二级索引,再补一次回表。

其余树的叶子都是"目录条目",只够用来定位,不够用来返回。

主键是谁也不是随便定的,有明确的挑选顺序:

顺序 选谁当聚簇索引 说明
1 显式声明的主键 最常见
2 第一个非空唯一索引 没写主键时
3 隐式 6 字节 row_id 两者都没有,全局共享一个计数器

第三条要留意:隐式 row_id 是全局的,不是每张表一个。所有没主键的表共用同一个自增,删了数据也不回收。

二、回表到底贵在哪

举个具体场景:表里一千万行,name 上有索引,执行

sql 复制代码
select * from t where name = 'tom';

假设 name 不唯一,命中 100 行。

步骤 动作 页访问
1 在 name 索引上从根走到叶,定位到 'tom' 约 3 页(树高 3)
2 沿叶子往后扫,收集命中的 100 个主键 多数在上一步读过的页里
3 用这 100 个主键逐个回表 约 100 次随机页读

前面 3 次是顺序的、可预读的;后面 100 次是散开的、跳着读的。

这就是回表贵的真正原因:不是多查了一次,是多了一百次随机 IO。

如果表的行宽大,一页只放十几行,这 100 次几乎每次都要真读盘。

命中缓存时会快很多,但缓存不是保证,写多的库随时会把页刷掉。

回表还是一行一行来的,凑不成批。命中越多,这些随机读就越难合并成顺序读。

所以"范围查询 + 大结果集"最吃亏:索引扫得飞快,回表把它拖垮。

判断一条 SQL 贵不贵,先看它要回表多少行,而不是看它扫了多少行。

三、覆盖索引:干脆不回表

如果查询要的列,索引里已经全有了,就不用回表。

这种索引叫覆盖索引 ,explain 里表现为 Extra: Using index。

还是那张表,name 上有索引,比较三种写法:

查询 能否覆盖 代价
select * from t where name='tom' 否 命中 100 行就回表 100 次
select id, name from t where name='tom' 能,叶子本来就有 id 只读索引,不回表
select name from t where name='tom' 能 只读索引,不回表

注意第二条:为什么 id 不用回表?

因为二级索引的叶子存的就是"索引列 + 主键",主键天然在索引里。

这也是上一期说"主键要短"的第二个理由:主键越长,所有二级索引跟着一起变胖。

select * 之所以被反复提醒,原因也在这儿:它要的列一多,覆盖就不可能成立,必然回表。

四、怎么让索引覆盖住

反过来说,要返回的列里只要有一列不在索引上,覆盖就不成立,只能老老实实回表。

覆盖不是自动的,得靠索引设计:

  • 把高频查询要返回的列,一起放进联合索引,例如 (name, age);
  • 顺序上把等值条件放前面,范围条件放后面,这样前缀还能用上;
  • 别为了让索引覆盖,把十几列都塞进一个索引。

再补一层:联合索引 (name, age) 遇到 where name = ? and age > ?,age 也能在索引上直接判断,不用等回表。这就是下一节要说的索引下推。

最后一条要展开说,因为很容易过头:

做法 收益 代价
索引多带 1 到 2 个高频列 省掉回表,查询快几倍 索引变大,写入稍慢
索引带 5 个以上列 覆盖范围变大 索引膨胀、页变少、缓存命中率下降
每个查询建一个覆盖索引 单条查询最优 写入放大严重,维护成本高

索引是拿写入换读取的生意。读多写少的表可以多建,写密集的表要克制。

五、看执行计划时盯哪几个字段

explain 的输出字段很多,真正影响判断的就三个:key、rows、Extra。

key 是 NULL,说明压根没用上索引;rows 是估算值,不是精确行数,别拿它当准数看。

Extra 常见值 含义 怎么理解
Using index 覆盖索引 没回表,最好
Using index condition 索引下推 条件下推到引擎层先过滤,少回表
Using where server 层过滤 数据取上来之后才判断
Using filesort 需要额外排序 索引没帮上 order by
Using temporary 用了临时表 group by、distinct 常见

Using index condition 值得单独记一句:它是 5.6 之后引入的索引下推。

本来条件是"先回表、再判断",它改成"在索引上先判断、不合格的就不回表了"。

所以同一条 SQL,有没有下推,回表次数可能差一个数量级。

六、面试官会往下问的三层

第一层:为什么二级索引的叶子存主键,不存行地址?

因为行地址会变。页分裂、行迁移之后,原来的地址就失效了,所有二级索引都得跟着改。

存主键则稳定,行挪了位置,主键不变,索引不用动。

MyISAM 存的就是行地址,因为它的索引和数据分成两个文件,它没有聚簇索引这一说。

追问一句:为什么聚簇索引只能有一个?因为整行数据只存了一份,存两份就不叫聚簇了。

第二层:没有主键会怎么样?

InnoDB 会退而求其次:先找第一个非空唯一索引,都没有就用隐式 6 字节 row_id。

问题是这个 row_id 全局共享,所有无主键的表都挤在一个计数器上,而且没法用它做业务查询。

第三层:覆盖索引是不是越多越好?

不是。索引本身要占空间、要在写入时维护,还要跟数据页抢 buffer pool。

覆盖索引解决的是"这条查询",代价是"所有写入"。判断标准还是读写比例。

顺便一个常被追问的点:select count(*) 为什么慢?

因为 InnoDB 不存总行数,得找一棵最小的索引扫一遍。这也是覆盖索引的一种用法------扫索引比扫整行便宜。

收尾

回表的本质是"第二次下树",覆盖索引的本质是"让第二次不必发生"。

理解了叶子上放什么,索引优化里大半的经验规则都能自己推出来。

相关推荐
赤生云电竞2 小时前
云游戏边缘节点部署:机房选址与网络拓扑优化实践——接入延迟、VXLAN与GPU虚拟化选型
数据库·游戏
呆呆敲代码的小Y2 小时前
【实战精选面试题】C# 面试题详细解答,按难易程度排序
android·java·游戏·c#·游戏开发·unity面试题·c#面试题
水无痕simon2 小时前
3、工程重构
java·spring cloud·重构
阿里云基础软件3 小时前
Agent 任务提速 10.71%,内存占用降低 21.60%|Alibaba Cloud Linux 4 重大发布
java·linux·人工智能·操作系统·agent
Y003 小时前
PostgreSQL 权限管理踩坑记:从 42501 到默认权限和 RLS
数据库·postgresql
呆萌很3 小时前
达梦(DM8)数据库常用SQL
数据库·sql
跟太阳说早安3 小时前
极简数据库SQL
数据库·sql
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 项目:需求分析与滑动数据设计
java·开发语言·交友
前端 贾公子4 小时前
第 5 章:Codex 从可运行程序到可交付工具
java·前端·数据库