写在前面:说实话,我见过太多人建索引跟撒胡椒面似的,不管啥字段都往上怼,结果查询照样慢,写操作还被拖垮。去年我帮一个兄弟看慢 SQL,一张表 8 个索引,查询还是用不上。一问才知道,他连"覆盖索引"四个字都没听过。今天咱就把覆盖索引和索引下推这两个利器讲透,看完你再去建索引,心里就有底了。
文章目录
- 一、为什么索引这么快?
- 1.1 一个直观的数字
- 1.2 索引原理速览
- 二、覆盖索引(Covering Index)
- 2.1 什么是覆盖索引?
- 2.2 回表到底是什么?
- 2.3 实战:联合索引设计实现覆盖查询
- 2.4 完整代码示例
- 2.5 问题与解答
- 三、索引下推(Index Condition Pushdown, ICP)
- 3.1 ICP 是什么?
- 3.2 原理对比
- 3.3 开关控制与验证
- 3.4 EXPLAIN 对比实验
- 3.5 ICP 的使用条件
- 四、联合索引设计最佳实践
- 4.1 最左前缀原则
- 4.2 字段顺序:区分度高的放前面
- 4.3 索引失效的常见坑
- 4.4 索引设计 Checklist
- 五、EXPLAIN 字段解读
- 5.1 type 列:访问类型
- 5.2 关键字段速查
- 5.3 完整 EXPLAIN 结果解读示例
- 六、踩坑指南
- 七、问题与解答
- 八、面试高频考点汇总
- 考点 1:什么是覆盖索引?怎么判断有没有触发?
- 考点 2:索引下推(ICP)的原理和好处?
- 考点 3:联合索引的最左前缀原则是什么?
- 考点 4:索引失效的常见场景有哪些?
- 考点 5:聚簇索引和非聚簇索引的区别?回表是什么?
- 九、模拟面试官提问与参考答案
- 场景题 1
- 场景题 2
- 场景题 3
- 场景题 4
- 场景题 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。
-- 假设有联合索引 INDEX idx_user (user_id, order_no, amount)SELECTorder_no,amountFROM`order`WHEREuser_id=10086;这条 SQL 查的三个字段user_id、order_no、amount都在索引idx_user里,MySQL 扫一遍索引树就完事了,一次回表都没有。
2.2 回表到底是什么?
想象你去图书馆找书:
- 回表:先从书名目录找到索书号,再拿索书号去书架找实体书。跑了两次。
- 覆盖索引:你要的信息(书名、作者、价格)目录卡片上全有,直接拿走,不用去书架。
每次回表都是一次随机 IO,性能开销不小。覆盖索引能减少甚至避免回表,尤其在数据量大的时候效果显著。
2.3 实战:联合索引设计实现覆盖查询
需求:根据user_id查order_no和amount。
索引设计:
CREATEINDEXidx_user_order_amountON`order`(user_id,order_no,amount);SQL 验证:
EXPLAINSELECTorder_no,amountFROM`order`WHEREuser_id=10086;EXPLAIN 结果解读:
| 字段 | 值 | 说明 |
|---|---|---|
| type | ref | 使用索引匹配 |
| key | idx_user_order_amount | 使用了联合索引 |
| key_len | 8 | user_id 字段长度 |
| Extra | Using index | 覆盖索引,无需回表 |
看到Using index这俩字,就知道成了。
2.4 完整代码示例
@ServicepublicclassOrderQueryService{@AutowiredprivateOrderMapperorderMapper;/** * 查询用户的订单号和金额(利用覆盖索引优化) */publicList<OrderBriefDTO>queryUserOrderBrief(LonguserId){// 这条SQL只查索引中存在的字段,触发覆盖索引returnorderMapper.selectOrderNoAndAmountByUserId(userId);}}// DTO 只包含索引中的字段dataclassOrderBriefDTO(val orderNo:String,val amount:BigDecimal)<selectid="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),查询:
SELECT*FROMuserWHEREnameLIKE'张%'ANDage=20;没有 ICP(MySQL 5.5 及以前):
- 存储引擎根据
name LIKE '张%'找到所有姓张的记录 - 每一条都回表查完整数据
- Server 层再判断
age = 20,把不符合的扔掉
有 ICP(MySQL 5.6+):
- 存储引擎根据
name LIKE '张%'找到记录 - 直接在存储引擎层判断
age = 20,不满足条件的直接跳过 - 只回表符合条件的记录
3.3 开关控制与验证
-- 查看 ICP 是否开启SHOWVARIABLESLIKE'optimizer_switch';-- 关闭 ICP(测试用)SEToptimizer_switch='index_condition_pushdown=off';-- 开启 ICPSEToptimizer_switch='index_condition_pushdown=on';3.4 EXPLAIN 对比实验
建表和索引:
CREATETABLEuser(idINTPRIMARYKEYAUTO_INCREMENT,nameVARCHAR(50),ageINT,addressVARCHAR(200),INDEXidx_name_age(name,age));关闭 ICP:
SEToptimizer_switch='index_condition_pushdown=off';EXPLAINSELECT*FROMuserWHEREnameLIKE'张%'ANDage=20;-- Extra: Using where(Server 层过滤)开启 ICP:
SEToptimizer_switch='index_condition_pushdown=on';EXPLAINSELECT*FROMuserWHEREnameLIKE'张%'ANDage=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),查询条件必须从最左开始才能用上索引:
WHEREa=1-- 用索引WHEREa=1ANDb=2-- 用索引WHEREa=1ANDb=2ANDc=3-- 用索引WHEREb=2-- 不用索引!WHEREa=1ANDc=3-- 只用 a4.2 字段顺序:区分度高的放前面
-- 不好:status 只有几种值,区分度低INDEXidx_status_name(status,name)-- 好:name 区分度高,过滤能力强INDEXidx_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 结果解读示例
EXPLAINSELECTorder_no,amountFROM`order`WHEREuser_id=10086ANDcreate_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+ 树紧凑。
十、互动话题
你在实际项目中有没有遇到过"明明建了索引但查询还是很慢"的情况?最后发现是什么原因?是字段顺序不对、隐式转换、还是索引根本没用上?评论区聊聊,大家一起排坑。
十一、参考资料
- MySQL 官方文档:Index Condition Pushdown Optimization
- MySQL 覆盖索引与回表 - 知乎
原创不易,如果觉得有帮助,点个免费的赞再走吧。你的支持是我持续输出的动力。