什么是 MySQL 回表查询?如何避免?
2025年12月25日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
什么是回表查询?
回表查询是指:数据库在使用非主键索引(二级索引)进行查询时,首先在二级索引树中找到符合条件的主键值,然后再根据这些主键值回到主键索引(聚簇索引)树中查找完整行数据的过程。
这个过程,就像 “查字典” :先查偏旁部首目录(二级索引)找到对应的页码(主键ID),然后再翻到正文页(主键索引)看详细内容。
原理:InnoDB 的索引结构
为了更好的理解 “回表查询”,我们需要从 InnoDB 的索引结构讲起。假设有一张用户表,结构如下:
CREATE TABLE users (
id INT PRIMARY KEY, -- 主键,聚簇索引
username VARCHAR(50), -- 建立二级索引
age INT,
email VARCHAR(100),
created_at DATETIME
);
-- 为 username 创建二级索引
CREATE INDEX idx_username ON users(username);
那么,InnoDB 的索引存储结构如下:
主键索引(聚簇索引)叶子节点:存储完整的行数据
↓
[ id=1 | username="quanxiaoha" | age=25 | email="a@a.com" | created_at=... ]
[ id=2 | username="bob" | age=30 | email="b@b.com" | created_at=... ]
二级索引(idx_username)叶子节点:只存储索引列 + 主键值
↓
[ username="quanxiaoha" | id=1 ]
[ username="bob" | id=2 ]
回表查询的完整过程
假设,我们查询用户名为 quanxiaoha 的记录,执行如下 SQL, 就会发生 “回表”:
SELECT * FROM users WHERE username = 'quanxiaoha';
其具体执行步骤如下:
- 第一步(索引扫描):在
idx_username二级索引树中查找username='quanxiaoha' - 第二步(获取主键):找到记录
[username="quanxiaoha", id=1] - 第三步(回表查询):拿着
id=1回到主键索引树中查找 - 第四步(获取数据):在主键索引中找到
id=1的完整行数据并返回
为什么回表查询影响性能?
-
额外的磁盘 I/O;
-- 假设查询返回 1000 条记录 SELECT * FROM users WHERE age > 20; -- age 有二级索引 -- 性能消耗: -- 1. 在 age 索引扫描:1000 次索引页读取(顺序/随机) -- 2. 回表查询:1000 次主键索引查找(大概率是随机I/O) -- 总I/O次数 ≈ 2000次 -
可能存在随机 I/O,它比顺序 I/O 要慢 10 - 100 倍;
-- 二级索引中 age 可能是顺序存储的 -- 但根据这些 age 找到的 id 可能是分散的 age索引: [20, id=100], [21, id=500], [22, id=300], ... -- 回表时:id=100, id=500, id=300... 是随机访问主键索引 -- 随机 I/O 比顺序 I/O 慢10-100倍! -
缓存效率降低;
-- 如果查询只需要部分字段,回表会加载整行数据到内存 -- 包括不需要的大字段(如TEXT、BLOB) SELECT id, username FROM users WHERE age > 20; -- 回表会把 email、created_at 等所有字段都加载出来 -- 浪费内存和缓存空间
如何避免回表查询?
-
使用覆盖索引(最常用、最有效):让被查询的所有字段都包含在索引中,则无需回表。
-
使用聚簇索引(主键查询):主键查询从不回表,因为数据就在主键索引中, 所以,针对高频查询尽量使用主键的方式;
-
只查询必要的列:只查索引包含的字段,永远不要
Select *, 切记! -
使用索引下推(ICP,MySQL 5.6+):注意,ICP 不是完全避免回表,而是减少了回表的次数。
-
使用 MRR 优化: MRR 会将随机回表变为顺序回表。
-- 没有MRR:id=100 → 回表,id=500 → 回表,id=300 → 回表(随机) -- 启用MRR:先收集所有id [100,500,300],排序后 [100,300,500],再批量回表(顺序) -- 启用 MRR SET optimizer_switch='mrr=on,mrr_cost_based=off'; -- 验证 EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30; -- Extra: Using MRR ✅