1. 先想清楚一个问题:索引和事务为什么总被放在一起考
最近团队做技术复盘,候选人几乎人手一份 MySQL 面试题,翻来覆去绕不开两块:索引和事务。我一开始觉得这只是面试官的偷懒题库,后来帮新人排查线上慢查询,又处理了一单库存扣减对不上账的问题,才意识到这两块知识根本就是同一个问题链上的两环——索引负责让 SQL 更快,事务负责让数据更稳,而它们底层都落在 InnoDB 的存储引擎上。
今天我不打算把教科书里的定义搬一遍,而是想从实际开发中“如果我真的在设计一个订单系统,我会关心什么”的角度,把索引和事务的核心知识点重新梳理一遍。文章面向的是用过 MySQL 但没系统学过原理的开发者,也可能对刚准备面试的人有帮助。你不需要提前掌握多少底层知识,但如果你写过几条 join 查询、加过几个普通索引、用过 @Transactional 注解,那么这篇文章里的很多问题你大概率都踩过。
先说清楚为什么这两个知识点值得一起复习。建索引和开事务,表面上互不相干,一个管读,一个管写,但 InnoDB 的行锁、MVCC、redo log 全都同时建立在索引和事务之上。事务要保证数据隔离,必须依赖索引去定位行;索引要保证高并发写入不乱,必须依赖事务日志去回滚和恢复。你如果只懂索引不认识事务,遇到“为什么这个 select 会锁住整个表”根本说不清;只懂事务不懂索引,遇到“为什么死锁检测报了错”又无从下手。所以这篇文章我会把两者之间互相咬合的部分也讲透,而不是单纯分成两个章节硬拼在一起。
最后说下阅读建议:如果你赶时间,可以直接跳到第三章的索引失效清单和第六章的事务失效场景,这两个小节像速查手册,能帮你先确认自己有没有踩过常见坑;如果你想把原理串起来,建议从第二、第五章开始按顺序读,我尽量用大白话把 B+ 树和 undo log 讲明白,不堆名词。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 索引篇:从 B+ 树到一条 SQL 是怎么走的
2.1 先回答一个最底层的问题:为什么 InnoDB 非要用 B+ 树
我在给新人讲索引时,习惯先问一个问题:如果你要在千万行数据里做等值查询和范围查询,你会选什么数据结构?哈希表,单行等值查确实快,但一遇到 id > 100 这种范围条件就直接跪了;平衡二叉树,查找稳定在 O(log n),但树高随数据量增长,而且每个节点只存一个数据,磁盘 IO 次数会多到让你怀疑人生;B 树解决了节点存储的问题,但非叶子节点同时还存着数据,同样的磁盘空间能装下的索引键数量变少,树自然比 B+ 树更高更胖。
B+ 树的解法是:把所有真正的数据行只放在叶子节点,并且叶子节点之间通过指针串成有序链表,非叶子节点只存索引键和子节点指针。这样整个结构可以获得两个红利。第一,树更矮更宽,三层 B+ 树基本就能承载千万级数据,磁盘 IO 次数被压到三次以内;第二,叶子节点有序,配合链表指针做范围扫描、排序、分组都极其顺滑,where id between 100 and 200 这种查询只要定位到起始位置,然后顺着链表往下读就行,不需要像 B 树那样频繁回溯父节点。
但这里有个新手特别容易忽略的点:B+ 树的“有序”不是天然免费的,它依赖主键顺序去维护排列。如果你用了 UUID 做主键,新插入的行主键是随机的大字符串,B+ 树为了维持有序,会频繁触发节点分裂和页重组,写入性能被拖得很厉害。这也是 InnoDB 为什么强烈建议使用自增主键的原因——有序追加写入的成本远低于随机插入。并不是 MySQL 不让你用 UUID,而是你要清楚这笔隐藏的账,备选方案可以做成“UUID 业务主键 + 自增 id 隐藏主键”的组合,牺牲一点存储换写入稳定。
2.2 聚簇索引和非聚簇索引:一次回表到底回的是什么
我把这个点单独拎出来,是因为很多人背过“聚簇索引叶子节点存整行数据,非聚簇索引叶子节点存主键”,但完全不理解这会造成什么实际差异。
InnoDB 的表本质上就是一个以主键为排序顺序的聚簇索引结构。你建的表如果没有显式主键,它会找第一个非空的唯一键当主键;都没有,就生成一个隐藏的 rowid 做聚簇索引。任何二级索引,也就是我们平时用 create index 建的普通索引、联合索引,它的叶子节点存的不是整行数据,而是对应的主键值。这意味着你走二级索引查询时,通常要分两步:先在二级索引的 B+ 树里找到主键,再拿主键去聚簇索引里定位整行数据。第二步,就被叫做回表。
举个具体例子,假设你有一个用户表,主键是 id,name 上有普通索引。执行 select * from user where name = '张三',MySQL 会先在 name 索引树里找到主键 id,再拿 id 到主键索引树里回表取整行。但如果执行的是 select id, name from user where name = '张三',由于查询需要的字段 id 和 name 都已经存在于 name 索引树中,MySQL 发现不需要回表,这种状态就叫覆盖索引,查询效率会明显高出一档。
索引设计里威力很大的一条优化思路就是“尽量把查询需要用到的字段塞进索引树里,避免回表”。所以你在联合索引里不要把字段顺序拍脑袋随便定,而是要考虑哪些字段用于过滤,哪些字段只是用来覆盖查询的。关于这条怎么落地,在下文索引规划里我详细说。
3. 写 SQL 前需要想明白的索引规划:联合索引、失效场景、下推机制
3.1 联合索引的顺序,真的不是你想怎么排就怎么排
联合索引的字段顺序决定索引树的第一排序关键字、第二排序关键字……它直接影响一条 SQL 能否命中最左前缀。我自己见过太多同事踩这个坑:他们建了 (a, b, c) 联合索引,以为 where b = 1 和 where c = 2 也能用的飞起,结果一查还是全表扫描。
先解释最左前缀原则的真正含义。联合索引相当于建立了一棵按多个字段逐级排序的树:先按 a 排,a 相同的再按 b 排,b 相同的再按 c 排。这意味着你可以利用的查询必须从最左边连续匹配。能用到这个索引的查询组合包括 (a)、(a, b)、(a, b, c),以及 a 字段先确定值、b 字段和 c 字段在各自范围内匹配的情况。而 where b = 1 and c = 2 这种把 a 跳过的查询走进索引树后会发现无
