索引下推 / 覆盖索引 / 回表
一句话速记
- 回表:用二级索引查到主键后,再去聚簇索引查完整行数据(两次查找)
- 覆盖索引:查询需要的列都在索引里,无需回表(Extra:
Using index) - 索引下推(ICP, Index Condition Pushdown):MySQL 5.6+,把 WHERE 条件的过滤下推到存储引擎层执行(在索引上过滤,减少回表次数),而不是读出行后在 Server 层过滤
通俗解释
类比:
表结构:
CREATE TABLE users(
id INT PRIMARY KEY, -- 聚簇索引(主键)
name VARCHAR(100),
age INT,
city VARCHAR(50),
INDEX idx_name_age(name, age) -- 二级联合索引
);
回表:
SELECT * FROM users WHERE name='Alice';
步骤 1:在 idx_name_age 索引树中找 name='Alice' 的叶节点
→ 找到 id=101, id=205(叶节点只存 name, age, id)
步骤 2:拿着 id=101, id=205 去聚簇索引(主键索引)中查完整行
→ 才能得到 city, 其他列
← 这一步就是"回表"
覆盖索引(避免回表):
SELECT id, name, age FROM users WHERE name='Alice';
↑ 只查 id, name, age
idx_name_age 索引叶节点存了:name, age, id
→ 索引已经包含了所有需要的列
→ 不需要回表!(Extra: Using index)
SELECT * FROM users WHERE name='Alice';
↑ 查所有列(包括 city)
→ 需要回表(city 不在索引里)
关键细节
1)索引下推(ICP)详解
没有 ICP(MySQL 5.5 以前):
WHERE name LIKE 'Alice%' AND age > 20
存储引擎:只用 name LIKE 'Alice%' 做索引过滤(前缀可用)
→ 找到所有 name 以 Alice 开头的行(可能很多)
→ 全部回表,拿到完整行
→ 传给 Server 层
Server 层:再用 age > 20 过滤
→ 大量不必要的回表
有 ICP(MySQL 5.6+):
存储引擎:用 name LIKE 'Alice%' AND age > 20 在索引层过滤
→ name 以 Alice 开头 且 age > 20 的行才回表
→ 大幅减少回表次数
→ Extra: Using index condition
ICP 的限制:
只适用于二级索引(聚簇索引没有"回表"概念)
WHERE 条件中,索引能覆盖的列才能下推
不适用于:
- 全文索引(FULLTEXT)
- 虚拟列上的索引
- 基于存储过程的条件
2)联合索引的最左前缀原则
INDEX idx_name_age_city(name, age, city)
可以使用索引的 WHERE 条件:
WHERE name='Alice' → 用 name
WHERE name='Alice' AND age=20 → 用 name + age
WHERE name='Alice' AND age=20 AND city='BJ' → 用 name + age + city
WHERE name='Alice' AND city='BJ' → 只用 name(跳过了 age,city 无法用)
不使用索引(或效率低):
WHERE age=20 → 不满足最左前缀(name 未指定)
WHERE city='BJ' → 同上
范围查询之后的列无法用索引:
WHERE name='Alice' AND age > 20 AND city='BJ'
→ name 全匹配,age 范围匹配,city 无法用索引(range 断了)
→ 但 city 可以被 ICP 下推(在存储引擎层过滤,不回表)
3)覆盖索引的设计技巧
-- 场景:频繁查询 user 的 name 和 status
SELECT name, status FROM users WHERE status=1 LIMIT 100;
-- 朴素方案:只给 status 加索引
CREATE INDEX idx_status ON users(status);
-- → 用 idx_status 找到行,但还需要回表取 name → 100 次回表
-- 覆盖索引:把 name 也加进索引
CREATE INDEX idx_status_name ON users(status, name);
-- → status + name 都在索引里,不需要回表(Extra: Using index)
-- 代价:索引变大(name 是字符串)
-- 平衡方案:
-- 如果 name 很长,可以只覆盖高频查询的短列(id、status、created_at 等)
-- 长文本字段通常不放进索引(空间代价大)覆盖索引 + 分页优化(减少回表):
-- 深分页慢查询(回表量大)
SELECT * FROM orders ORDER BY created_at DESC LIMIT 100000, 10;
-- 扫描 100010 行,100010 次回表
-- 延迟关联(先通过覆盖索引找 id,再回表)
SELECT o.*
FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY created_at DESC LIMIT 100000, 10
) t ON o.id = t.id;
-- 子查询:覆盖索引(id 在索引里,不回表),找到 10 个 id
-- 外层:只回表 10 次
-- 大幅提升性能4)回表的代价量化
聚簇索引(主键):叶节点存完整行数据,直接读(一次 I/O)
二级索引:叶节点存的是主键值,需要额外一次聚簇索引查找(一次 I/O)
回表代价:
- 顺序回表(主键有序):读取相邻页,I/O 代价小
- 随机回表(主键分散):每次回表可能是一次随机 I/O
- 随机 I/O 最贵,100 次随机 I/O 可能比全表扫描还慢
- 这就是为什么优化器有时选择全表扫描而不是走索引
-- 强制使用索引(当你确定索引更好时)
SELECT * FROM orders FORCE INDEX(idx_user_id) WHERE user_id=1;
5)面试题:ICP 和覆盖索引有什么区别?
覆盖索引(Using index):
查询需要的所有列都在索引里
→ 完全不需要回表
→ 更快
ICP(Using index condition):
索引包含过滤条件的列,但查询还需要其他列(需要回表)
→ 在索引层先过滤,减少需要回表的行数
→ 比没有 ICP 好,但不如覆盖索引
最优:覆盖索引
次优:ICP(减少回表)
次差:普通索引(回表)
最差:全表扫描(ALL)
延伸追问
- Q:什么情况下优化器会不选索引(而是全表扫描)?
→ 当优化器估算回表代价 > 全表扫描代价时。典型情况:查询命中率高(如
WHERE status=1而 90% 数据 status=1),随机回表比顺序全表扫更慢;或者统计信息不准(ANALYZE TABLE重建统计)。可用FORCE INDEX强制,但要确认确实更快。 - Q:聚簇索引(主键索引)需要回表吗? → 不需要。聚簇索引的叶节点直接存行的完整数据,查到即得到所有列。这是 InnoDB 和 MyISAM 的重要区别——MyISAM 的索引叶节点存的是行的物理地址,所有查询都需要”回表”(访问数据文件)。
- Q:
SELECT COUNT(*) FROM t为什么快? → InnoDB 会选择最小的索引(通常是覆盖 id 的辅助索引,比聚簇索引小得多)进行全扫描,Extra 显示Using index。不回表,只遍历索引树计数,比全表扫描快得多。
我的记法
- 回表 = 二级索引 → 主键 → 聚簇索引(两次查找)
- 覆盖索引 = 查询列都在索引里,不回表,
Extra: Using index - ICP = 把 WHERE 下推到存储引擎层,减少回表次数,
Extra: Using index condition - 联合索引遵循最左前缀,范围查询后面的列无法用索引(但可以 ICP)
- 深分页优化:延迟关联(子查询先用覆盖索引找 id,外层只回表 N 条)
- 一句话:「覆盖索引是最省事的(不回表),ICP 是退而求其次(少回表)」
状态
- 已背速记
- 能解释回表和覆盖索引的区别
- 能写延迟关联的深分页 SQL