先回答一个很多人在准备数据库面试时都会问的问题:MySQL 索引调优到底在面什么?
如果只看网上流传的“八股文”,你会发现索引相关的问题翻来覆去就是 B+Tree、聚簇索引、回表、最左前缀。这些概念确实重要,但真正到了面试现场,面试官不会只问你“什么是回表”,而是会换着花样考察你的理解深度和应用能力。
比如:
- 为什么明明给 where 条件里的字段建了索引,SQL 执行计划里还是走了全表扫描?
- 联合索引 abc 三列,查询条件是 b 和 c,为什么用不上索引?
- 一个几千万行的表,分页查最后几页特别慢,怎么优化?
- order by 排序字段到底要不要加索引?
这些问题的答案,都在索引调优里。
这篇文章要把 MySQL 索引调优和面试中最核心的考点拆开揉碎讲清楚。我做了这些内容规划:
- 先讲索引的底层数据结构,帮你把 B+Tree 为什么适合数据库这个问题彻底搞懂;
- 然后讲 InnoDB 的聚簇索引与二级索引,把回表、覆盖索引、索引下推这些高频考点一次说透;
- 接着结合 explain 执行计划,带你把一条慢 SQL 从“为什么慢”到“怎么改”完整走一遍;
- 再给出一批真实面试题和对应分析思路,覆盖联合索引、索引失效、深分页、排序优化、隐式转换等高频场景;
- 最后聊聊生产环境里建索引的工程规范与常见坑。
如果你正在准备数据库面试,或者工作中经常被慢查询折磨,这篇文章值得收藏后反复看。
1. 索引调优,先记住这一句话
索引调优这件事,如果只记一句话,那就是:索引是数据库用来加速数据查询的一种有序数据结构,它通过牺牲写入性能来换取查询性能。
这句话里有三个关键信息,对应面试常考的三个方向:
- “有序数据结构”对应的是:索引为什么底层用 B+Tree,而不是二叉树、哈希表或跳表。
- “加速查询”对应的是:索引到底解决了什么问题,MySQL 在哪些场景下会走索引,哪些场景下索引会失效。
- “牺牲写入性能”对应的是:索引不是越多越好,因为每次 insert、update、delete 都要同步维护索引,索引过多会拖慢写入。这是生产环境建索引时必须考虑的代价。
很多人背了一堆索引优化的口诀,却答不好一道看似简单的面试题,根源就在于没有建立起“索引是拿空间和写入耗时换查询耗时”的底层认知。
所以这篇文章不打算上来就罗列知识点,而是沿着“数据结构 -> 存储形态 -> 执行计划 -> 优化实践 -> 面试题”这条主线,把索引调优的知识体系串起来。你按这条线去复习,比零散地刷题效果要好得多。
2. 面试必问:为什么 MySQL 索引用 B+Tree?
2.1 哈希表、二叉树、B-Tree 有哪些问题?
先看几个“为什么不用”的理由,这是面试里非常容易考到的问题。
哈希索引的查询速度是 O(1),按等值查询时非常快。但哈希索引有两个致命问题:
- 不支持范围查询。因为哈希表把数据打散存储了,
age > 18这种查询没法利用哈希索引加速。 - 不支持排序。哈希表本身不维护有序性。
二叉查找树在数据量大时树的高度会变得很高。比如一棵存储一千万条记录的二叉树,如果数据分布不够均匀,树的高度可能达到 20 层甚至更高。每访问一层树节点,在存储引擎层面往往对应一次磁盘 IO。20 次磁盘 IO 在数据库场景里是不可接受的。
**B-Tree(多路平衡查找树)**解决了树高问题,它每个节点可以存多个键值和多个子节点指针,所以同样的数据量,B-Tree 的高度远低于二叉树。MySQL 默认的 InnoDB 页大小是 16KB,一个节点(页)里可以塞很多索引键值,一般三到四层就能存储千万级甚至亿级数据。
但 B-Tree 还有个问题:它的每个节点既存索引键,也存数据(或者说所有数据都集中在叶子节点),每个节点的空间利用率有限。而且 B-Tree 在做范围查询时,需要在中序遍历上做额外处理,相邻叶子节点之间缺少直接链接。
2.2 B+Tree 为何胜出
B+Tree 和 B-Tree 的关键区别有两点:
- 非叶子节点只存索引键,不存数据。这样每个非叶子节点能容纳更多索引键,树更矮,磁盘 IO 次数更少。
- 叶子节点之间通过双向链表连接,并且叶子节点按索引键排序存储。这让范围查询变得非常高效:找到起点后,顺着链表向后遍历即可。
下面是 B+Tree 的简化结构示意(不用严谨到每个指针都画全,抓住特点和面试回答思路即可):
[非叶子节点] [10, 20] / | \ [1..9] [11..19] [21..30] 叶子节点内部有序,叶子之间双向链表相连面试时如果被问到“为什么 InnoDB 用 B+Tree”,可以分四点回答:
- 树的高度低,磁盘 IO 次数少,适合海量数据存储;
- 叶子节点有序且链表相连,范围查询和排序性能好;
- 非叶子节点不存数据,页内能容纳更多索引键,进一步提升扇出;
- 数据都存储在叶子节点,查询路径稳定,所有查询都要走到叶子节点,性能可控。
2.3 与跳表对比:Redis 为什么用跳表?
跳表(Skip List)也是有序数据结构,查询、插入、删除的时间复杂度都是 O(log n)。所以有的面试官会问:既然跳表这么优秀,为什么 MySQL 不用它做索引?
跳表适合内存数据结构的场景,因为它依靠“多层指针”来实现快速查找,在内存中访问指针的开销很低,而且实现比平衡树简单得多。但 MySQL 的数据是持久化在磁盘上的,跳表的每一层指针在磁盘上会产生大量随机 IO,而且节点在磁盘上的物理分布不连续,不利于磁盘预读。B+Tree 的叶子节点按页组织,顺序读性能好,也更契合磁盘的存储特性。
一句话总结:存储引擎选什么数据结构,核心看底层存储介质是内存还是磁盘。内存场景如 Redis 用跳表,磁盘场景如 MySQL 用 B+Tree(虽然现代 SSD 的顺序和随机读写差距在缩小,但 B+Tree 的设计理念仍然成立)。
3. InnoDB 聚簇索引与二级索引,回表问题怎么答?
3.1 聚簇索引和数据行存在一起
InnoDB 的聚簇索引(clustered index)就是主键索引。它的叶子节点直接存储整行记录数据,也就是说,表数据本身就是按主键顺序组织的。
这个设计带来一个重要结论:InnoDB 表必须有聚簇索引。建表时如果没有显式定义主键,InnoDB 会选择一个非空的唯一索引作为聚簇索引;如果也没有唯一索引,InnoDB 会隐藏生成一个 ROW_ID 作为聚簇索引。
所以建表时显式指定主键,不只是一个规范,而是直接影响存储结构和性能的决策。
3.2 二级索引的叶子节点存主键值
除了聚簇索引之外,其他索引都叫二级索引(secondary index),也叫辅助索引或普通索引。二级索引的叶子节点存储的不是整行数据,而是索引列的值 + 主键值。
如果一条 SQL 的查询条件命中了二级索引,但需要返回的列不在索引列中,MySQL 就需要拿着主键值回到聚簇索引里查一遍整行数据,这个过程叫做回表。
回表意味着多一次磁盘 IO、多一次 B+Tree 搜索,所以在索引设计时,我们要想办法减少回表。
3.3 覆盖索引:把查询字段塞进索引
覆盖索引是指:查询的所有列都能在二级索引中找到,不需要回表。只要一条 SQL 用到的 select 列、where 条件列、order by 列都在同一个索引里,就可能实现覆盖索引扫描。
举个例子:
-- 表结构:员工表 CREATE TABLE `employee` ( `id` bigint NOT NULL AUTO_INCREMENT, `emp_no` varchar(32) NOT NULL, `name` varchar(64) NOT NULL, `age` int NOT NULL, `dept_id` bigint NOT NULL, PRIMARY KEY (`id`), KEY `idx_emp_no_age` (`emp_no`, `age`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;如果执行:
SELECT emp_no, age FROM employee WHERE emp_no = '10001';由于idx_emp_no_age索引里已经包含emp_no和age两列,MySQL 直接从二级索引拿到结果,不需要回表,这就是覆盖索引。
但如果执行:
SELECT emp_no, age, name FROM employee WHERE emp_no = '10001';name不在二级索引里,MySQL 必须先通过idx_emp_no_age找到主键 id,再回聚簇索引查整行,从中取出name,这就是回表。
覆盖索引的价值在面试里经常被提到,但真正实践时要注意:不要把太多字段塞进索引,因为索引列越多,索引体积越大,写入成本越高。覆盖索引是“性能优化 + 资源开销”之间的权衡,不是越宽越好。
3.4 索引下推,一个容易被忽略的优化
索引下推(Index Condition Pushdown,ICP)是 MySQL 5.6 引入的优化。
它的核心思路是:在二级索引遍历的过程中,提前使用 where 条件中属于索引列的过滤条件,筛选掉不满足条件的记录,减少回表次数。
假设联合索引是(name, age),执行:
SELECT * FROM employee WHERE name LIKE '张%' AND age = 30;如果没有索引下推,InnoDB 会先按name LIKE '张%'从索引中找出所有匹配的二级索引记录,然后逐条回表到聚簇索引,再对整行数据判断age = 30。
启用索引下推后,在遍历二级索引时就能同时判断age = 30这个条件,提前过滤掉 age 不等于 30 的记录,回表次数大幅减少。
面试中如果被问到索引下推,能讲到“提前过滤减少回表”这一层就算达标,能进一步指出它依赖存储引擎能力、并且只对二级索引生效,就会更显深度。
4. 动手实践:用 explain 读透一条慢 SQL
概念说完了,接下来进入最实战的部分:用explain分析 SQL 执行计划。
4.1 准备一张测试表
先建一张订单表,模拟一个稍微真实一点的数据场景。
CREATE TABLE `t_order` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` bigint NOT NULL COMMENT '用户ID', `sku_id` bigint NOT NULL COMMENT '商品ID', `amount` decimal(10,2) NOT NULL COMMENT '订单金额', `status` tinyint NOT NULL COMMENT '订单状态 1-待支付 2-已支付 3-已取消', `create_time` datetime NOT NULL COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_create_time` (`create_time`), KEY `idx_user_status` (`user_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;4.2 explain 输出里先看哪几个字段
执行:
EXPLAIN SELECT * FROM t_order WHERE user_id = 10086 AND status = 2;输出结果里字段很多,核心看这几个:
| 字段 | 含义 | 重点关注 |
|---|---|---|
| type | 访问类型 | const > eq_ref > ref > range > index > ALL,性能从好到差 |
| key | 实际使用的索引 | 为 NULL 说明没有走索引 |
| rows | 预估扫描行数 | 越小越好,只是预估 |
| filtered | 过滤比例 | 越高越好,100 表示全命中 |
| Extra | 额外信息 | Using index 代表覆盖索引;Using index condition 代表索引下推;Using where 代表存储引擎返回后再过滤;Using filesort 代表需要额外排序,要警惕 |
4.3 实操案例:从全表扫描到命中索引
场景一:没走索引
EXPLAIN SELECT * FROM t_order WHERE status = 2;因为 status 没有独立索引,MySQL 大概率走全表扫描,type 为 ALL,rows 很大。如果订单表有几十万行,这条 SQL 会扫全表,性能很差。
优化方式:给 status 加索引,或者把 status 纳入联合索引。但要注意区分度问题,status 只有 1、2、3 三个值,区分度很低,即使建索引,优化器也可能认为走索引还不如扫描全表快。这在实际项目中很常见,面试也经常考:低区分度字段到底要不要建索引?答案是:单独建索引通常没有价值,但作为联合索引中的一列用于过滤和排序可能有价值。
场景二:走二级索引 + 回表
EXPLAIN SELECT * FROM t_order WHERE user_id = 10086;如果在 user_id 上有普通索引,type 为 ref,key 为 idx_user_id,rows 是匹配的记录数。因为 select * 需要所有列,二级索引不包含完整数据,所以 Extra 里没有 Using index,必然伴随回表。
如果想优化成覆盖索引,可以把 select 列限制为索引覆盖的列,比如只查 user_id 和 status:
EXPLAIN SELECT user_id, status FROM t_order WHERE user_id = 10086;此时 Extra 会显示 Using index,表示不需要回表。
场景三:联合索引命中,但访问类型发生变化
执行:
EXPLAIN SELECT * FROM t_order WHERE user_id = 10086 AND status = 2;如果优化器选择 idx_user_status,此时 type 还是 ref,key 是 idx_user_status。但如果你把条件改成范围查询:
EXPLAIN SELECT * FROM t_order WHERE user_id > 10086 AND status = 2;联合索引(user_id, status)此时只能部分生效:user_id 的范围条件可以用索引定位,但 status 这一列由于 user_id 是范围匹配,索引中 status 的排序在范围内不再保证连续,所以无法继续用索引直接过滤。这就是“范围列之后的索引列会失效”的基本原因。
这也是面试里最经典的一个问题:联合索引下,范围查询为什么会让后面的字段失效?
答案要从 B+Tree 的存储结构解释:联合索引的叶子节点先按 user_id 排序,user_id 相同的记录再按 status 排序。当 user_id 是一个范围(大于 10086),这个范围内包含多个不同的 user_id,而每个 user_id 下的 status 排序是独立的。索引只能保证在某个确定 user_id 下 status 是有序的,无法保证整个 user_id 范围内 status 全局有序,所以优化器无法利用索引继续对 status 做等值定位。
4.4 一个完整的慢 SQL 调优流程
假设你在生产环境收到一条慢查询告警:
SELECT order_no, amount, create_time FROM t_order WHERE user_id = 10086 ORDER BY create_time DESC LIMIT 10;调优可以按下面五步走:
第一步,先用 explain 查看执行计划,确认有没有走索引,type 是什么,Extra 里有没有 Using filesort。
第二步,分析 where 条件。这里user_id = 10086是等值条件,可以使用idx_user_id索引。
第三步,分析 order by。ORDER BY create_time DESC需要按 create_time 排序。注意idx_user_id索引的叶子节点只在相同 user_id 内按主键排序,并没有按 create_time 排序,所以用它执行时会出现 Using filesort。
第四步,针对性优化。既然查询条件是 user_id + create_time,可以考虑建一个联合索引:
ALTER TABLE t_order ADD INDEX idx_user_create_time (user_id, create_time);这样一来,二级索引的叶子节点先按 user_id 排序,同一个 user_id 内按 create_time 排序,order by 可以直接利用索引顺序,避免额外排序。
第五步,再次执行 explain,确认 Extra 中没有 Using filesort,type 为 ref,rows 明显减少。
这个五步流程就是面试官最想看到的“调优思路”:定位慢 SQL、分析执行计划、明确瓶颈类型(回表 / 排序 / 全表扫描 / 扫描行数多)、通过索引调整解决问题、用 explain 闭环验证。
5. 索引失效的 8 类典型场景
“索引失效”是索引调优面试里题量最大的板块。下面八类场景,每一类都对应真实的线上事故,务必记牢。
5.1 对索引列做了函数计算
SELECT * FROM t_order WHERE DATE(create_time) = '2026-01-01';即使 create_time 上有索引,DATE() 函数会让优化器无法直接使用索引对 create_time 进行定位。因为索引里存的 create_time 是完整时间值,但查询条件是日期值,无法按索引顺序查找。
推荐改成范围查询:
SELECT * FROM t_order WHERE create_time >= '2026-01-01 00:00:00' AND create_time < '2026-01-02 00:00:00';5.2 对索引列做了隐式类型转换
如果 order_no 是 varchar 类型,索引也是 varchar 类型,但查询条件写成了数字:
SELECT * FROM t_order WHERE order_no = 10086;MySQL 会把字符串列和数字比较时尝试将字符串转为数字,隐式类型转换会让索引失效。更稳妥的写法是带上引号:
SELECT * FROM t_order WHERE order_no = '10086';5.3 联合索引不满足最左前缀法则
联合索引(user_id, status, create_time)可以用于查询条件:
- user_id
- user_id + status
- user_id + status + create_time
但如果跳过第一列直接查 status 或 create_time,则无法使用这个联合索引。
这里要特别说明:MySQL 8.0 引入了“跳跃扫描”(Skip Scan)优化,在某些条件下,即使查询条件没有使用联合索引第一列,优化器也可能选择扫描联合索引。但不要依赖这个特性,面试和实践中都建议按最左前缀设计索引。
5.4 like 以通配符开头
SELECT * FROM t_order WHERE order_no LIKE '%10086%';由于模糊匹配没有固定前缀,B+Tree 无法利用索引的有序性做快速定位。但如果是前缀匹配:
SELECT * FROM t_order WHERE order_no LIKE '10086%';此时可以走索引。
5.5 or 连接的条件存在非索引列
SELECT * FROM t_order WHERE user_id = 10086 OR status = 2;如果 user_id 有索引而 status 没有独立索引,or 条件会让优化器很难直接利用索引。因为 user_id = 10086 可以走索引,但 status = 2 需要全表扫描,两部分结果还要合并,优化器最终可能选择全表扫描。稳妥做法是:给或条件中的每一列都建可用的索引,或者把 SQL 改写成两个查询后用 union all 合并。
5.6 使用不等于或 not in
SELECT * FROM t_order WHERE status <> 2;不等于条件很难直接利用索引做快速定位。因为索引是有序的,优化器可以快速找到等于某个值的记录,但“不等于”需要排除一部分值,范围过大时优化器就会放弃索引。
5.7 低区分度字段独立建索引
比如性别、订单状态这种只有几个取值的字段,选择性太低。优化器估算后发现走索引需要大量回表,反而比直接扫描全表更慢,于是放弃索引。生产环境里常见的选择是:低区分度字段不要单独建索引,需要和其他高区分度字段组成联合索引。
5.8 数据量太小,优化器放弃索引
如果表只有几百行数据,全表扫描几乎不需要成本,优化器就不会走索引。这种情况不是索引失效,而是优化器认为“没必要用”。
面试答题时可以把这 8 类归纳成一条主线:索引能否被使用,取决于 B+Tree 的有序性是否还能被利用,以及优化器对扫描成本的估算。凡是破坏了索引有序性、导致无法有序搜索、或让回表代价过大的操作,都可能导致索引失效。
6. 高频面试题分析与标准答题框架
这一节盘点数据库 MySQL 面试中最容易出现、也最考验功底的几类索引题。建议不要死记硬背“标准答案”,而是掌握每道题背后的分析框架。
6.1 深分页为什么慢?怎么优化?
SELECT * FROM t_order ORDER BY create_time LIMIT 100000, 10;这条 SQL 慢的原因不是最后 10 条数据本身,而是 MySQL 需要先扫描到第 100000 行,再向后读 10 行。如果是二级索引排序,还要伴随大量回表,代价非常高。
常见的优化方案有三种。
第一种,标签记录法(也叫游标分页)。记住上一页最后一条记录的 create_time 和 id,下一页用条件去过滤:
SELECT * FROM t_order WHERE create_time < '2026-01-01 18:00:00' ORDER BY create_time DESC LIMIT 10;注意排序字段必须有唯一性兜底,否则可能出现相同排序列时数据遗漏。实际做法通常是ORDER BY create_time, id,并在 where 中用(create_time, id)组成复合比较条件。
第二种,子查询延迟关联。先通过覆盖索引查出需要的主键 id,再 join 原表取完整数据:
SELECT t.* FROM t_order t INNER JOIN ( SELECT id FROM t_order ORDER BY create_time LIMIT 100000, 10 ) tmp ON t.id = tmp.id;子查询里只用索引列(id + create_time),可以走覆盖索引,减少回表消耗。
第三种,业务上限制查询深度。比如后台管理系统只允许用户翻前 100 页,或者强制按时间范围查询。
深分页的面试答题框架是:先解释“为什么慢”,再给出优化方案,最后说明各方案的适用场景。
6.2 联合索引字段顺序怎么定?
这是没有标准答案、但最能区分候选人的题。
回答时可以从两个维度分析。
维度一:区分度。区分度高的字段放前面。比如(user_id, status)比(status, user_id)更合理,因为 user_id 的取值更多、选择性更强,可以先快速缩小查询范围。
维度二:查询模式。联合索引要贴合实际的 SQL 查询条件。如果业务上主要按 user_id + status 查询,那就应该优先保证这两个字段都能命中索引,而不是机械地按区分度排序。
还要考虑排序需求:如果查询里有WHERE user_id = ? ORDER BY create_time,那联合索引(user_id, create_time)不仅过滤了 user_id,还让 order by 直接走索引顺序,一石二鸟。
面试时如果能答出“区分度优先、查询频率优先、排序需求优先”的组合判断逻辑,就已经超过大多数背题选手了。
6.3 主键为什么要用自增?UUID 做主键有什么问题?
这个问题的内核是聚簇索引对数据插入性能的影响。
自增主键的特点是:新插入的行主键值比已有值更大,B+Tree 叶子节点从左往右顺序增长,新数据直接追加到最右边的叶子节点,页分裂概率低,插入性能稳定。
UUID 主键是随机值,新数据可能插入到 B+Tree 的任意位置,容易导致页分裂,产生碎片。而且 UUID 是字符串类型,占用的存储空间更大,二级索引里存的都是主键值,UUID 主键会让每个二级索引体积也变大。
所以 InnoDB 表的推荐做法是自增 bigint 主键,业务上需要分布式唯一 ID 时,也不建议直接把订单号当主键,而是两者同时保留,让订单号走唯一索引。
6.4 反连接:为什么 select * 不推荐?
select * 和覆盖索引直接相关。如果一个查询可以用覆盖索引扫描,而你把 select 列写成 *,就无法使用覆盖索引优化,只能回表取所有字段。把 select 列精确到需要的字段,不仅减少网络传输量,还给 MySQL 创造了走覆盖索引的机会。
面试中如果被问到“覆盖索引的意义”,可以顺便把 select * 的问题带出来,这样的回答是立体的,不是孤立背知识点。
6.5 order by 排序字段如何建索引?
当 order by 的字段没有索引时,MySQL 需要把查询结果放到排序缓冲区排序。如果数据量超过排序缓冲区大小,还会使用磁盘临时文件进行外部排序,性能很差。explain 中 Extra 显示 Using filesort 就是这个信号。
优化方式是把 order by 字段加入索引,让 MySQL 直接按索引顺序读取数据,避免额外排序。
但要注意:只有当 order by 字段满足了联合索引的排序规则时,索引才能替代排序。比如联合索引(a, b),查询WHERE a = 1 ORDER BY b可以利用索引;但如果查询WHERE a > 1 ORDER BY b,由于 a 是范围,b 无法保持全局有序,优化器仍可能选择 filesort。
回答这类题时,可以把“排序优化”归纳成两句话:让 order by 字段尽量进入索引,并且满足与 where 条件字段的组合顺序。
7. 必须把执行计划字段背下来
执行计划是索引调优的“仪表盘”,不能被跳过。除了前面提到的 type、key、rows、Extra,还有几个字段在面试和排查中也很重要。
7.1 type 字段的完整语义
- system:表只有一行,属于 const 的特例,几乎不会出现。
- const:最多只有一行匹配,主键或唯一索引等值查询时出现。
- eq_ref:连表查询中被驱动表通过主键或唯一索引等值匹配,每次只匹配一行。
- ref:通过普通索引等值匹配,可能匹配多行。
- range:范围查询,比如 >、<、BETWEEN、IN 等。
- index:遍历索引树,通常比 ALL 好一点,但如果查询条件不充分,代价也可能很高。
- ALL:全表扫描,最差的情况。
7.2 Extra 字段的含义
- Using index:覆盖索引,SQL 要的列都从索引中取得,没有回表。
- Using where:存储引擎返回记录后,MySQL Server 层再做条件过滤。
- Using index condition:发生了索引下推,存储引擎层在索引遍历时完成了部分条件过滤。
- Using filesort:需要额外排序,性能隐患,尽量优化。
- Using temporary:使用临时表,通常出现在 group by、distinct、多表排序等场景,也要警惕。
掌握了这些字段,再配合 explain 的输出,就能回答“这条 SQL 为什么会慢”和“怎么验证优化效果”这两类问题。
8. 生产环境索引设计的最佳实践
面试和实际工作隔着一层。把索引设计做好,真正依赖的是一套工程规范。
8.1 每个表都要有主键,优先自增 bigint
显式定义主键,不要依赖 InnoDB 的隐藏 ROW_ID。业务主键(如订单号、身份证号)可以做唯一索引,不要直接作为聚簇索引主键。
8.2 索引列要遵循“少而精”原则
索引不是越多越好。每多一个索引,insert、update、delete 时都要额外维护。一个表建议控制索引数量,比如单表不超过 5 到 6 个索引(不是硬性规定,是提醒你注意代价)。删除长期未使用、重复度极高的索引。
8.3 优先用联合索引,而不是建多个单列索引
两个单列索引和一个联合索引并不等价。联合索引可以同时服务于多个查询条件,并且能支持覆盖索引和排序优化。设计联合索引时,把等值条件字段放前面,把范围条件字段放后面。
8.4 对字符串索引考虑前缀索引
如果某个字段是长字符串,比如订单号、URL、文章标题,整列建索引会占用大量空间。可以使用前缀索引:
ALTER TABLE t_article ADD INDEX idx_title_prefix (title(20));前缀索引能显著减少索引体积,但也有代价:无法用于 order by 和 group by,也无法做覆盖索引。全列匹配时建议配合业务场景权衡。
8.5 在测试环境用 explain 和慢查询日志验证
线上变更要先在测试环境验证。开启慢查询日志,定期收集慢 SQL:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;注意这些是 session 或全局动态变量,生产环境需要结合配置文件持久化,具体配置因 MySQL 版本和部署方式不同,以实际环境为准。
8.6 大表加索引要注意 DDL 锁
在几千万行的大表上直接执行 alter table add index,可能造成长时间锁表,影响线上读写。常用方案是使用在线 DDL(Online DDL),InnoDB 在多数 alter 操作下支持在线执行,但即便如此,也建议在业务低峰期操作。更稳妥的方式是用 gh-ost、pt-online-schema-change 这类工具控制加索引的速率,降低主从延迟。
面试时提到这一点,能直接体现你的生产环境经验。
8.7 关注索引下推和优化器估算的边界
索引设计完成后,定期用 explain 检查高频 SQL 的执行计划。尤其当数据量发生数量级变化时,优化器的选择可能会改变。今天走索引的查询,半年后数据量翻倍,可能就不再走索引了,这不是索引坏了,而是优化器的成本模型在发生变化。
9. 容易被忽略的索引调优冷知识点
这部分内容不是面试的必考题,但答出来会给面试官留下“这个人是真做过优化”的印象。
9.1 主键顺序对插入性能的影响
如果主键不是自增的,比如业务生成的有序字符串主键,插入时仍然可能出现页分裂。只有自增主键(或单调递增的主键)才能最大程度避免随机插入。
9.2 二级索引数量与写入放大
每次写入一行数据,MySQL 需要同步维护该表上的所有索引。索引越多,写入放大越严重。对写多读少的业务,索引设计尤其要克制。
9.3 索引并不能解决所有排序问题
使用了 order by,但排序字段存在函数或表达式时,索引无法生效。比如ORDER BY LENGTH(name)、ORDER BY amount + 1这种写法,索引帮不上忙。
9.4 分组查询也可能用到索引
group by 的本质也是排序或分组扫描。如果 group by 字段满足索引顺序,可能避免 using temporary,优化器直接按索引扫描顺序分组,性能更好。
9.5 explain 的 rows 只是估值
rows 是优化器基于统计信息估算出的扫描行数,不是精确值。如果统计信息不准确(比如发生过大量增删改但未更新统计信息),rows 可能偏差较大。MySQL 8.0 引入了直方图等能力辅助优化器估算,但在实践里,最终判据还是要看实际查询耗时和响应时间。
10. 一套可复用的索引调优自查清单
把这套清单贴到团队文档或者自己面试前翻一翻,能省不少时间。
| 检查项 | 详情 | 如何验证 |
|---|---|---|
| 查询条件是否有可命中索引 | where 中的等值、范围条件对应的字段是否在索引中 | explain 查看 key |
| 是否出现隐式类型转换 | 字符串列和数字比较 | 检查索引列类型与参数类型 |
| 是否有函数操作 | 对索引列使用函数、表达式 | 改为范围查询或冗余字段 |
| 联合索引是否满足最左前缀 | 查询条件从联合索引第一列开始 | 查看 where 条件顺序 |
| 是否产生回表 | select 列不在索引中 | 查看 Extra 是否出现 Using index |
| 是否产生 filesort | order by 字段与索引顺序不匹配 | 查看 Extra 中的 Using filesort |
| 是否产生临时表 | group by、distinct 未用好索引 | 查看 Extra 中的 Using temporary |
| 是否扫描行数过多 | rows 偏大 | 考虑增加索引或改写 SQL |
| 分页效率是否合理 | 深分页是否超出业务需求 | 使用游标分页或限制分页深度 |
| 低区分度字段是否单独建了索引 | 状态、类型等字段独立索引 | 考虑联合索引替代 |
| 大表 DDL 是否评估过影响 | add index 是否可能锁表 | 使用在线 DDL 工具,低峰期执行 |
| 是否有重复索引 | 多个索引覆盖了相同查询前缀 | 使用数据库工具检查冗余索引 |
11. 面试现场怎么回答索引类题不翻车
最后聊一点面试技巧层面的内容,这部分对正在准备 MySQL 面试的读者最实用。
11.1 先给结论,再给证据
面试官问“这条 SQL 为什么慢”,不要上来就背索引失效的八个场景。先说结论:“这条 SQL 大概率是全表扫描,扫描行数多,还可能伴随额外排序”,然后补充证据:“explain 里 type 是 ALL,rows 是 10 万,Extra 里面还有 Using filesort”。
用执行计划当证据,比空谈概念有说服力得多。
11.2 遇到不会的问题,从存储结构推导
很多索引面试题没有固定答案。遇到没准备过的场景,可以从 B+Tree 的有序性、回表代价、优化器成本模型这三个角度去推导。
比如面试官问“in 查询一定走索引吗”,你不要直接说“一定”或“不一定”。你可以说:in 本质上是一组等值查询,如果 in 的值少、区分度高,优化器可能选择 range 访问;如果把 in 列表写得很大,可能导致扫描大量数据,优化器估算后可能改走全表扫描。然后补充:explain 里看 type 是否变为 range,rows 是否剧增即可确认。
这种推导思路背不下来,但一旦理解底层逻辑,遇到新题也能应对。
11.3 准备一个真实的调优案例
在面试中,一个完整调优案例比背二十个知识点更有价值。把你们项目里的一条真实慢 SQL 整理成“问题现象 -> explain 分析 -> 索引调整 -> 优化前后对比”四段式,面试时主动讲出来,比被动答题加分很多。
12. 下一步该往哪个方向学
索引调优是 MySQL 性能优化的核心,但它不是孤立的知识点。真正想在这个方向上走得深,建议按下面的路径继续补课:
- 学习 InnoDB 的锁机制和事务隔离级别,理解索引与锁的关系。比如间隙锁是在索引记录之间加锁,不同隔离级别下加锁范围不同。
- 深入学习优化器工作原理。为什么 MySQL 会“选错索引”?这涉及索引基数、统计信息、成本模型等知识。
- 学习慢查询日志、performance_schema、sys schema 等诊断工具,掌握系统化定位性能问题的方法。
- 了解分库分表下的索引设计。当单表数据量过大,索引再优化也撑不住时,数据拆分是另一个层面的问题。
索引调优这件事,没有“学会”的终点,只有不断在真实数据和业务场景里发现问题、分析问题、解决问题的循环。把 explain 用熟,把 B+Tree 和回表讲清,把联合索引的设计逻辑想透,你不仅能应付面试,也能在真实项目里做出更合理的数据库设计。