1. 聊聊为什么面试官总爱问索引
先说实话,我在后台看到有人留言问"能不能系统整理一下索引相关的面试题",刚好最近也在帮团队做技术面试,前前后后面了差不多三十个候选人,索引这块几乎是必考题。不是面试官偷懒,而是索引这个知识点真的太能看出一个人的水平了——你说你懂索引,那我问你B+树为什么是主流?覆盖索引能带来多大的性能提升?一条SQL明明走了索引却还是慢,问题出在哪?这几个问题一抛出去,是真懂还是背八股,立刻见分晓。
这篇内容不会跟你整那些"索引是什么"的教科书定义,直接从面试实战角度出发,把高频考点掰开揉碎了讲。不管你是准备校招还是社招,或者纯粹想把手上的SQL写得更优雅一点,这篇文章都值得认真读完。我尽量用大白话把原理讲清楚,配合实际案例,让你能真正理解而不是死记硬背。
我整理了一下近一年面试中索引相关的高频问题,大致可以归成几类:数据结构与原理、索引失效场景、SQL优化实战、复合索引设计、以及一些偏门但爱考的冷知识点。下面一个个来。
2. 先从最基础的问起:索引到底是个什么结构
2.1 为什么是B+树而不是哈希表
面试官问你索引结构,十有八九会追问一句"为什么用B+树"。记住,哈希索引虽然是O(1)的查询复杂度,看起来比B+树的O(logN)要快,但哈希表有个致命问题——它只能做等值查询,做不了范围查询。你在京东上买手机,想筛个"价格3000到5000"的商品,哈希索引直接就懵了,它没法比较大小。
再对比一下其他树结构。红黑树在内存里确实很强,但数据库的数据是存在磁盘上的,磁盘I/O才是真正的瓶颈。红黑树每个节点最多两个子节点,数据量一上来,树的高度就很吓人。我举个例子,2000万条数据的表,红黑树的高度得有二十多层,每查一次就要做二十多次磁盘I/O,这对数据库来说是不可接受的。
B+树在这方面做了两个关键优化。第一,非叶子节点只存索引键值不存数据,一个节点能放下更多的键,树就变矮了。InnoDB一个页默认16KB,主键如果是bigint占8字节,一个页大概能存上千个键,2000万条数据的树高也就三到四层。第二,叶子节点之间用双向指针串联,范围查询拿着起始位置顺着链表往后扫就行了,这效率哈希表根本没法比。
注意:MySQL的InnoDB引擎是聚簇索引,数据本身就在主键索引的叶子节点上。你建了二级索引之后,叶子节点存的是主键值,查的时候还要回表拿数据。这里埋个坑,后面讲覆盖索引的时候会详细说。
2.2 主键索引和二级索引的区别
这个问题看着简单,但很多人答不完整。主键索引(聚簇索引)叶子节点存的是整行数据,二级索引叶子节点存的是主键值。所以你在建表的时候选主键要慎重——如果主键是随机UUID,插入的时候数据页会频繁分裂,性能很难看。这也是为什么现在主流都推荐自增主键或者有序的雪花ID。
有个面试追问很常见:"如果一个表没有主键怎么办?"答案是InnoDB会选第一个非空的唯一索引作为聚簇索引;如果连这个都没有,就生成一个隐式的6字节rowid。这个隐藏在背后的机制会影响你对表结构的判断,我见过不少新人建表不设主键,真的不建议这么干,除了性能问题,后续数据同步和binlog解析都会很难受。
2.3 联合索引的存储结构
联合索引其实就是一个多列排序的B+树。假设你有索引(a, b, c),物理存储上先按a排序,a相同再按b排序,b也相同再按c排序。这个排序规则直接决定了"最左前缀原则"为什么存在——你跳过了a直接用b去查,B+树就不知道从哪儿开始遍历,索引自然就废了。
还有一个容易被忽视的点:联合索引和单列索引是竞争关系。同一个表,你建了(a, b)又建了(a),MySQL优化器大概率会走(a, b),因为它的信息量更大。所以建索引的时候要想着合并,别一根一根地插。
3. 面试必问:索引失效的几种情况
3.1 这条SQL为什么没走索引
索引失效的题目在面试中出现频率极高,就我统计的样本,差不多三分之二的候选人会被问到这里。而且这个问题通常会配一道活题,给你一条实际SQL让你判断有没有走索引。我把常见的失效场景整理成了一张表,都是我实际碰到的:
| 场景 | 失效原因 | 示例 |
|---|---|---|
| 对索引列使用了函数 | 索引存的是原始值,函数破坏了顺序 | WHERE DATE(create_time) = '2025-01-01' |
| 隐式类型转换 | 优化器认为需要转类型,放弃索引 | WHERE phone = 13800138000(phone是varchar) |
| 前缀模糊匹配 | 无法定位起始位置 | WHERE name LIKE '%张' |
| 索引列参与了运算 | 破坏了索引值的有序性 | WHERE age + 1 > 30 |
| 用OR连接非索引列 | 存在一条全表扫描的路径 | WHERE id = 1 OR name = '张三' |
| 联合索引不满足最左前缀 | 无法使用索引树的排序规则 | 索引(a,b),但条件只写了b |
| NOT IN / NOT EXISTS | 优化器认为回表代价过高 | WHERE status NOT IN (1, 2) |
这里我要多说一句不常见的坑。IS NOT NULL在某些数据分布下也可能走全表扫描,因为MySQL的优化器是基于代价估算的,如果它认为null值占比极低,直接扫表反而比走索引快。理解了这一点你就明白,索引失效的底层逻辑就三条:破坏了有序性、优化器认为走索引代价更高、数据分布太拉胯。
3.2 最经典的一道面试题
面试官最喜欢拿来当开场白的一道题是:select * from user where name like '%张三%',这个查询怎么优化?
很多人第一反应说"放弃这个查询需求,改成搜索引擎",这种回答不接地气,在实际业务里我们就得靠数据库扛。我当时遇到的场景是用户表800万条数据,做模糊搜索,确实不走索引,全表扫描400多毫秒,勉强能撑但已经不好受了。
我给面试官的优化思路是这样的:第一,如果是前缀匹配(张三%),直接建普通索引就行,B+树的排序特性可以支持前缀匹配加速。第二,如果必须是中缀或后缀匹配,可以考虑给表加冗余列存倒序值,后缀匹配%三可以用reverse(name) like '三%'加速。第三,覆盖索引能不能救场要看查询列是否都在索引里,比如只查id和name就能走索引下推(ICP),多少有点效果。第四,实在不行就上全文索引或者同步到ES,这属于架构层面的方案了。
这个回答的完整度,基本就能看出候选人的实际经验。纸上谈兵的人只会说"加索引",有经验的人会说"先看explain的type级别,再看过滤字段的区分度,然后看是否能把随机IO转成顺序IO"。
4. 高阶必考:覆盖索引与索引下推
4.1 覆盖索引为什么快
覆盖索引指查询的所有字段都在索引里,不需要回表。这事的核心收益是省掉了回表的随机I/O。B+树的数据库在磁盘上做随机读和顺序读,成本相差可能有一个数量级。所以把高频查询的字段塞进索引里,是性价比极高的优化手段。
我举个具体的例子。订单表有索引(shop_id, order_no),业务上要查某个店铺最近的订单号列表,SQL写成select order_no from orders where shop_id = 123 order by order_no desc limit 10。由于order_no已经在索引里面,MySQL扫描完索引就可以返回结果,完全不用碰数据行。要是再加个字段比如order_amount,就必须要回表了,因为amount不在这个索引里。
这也是为什么InnoDB把主键放在二级索引的末尾——每一个二级索引都自动携带了主键字段。你建索引(shop_id)的时候,实际上索引结构是(shop_id, id)。所以一个查询如果只需要id和shop_id,就算你只建了shop_id的索引,它也能实现覆盖索引。这个细节很隐蔽,但面试时说出来会让面试官眼前一亮。
4.2 索引下推是什么
索引下推(Index Condition Pushdown,ICP)是一块硬骨头。MySQL 5.6引入的优化,核心思想是把WHERE条件里面、索引覆盖不到的部分从Server层"下推"到存储引擎层,让存储引擎先把不符合的行过滤掉,减少回表次数。
网上有很多话术版本,我来说个容易理解的场景。联合索引(username, age),查询条件是username like '张%' and age = 25。如果没有索引下推,存储引擎会把所有username以"张"开头的行都捞出来,然后回表取整行数据,Server层再对age条件做过滤。如果有索引下推,存储引擎在遍历索引的过程中,直接就把age不等于25的记录跳过去了,只对剩下的少数行回表。
这个优化在索引区分度低、回表代价高的场景下收益非常明显。但注意了,索引下推对覆盖索引无效(因为覆盖索引压根不用回表,也就不需要"减少回表"的逻辑),对分区表支持也有限制。面试时能主动提一句"ICP开启条件是二级索引、且没有覆盖索引",说明你确实使用过explain看到过Using index condition这个标志。
5. 最容易被问但容易答飞的:倒排索引
5.1 倒排索引和数据库索引的关系
热门搜索词里"倒排序索引"出现了好几次,这其实是MapReduce经典案例,也侧面说明现在面试的广度在增加。倒排索引最典型的应用是全文搜索引擎——比如Elasticsearch、Lucene的核心结构。
我得先澄清一个概念:倒排索引和普通B+树索引是两回事。B+树解决的是"我有这个主键/键值,怎么快速找到数据",倒排索引解决的是"我有一段文本,怎么快速找到包含某个词的所有文档"。一个是正向查,一个是反向查。
倒排索引的结构也不复杂。先对每个文档做分词,然后维护一张词表,每个词后面挂一个"文档ID列表"(叫posting list)。你搜"索引"这个词,搜索引擎查一下词表,直接拿到包含"索引"的所有文档ID,像查字典一样快。
5.2 MapReduce排序与倒排索引的实现思路
这是大数据方向的经典题:"第1关:MapReduce排序-倒排序索引"。本质是让你用MapReduce实现一个简单的倒排索引构建过程。我用大白话讲一下流程:
- Map阶段:读取每个文档,按词分词,输出
(词, 文档ID)这样的键值对。 - Partition和Shuffle阶段:框架把同一个词的所有键值对送到同一个Reducer,并且按照词排序(这就是"排序"的作用)。
- Reduce阶段:同一个词会收到来自不同文档的多个记录,把这些文档ID汇总成一个列表,输出
(词, [文档ID1, 文档ID2, ...])。
面试里问这个,重点其实不是让你手写Hadoop代码,而是考察你有没有理解"通过MapReduce的排序特性,可以把散落在多个节点上的数据按目标key汇聚在一起"。这种分布式思维和数据库索引的局部性原理是相通的——都是通过预排序来加速后续查询。
6. 实战向:一张订单表教你设计索引
6.1 用一个业务场景把索引知识串起来
面试到最后,通常还有一个场景设计题。我拿一个最经典的来演练:订单表,总数据量3000万,核心查询有两个:
- 查某个用户最近的订单列表:
select * from orders where user_id = ? order by create_time desc limit 10 - 查某个商家某段时间内的订单统计:
select count(*) from orders where shop_id = ? and create_time between ? and ?
第一个SQL比较简单,建(user_id, create_time)联合索引就行,利用联合索引里create_time的有序性,直接倒序取10条,完美。
第二个SQL看着也简单,建(shop_id, create_time)联合索引。但这里有个很大的坑:count(*)统计需要遍历所有符合条件的索引条目,如果你只需要知道数量,更优做法是建立(shop_id, create_time, id)的覆盖索引,这样计算count时连回表都省了。这恰恰是"覆盖索引优化count"的常见套路。
再来看一个挖坑题:如果第一个SQL里还要加一个and status = 1的条件,会怎么样?理论上联合索引应该是(user_id, status, create_time)还是(user_id, create_time, status)?这是一个很多社招候选人都会卡住的点。实际上,如果status的区分度不高(就几个值),把它放中间并不会让索引树变得高效,反而破坏create_time的连续性。更好的方案可能是(user_id, create_time)联合索引,同时把status作为一个辅助过滤条件,因为命中user_id和create_time之后数据量已经很小,再用status过滤也没多大开销。
提示:设计索引的第一步永远是分析真实SQL的where条件和order by/group by字段,第二步才是考虑区分度和回表成本。不要一上来就想着"把所有条件都塞进一个索引"。
6.2 索引设计的三个原则和一个反例
我总结过一套实战中的索引设计原则,分享给你:
- 原则一:最左前缀优先,查询频率高的列放最前面,但别把区分度低的列(比如性别)放第一位。
- 原则二:尽可能覆盖高频查询,把select的列塞进索引末端,让回表消失。
- 原则三:控制索引数量,一个单表索引建议不要超过5个,因为每次写入都要维护索引树,索引太多会拖垮写性能。
一个反例是我曾经接手过一个业务表,16个索引,查一条记录都要几百毫秒。为什么?因为查询优化器在选择索引的时候需要基于统计信息做判断,索引一多,估算时间就长,而且索引互相干扰导致选择不是最优。后来我们砍到5个联合索引,把高频查询全部覆盖,效果确实立竿见影。
6.3 什么时候该用其他搜索引擎
数据库索引不是银弹。当你的查询涉及到全文检索、地理位置搜索、或者需要很复杂的聚合分析时,MySQL不是最好的工具。
- 全文检索:用Elasticsearch,倒排索引天生就是干这个的。
- 地理位置搜索:用Redis的GEO或者PostgreSQL的PostGIS。
- 大规模OLAP分析:用ClickHouse这类列式存储数据库,宽表聚合性能碾压MySQL。
面试时遇到"索引能解决所有性能问题吗",一定别答"能"。更好的回答是:索引解决的是OLTP场景下高频等值、范围查询的加速问题。超出这个场景,我们要考虑的是架构层面的选型。
7. 面试实战记录:一套完整的索引问答流程
为了让你更有代入感,我模拟一下真实面试现场。面试官坐对面,表情平静,抛出一张纸:
有一张用户表 user(id, name, phone, status, created_at),数据量1000万。现有一条查询特别慢:
select name, phone from user where status = 1 order by created_at desc limit 20,你会怎么优化?
这道题我把好的回答路径和踩坑路径都分析一下。
踩坑回答:直接说"给status加索引、给created_at加索引"。面试官追问"然后呢",就没下文了。
好的回答分四步:
- 先看执行计划。大概率走了status索引然后文件排序(filesort),因为created_at和status不在同一个索引里。
- 建一个联合索引
(status, created_at),这样记录先按status过滤,同时created_at有序返回,不再需要排序。 - 选字段name和phone不在索引里,需要回表。如果这是超级高频的查询,可以把联合索引扩展成
(status, created_at, name, phone),实现覆盖索引避免回表。 - 最后还要评估status的区分度。如果status是"0或1",区分度极低,MySQL统计后觉得走索引也捞不出来多少值,可能直接选择全表扫描了,这时就得换个思路,比如考虑把高热度数据单独存Redis缓存。
这套思路走下来,面试官基本就知道你是真的调过优的。尤其是第4步,绝大多数背题的人是想不到的。
8. 关于索引,你必须知道的几个"为什么"
8.1 为什么说主键默认就是索引
这里要理解InnoDB的物理存储。表本身就是一棵以主键为key的B+树,数据行是叶子节点上的"值"。所以主键天然是聚簇索引,不需要额外建。这也是为什么"mysql 创建索引"的热搜词下面总有人问重复问题——很多人以为主键和索引是两个独立的东西。
8.2 为什么无索引的字段偶尔也能查到快结果
数据量很小的时候,全表扫描也可能比走索引快。这个反常现象背后的逻辑是:数据量小,一次全表扫描就是几个数据页的顺序读,而走索引需要先查索引树再回表,是两次随机读。MySQL的优化器会根据表大小估算代价,所以小表真的没必要加索引。
8.3 为什么InnoDB要额外存储一个唯一索引?
这个问题偏冷,但是爱考原理的面试官会提。唯一索引和普通二级索引的区别在于,唯一索引的叶子节点上不只有主键值,还存储了"这个值在插入时需要检查唯一性"的约束。二级索引可以重复,唯一索引不能重复。物理上它们存储方式相似,但逻辑语义不同。
8.4 关于Oracle和MySQL的索引差异
热词里有oracle视图加索引、oracle索引相关的问题。Oracle和MySQL的优化器机制有很大不同。Oracle基于CBO(基于代价的优化器),支持位图索引、函索索引、分区索引等很多类型;MySQL的索引类型相对简单,主要就是B+树和Hash。Oracle视图不会自动使用基表的索引,除非视图语句里写了或视图是物化视图,这也是面试中容易答错的地方。
9. 维护索引的日常功课
9.1 定期用工具查看索引状态
MySQL里有一条非常经典的分析语句:
-- 查看索引使用情况 SHOW INDEX FROM orders; -- 分析表优化索引统计信息 ANALYZE TABLE orders; -- 查看特定SQL是否使用了索引 EXPLAIN SELECT * FROM orders WHERE user_id = 10086;在explain的输出里,重点关注几个字段:type(从好到坏依次是const > eq_ref > ref > range > index > ALL),rows(扫描行数估算),Extra(如果出现Using filesort或者Using temporary就需要考虑索引优化了)。
9.2 索引冗余要定期清理
开发周期变长之后,很多临时建的索引会变成僵尸索引。我曾经上线前排查过一张表,发现有两个索引完全互相覆盖,等于白白浪费了接近2GB的磁盘空间。定期清理冗余索引应该是DBA日常巡检的固定项目。
排查方法也很简单,打开慢查询日志收集一段时间,结合pt-duplicate-key-checker这类工具去分析。不过对很多中小团队来说,可能没有专职DBA,那我在附录里给你一个最简易的自查清单:
- 查看是否有前缀完全相同的索引
- 查看没有被任何查询使用的索引(可以通过
performance_schema.table_io_waits_summary_by_index_usage或者开启userstat来观察) - 删除索引之前先在线测试,避免高峰期操作
9.3 索引失效自查清单
我总结了一份避坑清单,踩过的坑都在这了:
- 确认字段是否有隐式类型转换,尤其varchar字段与数字比较
- 确认排序字段和过滤字段是否都在同一个联合索引
- 确认OR连接的条件是否都参与了同一个索引
- 确认是否对索引列使用了内置函数
- 确认联合索引的顺序是否与where条件的顺序匹配(不是必须完全一致,但最左前缀必须保证)
这套自查清单在面试的时候直接背出来,比你说"我懂索引优化"有说服力得多。
10. 扩展延伸:当索引遇到排序和分组
10.1 filesort到底有多伤
很多SQL慢不是因为查询条件,而是因为排序。MySQL在无法利用索引来满足order by的时候,就会在内存或磁盘上做文件排序(filesort)。数据量一大,filesort就要把数据分块写到临时文件里,再归并排序,这个开销极其恐怖。
所以在设计索引的时候,你要把order by的列跟where条件放在同一个索引里面。比如where user_id = ? order by create_time desc,联合索引(user_id, create_time)直接告诉你数据已经有序,查询直接就快了。
10.2 group by其实也耗索引
group by本质上"分组计数"。如果group by的字段不在索引中,MySQL会对全表做临时表分组。你可以利用联合索引让分组字段有序,这样执行计划里就不会出现Using temporary。
举个例子:select shop_id, count(*) from orders group by shop_id。shop_id没有索引的话,MySQL会把3000万行全部load到内存里建哈希表做分组。给shop_id建了索引之后,扫描索引就可以按shop_id的顺序直接累积计数,内存占用和执行时间都会大幅下降。
11. 最后分享一点个人感受
写了这么多,我想说点跟面试题稍微不一样的东西。索引这个话题之所以常问常新,是因为它连接了计算机的底层原理(磁盘I/O、数据结构、排序算法)和上层业务设计(查询模式、数据分布、读写比例)。
我在实际调优中最深的体会是,不要一上来就动手加索引,先花时间看慢查询日志、看explain、了解业务对这个查询的期望响应时间。绝大多数性能问题根本不需要什么高深技巧,一张覆盖索引就搞定了;真正困难的地方在于,怎么在正确的地方放正确的索引,同时不拖累写性能和其他查询。
还有一个个人经验:面试准备索引,别只刷"索引失效的原因",试着把每道题都还原成"我遇到过的真实场景"。我当时给自己出了一套自测题,包括"为什么明明建了索引却不用""为什么这个索引能覆盖查询但不能覆盖排序""为什么增加一个字段后索引突然失效了",把这几个"为什么"彻底搞懂之后,再去看网上那些八股文,一眼就能看出哪些是真正重要的知识点,哪些只是为了凑数。
这套思路放到日常工作中同样成立。慢SQL从来不会自己变快,但它会诚实地告诉你,索引设计哪里出了问题。希望你面试顺利,也希望大家生产环境的SQL都健步如飞。