背了 50 道 MySQL 面试题,面试官换个问法就答不上来。这种场景在索引调优考察中太常见了。
很多候选人能把“最左前缀原则”“覆盖索引”“索引下推”这些概念背得滚瓜烂熟,但面试官一旦把问题落到具体表结构上,比如“这个 SQL 到底走没走索引,为什么”,立刻卡壳。
核心原因只有一个:把索引调优当成了知识点背诵,而不是基于底层原理的推理过程。
本文不打算给你塞 50 个孤立问题。我会从面试官考察的逻辑出发,把 MySQL 索引调优最核心的底层原理、失效场景、Explain 分析方法和实践流程串成一条完整链路。读完以后,你不仅能回答“索引为什么快”,还能现场分析“这条 SQL 为什么慢”,并在真实项目里完成从慢查询定位到索引优化的完整闭环。
这篇文章适合正在准备中高级后端面试的 Java/Go 工程师,也适合工作中需要优化数据库查询但没有系统梳理过索引知识的开发者。
1. 面试官到底在考察什么能力
先说一个容易被忽视的事实:绝大多数 MySQL 索引面试题,考察的不是记忆力,而是判断力。
网上流传的各种“夺命连环问”列表,本质上都在围绕一个核心问题展开:你理不理解索引在 InnoDB 里是怎么组织的,以及不同查询条件下索引是怎么被使用的。
面试官常见的考察路径是这样的:
第一层,问原理。“InnoDB 为什么用 B+ 树做索引?”这是确认你有没有底层知识储备。
第二层,问应用。“这条 SQL 会不会走索引?为什么?”这是看你能否把原理落到具体 SQL 上。
第三层,问取舍。“这条 SQL 在你项目里跑了 3 秒,你怎么优化?”这是看你在不确定信息下能不能给出可执行的优化方案。
第四层,问风险。“你加了联合索引之后,怎么确认它真的生效了?如果上线后发现写入变慢了怎么办?”这是在考察工程意识。
所以,本文的叙事逻辑也按照这个层次来组织:
- 先理解 B+ 树与 InnoDB 索引结构,这是所有分析的地基。
- 再掌握索引失效和索引下推等核心机制,这是面试高频考点。
- 然后学会用 Explain 分析执行计划,这是调优的“证据链”。
- 最后给出慢查询定位和生产的优化流程,这是工程落地能力。
2. 先从底层聊起:InnoDB 为什么选择 B+ 树
想要现场分析一条 SQL 走不走索引,第一步不是背规则,而是理解索引在磁盘上到底长什么样。
2.1 磁盘 IO 与页的约束
MySQL 的数据最终落在磁盘上,而磁盘随机 IO 的速度比内存慢好几个数量级。操作系统和 InnoDB 都不会一条一条地读写数据,而是以“页”为单位。InnoDB 默认页大小是 16KB。
这个 16KB 非常关键。索引结构的设计目标就变成了:用尽量少的磁盘 IO,找到目标数据。磁盘 IO 次数越少,查询越快。
2.2 B+ 树如何减少磁盘 IO
B+ 树是一种多路平衡查找树。它的核心特点是:
- 非叶子节点只存储索引键值,不存储数据。
- 所有数据都存储在叶子节点。
- 叶子节点之间通过指针相连,形成一个有序链表。
这两个特点决定了它的优势。
假设一行数据包含很多字段,数据量很大。如果把数据也放在非叶子节点,每个节点能容纳的索引键就会变少,树的高度就会变高,查找路径上的磁盘 IO 次数就会增加。B+ 树把数据全部放在叶子节点后,非叶子节点能容纳更多键,树自然更矮。
简单估算一下:一个 16KB 的页,假设主键是 BIGINT 类型,占用 8 字节,指针占用 6 字节左右,那么一个非叶子节点大约能存储 16KB / 14 字节,约 1100 到 1170 个键。三层 B+ 树大约可以存储 1100 × 1100 × 16 = 上千万级别的行数据。
也就是说,一张千万级数据量的表,走主键索引查询,通常只需要 3 次磁盘 IO 左右。这个数量级是面试时可以放心讲的判断。
2.3 聚簇索引、二级索引与回表
InnoDB 里索引和数据的组织方式需要区分清楚。
- 聚簇索引:表数据本身就是一棵 B+ 树。叶子节点存放整行数据。InnoDB 表一定有且只有一个聚簇索引,一般以主键构建。如果没有主键,InnoDB 会选择一个唯一非空索引,如果也没有,则隐式生成一个 rowid 作为聚簇索引。
- 二级索引:也叫辅助索引或普通索引。它的叶子节点不存放完整行数据,只存放索引键和主键值。
- 回表:当查询走的是二级索引,但需要的字段在二级索引里找不到时,MySQL 会拿着主键值再到聚簇索引里查一次完整数据。这个过程叫回表。
回表是面试高频概念。面试官问你“为什么有时候加了索引还是慢”,很多场景的答案就是:虽然走了索引,但发生了大量回表。
下面用一个简单示例说明回表场景。
-- 用户表:id 主键,age 上有普通索引 CREATE TABLE `user` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) DEFAULT NULL, `age` INT DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_age` (`age`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 情况1:如果只查 id 和 age,走 idx_age 就能拿到数据,不需要回表 SELECT id, age FROM user WHERE age = 30; -- 情况2:如果查 name,二级索引里没有 name,就需要回表 SELECT id, age, name FROM user WHERE age = 30;第二条 SQL 的执行过程:先在idx_age这棵 B+ 树里找到 age = 30 的记录,拿到主键 id,再到聚簇索引里找到完整行,取出 name 字段。这个过程就是回表。
面试时可以把这个过程完整说出来,这比只回答“回表就是再查一次”要更有区分度。
3. Explain 执行计划:面试分析的“证据链”
面试时,面试官经常甩给你一条 SQL,问你“怎么判断它有没有走索引”。最直接的回答就是:用 Explain 分析执行计划。
Explain 的输出中,需要重点关注的字段有这几个:
| 字段 | 含义 | 面试关注点 |
|---|---|---|
| type | 访问类型 | 从好到差依次是 system > const > eq_ref > ref > range > index > ALL |
| key | 实际使用的索引 | 为 NULL 表示没走索引 |
| rows | 预估扫描行数 | 数值越大通常越慢 |
| filtered | 过滤比例 | 100 表示没过滤,越小说明筛选越多 |
| Extra | 附加信息 | 经常出现 Using index、Using where、Using index condition、Using temporary、Using filesort |
先看一个典型示例。
EXPLAIN SELECT id, age, name FROM user WHERE age = 30;假设输出中 type = ref,key = idx_age,Extra = NULL。这说明走了idx_age索引,但由于查询了 name 字段,需要回表。
如果改成只查 id 和 age:
EXPLAIN SELECT id, age FROM user WHERE age = 30;这时候 Extra 里会出现Using index。这代表查询所需的字段在二级索引中都能找到,不需要回表。面试中一定要分清Using index和Using where的区别:
Using index:表示使用了覆盖索引,不需要回表。Using where:表示在存储引擎层返回记录后,还要在 Server 层对记录进行过滤,跟“是否走索引”是两回事。
再举一个典型的全表扫描例子。
EXPLAIN SELECT * FROM user WHERE name = '张三';如果 name 上没有索引,type 通常是 ALL,key 为 NULL。这表示全表扫描,性能最差,也是面试中需要优先识别的信号。
4. 联合索引与最左前缀原则:高频考点的答题框架
联合索引是面试中出场率最高的话题。很多候选人知道“最左前缀”,但一旦面试官换一个联合索引定义,依然会答错。
4.1 联合索引的存储结构
联合索引(a, b, c)的 B+ 树并不是把 a、b、c 分别建立索引,而是按照字段顺序,先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。
所以,联合索引最核心的规律是:查询条件里如果跳过了索引定义中的某个字段,后面的字段就无法继续使用索引。
这就是最左前缀原则的本质,它不是 MySQL 随便定的规则,而是由联合索引的物理存储顺序决定的。
4.2 常见的联合索引失效场景
假设表结构如下:
CREATE TABLE `order` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `product_id` BIGINT NOT NULL, `status` TINYINT NOT NULL DEFAULT 0, `create_time` DATETIME NOT NULL, PRIMARY KEY (`id`), KEY `idx_user_prod_status` (`user_id`, `product_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;下面这几条 SQL 到底走不走索引,是面试的经典考点。
-- 走索引:查询条件包含最左边字段 user_id SELECT * FROM `order` WHERE user_id = 100; -- 走索引:虽然只查了第一个和第三个字段,但 user_id 在联合索引里最左,status 本身也是索引字段 -- 实际效果:MySQL 会先通过 user_id 精确定位,再对 status 做过滤 SELECT * FROM `order` WHERE user_id = 100 AND status = 1; -- 索引部分失效:跳过 product_id,直接查 status -- 结果:user_id 可以走索引定位,但 status 无法走索引,因为它在联合索引中被 product_id 隔开 SELECT * FROM `order` WHERE user_id = 100 AND status = 1; -- 完全失效:没有包含最左字段 user_id SELECT * FROM `order` WHERE product_id = 1000 AND status = 1;这里要特别注意第二条和第三条的差别。很多人都以为“只要查询条件里有联合索引中的字段,索引就一定生效”,这个理解是不完整的。联合索引的生效程度,取决于查询条件命中了联合索引定义中的哪些前缀字段。
更精确地说,user_id = 100 AND status = 1这条 SQL 中,user_id用于索引定位,status只能用做索引内过滤,无法继续利用索引的有序性来减少扫描范围。面试时如果能说出这一层,会明显拉开差距。
4.3 范围查询对联合索引的影响
再看一个范围查询的例子。
SELECT * FROM `order` WHERE user_id = 100 AND product_id > 1000 AND status = 1;这条 SQL 中,user_id = 100是等值匹配,product_id > 1000是范围匹配,status = 1是在范围条件后面的字段。由于product_id上已经使用了范围查询,它右边的status无法继续利用索引的有序性进行精确定位。
这就是网上常说的“范围查询右边的字段索引失效”。需要注意的是,这里的“失效”指的是无法利用索引做快速定位,而不是索引完全不生效。user_id和product_id本身仍然会用到索引。
面试答题时把这个细节讲清楚,会比只背结论好得多。
5. 单列索引的失效场景:题目“陷阱”重灾区
单列索引的失效场景虽然没有联合索引复杂,但在面试中同样高频出现。常考的包括函数计算、隐式类型转换、模糊查询、or 连接、不等于条件等。
下面统一用一张表来演示。
CREATE TABLE `account` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `account_no` VARCHAR(64) NOT NULL, `balance` DECIMAL(10,2) NOT NULL DEFAULT 0, `mobile` VARCHAR(20) DEFAULT NULL, `create_time` DATETIME NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_account_no` (`account_no`), KEY `idx_mobile` (`mobile`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;5.1 索引列上使用函数
-- 错误示例:索引列套了函数,索引失效 SELECT * FROM account WHERE DATE(create_time) = '2025-01-01'; -- 推荐写法:把条件改写成范围查询,索引生效 SELECT * FROM account WHERE create_time >= '2025-01-01 00:00:00' AND create_time < '2025-01-02 00:00:00';第一条 SQL 中,DATE(create_time)对索引列做了函数运算,MySQL 无法直接利用 B+ 树的有序性进行查找。更合理的思路是把函数运算转换成对原始列的范围查询。
5.2 隐式类型转换
最典型的场景是:字段类型是 VARCHAR,查询条件里却写了数字。
-- account_no 是 VARCHAR 类型 -- 这条 SQL 会导致索引失效 SELECT * FROM account WHERE account_no = 123456; -- 正确写法:加上引号,保持类型一致 SELECT * FROM account WHERE account_no = '123456';原因是 MySQL 会尝试把字符串列转换成数字进行比较,导致索引列上发生隐式类型转换。一旦索引列参与类型转换,索引就可能失效。
5.3 前缀模糊查询
-- 无法走索引:% 开头的模糊匹配,B+ 树的有序性无法利用 SELECT * FROM account WHERE mobile LIKE '%8901%'; -- 可以走索引:后缀模糊匹配仍然可以利用索引 SELECT * FROM account WHERE mobile LIKE '138%';LIKE '%keyword%'无法利用索引,原因在于 B+ 树是根据键值大小排序的,只有知道前缀才能快速定位。没有前缀条件时,只能遍历。
5.4 使用 or 连接非索引列
-- 如果 mobile 有索引,id 是主键,or 两边都有索引,可以走索引 SELECT * FROM account WHERE mobile = '13800138000' OR id = 1; -- 如果 status 没有索引,or 的另一边是索引列,整体无法走索引 SELECT * FROM account WHERE mobile = '13800138000' OR status = 1;or 条件要求两边条件都能走索引,才能用索引合并或者分别走索引。只要有一边是全表扫描,整个查询就可能退化成全表扫描。
这里给出一个面试回答技巧:遇到“这条 SQL 会不会走索引”的问题,不要只回答“走”或“不走”,而是先说明“索引列本身有没有被破坏”,再说明“查询条件是否符合索引用法”。这个答题结构很加分。
6. 覆盖索引与索引下推:两个容易混淆的进阶概念
覆盖索引和索引下推是面试中区分度最高的两个点,也是实际项目里优化回表和减少回表次数的关键机制。
6.1 覆盖索引(Using index)
覆盖索引是指:查询需要读取的字段,全部包含在某个二级索引的叶子节点中。这种情况下,查询只需要遍历二级索引,不需要回表。
前面已经演示过:
-- 覆盖索引:id 和 age 都能从 idx_age 里拿到,不需要回表 EXPLAIN SELECT id, age FROM user WHERE age = 30; -- 需要回表:name 不在 idx_age 中,必须回表 EXPLAIN SELECT id, age, name FROM user WHERE age = 30;实际项目中,高频查询可以针对性地设计覆盖索引。比如页面列表只需要展示 id、标题、状态,就可以建立一个包含这三个字段的联合索引,从而减少回表 IO。这就是面试中常说的“用空间换时间”的一种具体体现。
6.2 索引下推(Index Condition Pushdown)
索引下推是 MySQL 5.6 开始支持的优化,简称 ICP。
解释一下没有 ICP 时的流程:当二级索引查到一条记录时,如果查询条件里还有非索引字段的过滤条件,MySQL 需要先回表,拿到完整行数据后,再在 Server 层判断过滤条件是否满足。
有了 ICP 之后,部分过滤条件可以在索引遍历过程中直接判断。不满足条件的记录直接跳过,减少回表次数。
-- 联合索引 idx_user_prod_status(user_id, product_id, status) -- status 本不属于联合索引的可定位前缀,但可以用于索引内过滤 SELECT * FROM `order` WHERE user_id = 100 AND status = 1;如果没有 ICP,MySQL 会先通过user_id找到一批数据,然后全部回表,再判断status = 1。有了 ICP 后,MySQL 可以先用status = 1在索引层过滤掉不需要的记录,再回表。
面试时经常出现这样的追问:为什么这条 SQL 明明没有完整使用联合索引,但 SQL 执行计划里 type 是 ref 或 range,rows 也比较小?答案往往就是 ICP。
判断执行计划是否使用了索引下推,看 Extra 字段是否出现Using index condition即可。
7. 慢查询定位与调优实战流程
面试和工作的区别在于:面试要求你说出原理,工作要求你快速定位并解决问题。下面是一个完整的实战流程。
7.1 开启慢查询日志
先确认当前慢查询日志是否开启。
SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'long_query_time'; SHOW VARIABLES LIKE 'slow_query_log_file';如果没开启,可以在 MySQL 配置文件中开启,修改后需要重启 MySQL 服务。
[mysqld] slow_query_log = ON slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 log_queries_not_using_indexes = ON说明:long_query_time = 1表示查询时间超过 1 秒的记录会被记入慢查询日志;log_queries_not_using_indexes表示没有走索引的查询也会被记录。生产环境建议设置一个合理阈值,不要一开始就全局开启,避免日志量过大。
7.2 慢查询日志分析
可以用 MySQL 自带的mysqldumpslow工具汇总慢查询日志。
# 按平均查询时间排序,查看前 10 条 mysqldumpslow -s at -t 10 /var/log/mysql/mysql-slow.log也可以直接查看日志中的某一条慢 SQL,拿到后做两件事:
第一步,用 EXPLAIN 分析执行计划。看 type 是不是 ALL,key 是不是 NULL,rows 是不是很大,Extra 里有没有 Using filesort 或者 Using temporary。
第二步,分析表结构和数据分布。确认索引是否存在,字段类型是否匹配,是否有函数或类型转换破坏了索引。
7.3 一个典型的调优案例
假设业务上有这样一条慢 SQL:
SELECT id, user_id, amount, status FROM payment WHERE status = 1 AND create_time BETWEEN '2025-01-01' AND '2025-01-31' ORDER BY create_time DESC;Explain 后看到 type = ALL,rows 接近整表行数。此时优先考虑建立联合索引。由于查询中有等值条件status,又有范围条件create_time,联合索引字段顺序建议把等值字段放在前面。
ALTER TABLE payment ADD INDEX idx_status_create_time (status, create_time);在 MySQL 5.6 及以上版本中,索引下推可以进一步减少回表次数。这条 SQL 依靠(status, create_time)联合索引,先定位 status 范围,再在索引内过滤 create_time 范围,整体效率会明显提升。
不过这里有一个需要注意的坑:如果只建单列索引idx_status,查询里又有ORDER BY create_time,那么排序可能无法利用索引有序性,Extra 里会出现Using filesort。联合索引 (status, create_time) 的另一个优势在于,它可以让排序也直接走索引。
8. 索引调优的面试加分项与常见误区
到了这一步,原理、失效场景、Explain 和调优流程都讲完了。接下来整理几个能在面试里“加分”的判断,以及几个常见的回答误区。
8.1 区分度决定索引价值
索引不是越多越好。一个字段的区分度如果很低,比如 status 只有 0 和 1 两种值,那么单独给 status 建索引很难起到明显的过滤效果。
面试时可以说:对于低区分度字段,单独建索引往往收益有限,更合理的方案是把它作为联合索引中的等值条件字段,与其他高区分度字段配合使用。
8.2 联合索引字段顺序怎么定
联合索引字段顺序的总原则是:先等值,后范围;区分度高的字段放前面,但也要考虑查询频率。
为什么区分度高的字段放前面?联合索引的 B+ 树先按第一个字段排序,如果第一个字段区分度很高,同一值的记录数量很小,后续字段的排序和定位就更高效。
不过,这个原则不能绝对化。如果一个字段区分度很高但业务上很少作为查询条件,优先放入联合索引反而浪费。实际设计时要结合业务查询模式来定。
8.3 控制索引数量,关注写入开销
每个索引都对应一棵 B+ 树。写入数据时,不仅需要更新聚簇索引,还需要同步维护所有二级索引。索引越多,写入开销越大。
面试中如果被问到“加索引有什么代价”,不要只回答“占磁盘空间”,还要回答“写入性能下降”和“优化器选择成本增加”。这样才能体现工程意识。
8.4 常见误区与正确理解
| 误区 | 正确理解 |
|---|---|
| 字段有索引,SQL 就一定走索引 | 索引列发生函数运算、隐式类型转换等会导致索引失效 |
| 多个单列索引等于联合索引 | 联合索引才能同时高效处理多个字段的等值和范围条件 |
| 没有走索引就加索引 | 还要先确认 SQL 写法是否破坏了索引列,否则加了也白加 |
| 走了索引就一定快 | 如果大量回表、扫描行数很多,性能仍然可能很差 |
| 索引越多越好 | 索引会占用磁盘,增加写入开销,需要平衡读写场景 |
8.5 生产环境索引变更的注意事项
线上表加索引之前,需要考虑三个问题:
第一,数据量。千万级以上的表直接执行ALTER TABLE ADD INDEX可能锁表时间较长,产生业务影响。更稳妥的思路是评估在线 DDL 能力,并选择业务低峰期执行。
第二,验证方式。索引上线前,先在测试环境用真实数据量或抽样数据跑一遍 Explain,确认 key、rows、Extra 符合预期。
第三,回滚方案。如果上线后出现写入变慢或者查询计划异常,要能快速删除新索引并恢复原配置。
9. 常见问题与排查思路速查表
面试和实际排查中,最常遇到的几类问题整理成下表,建议收藏备用。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 明明建了索引,type 还是 ALL | 查询条件写错,或索引列被函数、类型转换破坏 | EXPLAIN 查看 key 和 Extra | 改写 SQL,避免在索引列上做运算或隐式转换 |
| 查询走了索引,但还是很慢 | 发生了大量回表 | 查看 Extra 是否只有 Using where,没有 Using index | 改造为覆盖索引,或减少 SELECT 的字段 |
| 联合索引查询,部分条件没生效 | 查询条件不符合最左前缀原则,或范围条件右边的字段失效 | 对比查询条件与索引字段顺序 | 调整联合索引字段顺序,或新增更匹配的联合索引 |
| ORDER BY 语句出现 Using filesort | 没有可用索引支持排序 | 查看 Extra 中的 Using filesort | 建立包含排序列的联合索引,让排序走索引 |
| 同一 SQL 在测试环境快,线上慢 | 线上数据量大,统计信息有偏差,或优化器选择不合适 | 查看 EXPLAIN 中 rows 和实际数据分布 | 更新统计信息,或使用索引提示 |
| 加索引后写入变慢 | 二级索引过多,写入维护成本高 | 查看表上索引数量 | 删除低收益索引,平衡读写性能 |
10. 如果准备面试,建议这样实践
索引调优的知识,只靠看文章很难真正变成“现场推理能力”。最有效的做法是找一台本地 MySQL,自己建表、自己造数据、自己写 SQL,然后用 EXPLAIN 验证自己的判断。
建议你按下面的清单过一遍:
第一,找一个至少 10 万条数据的表,自己构造联合索引,分别测试不同查询条件下的 Explain 结果,验证最左前缀原则。
第二,把索引失效的几类场景——函数运算、隐式类型转换、LIKE 前缀模糊、or 连接——全部写出来,逐个看 Explain,确认 type 和 key 的变化。
第三,熟悉 Extra 字段里的常见值:Using index、Using where、Using index condition、Using filesort、Using temporary,知道每个值对应的优化方向。
第四,准备一个小型慢查询案例,从开启慢查询日志、获取慢 SQL、Explain 分析到建立索引、再次验证,完整走一遍流程。
面试时,能用手里的 SQL 和 Explain 结果支撑自己的结论,比背出多少条理论都更有说服力。MySQL 索引调优本质上不是记忆竞赛,而是判断竞赛。理解索引的底层组织方式,掌握一条慢 SQL 的分析路径,才是应对“夺命连环问”的真正底气。