写在前面:说实话,我见过太多人建索引跟撒胡椒面似的,不管啥字段都往上怼,结果查询照样慢,写操作还被拖垮。去年我帮一个兄弟看慢 SQL,一张表 8 个索引,查询还是用不上。一问才知道,他连"覆盖索引"四个字都没听过。今天咱就把覆盖索引和索引下推这两个利器讲透,看完你再去建索引,心里就有底了。

文章目录
-
- 一、为什么索引这么快?
-
- [1.1 一个直观的数字](#1.1 一个直观的数字)
- [1.2 索引原理速览](#1.2 索引原理速览)
- [二、覆盖索引(Covering Index)](#二、覆盖索引(Covering Index))
-
- [2.1 什么是覆盖索引?](#2.1 什么是覆盖索引?)
- [2.2 回表到底是什么?](#2.2 回表到底是什么?)
- [2.3 实战:联合索引设计实现覆盖查询](#2.3 实战:联合索引设计实现覆盖查询)
- [2.4 完整代码示例](#2.4 完整代码示例)
- [2.5 问题与解答](#2.5 问题与解答)
- [三、索引下推(Index Condition Pushdown, ICP)](#三、索引下推(Index Condition Pushdown, ICP))
-
- [3.1 ICP 是什么?](#3.1 ICP 是什么?)
- [3.2 原理对比](#3.2 原理对比)
- [3.3 开关控制与验证](#3.3 开关控制与验证)
- [3.4 EXPLAIN 对比实验](#3.4 EXPLAIN 对比实验)
- [3.5 ICP 的使用条件](#3.5 ICP 的使用条件)
- 四、联合索引设计最佳实践
-
- [4.1 最左前缀原则](#4.1 最左前缀原则)
- [4.2 字段顺序:区分度高的放前面](#4.2 字段顺序:区分度高的放前面)
- [4.3 索引失效的常见坑](#4.3 索引失效的常见坑)
- [4.4 索引设计 Checklist](#4.4 索引设计 Checklist)
- [五、EXPLAIN 字段解读](#五、EXPLAIN 字段解读)
-
- [5.1 type 列:访问类型](#5.1 type 列:访问类型)
- [5.2 关键字段速查](#5.2 关键字段速查)
- [5.3 完整 EXPLAIN 结果解读示例](#5.3 完整 EXPLAIN 结果解读示例)
- 六、踩坑指南
- 七、问题与解答
- 八、面试高频考点汇总
-
- [考点 1:什么是覆盖索引?怎么判断有没有触发?](#考点 1:什么是覆盖索引?怎么判断有没有触发?)
- [考点 2:索引下推(ICP)的原理和好处?](#考点 2:索引下推(ICP)的原理和好处?)
- [考点 3:联合索引的最左前缀原则是什么?](#考点 3:联合索引的最左前缀原则是什么?)
- [考点 4:索引失效的常见场景有哪些?](#考点 4:索引失效的常见场景有哪些?)
- [考点 5:聚簇索引和非聚簇索引的区别?回表是什么?](#考点 5:聚簇索引和非聚簇索引的区别?回表是什么?)
- 九、模拟面试官提问与参考答案
-
- [场景题 1](#场景题 1)
- [场景题 2](#场景题 2)
- [场景题 3](#场景题 3)
- [场景题 4](#场景题 4)
- [场景题 5](#场景题 5)
- 十、互动话题
- 十一、参考资料
一、为什么索引这么快?
1.1 一个直观的数字
千万级数据表,没索引查一条记录要 3-5 秒 ,加了合适的索引只要 3-5 毫秒。差了一千倍。
这差距就跟找书一样------没有目录,你得一页一页翻;有目录,直接翻到对应页码。索引就是数据库的目录。
1.2 索引原理速览
MySQL 的 InnoDB 用 B+ 树 存索引:
-
聚簇索引:叶子节点存的是完整数据行(主键索引就是聚簇索引)
-
非聚簇索引:叶子节点存的是主键值,查完整数据需要"回表"
聚簇索引: 非聚簇索引(二级索引):
[10] [Alice] -> 10
/ \ [Bob] -> 20
[5] [20] [Carol] -> 10
走非聚簇索引时,先查到主键,再拿主键去聚簇索引查整行数据------这就是回表。
二、覆盖索引(Covering Index)
2.1 什么是覆盖索引?
查询的所有字段都在索引里,数据库不需要回表,直接从索引拿数据。EXPLAIN 的 Extra 列会出现 Using index。
sql
-- 假设有联合索引 INDEX idx_user (user_id, order_no, amount)
SELECT order_no, amount FROM `order` WHERE user_id = 10086;
这条 SQL 查的三个字段 user_id、order_no、amount 都在索引 idx_user 里,MySQL 扫一遍索引树就完事了,一次回表都没有。
2.2 回表到底是什么?
想象你去图书馆找书:
- 回表:先从书名目录找到索书号,再拿索书号去书架找实体书。跑了两次。
- 覆盖索引:你要的信息(书名、作者、价格)目录卡片上全有,直接拿走,不用去书架。
每次回表都是一次随机 IO,性能开销不小。覆盖索引能减少甚至避免回表,尤其在数据量大的时候效果显著。
2.3 实战:联合索引设计实现覆盖查询
需求 :根据 user_id 查 order_no 和 amount。
索引设计:
sql
CREATE INDEX idx_user_order_amount
ON `order`(user_id, order_no, amount);
SQL 验证:
sql
EXPLAIN SELECT order_no, amount
FROM `order`
WHERE user_id = 10086;
EXPLAIN 结果解读:
| 字段 | 值 | 说明 |
|---|---|---|
| type | ref | 使用索引匹配 |
| key | idx_user_order_amount | 使用了联合索引 |
| key_len | 8 | user_id 字段长度 |
| Extra | Using index | 覆盖索引,无需回表 |
看到 Using index 这俩字,就知道成了。
2.4 完整代码示例
java
@Service
public class OrderQueryService {
@Autowired
private OrderMapper orderMapper;
/**
* 查询用户的订单号和金额(利用覆盖索引优化)
*/
public List<OrderBriefDTO> queryUserOrderBrief(Long userId) {
// 这条SQL只查索引中存在的字段,触发覆盖索引
return orderMapper.selectOrderNoAndAmountByUserId(userId);
}
}
// DTO 只包含索引中的字段
data class OrderBriefDTO(
val orderNo: String,
val amount: BigDecimal
)
xml
<select id="selectOrderNoAndAmountByUserId" resultType="OrderBriefDTO">
SELECT order_no, amount
FROM `order`
WHERE user_id = #{userId}
</select>
踩坑提醒:如果查询里多加一个不在索引里的字段,比如
SELECT order_no, amount, status,覆盖索引立马失效,Extra 变成 Using where。我见过太多人索引建对了,SQL 里手贱多写了一个字段,性能直接打回原形。
2.5 问题与解答
Q1:覆盖索引只适用于 SELECT 吗?
主要是 SELECT 场景受益最大。但 INSERT/UPDATE/DELETE 时,MySQL 也要维护索引,索引越多写越慢。所以覆盖索引是查询优化手段,不是让你无节制建索引的理由。
Q2:联合索引字段顺序怎么定?
最左前缀原则。查询条件里用 WHERE user_id = ?,所以 user_id 放最左。后面跟着的 order_no, amount 是为了让查询能覆盖,减少回表。如果查询条件是 WHERE order_no = ?,这个索引就用不上了。
Q3:COUNT(*) 能走覆盖索引吗?
能。比如 SELECT COUNT(*) FROM order WHERE user_id = 10086,只要 user_id 有索引,MySQL 直接扫索引树计数,不需要回表查整行。
三、索引下推(Index Condition Pushdown, ICP)
3.1 ICP 是什么?
MySQL 5.6 引入的优化特性。简单说:把 WHERE 条件的过滤从 Server 层下推到存储引擎层,在索引扫描的时候就过滤掉不满足条件的记录,减少回表次数。
3.2 原理对比
假设有联合索引 INDEX idx (name, age),查询:
sql
SELECT * FROM user WHERE name LIKE '张%' AND age = 20;
没有 ICP(MySQL 5.5 及以前):
- 存储引擎根据
name LIKE '张%'找到所有姓张的记录 - 每一条都回表查完整数据
- Server 层再判断
age = 20,把不符合的扔掉
有 ICP(MySQL 5.6+):
- 存储引擎根据
name LIKE '张%'找到记录 - 直接在存储引擎层判断
age = 20,不满足条件的直接跳过 - 只回表符合条件的记录
3.3 开关控制与验证
sql
-- 查看 ICP 是否开启
SHOW VARIABLES LIKE 'optimizer_switch';
-- 关闭 ICP(测试用)
SET optimizer_switch='index_condition_pushdown=off';
-- 开启 ICP
SET optimizer_switch='index_condition_pushdown=on';
3.4 EXPLAIN 对比实验
建表和索引:
sql
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
age INT,
address VARCHAR(200),
INDEX idx_name_age (name, age)
);
关闭 ICP:
sql
SET optimizer_switch='index_condition_pushdown=off';
EXPLAIN SELECT * FROM user WHERE name LIKE '张%' AND age = 20;
-- Extra: Using where(Server 层过滤)
开启 ICP:
sql
SET optimizer_switch='index_condition_pushdown=on';
EXPLAIN SELECT * FROM user WHERE name LIKE '张%' AND age = 20;
-- Extra: Using index condition(ICP 生效)
看到 Using index condition,就是 ICP 在干活。
3.5 ICP 的使用条件
不是所有查询都能用 ICP,得满足:
- MySQL 5.6+(废话)
- 查询字段包含索引列和非索引列的联合条件
- 只能用于二级索引(聚簇索引本身就有完整数据,不需要)
- 不能用于子查询、存储过程(部分限制)
| 场景 | 是否触发 ICP | 原因 |
|---|---|---|
WHERE name = '张三' AND age = 20 |
否 | 两个都是索引列,直接走索引匹配 |
WHERE name LIKE '张%' AND age = 20 |
是 | name 走范围,age 在索引里但用不上最左匹配,ICP 来补 |
WHERE name = '张三' AND address = '北京' |
是 | address 不在索引里,ICP 下推到引擎层过滤 |
四、联合索引设计最佳实践
4.1 最左前缀原则
联合索引 (a, b, c),查询条件必须从最左开始才能用上索引:
sql
WHERE a = 1 -- 用索引
WHERE a = 1 AND b = 2 -- 用索引
WHERE a = 1 AND b = 2 AND c = 3 -- 用索引
WHERE b = 2 -- 不用索引!
WHERE a = 1 AND c = 3 -- 只用 a
4.2 字段顺序:区分度高的放前面
sql
-- 不好:status 只有几种值,区分度低
INDEX idx_status_name (status, name)
-- 好:name 区分度高,过滤能力强
INDEX idx_name_status (name, status)
区分度 = 不重复值数量 / 总记录数。越接近 1 越好。
4.3 索引失效的常见坑
| 操作 | 示例 | 结果 |
|---|---|---|
| 前导模糊 | LIKE '%张' |
索引失效 |
| OR 条件 | WHERE a = 1 OR b = 2 |
可能全表扫描 |
| 函数操作 | WHERE YEAR(create_time) = 2024 |
索引失效 |
| 隐式转换 | WHERE phone = 13800138000(字符串字段传数字) |
索引失效 |
| 计算表达式 | WHERE id + 1 = 100 |
索引失效 |
踩坑提醒:隐式类型转换这个坑我踩过。表字段是
VARCHAR手机号,Java 代码传了Long类型,MySQL 偷偷把字段转成了数字,索引直接作废。解决办法:Java 传 String,或者 SQL 里手动CAST。
4.4 索引设计 Checklist
| 检查项 | 是否通过 | 说明 |
|---|---|---|
| 查询条件字段是否在最左前缀? | - | 确保索引能被用到 |
| SELECT 字段能否被索引覆盖? | - | 减少回表 |
| 区分度低的字段是否放后面? | - | 提高索引过滤效率 |
| 是否有函数/隐式转换导致失效? | - | EXPLAIN 确认 |
| 索引数量是否过多? | - | 一般不超过 5-6 个 |
五、EXPLAIN 字段解读
5.1 type 列:访问类型
性能从高到低:
system > const > eq_ref > ref > range > index > ALL
| type | 含义 | 场景 |
|---|---|---|
| const | 主键或唯一索引等值匹配 | WHERE id = 1 |
| eq_ref | JOIN 中主键匹配 | 关联表主键查询 |
| ref | 非唯一索引等值匹配 | WHERE name = '张三' |
| range | 索引范围扫描 | WHERE id > 100 AND id < 200 |
| index | 全索引扫描 | SELECT count(*) FROM table |
| ALL | 全表扫描 | 没有索引或索引失效 |
5.2 关键字段速查
| 字段 | 重点关注 |
|---|---|
| key | 实际用了哪个索引,NULL 表示没用索引 |
| rows | 预估扫描行数,越小越好 |
| Extra | Using index(覆盖索引)、Using index condition(ICP)、Using where(Server 层过滤)、Using filesort(需要排序优化) |
5.3 完整 EXPLAIN 结果解读示例
sql
EXPLAIN SELECT order_no, amount
FROM `order`
WHERE user_id = 10086 AND create_time > '2024-01-01';
id: 1
select_type: SIMPLE
table: order
type: ref
possible_keys: idx_user_time, idx_user_order_amount
key: idx_user_order_amount
key_len: 8
ref: const
rows: 150
Extra: Using index condition; Using where
解读:
type: ref------ 非唯一索引等值匹配,还不错key: idx_user_order_amount------ 实际用了这个索引rows: 150------ 预估扫 150 行Extra: Using index condition------ ICP 生效,在引擎层过滤了create_time
六、踩坑指南
坑 1:索引不是越多越好
每多一个索引,INSERT/UPDATE/DELETE 就要多维护一棵树。我见过一张表 12 个索引,写操作慢得像蜗牛。一般单表索引不超过 5-6 个,联合索引优先于单列索引。
坑 2:选择性低的字段不适合单独建索引性别字段就两种值,建了索引 MySQL 也不一定用(优化器觉得全表扫描更快)。非要建的话,放在联合索引的后面。
坑 3:联合索引字段过多导致索引过大联合索引
(a, b, c, d, e)体积可能跟表差不多大,维护成本高。建议关键查询条件放前面,SELECT 里需要覆盖的字段精选一下。
坑 4:隐式类型转换导致索引失效
WHERE phone = 13800138000,phone 是 VARCHAR,MySQL 会把 phone 字段转成数字再比较,索引直接失效。解决办法:传 String 类型,或者写WHERE phone = '13800138000'。
七、问题与解答
Q1:覆盖索引和索引下推有什么区别?
覆盖索引解决的是"要不要回表"的问题------查询字段全在索引里,直接返回,零回表。ICP 解决的是"回表次数能不能减少"的问题------条件里有非索引字段,在引擎层先过滤,减少回表次数。两者可以叠加使用,效果更好。
Q2:为什么有时候 EXPLAIN 显示用了索引,查询还是很慢?
可能原因:1)索引虽然用了,但扫了太多行(rows 很大);2)回表次数太多(没有覆盖索引);3)数据在磁盘上分散,随机 IO 太多;4)锁竞争或并发高。EXPLAIN 只是执行计划,还得结合慢查询日志和性能测试综合分析。
Q3:MySQL 一定会用覆盖索引吗?
不一定。优化器会权衡成本,如果它觉得全表扫描更快(比如数据量很小、索引选择性差),就可能不用索引。可以用 FORCE INDEX 强制指定,但一般不建议,除非你很确定。
八、面试高频考点汇总
考点 1:什么是覆盖索引?怎么判断有没有触发?
答案 :查询的所有字段都在索引中,不需要回表查完整记录。看 EXPLAIN 的 Extra 列,出现 Using index 就是触发了。设计思路是把查询条件和 SELECT 字段尽量放到联合索引里。
考点 2:索引下推(ICP)的原理和好处?
答案 :MySQL 5.6 引入,将 WHERE 条件的过滤从 Server 层下推到存储引擎层。好处是在索引遍历阶段就过滤掉不满足条件的记录,减少回表次数。看 EXPLAIN 的 Extra 列,出现 Using index condition 表示 ICP 生效。
考点 3:联合索引的最左前缀原则是什么?
答案 :联合索引 (a, b, c),查询必须从最左边的字段开始按顺序使用,中间不能跳过。WHERE a=1 AND b=2 能用上索引,WHERE b=2 用不上。原理是 B+ 树按最左字段排序,跳过左边字段就无法二分查找。
考点 4:索引失效的常见场景有哪些?
答案 :1)前导模糊 LIKE '%xx';2)对索引字段做函数操作;3)隐式类型转换;4)OR 条件里部分字段没索引;5)计算表达式 WHERE id+1=100;6)不等于 <>、NOT IN(部分情况)。
考点 5:聚簇索引和非聚簇索引的区别?回表是什么?
答案:聚簇索引的叶子节点存完整数据行,InnoDB 的主键索引就是聚簇索引。非聚簇索引叶子节点存主键值,查完整数据需要根据主键再去聚簇索引查一次------这就是回表。覆盖索引的作用就是避免回表。
九、模拟面试官提问与参考答案
场景题 1
面试官:有一张订单表,查询场景是根据 user_id 查订单号和金额,你会怎么设计索引?
参考答案 :建联合索引 INDEX idx_user_order_amount (user_id, order_no, amount)。user_id 放最左,因为查询条件用它;order_no 和 amount 放后面,让查询变成覆盖索引,避免回表。用 EXPLAIN 验证 Extra 列出现 Using index。
场景题 2
面试官:现在需求变了,还要根据 create_time 范围筛选,同时保留原来的 user_id 查询,怎么办?
参考答案 :看哪种查询更频繁。如果 user_id = ? 的等值查询最多,保留 (user_id, order_no, amount),create_time 的范围查询可以用 ICP 在引擎层过滤,或者单独建一个 (user_id, create_time) 索引。如果范围查询是主要场景,把 create_time 放到联合索引后面:INDEX idx_user_time (user_id, create_time)。但要注意,这样可能就覆盖不了 order_no 和 amount 了,需要权衡。
场景题 3
面试官 :查询 SELECT * FROM user WHERE name LIKE '%张%' AND age = 20,有索引 (name, age),能走索引吗?
参考答案 :name LIKE '%张%' 是前导模糊,索引失效,整个查询变成全表扫描后在 Server 层过滤。即使 age 在索引里,最左前缀断了,后面的字段也用不上。解决办法:1)用搜索引擎(如 Elasticsearch)处理模糊匹配;2)如果业务允许,改成 name LIKE '张%' 就能走索引 + ICP;3)冗余一个拼音或分词字段做反向索引。
场景题 4
面试官:EXPLAIN 看到 type 是 index,这说明什么?好还是不好?
参考答案 :type: index 表示全索引扫描,MySQL 把整个索引树扫了一遍。比 ALL(全表扫描)好一点,因为索引通常比表小,但还是不够高效。如果是 SELECT COUNT(*) 或覆盖索引查询,index 是正常的;如果是普通查询带了 WHERE 条件还是 index,说明索引没用上,需要优化。
场景题 5
面试官:一张表数据量很大,读写比例 10:1,索引怎么设计?
参考答案 :读多写少可以适度多建索引,重点优化覆盖索引减少回表。分析慢查询日志,把 TOP 10 的查询拿出来,看能不能用联合索引覆盖。写操作虽然少,但批量写入时索引维护成本还在,所以单表索引控制在 5 个以内。另外,定期用 OPTIMIZE TABLE 或重建索引消除碎片,保持 B+ 树紧凑。
十、互动话题
你在实际项目中有没有遇到过"明明建了索引但查询还是很慢"的情况?最后发现是什么原因?是字段顺序不对、隐式转换、还是索引根本没用上?评论区聊聊,大家一起排坑。
十一、参考资料
原创不易,如果觉得有帮助,点个免费的赞再走吧。你的支持是我持续输出的动力。