MySQL 题如果只答「加索引」,很难拿到高分。面试官更想听的是:为什么这条 SQL 用不上索引、这条更新加了什么锁、RR 到底解决了什么又没解决什么。以下默认 InnoDB、MySQL 8.0。
| 场景 | 关键机制 | 常见误判 |
|---|---|---|
| 按主键查一行 | 聚簇索引,叶子即行数据 | 以为二级索引也要回表 |
| 按普通索引查整行 | 二级索引 → 主键 → 回表 | 忽略回表成本 |
| 查询列都在索引里 | 覆盖索引,免回表 | 以为必须 SELECT * |
| 范围条件 + 索引过滤 | 索引下推(ICP) | 以为过滤一定回表后做 |
| 高并发读写同一行 | 行锁 + 一致性读 | 以为 RR 下读都不加锁 |
1. InnoDB 的索引结构是什么?聚簇索引和二级索引有什么区别?
InnoDB 用 B+ 树组织索引。主键索引是聚簇索引,叶子节点直接存放整行数据,所以按主键查最快;如果没有主键,会选第一个非空唯一索引,再没有就生成隐藏的行 ID。
二级索引的叶子存放的是索引列加主键值。查整行时需要先查二级索引拿到主键,再回聚簇索引取数据,这个动作叫回表,也是「索引很多但查询仍然慢」的常见原因。
B+ 树的非叶子节点只存键和指针,扇出大、层数少,通常三到四层就能覆盖千万级数据。页大小默认 16KB,顺序主键插入能减少页分裂,随机 UUID 主键则会带来更多分裂和碎片。
MyISAM 的索引和数据分离,索引叶子存的是行地址,没有聚簇概念,也不支持事务和行锁。
2. 哪些写法会导致索引失效?
最常见的是在索引列上做函数或运算,例如 WHERE DATE(create_time) = '2026-10-01'、WHERE id + 1 = 10。应改成范围条件,或建立函数索引(MySQL 8.0.13 起支持)。
隐式类型转换也会失效:字符串列与数字比较时,MySQL 会把列转成数字,索引无法使用。参数类型要和列定义一致。
其他情况包括:LIKE '%关键字' 前导通配;复合索引不满足最左前缀;OR 两侧有一侧没有索引;!=、NOT IN 在选择性差时被优化器放弃;对索引列使用 IS NULL 并非一定失效,要看数据分布。
最后要理解「失效」很多时候是优化器的成本选择:当回表行数占比很高,全表扫描反而更便宜,强制走索引未必更快。
3. 什么是覆盖索引和索引下推?
覆盖索引指查询需要的列全部能在索引里拿到,不需要回表。EXPLAIN 的 Extra 会显示 Using index。
例如给 (user_id, status, created_at) 建联合索引,查询 SELECT status, created_at FROM orders WHERE user_id = ? 就能覆盖;如果再加 SELECT *,就得回表。
索引下推(ICP)是在存储引擎层用索引中的列提前过滤,减少回表次数。例如联合索引 (name, age),条件 WHERE name LIKE '张%' AND age = 20,age 可以在索引上先判断,Extra 显示 Using index condition。
覆盖索引和索引下推解决的是不同问题:前者省掉回表,后者减少回表。设计联合索引时,把等值条件放前面、范围条件放后面,通常能同时兼顾过滤和排序。
4. MVCC 是怎么实现的?RR 能完全解决幻读吗?
InnoDB 每行有隐藏的事务 ID 和回滚指针,回滚指针把旧版本串成 undo 链。读取时根据 ReadView 判断哪个版本可见:当前事务 ID 小于最小活跃事务 ID 的版本可见,大于等于最大事务 ID 的不可见,处于活跃列表中的也不可见。
RC 每条语句生成一个新的 ReadView,所以能读到别人已提交的修改;RR 在事务第一次快照读时生成,之后复用,因此同一事务内多次查询结果一致。
但 RR 不能在所有场景下消除幻读:快照读靠一致性视图,当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)会读最新版本并加锁。如果先快照读、再当前读,或者两个事务以不同顺序读写,仍可能看到「幻影」。
间隙锁和 next-key lock 是 RR 下当前读防幻读的手段,代价是更大的锁范围和更高的死锁概率。
5. InnoDB 有哪些锁?间隙锁和 next-key lock 怎么用?
按粒度分表锁、行锁和意向锁;按模式分共享锁和排他锁。行锁实际加在索引记录上,所以「没有索引的更新」可能退化成锁全表。
记录锁锁定单条索引记录;间隙锁锁定两条记录之间的开区间;next-key lock 是记录锁加前面的间隙锁。插入时还会有插入意向锁,用来表示「准备插入这个间隙」,它与间隙锁冲突。
RC 隔离级别下基本只用记录锁;RR 下范围查询会使用 next-key lock。这也是为什么线上常用 RC:锁范围更小、死锁更少,代价是同一事务内可能出现不可重复读。
死锁排查看 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK,MySQL 8 还可以查 performance_schema.data_lock_waits。降低死锁的关键是短事务、固定加锁顺序、WHERE 条件走索引。
6. EXPLAIN 重点看哪些列?慢 SQL 怎么优化?
先看 type:const、eq_ref、ref、range 较好,index 是全索引扫描,ALL 是全表扫描。再看 key 是否命中预期索引、key_len 是否符合联合索引的前缀长度、rows 和 filtered 估算的扫描量。
Extra 信息量很大:Using index 是覆盖索引;Using index condition 是索引下推;Using where 表示还需要在服务层过滤;Using temporary 和 Using filesort 往往意味着排序或分组没有用上索引。
优化顺序通常是:确认 SQL 语义和返回量 → 补或改索引 → 重写查询(避免函数、拆 OR、减少回表)→ 调整分页和批量大小 → 最后才考虑优化器提示。MySQL 8.0.18 起可以用 EXPLAIN ANALYZE 看实际执行耗时。
不要迷信 FORCE INDEX,它会把优化器的判断固化成风险;统计信息不准时先 ANALYZE TABLE。
7. 深分页为什么慢?怎么优化?
LIMIT 1000000, 20 需要先扫描并丢弃前 100 万行,再取 20 行,扫描量和偏移量成正比。即使命中索引,也要付出大量回表或遍历成本。
第一种方案是延迟关联:先在覆盖索引上查出主键,再关联原表取数据。子查询只扫索引,回表只发生在最终 20 行。
SELECT o.*
FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id;
第二种是游标分页:记住上一页最后一条的排序键,下一页用 WHERE id > ? ORDER BY id LIMIT 20。它稳定且高效,但只能顺序翻页,不适合随机跳页。
如果业务确实需要跳页,考虑限制最大页数、改为搜索条件缩小范围,或用搜索引擎/数仓承接这类查询。
8. 大表 DDL 和分库分表要注意什么?
MySQL 8 的 DDL 分 INSTANT、INPLACE 和 COPY 三类:加列、改默认值等在满足条件时可以秒级完成;改字符集、重建索引往往要 COPY 或 INPLACE,期间仍会消耗大量 IO。即使算法支持在线,也要先确认是否持锁、是否影响主从延迟。
执行前检查长事务和未提交事务,避免拿不到元数据锁导致后续请求排队。大表变更常用 gh-ost 或 pt-online-schema-change 做影子表切换,并安排低峰期和回滚预案。
分库分表不是第一选择:先考虑索引优化、归档冷数据、读写分离和分区表。确需拆分时,分片键要兼顾均匀性和查询命中率;跨库 join、分页、聚合和事务都会变复杂,通常要配套全局 ID、数据同步和路由层。
扩容是分库分表最容易低估的部分,提前设计好双写、迁移和校验方案,比事后补救成本低得多。
易错点与自测
- 「建了索引就一定走」和「走了索引就一定快」都不对,要看回表量和选择性。
- RR 的「可重复读」只对快照读成立,当前读看到的是最新已提交数据。
- 间隙锁只在 RR 及以上、且涉及范围条件时明显,RC 下不要照搬 RR 的锁分析。
自测:① 给出一个联合索引并解释最左前缀;② 用 ReadView 的四个字段判断某个版本是否可见;③ 写出深分页的延迟关联写法并说明为什么更快。
参考资料
- MySQL 8.4 参考手册:InnoDB 锁 — 记录锁、间隙锁与 next-key lock
- MySQL 8.4 参考手册:InnoDB 多版本并发控制 — 一致性读与隔离级别
- MySQL 8.4 参考手册:EXPLAIN 输出 —
type、key、Extra各列含义 - MySQL 8.4 参考手册:在线 DDL — INSTANT/INPLACE/COPY 的边界