MySQL 面试高频考点 · 图解

五个被问到最多的原理题。每一个都可以点开看它到底怎么运作——不是背结论,是看过程。

B+ 树:为什么 3 层就能放两千万行

面试官问这句,考的不是背诵,是你会不会算。算完你就明白了为什么 InnoDB 敢让所有查询都从树根开始走。

单行数据大小 1.0 KB

拉一下这个滑块,看行变宽之后能存的量掉得多快。

L1根节点 1 页 · 只存「键 + 指针」 1,170 个指针
↓
L2中间层 1,170 页 · 仍然只存键和指针 1,368,900 个指针
↓
L3叶子层 存完整行,用双向链表串起来 2,190 万行

这个数字是怎么来的

页大小16 KB = 16,384 字节
每个指针主键 8 B + 页指针 6 B = 14 B
一页放多少16,384 ÷ 14 ≈ 1,170 个
叶子页能放16 行(一页 = 16 KB ÷ 1.0 KB)
3 层合计1,170 × 1,170 × 16 ≈ 2,190 万行
所以「查一次只要 3 次 IO」这句话是有前提的。根节点几乎必定常驻 Buffer Pool,中间层也很少被淘汰,所以真实物理 IO 通常只有 1~2 次。这就是为什么几千万行的表,按主键查一行依然是毫秒级。
反过来说,这也是「大表加字段很危险」的原因。行变宽 → 每页装的变少 → 叶子层页数暴增 → 树的层数可能从 3 变 4 → 所有查询多一次 IO。你把滑块拉到 4 KB 就能看到这个过程。

回表:为什么多查一个字段就慢了一倍

假设有表 t(id 主键, a, b),并且在 a 上建了普通索引。下面两条 SQL,只差一个字段,代价差一倍。

idab
15x
27y
35z
49w

索引 idx_a 里到底存了什么

这是整道题的关键:二级索引的叶子节点存的是「索引列 + 主键值」,不是整行。所以:(5,1)、(5,3)、(7,2)、(9,4),按 a 排好序。

① 走 idx_a
定位到 a=5 的两条,拿到 id = 1、3
→
② 回表
拿 id 再查一次主键聚簇索引
→
③ 返回结果
b = x、z
走几棵树1 棵
是否回表否
结论覆盖索引
面试怎么答:「因为 a 上有索引,SELECT a 要的数据全在索引里,不用回表,这叫覆盖索引。SELECT b 就得多一步:从二级索引拿到主键,再回聚簇索引取整行。返回条数越多,回表的随机 IO 越贵——这就是 SELECT * 在大结果集上要命的地方。」
一个常被追问的点:那 SELECT id, a FROM t WHERE a = 5 需要回表吗?不需要。因为二级索引叶子本来就带主键值,id 是免费的。

最左前缀:为什么 (a,b,c) 用不了 b

建了联合索引 idx_abc (a, b, c)。点下面的条件,看哪几列真正参与了定位。

上下两行同一列互斥。试试只点 a = 1 和 c = 3,或者把 b 换成 b > 2。

a
—
—
b
—
—
c
—
—
索引定位a、b
只能过滤无
最终扫描—
规则只有一句:索引是按 a 排序、a 相同再按 b、b 相同再按 c 存的。所以你必须先能定位到 a,才有可能定位 b。断在任何一列,后面全部失效。
但「失效」不等于一点用没有。MySQL 5.6 之后有索引下推(ICP):虽然 b、c 不能用来缩小定位范围,但它们能在索引层先过滤一遍,不必都回表。所以 a = 1 AND c = 3 这种,c 是「不能定位、但能过滤」。
顺带记住两个高频坑:
① WHERE a = 1 AND b > 2 AND c = 3 —— b 用了范围,c 就断了,只能 ICP。
② WHERE a = 1 ORDER BY b 可以直接用索引的有序性;但 ORDER BY c 不行,会 Using filesort。

MVCC:同一行数据,两个事务看到的不一样

这是 MySQL 事务里最难讲清的一块。核心只有两个东西:版本链(undo log 串出来的)和 ReadView(决定你只能看见哪几个版本)。

隔离级别

时间线

① T2 第 1 次查询(此时 T1 还没提交)

行记录本身长什么样(两个隐藏列)

InnoDB 每行记录都有两个你看不见的列:trx_id(最后修改它的事务号)和 roll_pointer(指向 undo log 里上一个版本)。版本链就是靠 roll_pointer 串起来的,方向是从新到旧。

trx 20balance = 200最新版本 · T1 改出来的
↓ roll_pointer
trx 10balance = 100undo log 里的旧版本
T2 读到balance = 100
ReadViewm_ids = [20],min = 20,max = 31
判定trx 20 在 m_ids 里 → 不可见,沿指针往下找

判定规则,只有三条

从最新版本往下走,逐个判断,遇到第一个「可见」的就返回:

版本的事务号结论
小于 min_trx_id可见——这事务在我之前就提交完了
大于等于 max_trx_id不可见——这事务在我快照之后才开
在中间看是否在 m_ids 里:在 → 不可见;不在 → 可见(已提交)
RR 和 RC 的唯一区别,就是 ReadView 什么时候生成。
RC:每一条 SELECT 都新建一个 → 别人一提交,你下一条就能看见(不可重复读)。
RR:第一次 SELECT 时建一个,整个事务复用 → 别人提交了你也看不见(可重复读)。
把开关切到 RC 再拖动时间轴,你会看到值从 100 变成 200 —— 同一条 SQL,结果变了。
最后一句必须会说:MVCC 只对快照读(普通 SELECT)生效。SELECT ... FOR UPDATE、UPDATE、DELETE 都是当前读,读最新版本并加锁。

间隙锁:为什么 RR 会锁住一行不存在的记录

表里只有 5、10、15、20 这四个值。点下面的 SQL,看它到底锁住了哪一段——包括那些不存在的区间。

5
10
15
20
id=5
id=10
id=15
id=20
(-∞, 5)
(5, 10)
(10, 15)
(15, 20)
(20, +∞)
加锁范围(5, 10]
锁类型Next-Key Lock(记录锁 + 间隙锁)
被挡住的插入INSERT id = 6, 7, 8, 9 全部阻塞
为什么要锁「不存在的行」?为了防幻读。如果只锁 id=10 这一行,另一个事务插一条 id=7 再提交,你的事务里再查一次范围条件,就凭空多出一行——这就是幻读。间隙锁锁的是「这个空隙不许插」,从源头堵住。
代价也在这:间隙锁之间是互相兼容的(两个事务可以同时持有一段间隙锁),但它会阻塞所有插入。所以线上偶发的 Deadlock found 和「插入莫名卡住」,很大一部分是 next-key lock 造成的。这也是很多公司把隔离级别压到 RC 的原因——RC 下间隙锁基本不生效。
还有一个必考特例:如果 id 是唯一索引,且查询条件能命中唯一一行(WHERE id = 10),InnoDB 会把 next-key lock 退化成记录锁,只锁那一行,不锁间隙。