一、索引原理与结构(M01–M08)
M01. InnoDB 的聚簇索引(主键索引),其 B+ 树叶子节点存储的是?
考点:聚簇索引的叶子节点
A. 主键值 B. 完整的行记录数据 C. 指向行数据的物理地址指针 D. 索引列 + 主键值
答案:B
【考点】聚簇索引与二级索引(非聚簇索引)的存储差异。
【结论】 聚簇索引的叶子节点存的是完整的行记录 —— InnoDB 是索引组织表,数据行就长在 B+ 树的叶子层。选 B。
【逐项辨析】
- A 主键值:错。主键值只是叶子页里用于排序的键,整行数据同样在这一层,题干问的是叶子存的数据。
- B 完整的行记录数据:正确。 聚簇索引的叶子节点就是数据页,完整数据行直接放在叶子层,按主键查一次 B+ 树即可拿到整行。
- C 指向行数据的物理地址指针:错。错在“物理地址”四个字 —— 这是 MyISAM 非聚簇索引的做法(索引与数据分离),InnoDB 不存在这种分离。
- D 索引列 + 主键值:错。错在把二级索引的叶子内容当成了聚簇索引。
【知识点】 聚簇索引(clustered index)的定义:索引与数据在同一棵 B+ 树中,索引的叶子节点就是数据页。InnoDB 的表因此被称为“索引组织表(IOT)”;若未定义主键,InnoDB 会选用一个非空唯一索引,都没有时再隐式生成 6 字节的 ROWID 作聚簇索引键。
| 索引类型 | 叶子节点存什么 | 查完整行需几步 |
|---|---|---|
| 聚簇索引(主键索引) | 完整行记录 | 1 步(查到即拿到行) |
| 二级索引(普通 / 唯一 / 联合) | 索引列 + 主键值 | 2 步(先取主键,再回表) |
| MyISAM 索引(非聚簇) | 行记录的物理地址 | 2 步(按地址读数据文件) |
推导链条:叶子存整行 → 按主键查天然只需一次 B+ 树查找;二级索引叶子只有“索引列 + 主键” → 要取其余列就必须回表;MyISAM 的索引与数据是两个文件 → 叶子只能存地址。叶子放什么,直接决定查询要几步。
【记忆锚点】 “主键索引带全行,二级索引带主键”。
【易混对比】
- 聚簇索引 vs 二级索引:前者叶子 = 整行,后者叶子 = 索引列 + 主键值。
- InnoDB vs MyISAM:MyISAM 索引叶子一律存行地址,没有聚簇索引。
- 换问法:问“二级索引叶子存什么”,答“索引列 + 主键值”;问“MyISAM 索引叶子存什么”,答“行地址”。三者互为镜像,成对记。
【自测】 表 t(id 主键, name, age) 在 name 上建普通索引,SELECT * FROM t WHERE name = 'a' 一共要查几棵 B+ 树?
答:两棵。先查
name二级索引拿主键id,再回聚簇索引取整行,这就是“回表”。与 M03、M04 连考;大厂面试高频。
【知识关联】
- 补题关联:补-22(Buffer Pool 缓存的就是数据页/索引页)、补-25(ICP 在二级索引层过滤,减少回表)。
- 面试/工程:主键查询快的根因就是“数据即索引”;二级索引膨胀往往也是因为叶子要带主键,所以主键类型(自增 vs UUID)会连带拖累所有二级索引(见 M22/M23)。
- 面试追问:① 没有主键也没有唯一索引的 InnoDB 表,聚簇索引键是什么? ② 二级索引叶子上的主键值在什么场景下会成为瓶颈?
【拓展延伸】
- 变式问法:问“MyISAM 索引叶子存什么”答物理地址;问“InnoDB 二级索引叶子存什么”答索引列+主键;问“为什么主键不宜过长”答二级索引叶子都要冗余一份主键。
- 参数/命令:
SHOW INDEX FROM t可看索引与主键关系;information_schema.INNODB_BUFFER_PAGE能看到 Buffer Pool 中缓存的聚簇/二级索引页。主键缺失时可观察DB_ROW_ID(6 字节隐式 ROWID)。
M02. MySQL InnoDB 选择 B+ 树而非 B 树作为索引结构,下列不属于其原因的是?
考点:为什么选 B+ 树
A. B+ 树非叶子节点不存数据,扇出更大、树更矮,磁盘 IO 次数更少 B. B+ 树叶子节点用链表相连,更适合范围查询与排序 C. B+ 树支持更高效的点查询,因为它可以在非叶子节点提前命中目标 D. B+ 树每次查询都要走到叶子节点,因此查询性能更稳定(耗时接近)
答案:C
【考点】B 树与 B+ 树的结构差异及其对磁盘 IO 的影响。
【结论】 C 恰好说反了:能在非叶子节点提前命中目标的是 B 树;B+ 树数据只在叶子、非叶子只有索引键,不存在“提前命中”,故 C 不属于 B+ 树胜出的原因。选 C。
【逐项辨析】
- A 非叶子节点不存数据、扇出更大、树更矮、IO 更少:属正确原因。非叶子只存“键 + 指针”,一页能塞更多键,树高随之降低。
- B 叶子节点用链表相连、更适合范围查询与排序:属正确原因。InnoDB 的叶子节点是双向链表,范围扫描沿链表顺序走即可。
- C 支持更高效的点查询,因为可以在非叶子节点提前命中目标:正确(即“不属于原因”)。 这句话描述的是 B 树;B+ 树的非叶子节点只有索引键、没有数据,无从提前命中,点查询必到叶子。
- D 每次查询都要走到叶子节点、性能更稳定:属正确原因。所有路径长度都等于树高,耗时接近,无抖动。
【知识点】 结构差异一句话:B 树的数据散布在所有节点,B+ 树的数据只在叶子、非叶子纯索引。
| 对比项 | B 树 | B+ 树 |
|---|---|---|
| 数据存放位置 | 所有节点都可能存数据 | 仅叶子节点 |
| 非叶子节点内容 | 键 + 指针 + 数据 | 键 + 指针(无数据) |
| 叶子节点连接 | 无链表 | 双向链表相连 |
| 扇出 / 树高 | 扇出小、树高 | 扇出大、树矮 |
| 点查询耗时 | 不稳定(可能提前命中) | 稳定(必到叶子) |
| 范围查询 | 需中序遍历、多次回溯 | 沿叶子链表顺序扫描 |
扇出推导(InnoDB 默认页 16 KB):非叶子节点每项 = 键值 + 6 字节页指针,一页约可放 1170 项;叶子节点存整行,假设一行 1 KB 则一页约 16 行。于是 3 层 B+ 树可索引 1170 × 1170 × 16 = 21,902,400 ≈ 2190 万行(口头常说「约 2000 万」,写算式时按实数) —— 查任意一行最多 3 次磁盘 IO,这是 B+ 树成为 InnoDB 默认索引结构的根本原因。补充:哈希索引点查询复杂度为 O(1),但不支持范围查询与排序,故只用于 MEMORY、自适应哈希等场景。
【记忆锚点】 “B+ 树数据全在叶子,叶子串成链表”。
【易混对比】
- B 树 vs B+ 树:B 树点查可能更快(提前命中),B+ 树点查更稳定、范围查强得多 —— 数据库选 B+ 树是综合权衡。
- B+ 树 vs 哈希索引:哈希
O(1)但不支持范围、排序与最左前缀。 - B+ 树 vs 红黑树:红黑树是二叉,树高约
log₂N,千万级数据要 20 多层、20 多次 IO,扇出太小。 - 换问法:若题干问“下列哪项是 B+ 树的优点”,C 就成了错误选项。
【自测】 MySQL 为什么不用哈希索引作为默认索引结构?
答:哈希索引不支持范围查询、排序与最左前缀匹配,冲突多时性能还会退化;B+ 树叶子有序且成链,点查与范围查兼顾。与 M01、M05 连考。
【知识关联】
- 补题关联:补-22(Buffer Pool 缓存页命中率与树高直接相关)。
- 面试/工程:3 层 B+ 树约索引两千万行,是“亿级表点查仍可毫秒级”的结构基础;对比 MongoDB/B+ 类存储、LSM 树(写友好读放大)可加深选型理解。
- 面试追问:① 为什么不用二叉搜索树/红黑树做磁盘索引? ② 哈希索引在什么场景仍优于 B+ 树?
【拓展延伸】
- 变式问法:正向问“B+ 树优点”时 C 会变成错误描述;问“B 树相对 B+ 树的可能优势”才对应“非叶子可提前命中”。
- 参数/命令:InnoDB 默认页大小
innodb_page_size=16384;扇出≈页大小/(键长+指针)。自适应哈希innodb_adaptive_hash_index是点查加速,不是替代 B+ 树。 - 完整推导补充:非叶子项≈14B(8B 主键+6B 指针,按 14B 算)→ 16384/14≈1170;叶子若行 1KB 则约 16 行/页 → 1170×1170×16≈2190 万;层数=⌈log_扇出(N/每页行数)⌉。
M03. 表 t(id 主键, name, age),在 name 上建有普通索引。执行 SELECT * FROM t WHERE name = 'a' 的查找过程是?
考点:回表
A. 直接查 name 索引即可拿到整行,无需其他操作 B. 放弃索引,直接全表扫描 C. 先在 name 索引中查到主键 id,再用 id 到聚簇索引中取出整行(回表) D. 只查聚簇索引即可,name 索引不参与
答案:C
【考点】二级索引 → 回表 的两步查找过程。
【结论】 idx_name 的叶子只存 name 与主键 id,取不到 age,因此必须“先查二级索引拿主键、再回聚簇索引取整行” —— 这就是回表。选 C。
【逐项辨析】
- A 直接查
name索引即可拿到整行:错。错在“即可”二字 —— 二级索引叶子只有name与id,age不在其中。 - B 放弃索引、直接全表扫描:错。优化器只在回表代价超过全表扫描(选择性极差、命中行数占比很高)时才如此;本题按
name = 'a'定位,标准过程是走索引。 - C 先在
name索引中查到主键id,再用id到聚簇索引中取出整行(回表):正确。 完整描述了“二级索引定位 → 主键回表”这两步。 - D 只查聚簇索引、
name索引不参与:错。聚簇索引按id有序,name上没有任何顺序信息,只能全表扫描,白白浪费索引。
【知识点】 回表的定义:通过二级索引找到主键值后,再回到聚簇索引中按主键取出完整行记录的过程。它包含两次 B+ 树查找,第二次按主键随机定位,属随机 IO,是二级索引查询的主要代价。
① 在 idx_name 的 B+ 树中定位 name = 'a' → 得到主键 id = 7
② 在聚簇索引的 B+ 树中定位 id = 7 → 取出整行| 形态 | 触发条件 | 是否回表 | EXPLAIN 的 Extra |
|---|---|---|---|
| 回表 | 查询所需列不全在索引中 | 是 | 常出现 Using where |
| 覆盖索引 | 查询所需列全在索引中 | 否 | Using index |
| 索引下推(ICP) | 条件列在索引中但无法用于定位 | 视返回列而定 | Using index condition |
代价推导:命中 n 行时,磁盘 IO ≈ 二级索引扫描页数 + n 次随机回表。n 越大,随机 IO 越多,性能下降越明显 —— 这就是“SELECT * 比只取需要的列慢”的根因,优化方向是覆盖索引(见 M04)。
【记忆锚点】 “二级索引只给主键,要整行就回表”。
【易混对比】
- 回表 vs 覆盖索引:回表是“不得不回”,覆盖索引是“列全在索引里、压根不用回”。
- 回表 vs 索引下推:回表是取数据的第二步;索引下推是在索引层提前过滤、减少回表行数。
- InnoDB 回表 vs MyISAM 按地址读:MyISAM 按叶子里的物理地址直接读数据文件,没有第二次 B+ 树查找。
- 换问法:把
SELECT *改成SELECT name,答案就从“回表”变成“覆盖索引、无需回表”。
【自测】 表 t(id 主键, name, age),name 上有普通索引。SELECT name FROM t WHERE name = 'a' 需要回表吗?SELECT age FROM t WHERE name = 'a' 呢?
答:前者不需要(
name就是索引列,覆盖索引);后者需要(age不在idx_name里,必须回表)。与 M01、M04 连考。
【知识关联】
- 补题关联:补-25(索引下推在回表前过滤,减少回表次数)。
- 面试/工程:
SELECT *慢查询的头号原因就是回表放大;压测时用EXPLAIN看Using indexvsUsing where可立刻判断是否回表。 - 面试追问:① 回表一定是随机 IO 吗?按主键有序回表能否变顺序? ② 什么情况下优化器会直接放弃二级索引改全表扫描?
【拓展延伸】
- 变式问法:把
SELECT *改为只查索引列 → 覆盖索引;把条件改成范围且命中行很多 → 优化器可能选全表。 - 参数/命令:
EXPLAIN FORMAT=JSON/EXPLAIN ANALYZE(8.0.18+)可看实际回表次数;optimizer_switch中index_condition_pushdown控制 ICP。 - 完整推导补充:代价 ≈ 二级索引扫描页数 + N×随机回表;当 N/总行数 超过约 20%(经验值,非硬阈值)时常改全表。
M04. 表 t(id 主键, name, age) 上建有联合索引 idx_name_age(name, age)。下列哪个查询最稳妥地用到覆盖索引、无需回表(即使表以后新增列也不回表)?
考点:覆盖索引
A. SELECT * FROM t WHERE name = 'a' B. SELECT age FROM t WHERE name = 'a' C. SELECT id, name, age FROM t WHERE age = 20 D. SELECT name FROM t WHERE age = 20
答案:B
【考点】覆盖索引的定义:查询涉及的列全部包含在索引中。
【结论】 只有 B 的条件列 name 与返回列 age 都在联合索引 (name, age) 中,且 name 满足最左前缀,因此无需回表。选 B。
【逐项辨析】
- A
SELECT * FROM t WHERE name = 'a':在本题表仅 3 列、二级索引叶子含主键的条件下,id/name/age恰好全在索引中,EXPLAIN 也会显示Using index;但SELECT *依赖“列恰好凑齐”,表一旦加列就必然回表,不是覆盖索引的最佳实践,故不选。(若题干问“本题数据下能否覆盖”,A 也成立;本题强调可靠写法,标准答案为 B。) - B
SELECT age FROM t WHERE name = 'a':正确。 条件列name、返回列age恰好是联合索引(name, age)的两列,name又满足最左前缀,全程只查索引。 - C
SELECT id, name, age FROM t WHERE age = 20:错。错在条件列age不满足最左前缀 —— 跳过name无法定位,该索引用不上,与“覆盖”无关。 - D
SELECT name FROM t WHERE age = 20:错。同 C,WHERE age = 20跳过最左列name,索引无法定位。
【知识点】 覆盖索引(covering index)的定义:一条查询所涉及的条件列与返回列全部包含在同一索引中,因此不需回表,直接从索引叶子取数即可返回结果。它是 MySQL 最常用的优化手段之一 —— 因为二级索引比聚簇索引小得多,扫描代价更低。
判定方法:EXPLAIN 的 Extra 出现 Using index 即覆盖索引;出现 Using index condition 是索引下推;出现 Using where 常意味着需回表后再过滤。
| 查询 | 条件列 | 返回列 | 是否覆盖 | 原因 |
|---|---|---|---|---|
SELECT age FROM t WHERE name='a' | name | age | 是 | 两列都在 (name, age) 中,且 name 满足最左前缀 |
SELECT * FROM t WHERE name='a' | name | 全部列 | 是(仅本题表成立) | 本题表只有 id/name/age,而二级索引叶子天然含主键,故 SELECT * 也落在索引内、EXPLAIN 显示 Using index;但这是「列恰好凑齐」的巧合,表一加列就退化为回表,所以不算覆盖索引的推荐写法(【逐项辨析】A 项按此判) |
SELECT id, name, age FROM t WHERE age=20 | age | id, name, age | 否 | 条件列不满足最左前缀,索引根本用不上 |
SELECT name FROM t WHERE age=20 | age | name | 否 | 同上 |
三条推论:① 条件列不满足最左前缀 → 索引根本用不上,与“覆盖”无关;② 二级索引叶子天然含主键列,故 SELECT id 也算被覆盖;③ 表越宽越难覆盖,显式列出所需列才可靠。
【记忆锚点】 “条件和返回,全在索引里” —— SELECT * 是覆盖索引的头号反面写法。
【易混对比】
- 覆盖索引 vs 回表:分水岭是“列是否全在索引中”。
- 覆盖索引 vs 最左前缀:最左前缀管“能不能用这个索引”,覆盖管“用了之后要不要回表”。C、D 栽在最左前缀上,不是栽在覆盖上 —— 必须分清。
- 覆盖索引 vs 索引下推:前者省掉回表,后者减少回表行数(
Using indexvsUsing index condition)。
【自测】 联合索引 idx(a, b, c),SELECT b FROM t WHERE a = 1 是否用到覆盖索引?
答:是。
a满足最左前缀可用于定位,返回列b也在索引中;若改成SELECT d FROM t WHERE a = 1就必须回表。与 M03、M05 连考。
【知识关联】
- 补题关联:补-25(ICP 与覆盖索引常一起考:ICP 省回表行数,覆盖省掉整次回表)。
- 面试/工程:大厂 SQL review 高频点:禁止
SELECT *、高频列表接口优先设计覆盖索引;统计类 SQL 尽量只取必要列。 - 面试追问:① 联合索引
(a,b)下SELECT b,a FROM t WHERE a=1是否覆盖? ② 前缀索引能否做覆盖索引?
【拓展延伸】
- 变式问法:题干改成“哪些写法能避免回表”;或给
EXPLAIN Extra=Using index反推是否覆盖。 - 参数/命令:
EXPLAIN的Extra: Using index是覆盖索引铁证;Using index condition只是 ICP,不等于覆盖。SET optimizer_switch='index_condition_pushdown=off'可做对照实验。
M05. 联合索引 idx(a, b, c),下列哪个查询条件无法利用该索引?
考点:最左前缀原则
A. WHERE a = 1 B. WHERE a = 1 AND b = 2 C. WHERE b = 2 AND c = 3 D. WHERE a = 1 AND c = 3
答案:C
【考点】联合索引的最左前缀匹配规则。
【结论】 联合索引按 (a, b, c) 的顺序逐列建树,必须先匹配最左列 a;C 从 b 开始、跳过了 a,无法定位索引。选 C。(两条版本口径:① MySQL 8.0.13+ 的 Skip Scan 在 a 列基数很低时可能允许 C 这类条件走索引,属优化器特例,标准答案仍按最左前缀取 C;② 若 SELECT 的列全在索引里,C/D 都可能退化成 index-only 全索引扫描——那叫“扫索引”,不叫“用索引定位”,与本题问法不同。)
【逐项辨析】
- A
WHERE a = 1:可用。命中最左列a,能定位到a = 1的区间。 - B
WHERE a = 1 AND b = 2:可用。a、b依次匹配,定位更精确。 - C
WHERE b = 2 AND c = 3:正确(即“无法利用”)。 错在跳过了最左列a—— B+ 树先按a排序,b只在a相同时才有序列,脱离a谈b毫无意义。 - D
WHERE a = 1 AND c = 3:可用。a用于索引定位,c虽不能参与定位,但可在索引层过滤(索引下推)或回表后过滤 —— 索引并没有失效。
【知识点】 最左前缀原则:联合索引 idx(a, b, c) 等价于按 (a)、(a, b)、(a, b, c) 三种前缀有序,故查询条件必须从最左列开始、连续匹配、不能跳列。
| 查询条件 | 可用于定位的列 | 能否用索引 |
|---|---|---|
a = 1 | a | 能 |
a = 1 AND b = 2 | a, b | 能 |
a = 1 AND b = 2 AND c = 3 | a, b, c | 能 |
b = 2 AND c = 3 | 无 | 不能 |
a = 1 AND c = 3 | a(c 仅过滤) | 能 |
a > 1 AND b = 2 | a(b 失效) | 能(仅 a) |
两条细节:① 范围查询会截断后续列 —— a = 1 AND b > 2 AND c = 3 中,c 因前面出现范围条件而无法参与定位(b > 2 之后 c 全局无序);② ORDER BY 同样遵守最左前缀,ORDER BY a, b 可借索引免排序,ORDER BY b, c 不行。
【记忆锚点】 “从左往右、不能跳列;一旦范围,后面全废”。
【易混对比】
- 跳列 vs 断列:
(a, c)是跳列,还能用到a;(a > 1, b)是断列,b用不上。跳列不一定全废,范围之后必然全废。 - 最左前缀 vs 覆盖索引:前者管“索引能不能用”,后者管“用了要不要回表”。
- 列顺序:
(a, b)与(b, a)是两个不同索引,建索引时应把等值条件列、区分度高的列放前面。 - 换问法:若改成
WHERE a = 1 AND b > 2 AND c = 3,可用于定位的只剩a、b两列(c失效)。
【自测】 联合索引 idx(a, b, c),WHERE a = 1 AND c = 3 与 WHERE b = 2 AND c = 3,哪一个能用上索引?
答:前者能(用到
a,c只能过滤);后者不能(跳过了最左列a)。与 M04、M06 连考;大厂面试高频。
【知识关联】
- 补题关联:补-25(最左前缀满足后 ICP 才有意义)。
- 面试/工程:联合索引列顺序设计口诀:等值高区分在前、范围在后、排序列最后;线上“索引建了却不用”多半是最左前缀或类型问题。
- 面试追问:①
a=1 AND b>2 AND c=3能用到几列? ②ORDER BY a,b与ORDER BY b,a哪个能免 filesort?
【拓展延伸】
- 变式问法:给出
EXPLAIN key=idx_abc但只用了ref=a,判断 b/c 是否参与定位;或问“跳列与断列区别”。 - 参数/命令:
EXPLAIN中key_len可反推实际用了几列(受字符集、是否可空影响);8.0 的EXPLAIN ANALYZE能看到实际过滤位置。
M06. 下列哪种写法通常会导致索引失效?
考点:索引失效(函数操作)
A. WHERE name = 'abc' B. WHERE name LIKE 'abc%' C. WHERE LEFT(name, 3) = 'abc' D. WHERE name > 'abc'
答案:C
【考点】索引失效的常见情形之一:对索引列使用函数或表达式。
【结论】 B+ 树里存的是列的原始值,LEFT(name, 3) 让 MySQL 无法用原值去索引中定位,索引因此失效。选 C。
【逐项辨析】
- A
WHERE name = 'abc':可用。标准的等值匹配,直接定位到'abc'。 - B
WHERE name LIKE 'abc%':可用。前缀匹配(通配符在尾部)可转化为区间扫描['abc', 'abd'),仍走索引。 - C
WHERE LEFT(name, 3) = 'abc':正确(即“会导致索引失效”)。 错在把函数套在了索引列上 —— 索引按name的原值排序,函数结果没有对应的顺序信息。 - D
WHERE name > 'abc':可用。范围查询能定位到'abc'之后的区间。
【知识点】 索引失效的本质只有一句话:任何让“索引列原值”无法直接用于定位的写法,都会失效。
| 失效情形 | 例子 | 失效原因 |
|---|---|---|
| 索引列上用函数 | LEFT(name,3)='abc'、DATE(create_time)='2026-09-15' | 索引按原值有序,函数结果无序 |
| 索引列上做运算 | id + 1 = 10 | 同上,应改写为 id = 9 |
| 隐式类型转换 | phone = 13800138000(phone 是 varchar) | 等价于对列套了转换函数(见 M07) |
前置通配 LIKE(% 在开头) | name LIKE '%abc' | 无可用前缀,无法定位区间 |
OR 两侧有一侧无索引 | a = 1 OR b = 2(b 无索引) | 一侧需全表扫描,整体退化为全表扫描 |
| 不满足最左前缀 | 联合索引 (a,b) 上写 WHERE b = 2 | 跳过了最左列(见 M05) |
需分情况看待的几种:!= / NOT IN 在低选择性列上通常全表扫描,高选择性列上仍可能走索引;IS NULL 是否走索引取决于列是否允许 NULL、NULL 占比与优化器代价估算;ORDER BY 与索引顺序不一致会产生 Using filesort(不算失效,但同样变慢)。
改写技巧:把函数从列侧挪到常量侧 —— DATE(create_time) = '2026-09-15' 改成 create_time >= '2026-09-15 00:00:00' AND create_time < '2026-09-16 00:00:00'。
【记忆锚点】 “列上带函数,索引全白建;通配符在头,索引也不走”。
【易混对比】
LIKE 'abc%'vsLIKE '%abc':前者是尾随通配(%在末尾)、前缀确定可走 range;后者是前置通配(%在开头)、无单调区间故失效。- 索引失效 vs 索引选不上:前者是写法问题(必然失效),后者是优化器按代价估算主动放弃,不一定是写法错。
【自测】 create_time 上有索引,WHERE DATE(create_time) = '2026-09-15' 会走索引吗?如何改写?
答:不会(列上套了
DATE()函数)。改写为create_time >= '2026-09-15 00:00:00' AND create_time < '2026-09-16 00:00:00'即可生效。与 M05、M07 连考;大厂面试高频。
【知识关联】
- 补题关联:补-25(失效后无法下推,直接全表)。
- 面试/工程:日期查询写成
DATE(col)=...是最常见生产事故;ORM 自动生成 SQL 时也要警惕对列套函数。 - 面试追问:① 为什么
LIKE 'abc%'可以走索引而LIKE '%abc'不行? ② MySQL 8.0 函数索引/表达式索引如何解决“列上函数”问题?
【拓展延伸】
- 变式问法:正向题给“能走索引”的写法;或比较
WHERE id+1=10与WHERE id=9。 - 参数/命令:8.0 支持函数索引:
ALTER TABLE t ADD INDEX ((CAST(create_time AS DATE)));LIKE走索引还受collation与前缀选择性影响。
M07. phone 字段类型为 varchar(20) 且建有索引,执行 SELECT * FROM t WHERE phone = 13800138000(传入数字,不带引号)会?
考点:隐式类型转换导致索引失效
A. 正常走索引,效率与带引号一致 B. 发生隐式类型转换(字符串列被转为数字比较),导致索引失效、全表扫描 C. 直接报语法错误,无法执行 D. 走索引,但返回结果错误
答案:B
【考点】隐式类型转换与索引失效的因果关系。
【结论】 字符串列与数字比较时,MySQL 把列的值转成数字再比较,等价于对索引列施加了函数,索引失效、退化为全表扫描。选 B。
【逐项辨析】
- A 正常走索引、效率与带引号一致:错。错在“效率一致” —— 转换发生在列侧,索引按字符串原值排序,按转换后的数字无法定位。
- B 发生隐式类型转换(字符串列被转为数字比较),导致索引失效、全表扫描:正确。 准确描述了转换方向与后果。
- C 直接报语法错误、无法执行:错。这是运行期的隐式转换,不是语法错误,语句能正常执行,只是变慢(可能伴随精度截断警告)。
- D 走索引,但返回结果错误:错。索引实际会失效;且本例中列值都能正常转成数字,结果通常仍正确,代价只是性能。
【知识点】 MySQL 比较运算的类型转换规则:两个操作数一个是字符串、一个是数字时,把字符串转换为数字(向“数值类型”看齐),而不是把数字转成字符串。于是:
| 列类型 | 传入参数 | 转换落在哪一侧 | 是否走索引 |
|---|---|---|---|
varchar | 数字 13800138000 | 列侧(相当于 CAST(phone AS DOUBLE)) | 失效,全表扫描 |
varchar | 字符串 '13800138000' | 无需转换 | 走索引 |
int | 字符串 '1' | 常量侧('1' → 1) | 仍走索引 |
int | 数字 1 | 无需转换 | 走索引 |
判定口诀:看转换落在谁头上 —— 落在列上就失效,落在常量上不影响。因为索引 B+ 树是按列的原值(字符串)排序的,一旦要按“转换后的数字”比较,原值顺序就派不上用场,优化器只能逐行读出、转换后再比较,即全表扫描。
附带风险:varchar 列与数字比较时,非数字开头的值会被转成 0 并产生警告;字符串按数字语义比较也会反直觉('10' > '9' 为假,10 > 9 为真)。规范做法是参数类型与列类型严格一致,字符串常量一律加引号。
【记忆锚点】 “字符串列遇数字,列被转换、索引作废;数字列遇字符串,常量被转换、索引无碍”。
【易混对比】
phone = 13800138000vsid = '1':前者失效(转列),后者可用(转常量)。同是类型不匹配,结论相反 —— 最经典的送分陷阱。- 隐式转换 vs 显式函数:
WHERE phone = 13800138000与WHERE CAST(phone AS DOUBLE) = 13800138000对索引的破坏完全等价。 - 换问法:若题干改成
WHERE id = '100'(id是int),答案变成“仍走索引”。
【自测】 订单号 order_no 是 varchar(32) 且建有索引。WHERE order_no = 20260915001 与 WHERE order_no = '20260915001' 性能有何差别?
答:前者索引失效、全表扫描(列被隐式转成数字);后者正常走索引。修复只需给常量加引号。与 M06 连考;线上排障高频。
【知识关联】
- 补题关联:补-24(金额用 DECIMAL,避免浮点与隐式转换坑)。
- 面试/工程:订单号、手机号、身份证一律字符串;MyBatis
#{}会保留类型,${}与手写拼接更容易踩隐式转换。 - 面试追问:①
int列写WHERE id='100'为何仍走索引? ② 隐式转换除了索引失效还会带来哪些正确性风险?
【拓展延伸】
- 变式问法:给
phone varchar与id int两组对比;或问“转换发生在列侧还是常量侧”。 - 参数/命令:观察
SHOW WARNINGS可见截断/转换警告;EXPLAIN中type=ALL且无可用 key 是典型信号。JDBC URL 建议统一字符集characterEncoding=utf8mb4。
M08. 对 varchar(255) 的 email 列建索引时,更合理的做法是?
考点:前缀索引与索引选择性
A. 直接建全列索引,不考虑长度 B. 把 email 改成 text 再建索引 C. 不建索引,靠全表扫描 D. 使用前缀索引 INDEX(email(10)),在节省空间与保证选择性之间取平衡
答案:D
【考点】前缀索引与索引选择性的权衡。
【结论】 长字符串列建全列索引会让索引体积膨胀,而前缀索引 email(10) 只索引前 10 个字符,在索引体积与选择性之间取得平衡。选 D。
【逐项辨析】
- A 直接建全列索引、不考虑长度:错。错在“不考虑长度” ——
varchar(255)的全列索引体积大、页数多、树高、IO 增加;utf8mb4 下索引键长达 1020 字节,部分行格式下还会触及单列索引长度上限。 - B 把
email改成text再建索引:错。错在“改成text” ——TEXT列建索引必须指定前缀长度,且大文本会存到溢出页、读写更慢,反而更差。 - C 不建索引、靠全表扫描:错。错在“不建索引” —— 这是用性能换空间,方向反了;邮箱通常是高频查询条件。
- D 使用前缀索引
INDEX(email(10)),在节省空间与保证选择性之间取平衡:正确。 既压缩了索引体积,又能保住大部分区分度。
【知识点】 前缀索引(prefix index)的定义:对字符串列只取前 n 个字符建立索引,即 INDEX(col_name(n))。
| 对比项 | 全列索引 | 前缀索引 col(n) |
|---|---|---|
| 索引体积 | 大(含完整值) | 小(只存前 n 个字符) |
| 选择性 | 最高 | 可能降低(前缀相同的值区分不开) |
| 能否做覆盖索引 | 能 | 不能(不含完整列值,无法直接返回) |
能否用于该列的 ORDER BY / GROUP BY | 能 | 不能(索引中不是完整值,无法保证有序) |
| 回表比对 | 无需额外比对 | 前缀相同者需回表比对完整值 |
n 怎么定?选择性(区分度) 的定义是 COUNT(DISTINCT col) / COUNT(*),越接近 1 越好,逐步试出区分度接近全列索引的最小 n:
SELECT COUNT(DISTINCT LEFT(email, 7)) / COUNT(*) AS sel7,
COUNT(DISTINCT email) / COUNT(*) AS sel_full
FROM t;再加大 n 收益很小,却白白增大索引体积。
【记忆锚点】 “前缀索引省空间,代价是选择性、覆盖索引和排序能力”。
【易混对比】
- 前缀索引 vs 联合索引:前者是“同一列截前
n个字符”,后者是“多列组合”;前者丢列内的后缀信息,后者丢列间的组合信息。 - 前缀索引 vs 覆盖索引:前缀索引天然不能做覆盖索引(索引里没有完整列值),也不能用于该列的
ORDER BY/GROUP BY。 - 选择性高 vs 选择性低:
email、订单号适合建索引;性别、状态选择性极低,建索引往往不如不建。
【自测】 email 列 100 万行,COUNT(DISTINCT email) = 100 万,COUNT(DISTINCT LEFT(email, 8)) = 99.9 万,选 email(8) 划算吗?
答:划算。区分度已达 99.9%,几乎不损失选择性,索引体积却大幅下降;代价是不能做覆盖索引、不能用于
ORDER BY/GROUP BY。与 M04、M05 连考。
【知识关联】
- 补题关联:补-24(长字段类型选择)、补-25(索引体积影响扫描代价)。
- 面试/工程:邮箱、URL、JSON 摘要列常用前缀索引;但报表“按邮箱排序/覆盖查”就不能再用前缀索引。
- 面试追问:① 如何用 SQL 选最小前缀长度 n? ② 前缀索引对
ORDER BY email有何影响?
【拓展延伸】
- 变式问法:问“前缀索引的代价”而非“是否该建”;或对比全文索引/倒排在搜索场景的替代。
- 参数/命令:InnoDB 索引键长上限相关:DYNAMIC 行格式下前缀可达 3072 字节;
innodb_large_prefix(旧版本)已废弃。选择性 SQL:COUNT(DISTINCT LEFT(email,n))/COUNT(*)。
M09. MySQL InnoDB 存储引擎默认的事务隔离级别是?
考点:默认隔离级别
A. READ UNCOMMITTED(读未提交) B. READ COMMITTED(读已提交) C. SERIALIZABLE(可串行化) D. REPEATABLE READ(可重复读)
答案:D
【考点】四种隔离级别的定义与 MySQL 默认值。
【结论】 MySQL InnoDB 的默认隔离级别是 REPEATABLE READ(可重复读,RR),并在 RR 下用间隙锁 + Next-Key Lock 在很大程度上避免了幻读。选 D。
【逐项辨析】
- A READ UNCOMMITTED:错。最低级别,允许脏读,并发问题最多,不可能作默认。
- B READ COMMITTED:错。错在把“别人的默认”当成了 MySQL 的默认 —— RC 是 Oracle / SQL Server / PostgreSQL 的默认值。
- C SERIALIZABLE:错。串行化用加锁把事务变成串行执行,并发度最低,仅用于特殊场景。
- D REPEATABLE READ:正确。 MySQL InnoDB 的默认值,且额外解决了标准 RR 允许的幻读问题。
【知识点】 SQL 标准定义了四种隔离级别,级别越高一致性越强、并发度越低:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 默认使用者 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 极少使用 |
| READ COMMITTED | 不会 | 可能 | 可能 | Oracle / SQL Server / PostgreSQL |
| REPEATABLE READ | 不会 | 不会 | 标准允许,InnoDB 基本避免 | MySQL InnoDB |
| SERIALIZABLE | 不会 | 不会 | 不会 | 特殊场景 |
InnoDB 在 RR 下的实现:快照读(普通 SELECT)靠 MVCC + ReadView 提供一致性读,ReadView 在事务首次快照读时创建,故同事务内反复读结果一致;当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)靠 Next-Key Lock(记录锁 + 间隙锁) 阻塞范围内插入,从而避免幻读。查看当前值:MySQL 8.0 用 SELECT @@transaction_isolation;,5.7 及以前用 SELECT @@tx_isolation;。
【记忆锚点】 “MySQL 默认 RR,Oracle 默认 RC”。
【易混对比】
- RR vs RC:RR 的
ReadView只建一次,反复读结果不变;RC 每次快照读都新建ReadView,能读到别人已提交的新数据。 - 标准 RR vs InnoDB RR:标准下 RR 允许幻读,InnoDB 用 Next-Key Lock 基本避免 —— 最常考的差异点。
【自测】 Oracle 上要求“同一事务内两次查询结果完全一致”,隔离级别要调成什么?
答:REPEATABLE READ。Oracle 默认 READ COMMITTED 会出现不可重复读,需显式提升到 RR。与 M10 连考;国企/互联网笔试高频。
【知识关联】
- 补题关联:补-17(隔离性与持久性)、补-18(2PL 保证可串行化)。
- 面试/工程:互联网业务多用 RC + 合理索引换取并发;金融账务偶用 RR;切级别要评估“幻读是否可接受”与锁行为差异。
- 面试追问:① 为什么很多公司将 MySQL 从 RR 改成 RC? ② RC 下还能用间隙锁吗?
【拓展延伸】
- 变式问法:问 Oracle 默认级别(RC);问“标准 RR 允许幻读但 InnoDB 如何处理”。
- 参数/命令:8.0:
SET GLOBAL transaction_isolation='READ-COMMITTED';或SET SESSION ...;查看SELECT @@transaction_isolation;。5.7 用tx_isolation。
M10. “同一事务内两次执行同一条范围查询,第二次读到了其他事务新插入的行”,这属于哪种并发问题?
考点:幻读的识别
A. 脏读 B. 不可重复读 C. 幻读 D. 丢失更新
答案:C
【考点】脏读 / 不可重复读 / 幻读的区分标准。
【结论】 题干是“同一事务内两次执行同一条范围查询,第二次多出别人新插入的行”,即结果集行数变了,这是幻读。选 C。
【逐项辨析】
- A 脏读:错。脏读的关键字眼是“未提交” —— 读到了别的事务尚未提交、随后可能回滚的数据,而题干读到的是已提交的新插入行。
- B 不可重复读:错。不可重复读针对“同一行的值被
UPDATE改掉”,是值的改变;题干是行数的增加。 - C 幻读:正确。 判定标志是“同一范围两次查询,结果集行数因其它事务
INSERT/DELETE而变化”。 - D 丢失更新:错。丢失更新是两个事务基于同一旧值各自
UPDATE、后者覆盖前者,属写冲突而非“读现象”,也不是隔离级别能解决的问题。
【知识点】 三个“读现象”只用一把尺子区分 —— 第二次读到了什么变化:
| 现象 | 读到的变化 | 触发操作 | 关键词 |
|---|---|---|---|
| 脏读 | 别的事务未提交的修改 | UPDATE | “未提交” |
| 不可重复读 | 同一行的值被改了 | UPDATE | “行、值变” |
| 幻读 | 同一范围的行数变了 | INSERT / DELETE | “范围、行数变” |
记忆顺序:脏读看“提交没提交”,不可重复读看“同一行的值”,幻读看“同一范围的行数”。
补充:丢失更新不属于 SQL 标准定义的读现象,隔离级别也解决不了,需靠悲观锁(SELECT ... FOR UPDATE 先锁行再更新)或乐观锁(UPDATE ... SET v = v + 1 WHERE id = ? AND version = ?)处理。InnoDB 在 RR 下,快照读由 MVCC 保证同事务内读结果一致,当前读由 Next-Key Lock 阻止范围内插入,幻读基本被避免;考试按标准定义判定即可。
【记忆锚点】 “脏读看未提交,不可重复读看值,幻读看行数”。
【易混对比】
- 不可重复读 vs 幻读:值变 → 不可重复读(
UPDATE);行数变 → 幻读(INSERT/DELETE)。看的是“改行内”还是“改行数”。 - 幻读 vs 丢失更新:幻读是“读”的问题,由隔离级别解决;丢失更新是“写”的问题,由锁 / 版本号解决。
- RR 下的幻读:快照读走 MVCC,当前读走 Next-Key Lock,两者共同压制幻读(见 M09)。
- 换问法:若题干改成“第二次读到的行值与第一次不同”,答案就变成不可重复读。
【自测】 事务 A 内两次执行 SELECT COUNT(*) FROM t WHERE age > 20,第一次 5 行、第二次 8 行;期间事务 B 提交了 3 条 age = 30 的插入。这属于什么现象?
答:幻读(同一范围、行数由 5 变 8,由
INSERT引起)。若 B 只是把某行age从 18 改成 30,则属不可重复读。与 M09 连考;大厂面试高频。
【知识关联】
- 补题关联:补-17(隔离性)、补-18(2PL)、补-19(S/X 相容)——幻读是“范围+插入”型异常。
- 面试/工程:识别题的关键是看“两次查询结果集是否多出行”。不可重复读是同一行值变了(UPDATE),幻读是行数变多了(INSERT)。互联网面试常连着问“RR 真的完全防住幻读了吗”——InnoDB RR + Next-Key 对当前读防得较好,快照读靠 MVCC;极端混合读写仍有讨论空间。
- 面试追问:① 不可重复读与幻读能否只用行锁区分?(不能,插入需要锁区间/唯一约束,故要 Gap/Next-Key) ② RC 下有幻读吗?(有,因为无普通间隙锁)
【拓展延伸】
- 变式问法:给出四段场景(脏读/不可重复读/幻读/都能避免)选级别;或直接给 SQL 序列问异常类型。
- 参数/命令:
SELECT @@transaction_isolation;SET TRANSACTION ISOLATION LEVEL;SHOW ENGINE INNODB STATUS的锁等待段;压测复现用两个会话交错事务。
M11. InnoDB 在 REPEATABLE READ 级别下,主要依靠什么机制避免幻读?
考点:InnoDB 如何避免幻读
A. 间隙锁(Gap Lock)与 Next-Key Lock 锁住范围,配合 MVCC 快照读 B. 表级排他锁 C. 只靠 MVCC,不需要加锁 D. 强制升级为串行化执行
答案:A
【考点】当前读与快照读的区分,以及间隙锁在避免幻读中的作用。
【结论】 选 A —— InnoDB 在 REPEATABLE READ 下靠两条腿避免幻读:快照读走 MVCC(读历史版本,看不到新插入的行),当前读走间隙锁 / Next-Key Lock(锁住区间,让别人插不进来)。
【逐项辨析】
- A 间隙锁与 Next-Key Lock 锁住范围,配合 MVCC 快照读:正确。 两条腿都写到了:MVCC 管快照读,间隙锁管当前读,缺一不可。
- B 表级排他锁:错。表级 X 锁确实能挡住幻读,但那是“串行化”的思路,InnoDB 的 RR 并不靠它,题干问的是“主要依靠什么机制”。
- C 只靠 MVCC,不需要加锁:错。MVCC 只管快照读;
SELECT ... FOR UPDATE这类当前读若不锁区间,别的事务插入新行后本事务会读到多出来的行,仍会幻读。 - D 强制升级为串行化执行:错。那是 SERIALIZABLE 的做法,RR 无需升级,说“强制升级”是错的。
【知识点】 把“两种读”讲清楚,幻读问题就迎刃而解。
| 读法 | 典型语句 | 读的是什么 | 靠什么防幻读 |
|---|---|---|---|
| 快照读(一致性读) | 普通 SELECT | 事务首次读时生成的 ReadView 对应的历史版本 | MVCC:ReadView 生成后新插入的行 trx_id 超出可见范围,天然读不到 |
| 当前读(锁定读) | SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE、INSERT | 最新已提交版本 | Next-Key Lock:锁住区间,别的事务插不进来 |
Next-Key Lock = Record Lock + Gap Lock,加锁区间是左开右闭 (a, b](常见于非唯一索引或范围查询)。例如非唯一索引上有 5、10 两个值,执行范围锁定时,InnoDB 可能锁住 (5, 10] 这一段。注意:若 id 为唯一索引(主键)且做等值匹配、目标行不存在(如 WHERE id = 7 FOR UPDATE),InnoDB 只加 Gap Lock,锁定开区间 (5, 10),不会对记录 10 再加 Record Lock。无论哪种,别的事务都无法在 5 与 10 之间插入 id = 6、7、8 的新行 → 幻读被堵住。
推导:幻读的定义是“同一事务内两次相同的范围查询,第二次多出了行”。多出来的行必然来自其他事务的插入,所以防幻读的本质就是“别让人在我关心的区间里插入” —— 这正是间隙锁的职责。而快照读读的是历史版本,新插入的行对它不可见,因此无需加锁。
两个必须记住的边界:① 间隙锁只在 REPEATABLE READ 及以上生效,READ COMMITTED 下没有间隙锁,所以 RC 无法避免幻读;② 间隙锁只在当前读时加,普通 SELECT 不加锁。
【记忆锚点】 “快照读走 MVCC,当前读走间隙锁” —— 两条腿走路,缺一条就漏幻读。
【易混对比】
- 幻读 vs 不可重复读:不可重复读是“同一行的值被改了”,幻读是“结果集的行数变了”(多了 / 少了行);前者靠行锁 + MVCC,后者必须靠间隙锁。
- 间隙锁 vs Next-Key Lock:间隙锁只锁“缝”
(a, b),Next-Key Lock 是“记录 + 缝”(a, b];RC 下退化为 Record Lock。 - RR 是否完全避免幻读:快照读下完全避免,当前读下靠间隙锁避免;但当前读与快照读混用时(先普通
SELECT、再FOR UPDATE),仍可能看到此前已提交的插入行,这是“看起来像幻读”的经典现象。 - 换问法:若题目问“哪个隔离级别下没有间隙锁”,答案是 READ COMMITTED。
【自测】 事务 A 在 RR 下先执行 SELECT * FROM t WHERE id > 10(普通查询),事务 B 插入 id = 15 并提交,事务 A 再执行同一条普通查询。A 会看到 id = 15 吗?若第二次改成 SELECT * FROM t WHERE id > 10 FOR UPDATE 呢?
答:第一次不会看到(快照读走 MVCC,ReadView 在首次查询时已确定);第二次会看到(当前读读最新数据,而间隙锁在第二次查询时才加,锁不住已经提交的插入)。与 M12、M18 连考;大厂面试高频。
【知识关联】
- 补题关联:补-19(锁相容)、补-18(2PL 与隔离级别映射)。
- 面试/工程:RR + Next-Key 是“默认更强”的卖点,但锁范围更大,热点更新更容易锁等待;不少团队改 RC 正是为了减少锁冲突。
- 面试追问:① 什么情况下 RR 下仍可能出现幻读? ②
WHERE id=1(唯一索引等值)会加间隙锁吗?
【拓展延伸】
- 变式问法:问“快照读靠什么避免幻读”(MVCC)vs“当前读靠什么”(Next-Key Lock)。
- 参数/命令:
innodb_locks_unsafe_for_binlog(旧)曾影响间隙锁;8.0 用performance_schema.data_locks查看锁详情。等值命中唯一键后间隙锁会退化为记录锁。
M12. InnoDB 的 MVCC(多版本并发控制)主要依赖哪些要素实现?
考点:MVCC 的实现要素
A. 仅 ReadView 即可 B. redo log + binlog C. 表级锁 + 行级锁 D. undo log + 隐藏列(trx_id、roll_pointer)+ ReadView
答案:D
【考点】MVCC 的三大组成要素及其协作方式。
【结论】 选 D —— MVCC 靠隐藏列(DB_TRX_ID + DB_ROLL_PTR)+ undo log 版本链 + ReadView 可见性判断三者协作实现;redo log 与 binlog 都不参与。
【逐项辨析】
- A 仅 ReadView 即可:错。ReadView 只是一张“可见性判据表”,没有 undo log 中的历史版本就无从比对,错在这个“仅”字。
- B redo log + binlog:错。redo log 管崩溃恢复(持久性),binlog 管主从复制与归档,二者都不参与版本可见性判断。
- C 表级锁 + 行级锁:错。MVCC 的设计目标恰恰是“读不加锁、读写不阻塞”,锁与 MVCC 是两条并列的并发控制路线。
- D undo log + 隐藏列(
trx_id、roll_pointer)+ ReadView:正确。 三者缺一不可:隐藏列提供“版本指针”,undo log 提供“旧版本数据”,ReadView 提供“哪些版本可见”的判据。
【知识点】 MVCC 要解决的问题是:同一条记录被多个事务并发读写时,读的人如何在不加锁的前提下看到“自己该看到的那一版”。解法是“每条记录保留多个历史版本,读的时候按规则挑一版”。
| 要素 | 具体内容 | 作用 |
|---|---|---|
| 隐藏列 | DB_TRX_ID(6 字节,最后修改该行的事务 ID)、DB_ROLL_PTR(7 字节,回滚指针,指向 undo log 中的旧版本)、DB_ROW_ID(6 字节,仅在无主键且无唯一非空索引时才有) | 每行自带的“版本戳”与“上一版指针” |
| undo log | 记录反向操作,通过回滚指针串成版本链 | 提供历史版本数据 |
| ReadView | 四个字段:m_ids(生成时的活跃事务 ID 列表)、min_trx_id、max_trx_id、creator_trx_id | 提供可见性判据 |
版本链的形成:每次修改一行,旧值先写入 undo log,新行的 DB_ROLL_PTR 指向这条 undo 记录,于是形成 当前行 → 旧版本 → 更旧版本 → … 的单向链表。
可见性判断规则(沿版本链从新到旧逐个比对,取第一个可见版本):
设该版本的 trx_id = T,ReadView 的活跃事务列表为 m_ids
① T == creator_trx_id → 可见(本事务自己改的)
② T < min_trx_id → 可见(该事务早已提交)
③ T >= max_trx_id → 不可见(该事务在 ReadView 之后才开启)
④ min_trx_id <= T < max_trx_id → 若 T 在 m_ids 中则不可见(尚未提交),否则可见RC 与 RR 的差别只在 ReadView 的生成时机:
| 隔离级别 | ReadView 生成时机 | 效果 |
|---|---|---|
| READ COMMITTED | 每次 SELECT 都重新生成 | 能读到别人刚提交的数据 → 产生不可重复读 |
| REPEATABLE READ | 第一次 SELECT 时生成,整个事务复用 | 每次读都是同一张快照 → 可重复读 |
【记忆锚点】 “一行两列一条链,一张快照定可见” —— 隐藏列是“戳”,undo 链是“旧版本仓库”,ReadView 是“裁判”。
【易混对比】
- undo log vs redo log:undo 管“回滚与旧版本”(逻辑日志,反向操作),redo 管“崩溃恢复”(物理日志,正向重做),一个向后、一个向前。
DB_TRX_IDvsDB_ROLL_PTR:前者回答“谁改的”(判可见性),后者回答“旧版在哪”(找上一版)。- MVCC vs 锁:MVCC 让“读”不加锁,锁让“写”互斥;RR 下当前读仍会加 Next-Key Lock(见 M11)。
- 换问法:若题目问“ReadView 包含哪些字段”,答案就是
m_ids/min_trx_id/max_trx_id/creator_trx_id。
【自测】 事务 D 在 RR 下第一次 SELECT 后,事务 B 修改了同一行并提交,事务 D 再次 SELECT 该行。D 看到的是 B 修改后的值吗?
答:看不到。RR 下 ReadView 在 D 第一次
SELECT时生成并复用,B 的trx_id落在 D 的m_ids(活跃列表)中 → 判为不可见 → D 沿版本链找到自己该看的那一版。与 M11、M13、M15 连考;大厂面试高频。
【知识关联】
- 补题关联:补-17(隔离性实现)、补-20(崩溃恢复与 undo 的关系)。
- 面试/工程:长事务导致 undo 链过长、回滚段膨胀、purge 滞后,是大促后数据库变慢的经典根因。
- 面试追问:① ReadView 何时创建?RC 与 RR 差在哪? ② 当前读会不会走 MVCC?
【拓展延伸】
- 变式问法:问“哪些列参与可见性判断”(trx_id、roll_pointer、ReadView);问“删除在 MVCC 里如何表示”(delete mark)。
- 参数/命令:
information_schema.INNODB_TRX看活跃事务;SHOW ENGINE INNODB STATUS的 TRANSACTIONS 段;避免autocommit=0长时间挂起。
M13. 关于 InnoDB 的 redo log,下列说法正确的是?
考点:redo log
A. 是 InnoDB 的物理日志,记录数据页的物理修改,用于崩溃恢复,保证持久性 B. 是 MySQL Server 层的逻辑日志,主要用于主从复制 C. 用于事务回滚,保存数据的旧版本 D. 记录的是原始 SQL 语句文本
答案:A
【考点】redo log(重做日志)的所属层级、日志类型、写入方式与职责。
【结论】 选 A —— redo log 是 InnoDB 引擎层的物理日志,记录数据页的物理修改,采用固定大小、循环写,靠 WAL 机制保证崩溃后已提交事务不丢(持久性)。
【逐项辨析】
- A InnoDB 的物理日志,记录数据页的物理修改,用于崩溃恢复,保证持久性:正确。 四个要点全对:引擎层、物理日志、崩溃恢复、持久性。
- B MySQL Server 层的逻辑日志,主要用于主从复制:错。这是 binlog 的描述,redo log 在引擎层且是物理的。
- C 用于事务回滚,保存数据的旧版本:错。这是 undo log 的职责,且方向恰好相反 —— undo 是“反向撤销”,redo 是“正向重做”。
- D 记录的是原始 SQL 语句文本:错。那是 binlog 的
STATEMENT格式;redo log 记的是“某表空间某页某偏移处改成了什么”,不含 SQL 文本。
【知识点】 三个日志必须一起记,先看总表:
| 日志 | 所属层 | 类型 | 记录内容 | 写入方式 | 主要职责 |
|---|---|---|---|---|---|
| redo log | InnoDB 引擎层 | 物理日志 | 数据页的物理修改 | 固定大小、循环写 | 崩溃恢复 → 持久性(D) |
| undo log | InnoDB 引擎层 | 逻辑日志 | 反向操作(旧版本) | 随事务增长,由 purge 清理 | 回滚 → 原子性(A);MVCC 版本链 |
| binlog | MySQL Server 层 | 逻辑日志 | 语句或行变更 | 追加写,持续增长 | 主从复制、基于时间点的恢复 |
redo log 的核心机制是 WAL(Write-Ahead Logging):修改数据时先写 redo log、再改内存中的数据页,脏页由后台线程异步刷盘。这样即使刷脏页之前断电,重启后也能用 redo log 把已提交的修改“重做”出来。
写盘节奏由 innodb_flush_log_at_trx_commit 控制:
| 取值 | 行为 | 数据安全性 |
|---|---|---|
1(默认) | 每次事务提交都 fsync 到磁盘 | 最安全,不丢已提交事务,但 IO 开销最大 |
0 | 每秒 fsync 一次 | 可能丢 1 秒内提交的事务 |
2 | 每次提交写入 OS cache,不 fsync | MySQL 进程崩溃不丢,机器断电可能丢 |
推导:redo log 是固定大小、循环覆盖写的(总容量由 innodb_log_file_size × innodb_log_files_in_group 决定),因此它不能长期归档;而“崩溃后靠重做恢复”只要求日志在脏页落盘之前不丢失,循环写的容量恰好够用 —— 这就是它必须“小、快、循环”的原因。
【记忆锚点】 “redo 重做保持久、undo 回滚保原子、binlog 归档保复制” —— 三个日志三件事,别串门。
【易混对比】
- redo log vs binlog(核心三差异):① 层级(引擎层 vs Server 层);② 内容(物理 vs 逻辑);③ 写入(循环写 vs 追加写),详见 M14。
- redo log vs undo log:redo 是“向前重做”(崩溃后把已提交的改回来),undo 是“向后回滚”(撤销未提交的改)。
- redo log 的边界:它只保证已提交事务不丢;未提交事务的撤销要靠 undo log。
- 换问法:若题目问“哪个日志是 Server 层、追加写、用于主从复制”,答案换成 binlog。
【自测】 把 innodb_flush_log_at_trx_commit 从 1 改成 2,事务提交后机器突然断电,已提交的事务会丢吗?
答:可能丢。取值
2时提交只写入 OS cache 未 fsync,机器断电会导致 OS cache 内容丢失;但若只是 MySQL 进程崩溃而机器未断电,则不会丢。与 M14、M16 连考;生产调优高频。
【知识关联】
- 补题关联:补-20(日志+检查点恢复)。
- 面试/工程:redo 让数据库具备 crash-safe;SSD 下
innodb_flush_log_at_trx_commit=1的延迟已可接受,金融场景不应为吞吐改为 2。 - 面试追问:① redo 是物理日志还是逻辑日志? ② 为什么 redo 要循环写、空间有限也能工作?
【拓展延伸】
- 变式问法:对比 redo vs binlog 的“是否崩溃恢复必需”“是否引擎无关”。
- 参数/命令:
innodb_log_file_size、innodb_log_files_in_group(8.0.30+ 改为innodb_redo_log_capacity)、innodb_flush_log_at_trx_commit(0/1/2)。
M14. 关于 binlog(归档日志),下列说法正确的是?
考点:binlog
A. 是 InnoDB 引擎层日志,记录数据页的物理修改 B. 采用固定大小循环写,写满即覆盖 C. 是 MySQL Server 层日志,记录逻辑变更(语句或行变更),主要用于主从复制与数据恢复 D. 是实现 MVCC 的核心
答案:C
【考点】binlog 的所属层级、日志格式、写入方式,以及与 redo log 的三大区别。
【结论】 选 C —— binlog 是 MySQL Server 层的逻辑日志,记录语句或行的逻辑变更,采用追加写,用于主从复制与基于时间点的数据恢复。
【逐项辨析】
- A InnoDB 引擎层日志,记录数据页的物理修改:错。这是 redo log 的描述;binlog 属 Server 层,与存储引擎无关(MyISAM 也有 binlog)。
- B 采用固定大小循环写,写满即覆盖:错。“循环写、会覆盖”是 redo log 的特征;binlog 是追加写,文件只增不减,靠过期清理策略删除旧文件(8.0 用
binlog_expire_logs_seconds,5.7 用expire_logs_days)。 - C MySQL Server 层日志,记录逻辑变更,用于主从复制与数据恢复:正确。 层级、内容、用途三项全对。
- D 是实现 MVCC 的核心:错。MVCC 靠隐藏列 + undo log + ReadView(见 M12),binlog 完全不参与。
【知识点】 binlog 是逻辑日志,“逻辑”的含义是:它记的是“发生了什么变更”,而不是“哪个页的哪个字节变成了什么”。三种格式对照:
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
STATEMENT | 原始 SQL 语句 | 日志量小 | 含不确定函数(NOW()、UUID())时主从可能不一致 |
ROW(生产推荐) | 每行数据的前后值 | 主从一致性强,可精准恢复 | 日志量大(尤其批量 UPDATE) |
MIXED | 自动在两者间切换 | 兼顾体积与一致性 | 行为依赖 MySQL 判断,不易预测 |
binlog 与 redo log 的三大区别(高频考点):
| 对比项 | redo log | binlog |
|---|---|---|
| 所属层级 | InnoDB 引擎层 | MySQL Server 层(所有引擎共用) |
| 日志内容 | 物理日志(数据页修改) | 逻辑日志(语句 / 行变更) |
| 写入方式 | 固定大小、循环写 | 追加写,持续增长 |
推导:正因为 binlog 是 Server 层的、追加写的、跨引擎通用的,它才能充当“全局操作流水账”——从库重放这份流水账就能追上主库;也正因为文件不会被覆盖,才能用它做“恢复到任意时间点”(先还原全量备份,再重放 binlog 到目标时刻)。
崩溃恢复中的分工:redo log 决定“已提交的事务要不要提交”,binlog 决定“这份变更要不要发给从库”,二者的衔接靠两阶段提交(见 M16)。
【记忆锚点】 “redo 是引擎层物理循环,binlog 是 Server 层逻辑追加” —— 三层三词,一字不差。
【易混对比】
- binlog vs redo log:见上表三差异;考试最爱把“循环写、写满覆盖”安到 binlog 头上(本题 B 项)。
- 用途对照:binlog → 主从复制 + 时间点恢复;redo log → 崩溃恢复(持久性)。
STATEMENTvsROW:前者记“做了什么操作”,后者记“数据变成了什么样”;涉及不确定函数、触发器、无ORDER BY的LIMIT时,ROW更安全。- 换问法:若题目问“哪个日志可用于基于时间点的恢复”,答案仍是 binlog。
【自测】 生产环境用 ROW 格式的 binlog,某次误执行 UPDATE t SET status = 0 影响 100 万行,binlog 会记录 100 万行变更还是 1 条 SQL?
答:100 万行变更。
ROW格式逐行记录前后值,日志体积会暴涨 —— 这也是大事务必须拆批的原因之一。与 M16 连考;生产运维高频。
【知识关联】
- 补题关联:补-20(恢复依据)。
- 面试/工程:主从复制、数据订阅(Canal)、时间点恢复(PITR)都靠 binlog;误删数据后的标准动作是停写 + 从备份与 binlog 做 PITR。
- 面试追问:① binlog 三种格式如何选? ② 为什么推荐 ROW?
【拓展延伸】
- 变式问法:问“redo 与 binlog 谁先写谁后写”(两阶段提交耦合);问“从库拿什么重放”。
- 参数/命令:
binlog_format=ROW|STATEMENT|MIXED;binlog_row_image=FULL|MINIMAL|NOBLOB;PURGE BINARY LOGS;mysqlbinlog --base64-output=DECODE-ROWS -v。
M15. undo log 的主要作用是?
考点:undo log
A. 崩溃恢复时重做已提交的修改 B. 事务回滚(原子性)+ 为 MVCC 提供历史版本(快照读) C. 主从复制的数据传输 D. 记录慢查询语句
答案:B
【考点】undo log 的两大职责,以及它与 redo log 的分工。
【结论】 选 B —— undo log 承担两大职责:事务回滚保证原子性,以及为 MVCC 提供历史版本支撑快照读。
【逐项辨析】
- A 崩溃恢复时重做已提交的修改:错。这是 redo log 的职责,方向是“重做”而非“撤销”。
- B 事务回滚(原子性)+ 为 MVCC 提供历史版本(快照读):正确。 两大职责缺一不可,这也是 undo log 最容易被漏记的地方。
- C 主从复制的数据传输:错。主从复制靠 binlog 传输变更,undo log 不参与。
- D 记录慢查询语句:错。慢查询记录在慢查询日志(
slow_query_log)中,与 undo log 无关。
【知识点】 undo log 是逻辑日志,记录的是“反向操作”:
| 原操作 | undo log 中记录的反向操作 |
|---|---|
INSERT | DELETE(回滚时删掉这条新插入的记录) |
DELETE | INSERT(回滚时把删掉的记录插回去) |
UPDATE | 反向 UPDATE(把旧值写回) |
职责一 · 事务回滚(原子性):事务执行过程中每一步修改都先写 undo log;一旦出错或显式 ROLLBACK,就按 undo log 逆序执行反向操作,把数据恢复到事务开始前的状态 —— 这就是“要么全做,要么全不做”的实现。
职责二 · MVCC 快照读:每行记录的 DB_ROLL_PTR 指向 undo log 中的旧版本,多个旧版本串成版本链,快照读时沿链找到符合自己 ReadView 的那一版(见 M12)。所以 undo log 并非“回滚完就删”,它必须多活一段时间。
两类 undo log 与生命周期:
| 类型 | 产生于 | 提交后能否立即删除 |
|---|---|---|
| insert undo log | INSERT | 可以,提交后即无用(回滚已不可能) |
| update undo log | UPDATE / DELETE | 不可以,需保留供 MVCC 快照读,由 purge 线程在确认无事务需要时清理 |
一个易漏的关键点:undo log 本身也受 redo log 保护。因为 undo 记录写在 undo 表空间的数据页上,对这些页的修改同样要记 redo log,否则崩溃后 undo log 丢失,回滚与 MVCC 都无从谈起。这也解释了“长事务为什么危险”:长事务导致 undo 版本链越拉越长,purge 无法清理,undo 表空间持续膨胀,同时拖慢快照读。
【记忆锚点】 “undo 管回滚与旧版本,redo 管重做与恢复” —— 一个向后撤销,一个向前重做。
【易混对比】
- undo log vs redo log:职责(回滚 / 恢复)、方向(撤销 / 重做)、类型(逻辑 / 物理)全不同;但 undo 页的修改又由 redo 保护,二者并非完全独立。
DB_TRX_IDvsDB_ROLL_PTR:前者判可见性,后者找旧版本,MVCC 中二者配合使用。- 长事务的三重危害:① undo 表空间膨胀;② 版本链过长拖慢快照读;③ 锁持有时间长、易死锁。
- 换问法:问“哪个日志保证原子性”,答案是 undo log;问“哪个保证持久性”,答案是 redo log。
【自测】 事务 T 执行 UPDATE t SET a = 100 WHERE id = 1(原值 a = 10)后主动 ROLLBACK。undo log 里记录的是什么?回滚由谁触发?
答:记录的是反向操作“把 a 改回 10”(逻辑日志,不记物理页变化);回滚由事务的
ROLLBACK触发,按 undo log 逆序执行。与 M12、M13、M16 连考;大厂面试高频。
【知识关联】
- 补题关联:补-17(事务 ACID 与隔离性实现)、补-20(日志+检查点恢复,undo 参与回滚段与历史版本)。
- 面试/工程:长事务 / 大事务会把 undo 撑大:历史版本链变长 → purge 追不上 → 回滚段与
history list length膨胀 → Buffer Pool 命中率下降、统计信息漂移。大促后“数据库突然变慢”常先查INNODB_TRX找长事务。在线 DDL、回滚大事务、闪回(flashback)都依赖 undo。 - 面试追问:① undo 是否参与崩溃恢复?(参与:未提交事务靠 undo 回滚;已提交但 purge 未完成的版本也可在恢复中清理) ② RR 的可重复读为什么离不开 undo?(快照读要沿 roll_pointer 重建“事务开始时”的版本,版本链就挂在 undo 上)
【拓展延伸】
- 变式问法:问“undo 与 redo 分工”→ undo 保证原子性/支撑 MVCC,redo 保证持久性;问“MVCC 版本链存哪”→ undo 页;问“delete 在 MVCC 里如何表示”→ delete mark,真正物理删除由 purge 完成。
- 参数/命令:
innodb_undo_tablespaces(8.0 默认 2,可独立 truncate);innodb_max_undo_log_size+innodb_undo_log_truncate;观察SHOW ENGINE INNODB STATUS的 History list length、information_schema.INNODB_METRICS的 purge 相关计数;INNODB_TRX.trx_started排查长事务;业务侧拆大事务、及时提交。
M16. MySQL 通过什么机制保证 redo log 与 binlog 的一致性?
考点:两阶段提交
A. 两阶段提交(prepare → 写 binlog → commit) B. 只要写完 redo log 即可,binlog 无关紧要 C. 只要写完 binlog 即可 D. 用表锁把两个日志的写入串起来
答案:A
【考点】内部 XA 两阶段提交(2PC)的执行流程与崩溃恢复判断规则。
【结论】 选 A —— redo log 与 binlog 是两套独立日志,MySQL 用两阶段提交(redo 写 prepare → 写 binlog → redo 置 commit)保证二者一致,进而保证主从一致。
【逐项辨析】
- A 两阶段提交(prepare → 写 binlog → commit):正确。 三步顺序与状态名完全正确。
- B 只要写完 redo log 即可,binlog 无关紧要:错。binlog 决定“这份变更是否发给从库”;若 redo 有而 binlog 无,主库提交了、从库收不到 → 主从不一致。
- C 只要写完 binlog 即可:错。反过来若 binlog 有而 redo 无,从库重放了、主库却回滚了 → 同样主从不一致。
- D 用表锁把两个日志的写入串起来:错。两阶段提交是日志状态机的协调机制,不是靠加锁;加锁既解决不了“崩溃后如何判断”,还会严重损害并发。
【知识点】 问题的根源是:redo log 与 binlog 是两套独立日志,无法用一次原子写完成。任何“先写一个再写另一个”的顺序,在两步之间崩溃都会产生不一致:
| 写入顺序 | 崩溃时机 | 后果 |
|---|---|---|
| 先 redo 后 binlog | redo 写完、binlog 未写 | 主库提交、从库收不到 → 主从不一致 |
| 先 binlog 后 redo | binlog 写完、redo 未写 | 从库重放、主库回滚 → 主从不一致 |
两阶段提交的执行流程:
① 写 redo log,标记为 prepare 状态(事务尚未真正提交)
② 写 binlog,并 fsync 落盘
③ 写 redo log,标记为 commit 状态(事务真正提交)崩溃恢复的判断规则(重启后扫 redo log):
| redo log 状态 | binlog 是否完整 | 处理 |
|---|---|---|
| 已 commit | — | 直接提交(事务已完成) |
| prepare | 完整 | 提交(binlog 已落盘,从库能收到,主从一致) |
| prepare | 不完整 | 回滚(binlog 残缺,若提交则主从必然不一致) |
推导要点:判断的锚点是 binlog 是否完整。只要 binlog 完整落盘,从库就一定能把这份变更重放出来,那么主库也必须提交才能与从库对齐;反之 binlog 残缺,从库无从重放,主库只能回滚。
配套优化:MySQL 5.6 引入 binlog 组提交(group commit),把多个事务的 binlog fsync 合并为一次,缓解两阶段提交带来的额外 IO 开销。
【记忆锚点】 “prepare → binlog → commit,崩了看 binlog 全不全” —— 三步两状态,判据只有一个。
【易混对比】
- 两阶段提交(2PC)vs 两阶段锁(2PL):前者是日志一致性机制,后者是并发控制机制(加锁 / 释放锁两阶段),同名不同物,考试常设混淆项。
- 内部 XA vs 外部 XA:这里的 2PC 是 MySQL 内部用于协调 redo 与 binlog 的;外部 XA 是跨数据库的分布式事务。
- 双 1 配置:
innodb_flush_log_at_trx_commit = 1与sync_binlog = 1同时开启,才能保证崩溃后两阶段提交的判断可靠。 - 换问法:若题目问“崩溃后发现 redo 处于 prepare、binlog 不完整,该怎么办”,答案是回滚。
【自测】 事务提交过程中,写 redo log(prepare)成功、写 binlog 成功,但把 redo log 置为 commit 时断电。重启后该事务会提交还是回滚?
答:提交。此时 redo 处于 prepare 且 binlog 完整,按规则判为提交 —— 否则主库回滚、从库却已重放,造成主从不一致。与 M13、M14 连考;大厂面试高频。
【知识关联】
- 补题关联:补-20(恢复时 redo/binlog 如何对齐)。
- 面试/工程:组提交(group commit)优化的是 2PC 的 fsync 次数;半同步复制也建立在 binlog 落盘之上。
- 面试追问:① 2PC 阶段失败分别怎么恢复? ② 为什么 redo 已写、binlog 未写时必须回滚?
【拓展延伸】
- 变式问法:给出崩溃点(prepare 后 / binlog 写后 / commit 后)判断事务是否提交。
- 参数/命令:
sync_binlog(0/1/N)、innodb_support_xa(历史参数)、binlog_order_commits;恢复内部用 XID 把 redo 与 binlog 对齐。
M17. InnoDB 中,对没有索引的列作为 WHERE 条件执行 UPDATE 时,会发生什么?
考点:锁退化(无索引更新)
A. 只锁住匹配的行,效率很高 B. 不加任何锁,直接更新 C. 由于无法通过索引定位,会退化为锁住全表(大量行锁),并发性能急剧下降 D. 直接报错,拒绝执行
答案:C
【考点】行锁的加锁依赖索引;无索引更新导致锁范围放大。
【结论】 行锁加在索引记录上,不是加在行上;WHERE 列没有索引 → 定位不到记录 → 逐行扫全表加锁,退化成锁全表。
【逐项辨析】
- A 只锁匹配的行、效率很高:错。这是“有索引”时才会出现的效果,而本题的前提恰恰是没索引。
- B 不加任何锁、直接更新:错。
UPDATE必然加 X 锁,区别只在锁的范围。 - C 退化为锁住全表(大量行锁):正确。 定位不到索引记录,只能扫聚簇索引上的每一条记录并逐一加锁。
- D 直接报错、拒绝执行:错。MySQL 不会因此报错,它会“默默”把锁放大 —— 这才是最危险的地方。
【知识点】 关键在“行锁加在索引上”这一句。InnoDB 加锁的对象是索引记录(index record),所以:
WHERE 条件 | 加锁方式 | 实际锁范围 |
|---|---|---|
主键等值(id = 5) | 主键索引定位到 1 条记录 | 只锁该行 |
| 二级索引等值 | 二级索引定位 + 回表锁主键行 | 少量行 |
| 无索引列 | 扫聚簇索引全部记录、逐条加锁 | ≈ 全表 |
推论:既然锁加在索引上,索引失效 = 锁范围放大。WHERE 列上套函数、隐式类型转换、OR 一侧无索引(见 M06、M07)都会让“本来有索引”的语句退化成全表加锁 —— 这是线上并发雪崩与死锁的常见根因。
再补一个易漏的边界:隔离级别影响锁的释放时机。REPEATABLE READ 下,扫描过但不匹配的行也会一直持锁到事务结束;READ COMMITTED 下 InnoDB 会对不匹配的行提前释放锁(与 semi-consistent read 相关),锁范围相对小一些 —— 但全表扫描的代价依然存在,所以“建索引”仍是唯一正解。
【记忆锚点】 “行锁靠索引,索引一失效、行锁变表锁” —— 判据是“能不能用索引定位到记录”。
【易混对比】
- 无索引 → 锁全表(本题) vs 索引失效 → 同样锁全表(M06、M07):两者后果相同,只是成因不同。
- 锁范围 vs 隔离级别:锁范围由索引决定,不由隔离级别决定;隔离级别只决定“加什么锁(记录锁 / 间隙锁)”以及“锁何时释放”。
- 表锁 vs 行锁退化:前者是显式
LOCK TABLES或引擎不支持行锁(MyISAM);后者是 InnoDB 因无索引而“被迫”逐行加锁,本质仍是行锁,只是数量等于全表行数。 - 换问法:若题干改成“
WHERE列有索引但用了函数”,答案仍是“锁范围放大”。
【自测】 表 t(id 主键, name, age),name 有索引、age 无索引。事务 A 执行 UPDATE t SET age = 1 WHERE age = 20,事务 C 执行 UPDATE t SET age = 2 WHERE id = 1。C 会被阻塞吗?
答:会被阻塞。A 的
WHERE列age无索引 → 逐行扫全表加 X 锁 →id = 1这行也在锁范围内。与 M06、M07、M23 连考;大厂面试高频。
【知识关联】
- 补题关联:补-19(S/X)、补-23(死锁后牺牲事务)。
- 面试/工程:无索引
UPDATE/DELETE是把整表锁住的高危操作;上线前必须EXPLAIN确认访问路径。 - 面试追问:① RC 下无索引更新的锁范围与 RR 有何不同? ② 如何用锁等待诊断“整表卡死”?
【拓展延伸】
- 变式问法:问“DELETE 全表无 WHERE”比“UPDATE 无索引”更危险吗(还可能更慢更锁久)。
- 参数/命令:
SHOW PROCESSLIST、performance_schema.data_lock_waits;innodb_lock_wait_timeout(默认 50s);避免在业务高峰做无索引批量更新,改按主键分批。
M18. InnoDB 中意向锁(IS / IX)的作用是?
考点:意向锁
A. 用于行锁之间的互斥 B. 是一种表级锁,用于快速判断“表中是否存在行锁”,避免加表锁时逐行检查 C. 用于实现 MVCC 的可见性判断 D. 用于主从复制的一致性校验
答案:B
【考点】意向锁的定位:表级锁 + 兼容性矩阵 + 与行锁的关系。
【结论】 选 B —— 意向锁是 InnoDB 自动添加的表级锁,唯一作用是快速判断“表中是否已有行锁”,让加表锁时不必逐行检查。
【逐项辨析】
- A 用于行锁之间的互斥:错。行锁之间的互斥靠索引记录上的锁本身判断,与意向锁无关;意向锁是表级的。
- B 是一种表级锁,用于快速判断表中是否存在行锁,避免加表锁时逐行检查:正确。 定位(表级)、作用(快速判断)、价值(避免逐行扫描)三项全对。
- C 用于实现 MVCC 的可见性判断:错。可见性判断靠 ReadView + 隐藏列 + undo log(见 M12),意向锁不参与。
- D 用于主从复制的一致性校验:错。主从一致性靠 binlog 与两阶段提交(见 M16),与锁无关。
【知识点】 先明确意向锁是什么:意向锁(IS / IX)是表级锁,由 InnoDB 在给行加锁时自动添加,用户无法手动干预。
| 意向锁 | 全称 | 何时自动添加 |
|---|---|---|
| IS | 意向共享锁(Intention Shared) | 事务准备给某行加 S 锁(共享锁)之前 |
| IX | 意向排他锁(Intention Exclusive) | 事务准备给某行加 X 锁(排他锁)之前 |
它要解决的问题:假设事务 A 已给表中 3 行加了行锁,事务 B 想给整张表加表级 X 锁。若没有意向锁,B 必须逐行扫描全表才能确认“有没有行锁”,表越大越慢;有了意向锁,B 只要看一眼“这张表上有没有 IX / IS”就立即得到答案。意向锁就是行锁的“表级代言人”。
兼容性矩阵(✅ 兼容,❌ 冲突):
| 已持有 ↓ / 请求 → | IS | IX | 表级 S | 表级 X |
|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ❌ |
| IX | ✅ | ✅ | ❌ | ❌ |
| 表级 S | ✅ | ❌ | ✅ | ❌ |
| 表级 X | ❌ | ❌ | ❌ | ❌ |
矩阵读法(三条结论):
- 意向锁之间全部互相兼容 —— IS / IX 只表示“我打算在某行上加锁”,多个事务可以同时在不同行上加锁。
- IS 与表级 S 兼容 —— 因为表级 S 与行级 S 方向一致,都是读。
- IX 与表级 S / X 都冲突 —— IX 意味着“表中有行要写”,与整表加读锁或写锁都无法共存。
推导:矩阵的实质是“表级锁必须与表内所有行级锁兼容才不冲突”。因为意向锁忠实地代表了“表中行锁的存在与类型”,所以判断“表锁能否加”只需检查意向锁,逻辑上等价于检查全表行锁。
【记忆锚点】 “意向锁是行锁的代言人,表锁问它就行” —— 意向锁之间不打架,只跟表锁打架。
【易混对比】
- 意向锁(IS / IX)vs 表级锁(S / X):都是表级,但意向锁是自动加的、仅作标记,不真正阻塞别人读写行;表级 S / X 才真正锁住整表。
- 意向锁 vs 行锁:行锁加在索引记录上(见 M17),意向锁加在表上;一次加行锁会连带加意向锁。
- IS vs IX:IS 对应“要加行 S 锁”,IX 对应“要加行 X 锁”;IX 更“霸道”,与表级 S 也冲突。
- 换问法:若题目问“事务要给某行加 X 锁,会先在表上加什么锁”,答案是 IX 锁。
【自测】 事务 A 执行 SELECT * FROM t WHERE id = 1 FOR UPDATE(id 是主键)。此时表 t 上会存在哪两种锁?
答:表级 IX 锁 + 主键索引记录上的 X 锁。先自动加 IX 表示“表内有行要写”,再在
id = 1的索引记录上加 X 锁。与 M11、M17 连考;大厂面试高频。
【知识关联】
- 补题关联:补-19(表锁与行锁的兼容检查)。
- 面试/工程:意向锁让
LOCK TABLES之类表级请求不必逐行检查;理解它能解释为什么“有行锁时整表读仍可能被挡住”。 - 面试追问:① IS 与 IX 互相兼容吗? ② 意向锁会阻塞普通
SELECT吗?
【拓展延伸】
- 变式问法:问“意向锁存在的唯一目的是什么”(快速判断表级与行级冲突)。
- 参数/命令:
SELECT * FROM performance_schema.data_locks WHERE lock_type='TABLE'可看到 IS/IX;LOCK TABLE t READ/WRITE会触发表级与意向锁交互。
M19. EXPLAIN 结果的 type 字段中,访问效率最好的是?
考点:EXPLAIN 的 type 字段
A. ALL(全表扫描) B. index(全索引扫描) C. range(范围扫描) D. const(通过主键或唯一索引等值查询,最多一行)
答案:D
【考点】EXPLAIN 中 type 字段的访问类型与优劣顺序。
【结论】 选 D —— type 效率从高到低大致为 system > const > eq_ref > ref > range > index > ALL,其中 const 通过主键或唯一索引等值查询最多命中一行,效率最高。
【逐项辨析】
- A
ALL全表扫描:错。这是最差的访问类型,是首要优化信号,不是最好。 - B
index全索引扫描:错。它比ALL略好(扫的是更小的索引文件,且通常有序),但仍需扫完整棵索引树,属需要优化的类型。 - C
range范围扫描:错。range已能用索引定位区间,是可接受的类型,但仍不如const精准。 - D
const通过主键或唯一索引等值查询,最多一行:正确。 优化阶段就能确定结果集最多一行,访问代价最低。
【知识点】 type 表示“MySQL 决定用什么方式访问这张表”,是 EXPLAIN 中最该先看的一列。
type | 含义 | 典型场景 | 评价 |
|---|---|---|---|
system | 表只有一行(系统表) | 极少见 | 最好 |
const | 通过主键 / 唯一索引等值查询,最多一行 | WHERE id = 5 | 最好(实用场景) |
eq_ref | 被驱动表通过唯一索引等值匹配,一行 | 多表 JOIN 中被驱动表走主键 | 很好 |
ref | 通过普通索引等值匹配,可能多行 | WHERE name = 'a'(name 为普通索引) | 好 |
range | 范围扫描索引 | BETWEEN、>、<、IN | 可接受 |
index | 全索引扫描 | 扫完整个索引树 | 需优化 |
ALL | 全表扫描 | 无可用索引 | 最需优化 |
推导:效率的本质是“要读多少数据、能不能精确定位”。const 在优化阶段就能借助唯一性约束推断出“最多一行”,连多读一次都不必;ref 只能确定“一批行”,要到执行阶段才知道有多少;range 要遍历区间内的所有索引项;index 要遍历整棵索引树;ALL 要遍历整张表的所有数据页 —— 从“精确一行”到“全表所有行”,扫描量单调递增,效率单调递减。
其他常见取值:ref_or_null(ref + 额外查 NULL)、index_merge(索引合并)、unique_subquery / index_subquery(子查询优化)、fulltext(全文索引)。
【记忆锚点】 "system > const > eq_ref > ref > range > index > ALL" —— 背下这条链,看 EXPLAIN 先对号入座。
【易混对比】
constvseq_ref:const是单表查询最多一行;eq_ref是多表 JOIN 中被驱动表每匹配一行。二者都基于唯一索引等值,区别在单表 / 关联。refvsrange:ref是等值匹配(可能多行),range是范围匹配(区间)。indexvsALL:都“扫全”,区别是index只扫索引树(更小、通常有序),ALL扫数据页。- 换问法:若题目问“哪个
type说明需要重点优化”,答案是ALL(其次index)。
【自测】 表 t 有主键 id 与普通索引 name。EXPLAIN SELECT * FROM t WHERE name = 'a' 的 type 是什么?改成 WHERE id = 5 呢?
答:前者是
ref(普通索引等值,可能多行);后者是const(主键等值,最多一行)。与 M20 连考;大厂面试高频。
【知识关联】
- 补题关联:补-25(ICP 时 type 可能是 range/ref,Extra 才是关键)。
- 面试/工程:SQL 优化第一眼就是 type 与 key;
ALL/index大范围出现意味着索引设计或写法失败。 - 面试追问:①
type=range一定比ref慢吗? ② 为什么index(全索引扫描)有时仍可接受?
【拓展延伸】
- 变式问法:给一串 type 要求排序:
system > const > eq_ref > ref > range > index > ALL。 - 参数/命令:
EXPLAIN FORMAT=TREE(8.0.16+)/EXPLAIN ANALYZE更直观;optimizer_trace可看为何没选更好 type。
M20. EXPLAIN 的 Extra 中出现 Using filesort 说明?
考点:EXPLAIN 的 Extra 字段
A. 使用了磁盘临时文件排序,是需要优化的信号 B. 属于正常现象,无需关注 C. 说明已经利用了索引的有序性完成排序 D. 说明使用了覆盖索引
答案:A
【考点】Extra 中常见提示(Using filesort / Using temporary / Using index / Using where)的含义与优化方向。
【结论】 选 A —— Using filesort 说明 MySQL 无法利用索引的有序性、需要额外做一次排序,是需要优化的信号。
【逐项辨析】
- A 使用了磁盘临时文件排序,是需要优化的信号:正确。 严格口径是:
Extra里出现Using filesort表示本次查询发生了 filesort —— 索引的有序性用不上,MySQL 必须额外做一次排序,因此是需要优化的信号。至于“磁盘临时文件”:排序先在sort_buffer_size的内存缓冲区里进行,只有数据量放不下时才落盘做归并排序,所以落盘是最坏情况而非必然动作(详见下文澄清)。本题仍选 A 的理由:四个选项里只有 A 命中“额外排序 + 需要优化”这一核心语义,其余三项都与Using filesort直接矛盾(B 说无需关注、C 说已用上索引有序性、D 说覆盖索引),按“最优选项”原则取 A。 - B 属于正常现象、无需关注:错。它意味着多了一次排序开销,是明确的优化信号,不能说“无需关注”。
- C 说明已经利用了索引的有序性完成排序:错。恰恰相反 —— 如果索引有序性够用,
Extra里根本不会出现Using filesort。 - D 说明使用了覆盖索引:错。覆盖索引的标志是
Using index,与排序无关。
【知识点】 Extra 列是 EXPLAIN 的“诊断意见”,比 type 更能指出具体的性能病灶。四个高频值对照:
Extra | 含义 | 好坏 | 优化方向 |
|---|---|---|---|
Using filesort | 无法用索引有序性,额外做一次排序 | ❌ 需优化 | 为 WHERE + ORDER BY 建联合索引,让索引天然有序 |
Using temporary | 使用了临时表 | ❌ 需优化 | 常见于 GROUP BY / DISTINCT 未走索引,补索引 |
Using index | 用到了覆盖索引,无需回表 | ✅ 好事 | 保持,这是理想状态 |
Using where | 需要在取到行之后再过滤 | ⚠️ 中性 | 视情况把过滤条件下推到索引 |
关于 Using filesort 名称的关键澄清:它不一定真的用磁盘文件。MySQL 会先在内存中排序(缓冲区大小由 sort_buffer_size 决定),只有数据量超出排序缓冲区时才退化为磁盘临时文件归并排序。所以 filesort 应当理解为“额外的一次排序操作”,而非“一定落磁盘”。
Using filesort 的三条常见成因:
ORDER BY的列与索引的顺序不匹配(例如索引是(a, b),却按b排序);- 排序方向不一致(例如索引是
(a ASC, b ASC),却ORDER BY a ASC, b DESC,MySQL 8.0 之前无法直接利用); - 排序列未包含在本次使用的索引中。
推导:B+ 树索引本身天然有序(叶子节点按键值顺序用双向链表相连),所以只要 ORDER BY 的列顺序与索引顺序完全对齐,MySQL 只需顺着索引叶子链表读就能拿到有序结果,无需再排。一旦对不齐,索引的有序性就“用不上”,只能把结果集取出来重新排一遍 —— 这就是 Using filesort 的由来。
【记忆锚点】 “Using index 是奖状,Using filesort / Using temporary 是病危通知” —— 一个说“省了回表”,两个说“多了一道开销”。
【易混对比】
Using filesortvsUsing temporary:前者多一次排序,后者多建一张临时表;GROUP BY走不了索引时两者常同时出现。Using indexvsUsing where:Using index表示“索引里就有全部所需列”(覆盖索引,好事);Using where表示“存储引擎返回后还要再过滤”(中性)。Using indexvsUsing index condition:前者是覆盖索引(不回表),后者是索引条件下推(ICP),仍可能回表,切勿混为一谈。- 换问法:若题目问“
Extra中出现哪个值说明用到了覆盖索引”,答案是Using index。
【自测】 表 t 有联合索引 idx(a, b)。SELECT a, b FROM t ORDER BY a, b 与 SELECT a, b FROM t ORDER BY b 的 Extra 分别是什么?
答:前者不出现
Using filesort(排序键与索引顺序(a, b)一致,且两列都在索引中,会出现Using index);后者出现Using filesort(排序键只有b,与索引顺序不对齐,用不上有序性)。与 M19 连考;大厂面试高频。
【知识关联】
- 补题关联:补-25(Extra 字段家族辨析)。
- 面试/工程:
Using filesort不一定是“磁盘排序”,内存排序也会这么显示;优先消除的是“无法用索引有序”的根因。 - 面试追问:①
Using temporary与Using filesort常一起出现吗? ② 如何消掉 filesort?
【拓展延伸】
- 变式问法:把 Extra 选项改成
Using index/Using where/Using index condition互考。 - 参数/命令:
sort_buffer_size、max_length_for_sort_data;ORDER BY借索引时 Extra 无 filesort;EXPLAIN ANALYZE可见 actual sort time。
M21. 开启并配置 MySQL 慢查询日志,涉及的参数是?
考点:慢查询日志
A. slow_query_log 与 long_query_time B. binlog_format C. innodb_buffer_pool_size D. max_connections
答案:A
【考点】慢查询日志的开启方式与配套参数。
【结论】 选 A:slow_query_log 管开关、long_query_time 管阈值,一开一卡,超过阈值的语句才会被记进慢查询日志。
【逐项辨析】
- A
slow_query_log与long_query_time:正确。 前者是慢查询日志的总开关(ON/OFF),后者是“超过多少秒算慢”的阈值(单位秒,支持小数如0.2),两者配套使用才生效。 - B
binlog_format:错。它管的是 binlog 的记录格式(STATEMENT/ROW/MIXED),决定的是“变更怎么记”,与“慢语句要不要记”无关。 - C
innodb_buffer_pool_size:错。这是 InnoDB 缓冲池大小,属于性能调优参数,改它不影响慢查询日志是否开启。 - D
max_connections:错。这是允许的最大并发连接数,属连接层参数,与慢查询日志没有任何关系。
【知识点】 慢查询日志是 MySQL 的“慢 SQL 侦查器”:一条语句的实际执行时间超过 long_query_time,MySQL 就把它的原文、耗时、扫描行数、返回行数等写进日志文件,供事后分析。
| 参数 | 作用 | 典型取值 |
|---|---|---|
slow_query_log | 慢查询日志总开关 | ON / OFF |
slow_query_log_file | 日志文件路径 | /var/log/mysql/slow.log |
long_query_time | 阈值(秒,支持小数) | 0.2 / 0.5 / 1 |
log_queries_not_using_indexes | 额外记录未走索引的语句 | 线上慎开(量极大) |
log_output | 输出到文件还是表 | FILE / TABLE |
生产排查是一条固定流水线:慢查询日志 → mysqldumpslow / pt-query-digest 汇总排序 → 对 TOP 慢 SQL 执行 EXPLAIN 看执行计划 → 加索引或改写 SQL。两个容易踩的点:一是 long_query_time 默认值通常为 10 秒,默认阈值偏高,线上一般调到 0.2~1 秒才抓得住问题;二是 log_queries_not_using_indexes 虽然能捞到未走索引的语句,但在大表上会瞬间刷爆日志文件,只建议短时排查时临时打开。
【记忆锚点】 “开日志看 slow_query_log,定快慢看 long_query_time” —— 一个管开关,一个管标尺。
【易混对比】
- 慢查询日志 vs binlog:慢查询日志面向“性能排查”(记执行慢的语句);binlog 面向“数据复制与恢复”(记所有变更)。两者目的完全不同,
binlog_format管不了慢查询。 long_query_timevslog_queries_not_using_indexes:前者按时间筛,后者按是否走索引筛,排查时通常两者一起看。- 换问法:若题目问“记录未使用索引的语句用哪个参数”,答案就变成
log_queries_not_using_indexes;若问“慢查询日志写到哪个文件”,答案是slow_query_log_file。
【自测】 某线上库想抓出所有执行时间超过 200 毫秒的 SQL,除 slow_query_log = ON 外,还要把哪个参数设成什么值?
答:
long_query_time = 0.2。该参数单位是秒且支持小数,200 毫秒即 0.2 秒。与 M19、M20 同属日志与排查链路;DBA 岗与运维面试高频。
【知识关联】
- 补题关联:补-25(慢 SQL 分析常从 type/Extra/ICP 入手)。
- 面试/工程:慢查询日志 +
mysqldumpslow/pt-query-digest是 DBA 日常;应用侧还应配合 APM 采样,避免只看库不看调用方。 - 面试追问:①
long_query_time=0有什么风险? ② 慢日志里的 Rows_examined 远大于 Rows_sent 说明什么?
【拓展延伸】
- 变式问法:问“只记录未走索引的 SQL”用什么参数;问“如何动态开启不用重启”。
- 参数/命令:
slow_query_log、slow_query_log_file、long_query_time、log_queries_not_using_indexes、min_examined_row_limit;注意开关参数没有long_query_log这个名字 —— 5.6 之前叫log_slow_queries,5.6 起统一为slow_query_log;8.0 支持SET GLOBAL slow_query_log=ON热更新。
M22. InnoDB 建议使用自增主键的原因,不包括?
考点:主键选型
A. 自增主键顺序写入,减少页分裂与数据移动,插入性能更好 B. 自增主键占用空间小,而二级索引叶子要存主键值,因此索引更省空间 C. 自增主键天然唯一,避免业务键冲突 D. 自增主键可以避免回表
答案:D
【考点】主键选型对插入性能与索引体积的影响。
【结论】 选 D。回表与否只取决于“查询需要的列是否都在所用索引里”,与主键是否自增毫无关系;A、B、C 都是自增主键的真实优势,因此“不包括”的正是 D。
【逐项辨析】
- A 自增主键顺序写入、减少页分裂与数据移动:是优势,不选。新记录总落在 B+ 树最右侧页,几乎不触发结构调整,插入是顺序 IO。
- B 自增主键占用空间小、二级索引更省空间:是优势,不选。二级索引叶子要存主键值,主键越短,每个索引页能装的条目越多,树越矮。
- C 自增主键天然唯一、避免业务键冲突:是优势,不选。自增值由数据库生成、不含业务语义,业务规则变更也不会牵动主键。
- D 自增主键可以避免回表:错误命题。 回表的成因是“二级索引叶子只有索引列 + 主键,缺其余列”,与主键是否自增无关 —— 用自增主键去查非索引列,一样要回表。
【知识点】 主键选型的三条判据:有序性、长度、业务无关性。
| 主键类型 | 写入有序性 | 长度 | 二级索引体积 | 典型问题 |
|---|---|---|---|---|
自增 BIGINT | 顺序追加,几乎不页分裂 | 8 字节 | 最小 | 主键值可被外部枚举 |
| 无序 UUID(v4) | 随机插入,频繁页分裂 | 36 字节字符串 | 膨胀数倍 | 写放大、碎片多、命中率低 |
| 有序 UUID(v7 / 雪花 ID) | 近似顺序 | 16 字节或更长 | 较大 | 实现复杂、依赖时钟 |
| 业务键(手机号 / 身份证) | 随机 | 较长 | 较大 | 业务一变就要改主键 |
关键推导有两条。其一(写性能):InnoDB 的聚簇索引按主键有序组织,新主键若总是当前最大值,新行就追加到最右页,写满直接申请新页;若主键随机,新行要插进已有页的中间,页满时触发页分裂(一页拆两页、搬移约一半数据),同时产生空间碎片与随机 IO。其二(读性能):二级索引叶子存的是“索引列 + 主键值”,主键每长 1 字节,二级索引的每个条目就长 1 字节;主键从 8 字节 BIGINT 换成 36 字节 UUID,二级索引体积膨胀约 4 倍以上(口径:按窄索引列估算 —— 实算主键 1 字节时 4.11 倍、10 字节 2.56 倍、20 字节 2.00 倍,列越宽倍数越小),同样的缓冲池只能缓存更少的索引页,命中率随之下降。结论:自增 BIGINT 是 InnoDB 主键的默认最优解;必须用 UUID 时优先选有序 UUID。
【记忆锚点】 “自增主键三好处:写得顺(有序)、占得少(短)、管得省(无业务含义)” —— 回表不在这三条里。
【易混对比】
- 自增主键的优势 vs 回表:前者讲“写入与索引体积”,后者讲“查询能否只查一棵树”,两者维度不同。本题的陷阱正是把这两件事硬扯上因果关系。
- 自增主键 vs 业务主键:自增主键的缺点是可被外部枚举、分库分表后需改造(改用雪花 ID);业务主键的缺点是业务一变更就要改主键,代价极大。
- 换问法:若题干改成“下列哪项是自增主键的优势”,答案就是 A / B / C 中任意一项;若问“二级索引叶子存什么”,答案仍是“索引列 + 主键值”(与 M01 互为镜像)。
【自测】 某表原用自增 BIGINT 主键,后改用 UUIDv4 字符串做主键,结果二级索引文件明显变大、插入 TPS 下降。这两个现象各自的直接原因是什么?
答:索引变大是因为二级索引叶子要存主键值,主键由 8 字节变 36 字节;插入变慢是因为 UUIDv4 随机无序,新行插在已有页中间,触发频繁页分裂与随机 IO。与 M01、M23 连考;大厂面试高频。
【知识关联】
- 补题关联:补-23(死锁检测与主键冲突场景)、补-24(金额用 DECIMAL,类型选型与主键长度同属“类型代价”)。
- 面试/工程:分库分表后自增失效,常见雪花/号段发号器;对外暴露自增 ID 可被竞品枚举订单量,需混淆(雪花、加密 ID)。主库高并发写入时,自增近似顺序追加,UUID 则把写放大打满磁盘 IOPS——压测里同一业务换主键 TPS 可差数倍。
- 面试追问:① 雪花 ID 会不会页分裂?(会,但远少于 UUIDv4:时间戳高位大致递增,只有同毫秒内或时钟回拨才可能乱序) ② 为什么二级索引更怕长主键?(每个二级索引叶子都要冗余一份主键,主键从 8B→16B,所有二级索引体积与 Buffer Pool 占用同步放大)
【拓展延伸】
- 变式问法:正向问“InnoDB 主键推荐”→ 自增 BIGINT;反向问“自增主键缺点”→ 可枚举、分库分表难扩展、无法做业务语义。把“手机号当主键”设为错误项(业务变更+长度+随机)。
- 参数/命令:
innodb_autoinc_lock_mode(0 传统 / 1 连续 / 2 交错,影响批量插入与 statement 复制安全);SHOW CREATE TABLE看主键定义;UUIDv7/雪花可存BINARY(16)而非CHAR(36)省一半空间;压测对比时用performance_schema看 page splits。
M23. 使用无序的 UUID 作为 InnoDB 主键,最主要的缺点是?
考点:页分裂
A. 无序插入导致频繁页分裂与数据移动,插入性能差;且长度长,二级索引占用空间更大 B. UUID 不保证唯一性 C. UUID 列不能建索引 D. UUID 无法参与排序
答案:A
【考点】页分裂的产生原因及其对写性能的影响。
【结论】 选 A:无序 UUID 会让新记录插到任意页中间,页满即页分裂并搬移约一半数据;叠加 36 字节的长度,二级索引也被撑大。
【逐项辨析】
- A 无序插入导致频繁页分裂与数据移动、插入性能差;且长度长、二级索引占用空间更大:正确。 一句话把“写放大”与“空间膨胀”两个真实代价都点到了。
- B UUID 不保证唯一性:错。唯一性恰恰是 UUID 的强项,它的短板是随机无序与长度长,与唯一性无关。
- C UUID 列不能建索引:错。UUID 列完全可以建索引,只是索引体积更大、写入更慢 —— 把“代价高”说成了“做不到”。
- D UUID 无法参与排序:错。UUID 是字符串,可比较、可排序,
ORDER BY照常工作,只是排序结果按字典序而非时间序。
【知识点】 页分裂的根源是“B+ 树要求页内有序”,是否分裂取决于插入位置是否连续。
| 主键形态 | 新记录落点 | 是否触发页分裂 | IO 特征 |
|---|---|---|---|
自增 BIGINT | 最右页尾部追加 | 仅在页满时申请新页 | 顺序 IO |
| 无序 UUID | 任意页中间随机插入 | 频繁分裂(拆页 + 搬移约一半数据) | 随机 IO |
推导链条:聚簇索引的叶子页内部按键值升序排列 → 插入一条“键值落在中间”的记录,必须塞进对应位置 → 该页写满时 InnoDB 只能把一页拆成两页并重新分配记录(通常各占约一半)→ 拆分后两页都只填了一半,空间利用率下降、碎片增多,同时被拆页及其父节点都要落盘,产生额外的随机写。因此 UUID 表的典型症状是:写入 TPS 下降、表空间虚高、缓冲池命中率下降。补两个口径:一是有序 UUID(UUIDv7 / 雪花 ID)能大幅缓解但无法完全消除;二是自增主键并非绝对免疫,若删掉中间一段再随机回插,同样会触发页分裂。
【记忆锚点】 “自增往后加,UUID 到处插;插到页中间,一页变两页”。
【易混对比】
- 页分裂 vs 页合并:分裂由“页写满 + 中间插入”触发;合并由“大量删除后页利用率过低”触发。二者是方向相反的动作。
- UUIDv4 vs UUIDv7 / 雪花 ID:前者完全随机、必然页分裂;后者时间有序、接近顺序追加,是“用 UUID 又不吃页分裂亏”的主流方案。
- 换问法:若题目问“为什么推荐
BIGINT而不是 UUID 做主键”,答案仍是“顺序写入减少页分裂 + 长度短使二级索引更小”(与 M22 是同一知识点的正反两面)。
【自测】 一张日志表主键是自增 BIGINT,日常只追加写入、性能良好。某天业务改为“按时间回补历史数据”,随机插入大量旧时间戳记录,写性能明显恶化。原因是什么?
答:回补的旧记录键值落在已有页中间,破坏了“顺序追加”的前提,触发频繁页分裂与随机 IO。缓解方式是按时间分区或批量顺序回填。与 M22 连考;DBA 岗高频。
【知识关联】
- 补题关联:补-22(页被载入 Buffer Pool,分裂增加 IO)。
- 面试/工程:导入历史数据时先关索引再开,或按主键有序落地;监控
Innodb_row_inserts与磁盘写放大可侧面看到分裂代价。 - 面试追问:① 页分裂与页合并分别何时触发? ② 有序 UUID 能否完全避免分裂?
【拓展延伸】
- 变式问法:问“为什么日志表突然变慢”——常是随机回补导致分裂。
- 参数/命令:
INFO: innodb_buffer_pool_pages_split(状态计数);页填充因子相关实践(预留空间减少分裂);innodb_page_size影响一页能装多少行。
M24. SELECT * FROM t ORDER BY id LIMIT 1000000, 10 执行很慢,合理的优化思路是?
考点:深分页优化
A. 再多建几个索引就能解决 B. 采用延迟关联(先用覆盖索引取出主键,再 JOIN 回原表)或基于游标翻页(WHERE id > 上次最大 id LIMIT 10) C. 加上 ORDER BY 就能加速 D. 深分页无法优化
答案:B
【考点】深分页的性能瓶颈与两种标准解法。
【结论】 选 B。LIMIT 1000000, 10 的瓶颈是必须先扫过并丢弃前 1000000 行,解法就是“让这 100 万行不必回表”——延迟关联或游标翻页。
【逐项辨析】
- A 再多建几个索引就能解决:错。多建索引改变不了“要跳过 N 行”这个动作本身,甚至因索引更多、优化器选择变差而更慢;问题出在偏移量,不在索引数量。
- B 延迟关联或基于游标翻页:正确。 延迟关联把“排序 + 偏移”压进覆盖索引内完成,只对最终 10 行回表;游标翻页把偏移量换成范围条件,彻底绕开“扫描丢弃”。
- C 加上
ORDER BY就能加速:错。原语句本来就有ORDER BY id,再加没有意义;ORDER BY本身也不是加速手段,能否加速取决于排序能否用上索引的有序性。 - D 深分页无法优化:错。属于“放弃治疗”式表述,B 的两种方案都是成熟工程实践。
【知识点】 深分页慢的本质是偏移量被当成了“要扫过的行数”。
LIMIT m, n 的执行语义不是“直接跳到第 m 行”,而是“先取前 m + n 行,再丢掉前 m 行,返回剩下 n 行”。若查询走的是二级索引,被丢掉的那 100 万行每一行都要回表取 * 所需的列,于是 100 万次随机 IO 白做。两种解法正是针对这两点分别下刀:
| 方案 | 写法要点 | 优化掉的是什么 | 代价 |
|---|---|---|---|
| 延迟关联 | 子查询只在覆盖索引里 ORDER BY ... LIMIT m, n 取主键,外层 JOIN 回表取整行 | 把“100 万次回表”降为只有最终 n 行回表 | 仍需扫描并丢弃前 m 行(但只扫索引,代价小得多) |
| 游标 / 书签翻页 | 记住上一页最大 id,改成 WHERE id > 上次最大 id ORDER BY id LIMIT n | 把“偏移量”换成范围查询,不再扫描丢弃 | 不支持随机跳页,只能上一页 / 下一页 |
延迟关联的完整形态:
SELECT t.* FROM t
JOIN (SELECT id FROM t ORDER BY id LIMIT 1000000, 10) AS x USING (id);子查询只需要 id 一列,而 id 恰好在主键索引(聚簇索引)上,因此这一步是覆盖索引扫描、不回表;外层 JOIN 只对 10 个 id 回表。游标翻页则把复杂度从 O(m + n) 降到 O(log N + n)(借助 B+ 树定位起点)。选型建议:C 端列表页(只能上一页 / 下一页)用游标翻页;后台管理页(需要跳页)用延迟关联。
【记忆锚点】 “深分页慢在”白扫一大片“——延迟关联让扫描不回表,游标翻页让扫描不存在”。
【易混对比】
- 延迟关联 vs 游标翻页:前者保留跳页能力、性能改善有限;后者性能最好但只能顺序翻页。二者是“能力与性能”的取舍。
- 深分页 vs
ORDER BY无索引:两者都可能慢,但成因不同 —— 深分页慢在偏移量大(即使有索引也慢),无索引排序慢在排序本身(加合适索引即可解决)。本题ORDER BY id已走主键,属前者。 - 换问法:若题目问“必须支持跳页时怎么优化”,答案就是延迟关联;若问“只支持下一页时最优解”,答案就是游标翻页。
【自测】 表 t 有 500 万行,业务必须支持跳页。SELECT * FROM t ORDER BY id LIMIT 4000000, 20 很慢,改写成延迟关联后明显变快。请说明“变快”的直接原因。
答:排序与偏移 400 万行的动作被压进了覆盖索引(只需
id一列、不回表),只有最终 20 行回表取整行,省掉了约 400 万次随机回表。与 M19、M20 连考;大厂面试高频。
【知识关联】
- 补题关联:补-25(ICP 在索引层过滤,与延迟关联同属“少回表”)、补-22(Buffer Pool 命中率受索引/扫描方式影响)。
- 面试/工程:后台导出、报表深分页是慢查询 TOP 来源;C 端“加载更多”几乎都应改成游标。电商订单列表若仍
LIMIT 1000000,10,DBA 会直接打回。 - 面试追问:① 延迟关联为什么仍可能扫很多索引页?(前 m 行仍要在覆盖索引里顺序扫过并丢弃,只是不回表) ② 游标翻页如何保证不丢不重?(依赖稳定排序键,通常用主键或 (create_time,id) 复合游标,避免同时间戳抖动)
【拓展延伸】
- 变式问法:题干给出
LIMIT 1000000,10问优化方案;或问“为什么WHERE id > 1000000 LIMIT 10比LIMIT 1000000,10快”——前者走主键范围定位,复杂度 O(log N + n)。 - 参数/命令:
EXPLAIN看rows与Extra(Using index vs Using filesort);延迟关联模板见知识点;游标字段建议唯一且递增;若必须跳页,可粗分桶(按日期分区)再页内精定位。
M25. 关于 COUNT 函数,下列说法正确的是?
考点:MySQL 的 count
A. COUNT(*) 会跳过 NULL 行,COUNT(列) 统计所有行 B. COUNT(*)、COUNT(1)、COUNT(主键) 三者在语义上完全等价 C. COUNT(列) 会跳过该列为 NULL 的行,COUNT(*) 统计所有行 D. COUNT(1) 会跳过 NULL
答案:C
【考点】COUNT 的语义差异与 InnoDB 的计数优化。
【结论】 选 C。COUNT(列) 数的是“该列非 NULL 的行数”,所以跳过 NULL;COUNT(*) 数的是行数,与列值无关。
【逐项辨析】
- A
COUNT(*)会跳过NULL行、COUNT(列)统计所有行:错。方向恰好说反 —— 数所有行(因而不受 NULL 影响)的是COUNT(*),跳过 NULL 的是COUNT(列)。 - B
COUNT(*)、COUNT(1)、COUNT(主键)三者语义完全等价:错。表述过于绝对 ——COUNT(*)与COUNT(1)确实等价(都数行),而COUNT(主键)严格说是“数主键非 NULL 的行数”,只因主键不允许为 NULL,结果才恰好一致。 - C
COUNT(列)跳过该列为 NULL 的行,COUNT(*)统计所有行:正确。 这正是两者的分水岭。 - D
COUNT(1)会跳过 NULL:错。COUNT(1)里的1是常量,根本不涉及列值判断,不存在“跳过 NULL”一说。
【知识点】 聚合函数对 NULL 的处理规律 + InnoDB 的计数实现。
| 写法 | 数的是什么 | 是否受 NULL 影响 | 说明 |
|---|---|---|---|
COUNT(*) | 行数 | 否 | 优化器专门处理,选最小的可用索引遍历 |
COUNT(1) | 行数 | 否 | 与 COUNT(*) 结果相同,语义等价 |
COUNT(列) | 该列非 NULL 的个数 | 是,跳过 NULL | 列可空时结果小于总行数 |
COUNT(主键) | 主键非 NULL 的个数 | 否(主键非空) | 结果等于行数,但语义上仍走“判空” |
推导要抓两点。其一(语义):COUNT(expr) 的规则是“对每一行求 expr,结果不为 NULL 则计数加一”;COUNT(*) 是这条规则的特例 —— 它不针对任何列,直接数行。其二(实现):InnoDB 不像 MyISAM 那样保存行数(MyISAM 是表级锁、无 MVCC,可以存一个计数器;InnoDB 有 MVCC,同一时刻不同事务看到的行数可能不同,无法缓存统一值),因此 COUNT(*) 必须真的去扫。优化器的做法是选一棵最小的二级索引来遍历(二级索引叶子比聚簇索引小,扫的页更少),这也解释了“COUNT(*) 通常比 COUNT(某个可空列) 更快” —— 后者必须读列值判空,用不上这个优化。
补两个工程口径:一是超大表求总数,COUNT(*) 依然要扫全索引,建议改用近似值(EXPLAIN 的 rows 估算)、Redis 计数器或单独维护计数表;二是 COUNT(*) 与 COUNT(1) 在 MySQL 8.0 中已被优化到基本一致,不必纠结用哪个。
【记忆锚点】 “COUNT(*) 和 COUNT(1) 数行不挑食;COUNT(列) 数值挑食,NULL 一律不算”。
【易混对比】
COUNT(*)vsCOUNT(列):前者数行、含 NULL 行;后者数值、跳 NULL 行。COUNT(1)vsCOUNT(列):前者不读列、可走最小索引;后者必须读列判空,通常更慢。- NULL vs 空字符串:
''是“已知的空”,会被COUNT(列)计入;NULL 是“未知”,不计入。二者在 SQL 里语义完全不同。 - 换问法:若题干改成“某列有 3 行为 NULL、表共 10 行,求
COUNT(该列)”,答案就是 7(与补-09 是同型计算题)。
【自测】 表 t 共 100 万行,列 email 有 5 万行为 NULL。分别求 COUNT(*) 与 COUNT(email),并说明 COUNT(email) 为什么通常更慢。
答:
COUNT(*)= 1000000;COUNT(email)= 950000。COUNT(email)更慢是因为它必须读取
【知识关联】
- 补题关联:补-09(COUNT(*) 与 COUNT(col)/NULL 语义)。
- 面试/工程:大表
COUNT(*)会拖垮主库,应近似值(表统计信息)或异步计数表;分页总数可缓存。 - 面试追问:①
COUNT(1)与COUNT(*)等价吗? ②COUNT(主键)与COUNT(*)谁可能更慢?
【拓展延伸】
- 变式问法:给含 NULL 的表算
COUNT(*)/COUNT(col)/COUNT(DISTINCT col)。 - 参数/命令:
SHOW TABLE STATUS的Rows是估算;EXPLAIN对COUNT(*)有时给 rows 估算;可用触发器/应用维护精确计数。
M26. char(10) 与 varchar(10) 的区别,正确的是?
考点:char 与 varchar
A. char 定长,不足部分用空格补齐;varchar 变长,需额外 1~2 字节记录实际长度 B. 两者完全相同,只是写法不同 C. varchar 是定长,char 是变长 D. char 的最大长度只能是 255 个字符
答案:A
【考点】定长与变长字符类型的存储差异及适用场景。
【结论】 选 A:char 定长、不足部分用空格补齐;varchar 变长、额外用 1~2 字节记录实际长度。
【逐项辨析】
- A
char定长不足补空格,varchar变长需额外 1~2 字节记长度:正确。 这一句把两者的存储差异说全了。 - B 两者完全相同、只是写法不同:错。存储方式根本不同(定长 vs 变长),一个补空格、一个记长度,二者不可互换。
- C
varchar是定长、char是变长:错。把两者的定义完全说反。 - D
char的最大长度只能是 255 个字符:错在答非所问 —— 本题问的是char(10)与varchar(10)的存储差异,D 没有回答差异。(补充口径:CHAR的声明上限确实是 255 个字符,与字符集无关;utf8mb4 下CHAR(255)占 1020 字节,仍可正常建表,只是它会吃掉整行 65535 字节上限里的一大块。别把「字节数放大」误记成「字符数上限变小」。这一点在 5.7/8.0 一致。)
【知识点】 定长与变长的存储模型差异。
| 类型 | 存储方式 | 长度开销 | 尾部空格 | 适用场景 |
|---|---|---|---|---|
char(n) | 定长,永远占 n 个字符的位置 | 无额外字节 | 不足补空格(读取时去掉尾部空格) | 长度固定或变化极小:性别、手机号、邮编、MD5、定长 UUID |
varchar(n) | 变长,按实际长度存 | 额外 1~2 字节记长度 | 不补空格,原样保存 | 长度差异大:姓名、地址、备注、URL |
varchar 的长度前缀规则是:按该列定义的最大可能字节数判定 —— ≤ 255 用 1 字节,> 255 用 2 字节(最大可能字节数 = 声明字符数 × 字符集单字符最大字节数)。两点要抓住:① 判据是字节数而非字符数;② 是声明推出的上限而非某行实际长度,因为同一列每行前缀宽度必须一致。utf8mb4 下 varchar(100) 上限 400 字节 > 255,故恒为 2 字节前缀;latin1 下才是 1 字节。
选型逻辑:char 的优势是定长 —— 行内偏移可直接算出,读取不必先解长度、更新也不会因变长而挪动后续记录、不易产生碎片;代价是浪费空间(存一个“男”也要占满 10 个字符)。varchar 的优势是省空间;代价是更新时若长度变长,可能触发页内记录搬移。还有一个易忽略的语义差异:char 会去掉尾部空格,varchar 会保留,做等值比较时这一点可能造成结果不同。
【记忆锚点】 “char 定长补空格,varchar 变长带长度” —— 判据是“长度是否固定”,不是“哪个更省空间”。
【易混对比】
charvsvarchar:定长 vs 变长;补空格 vs 记长度;长度固定列 vs 长度不定列。- 字符数 vs 字节数:
varchar(100)的 100 是字符数,但长度前缀按字节数判断,两者不能混。 char去尾空格 vsvarchar保留空格:写 3 个空格进varchar就存 3 个;写进char会被并入补齐逻辑并在读取时去除。- 换问法:若题目问“
varchar(100)的长度前缀占几个字节”,判据是该列的最大可能字节数(声明字符数 × 字符集单字符最大字节数)是否超过 255,而不是某一行实际存了多少字节——同一列每行的前缀宽度必须一致,否则记录无法解析。utf8mb4 下 100×4=400>255 → 恒为 2 字节;latin1 下 100×1=100≤255 → 1 字节。
【自测】 列 code 固定存 32 位十六进制 MD5 值,列 remark 存用户备注(几字到几百字不等)。分别应选 char 还是 varchar?
答:
code用char(32)(长度固定、读取快、无碎片);remark用varchar(长度差异大、省空间)。与 M22 主键长度、M24 索引体积连考;建表评审高频。
【知识关联】
- 补题关联:补-24(金额用 DECIMAL,同属类型选型)、补-28(编码思想可类比:省内存 vs 灵活性)。
- 面试/工程:定长且几乎不变用 CHAR(如状态码、MD5 定长哈希),更新少、不易碎片;可变长度用 VARCHAR,更省空间但更新可能导致行迁移。UTF-8 下 VARCHAR(n) 的 n 是字符数,实际字节上限受 row_format 与整行 65535 限制约束。大文本不要塞 VARCHAR,用 TEXT 并注意是否溢出页存储。
- 面试追问:① VARCHAR(255) 与 VARCHAR(256) 存储差在哪?(单字节字符集下恰是长度前缀 1↔2 字节的临界点,5.0.3+ 语义;换 utf8mb4 则临界点提前到 varchar(63)/varchar(64),因为 64×4=256 已超 255) ② 为什么金额不能用 FLOAT?(二进制浮点无法精确表示十进制小数,会累计误差)
【拓展延伸】
- 变式问法:给出字段场景选类型(手机号 CHAR(11)、用户名 VARCHAR、价格 DECIMAL、简介 TEXT)。
- 参数/命令:
CHAR/VARCHAR/TEXT/DECIMAL(M,D);row_format=DYNAMIC;information_schema.TABLES看数据长度;SHOW CREATE TABLE审字符集与排序规则。
M27. 在 MySQL 5.6 及以上版本中,对已有大表执行 ALTER TABLE ... ADD INDEX ... 的实际情况是?
考点:Online DDL 加索引
A. 一定全程锁表,业务无法读写 B. 支持 Online DDL,多数场景不阻塞读写(仅开始与结束阶段需要短暂获取元数据锁) C. 大表根本无法加索引 D. 只能通过“建新表 + 导数 + 改名”的方式实现
答案:B
【考点】Online DDL 机制与 DDL 变更的可用性权衡。
【结论】 选 B。MySQL 5.6 起 ADD INDEX 走 Online DDL,绝大多数时间不阻塞读写,只有开始与结束两个瞬间需要短暂获取元数据锁。
【逐项辨析】
- A 一定全程锁表、业务无法读写:错。这是 MySQL 5.5 及更早的
COPY方式(拷全表期间禁写),5.6 之后已被 Online DDL 取代,“一定全程”是过时口径。 - B 支持 Online DDL、多数场景不阻塞读写,仅开始与结束阶段需短暂获取元数据锁:正确。 关键在于“短暂 MDL”这个限定,它才是线上真正的风险点。
- C 大表根本无法加索引:错。大表可以加索引,只是耗时长、资源占用高,需要配合低峰执行或外部工具。
- D 只能通过“建新表 + 导数 + 改名”实现:错。这是
pt-online-schema-change等外部工具的思路,属“可选方案”而非“唯一途径”,说“只能”过于绝对。
【知识点】 DDL 的三种执行算法与“期间能否并发 DML”的对应关系。
| 算法 | 执行方式 | 期间能否 DML | 典型场景 |
|---|---|---|---|
COPY | 建临时表、逐行拷贝数据 | 不能(阻塞写) | 5.5 时代;改列类型等重操作 |
INPLACE | 在原表上直接改,不拷全表 | 多数可以 | 5.6+ 的 ADD INDEX、部分 ADD COLUMN |
INSTANT | 只改元数据,秒级完成 | 可以 | 8.0+ 的 ADD COLUMN(加在末尾)、改默认值 |
Online DDL 的实现思路:在原表上直接构建新索引,同时把构建期间发生的 DML 变更记录到一块在线日志(row log)中,待索引构建完成后把这批增量补到新索引上,最后短暂加锁切换。整个过程真正的“锁”出现在两端 —— 开始阶段要拿 MDL 共享锁确认表结构、结束阶段要升级为排他锁完成切换;若此刻有长事务未提交,它会一直持有 MDL,把 DDL 卡在中间,进而阻塞该表上后续所有查询,这就是线上“加个索引把库搞挂”的经典事故链。
生产建议三条:一是低峰执行;二是先确认无长事务(查 information_schema.innodb_trx);三是超大表或需要变更列类型等 COPY 类操作,改用 pt-online-schema-change(建影子表 + 触发器同步增量)或 gh-ost(读 binlog 同步增量、不用触发器)。
【记忆锚点】 “Online DDL 两头锁、中间放开” —— 怕的不是建索引本身,而是“长事务把 MDL 卡住”。
【易混对比】
- Online DDL vs 外部工具:前者是原生能力(
ALGORITHM=INPLACE),适合常规加索引;后者适合超大表与COPY类变更,能限速、可暂停,但多一层复杂度。 - MDL 元数据锁 vs InnoDB 行锁:MDL 保护表结构,DDL 与 DML 靠它互斥;行锁保护数据行。DDL 被长事务卡住,卡的是 MDL。
INPLACEvsINSTANT:INPLACE仍要重建索引 / 修改数据,耗时可观;INSTANT只改元数据,秒级完成。- 换问法:若题目问“Online DDL 期间最大的风险是什么”,答案就是“开始 / 结束时需要 MDL,被长事务阻塞后会连锁阻塞查询”。
【自测】 业务低峰执行 ALTER TABLE big_t ADD INDEX idx_c(c),命令长时间不返回,同时监控显示该表上的普通 SELECT 也全部卡住。最可能的原因是什么?
答:存在未提交的长事务一直持有 MDL,DDL 在等待元数据锁,并把后续查询一起挡住。应先查
information_schema.innodb_trx找出长事务并结束它。与 M18 意向锁、M28 复制连考;DBA 岗高频。
【知识关联】
- 补题关联:补-22(DDL 期间 Buffer Pool/IO 压力)。
- 面试/工程:大表加索引用 Online DDL + 低峰窗口,或 gh-ost/pt-osc;必须观察磁盘与复制延迟。
- 面试追问:① Online DDL 的“online”指什么? ② 什么 DDL 仍会锁表?
【拓展延伸】
- 变式问法:5.5 及以前是复制表+重建(表锁),5.6+ 默认 Online ADD INDEX。
- 参数/命令:
ALTER TABLE t ADD INDEX idx(c), ALGORITHM=INPLACE, LOCK=NONE;innodb_online_alter_log_max_size;lock_wait_timeout。
M28. MySQL 主从复制中,从库的 SQL 线程的作用是?
考点:主从复制的线程模型
A. 从主库拉取 binlog B. 在主库上生成 binlog C. 读取 relay log 并在从库重放,把变更应用到从库数据 D. 执行 redo log 的刷盘
答案:C
【考点】主从复制的三个关键线程及其分工。
【结论】 选 C。从库 SQL 线程的职责是读 relay log 并在从库重放变更,把数据真正“落”到从库。
【逐项辨析】
- A 从主库拉取 binlog:错。这是从库 IO 线程的职责(连主库、收 binlog、写 relay log),不是 SQL 线程。
- B 在主库上生成 binlog:错。binlog 是主库事务提交时由 Server 层写入的,位置在主库不在从库,也不是由某个复制线程专门生成。
- C 读取 relay log 并在从库重放:正确。 SQL 线程是“执行者”,消费 IO 线程落地的中继日志。
- D 执行 redo log 的刷盘:错。redo log 刷盘是 InnoDB 引擎的行为(受
innodb_flush_log_at_trx_commit控制),与复制线程无关。
【知识点】 一条 binlog 从主库到从库要经过三个线程、两份日志。
| 线程 | 所在位置 | 职责 | 产出 |
|---|---|---|---|
| dump 线程 | 主库 | 监听 binlog,把变更推送给从库 | 网络传输 |
| IO 线程 | 从库 | 连接主库、接收 binlog、写入本地 relay log | relay log |
| SQL 线程 | 从库 | 读取 relay log,重放变更到从库数据 | 从库数据更新 |
流水线是:主库写 binlog → dump 线程推送 → 从库 IO 线程落 relay log → 从库 SQL 线程重放。注意“两份日志”:binlog 在主库、是复制源;relay log 在从库、只是 binlog 的本地暂存副本,重放完即可清理(relay_log_purge 默认开启)。配套参数还有 read_only / super_read_only(防止从库被误写)。
延迟的根也在这里:IO 线程始终是单线程,而 SQL 线程在 MySQL 5.6 之前只能单线程重放(slave_parallel_workers 只作用于 SQL 线程,不影响 IO 线程)—— 主库多并发写入、从库串行重放,队列越积越长,这就是主从延迟的主要成因。并行复制的演进分三代:5.6 引入基于库(schema)粒度的并行复制(slave_parallel_workers > 0,只有分属不同库的事务能并发重放,单库写入仍然串行);5.7 才引入基于组提交的逻辑时钟 LOGICAL_CLOCK(同一组提交里的事务可并发重放,不再依赖多库);8.0 进一步用 WRITESET(依赖追踪参数 binlog_transaction_dependency_tracking 自 5.7.20 起提供,8.0 起默认 WRITESET),并行度最高。
【记忆锚点】 “主库 dump 往外推,从库 IO 往回收,从库 SQL 往里放” —— 一推、一收、一放,对应 binlog → relay log → 数据。
【易混对比】
- IO 线程 vs SQL 线程:IO 线程负责“把日志搬回来”(网络 + 落盘),SQL 线程负责“把日志执行掉”(重放)。延迟高时要先判断是 IO 落后还是 SQL 落后,优化方向完全不同。
- binlog vs relay log:前者在主库、是复制源;后者在从库、是复制中转,内容同源。
- 主库 dump 线程 vs 从库 SQL 线程:前者只发送、不执行;后者只执行、不传输。
- 换问法:若题目问“从库 IO 线程的作用”,答案就换成“连接主库拉取 binlog 并写入 relay log”(与本题互为镜像,务必成对记)。
【自测】 从库 SHOW SLAVE STATUS 显示 Slave_IO_Running: Yes、Slave_SQL_Running: Yes,但 Seconds_Behind_Master 持续增大。是哪个线程跟不上?
答:SQL 线程。IO 线程正常说明 binlog 能拉回来,延迟来自重放能力不足,优化方向是开并行复制
slave_parallel_workers。与 M29 连考;运维面试高频。
【知识关联】
- 补题关联:补-20(redo/undo 与检查点的崩溃恢复判据;复制链路里的日志角色见本题正文)。
- 面试/工程:单线程 SQL 线程是延迟主因之一;并行复制的标准演进是 5.6 基于库(schema)粒度 → 5.7 基于组提交的
LOGICAL_CLOCK→ 8.0 基于WRITESET,版本归属别记混。 - 面试追问:① IO 线程与 SQL 线程分别做什么? ② 并行复制如何保证事务顺序?
【拓展延伸】
- 变式问法:问“relay log 是谁写的”(IO 线程);问“谁负责重放”(SQL 线程)。
- 参数/命令:
SHOW REPLICA STATUS(8.0.22+)/SHOW SLAVE STATUS;replica_parallel_workers、binlog_transaction_dependency_tracking(取值COMMIT_ORDER/WRITESET)。
M29. 下列不属于主从延迟常见原因的是?
考点:主从延迟的原因
A. 从库 SQL 线程单线程重放,跟不上主库并发写入 B. 主库执行了长时间的大事务(如一次删除百万行) C. 从库硬件配置远低于主库 D. 主库使用了 InnoDB 存储引擎
答案:D
【考点】主从延迟的成因与缓解手段。
【结论】 选 D。InnoDB 是主从双方通用的存储引擎,它既不制造延迟、也不区分主从,与延迟无因果关系。
【逐项辨析】
- A 从库 SQL 线程单线程重放、跟不上主库并发写入:是真实成因。主库多线程并发提交、从库单线程串行重放,是延迟的第一大来源。
- B 主库执行长时间大事务(如一次删除百万行):是真实成因。大事务产生的 binlog 必须整体重放完才在从库可见,期间延迟持续累积。
- C 从库硬件配置远低于主库:是真实成因。磁盘 IOPS、CPU、内存不足会直接拖慢重放速度。
- D 主库使用了 InnoDB 存储引擎:不是成因。 InnoDB 是 MySQL 默认引擎,主从两侧通常都用它,既不制造也不放大延迟,属于“与结论无关的选项”。
【知识点】 主从延迟的本质是“从库消费 binlog 的速度跟不上主库生产的速度”,一切成因都能归到这条流水线的某个环节。
| 环节 | 成因 | 表现 | 缓解手段 |
|---|---|---|---|
| 主库生产 | 大事务(一次删百万行、大批量 INSERT) | 单事务 binlog 达几百 MB,必须整体重放 | 拆小事务、分批提交 |
| 网络传输 | 带宽不足、跨机房链路抖动 | Slave_IO_Running 落后 | 提升带宽、就近部署 |
| 从库重放 | SQL 线程单线程 / 并行粒度不够 | Seconds_Behind_Master 持续增大 | 开并行复制(slave_parallel_workers) |
| 从库资源 | 硬件弱、还承担大量只读查询 | 重放被只读流量挤占 | 升配、读流量分流到专用从库 |
| 锁冲突 | 从库上长查询 / 大事务占锁 | 重放线程被阻塞 | 控制从库大查询 |
诊断思路:先看 Seconds_Behind_Master,再分别看 Slave_IO_Running 与 Slave_SQL_Running —— IO 线程落后说明“日志搬不回来”(网络 / 主库压力),SQL 线程落后说明“日志执行不完”(重放能力 / 锁冲突)。补两个口径:一是 Seconds_Behind_Master 只是估算值(基于主库 binlog 时间戳与从库当前时间之差),主库长时间无写入时它可能显示 0 却掩盖真实差距,更可靠的是 pt-heartbeat 这类心跳工具;二是主从延迟无法彻底消除,只能压到业务可接受的范围,对一致性要求高的读(如刚下单后立刻查订单)应强制走主库。
【记忆锚点】 “生产快、传输慢、重放更慢” —— 延迟由三段流水线里“最慢那一环”决定,InnoDB 不在任何一环里。
【易混对比】
- 主从延迟 vs 主从数据不一致:延迟是“暂时落后”(最终会追上),不一致是“永久错误”(误操作、复制中断、
sync_binlog配置不当导致丢事务),严重程度完全不同。 - 异步复制 vs 半同步复制(
semi-sync):异步下主库不等从库确认就返回,延迟不影响主库;半同步要等至少一个从库收到 binlog 才返回,能降低丢数据风险,代价是增加主库提交延迟。 - 延迟 vs 复制中断:
Slave_SQL_Running: No是复制停了(如主键冲突),不是“慢”,需人工介入跳过或修复。 - 换问法:若题目改成“下列属于主从延迟原因的是”,A / B / C 任选其一都对,D 仍不对 —— 否定型题目要把每个选项独立判真伪,不要被“哪个更像”带偏。
【自测】 主库一次 DELETE FROM log WHERE dt < '2025-01-01' 删掉 800 万行,此后从库 Seconds_Behind_Master 持续增长数小时。最有效的直接措施是什么?
答:把大事务拆成多批小事务(如按天分批
DELETE ... LIMIT 10000循环),让 binlog 分片、从库可持续追赶。与 M28 连考;运维与后端面试高频。
【知识关联】
- 补题关联:补-20(redo/undo 与检查点的崩溃恢复判据;复制链路里的日志角色见本题正文)、补-18(主从并发重放与 2PL/锁的关系可对照理解)。
- 面试/工程:读写分离架构必须容忍秒级延迟;关键读(支付结果、库存扣减后的确认)应强制走主库。延迟五大主因:大事务、无主键/无索引更新导致从库回放变慢、DDL、从库单线程或并行度不足、机器资源(CPU/IO/内存)不够。大促前要压测从库回放能力而不只是主库写入。
- 面试追问:① 从库延迟如何定量观测?(
Seconds_Behind_Source有局限;更准看performance_schema.replication_connection_status的 heartbeat 与位点差、或 GTID executed 集合差) ② 为什么“无主键表的更新”特别伤复制?(从库回放行变更时要全表扫定位行,把顺序回放打成随机 IO)
【拓展延伸】
- 变式问法:把“网络抖动 / 大事务 / 从库规格低 / 主库大 DDL”混在选项里辨认;或问“并行复制为何能降延迟”——把无冲突事务在从库并发重放。
- 参数/命令:
SHOW REPLICA STATUS\G(8.0.22+)看Seconds_Behind_Source、Retrieved_Gtid_Set/Executed_Gtid_Set;replica_parallel_workers、replica_parallel_type;binlog_transaction_dependency_tracking=WRITESET(该参数 5.7.20 引入,8.0 起默认 WRITESET)提高主库组提交后的可并行度;业务侧关键读/*+ MASTER */或路由到主库。
M30. 什么时候应该考虑分库分表?
考点:分库分表的时机
A. 表刚建好时就分,一劳永逸 B. 单表数据量过大(通常千万级以上)导致查询/写入性能显著下降,且已通过索引优化、缓存、读写分离仍无法满足时 C. 只要建了索引就不需要分库分表 D. 任何表都应该尽早分库分表
答案:B
【考点】分库分表的适用条件与代价。
【结论】 选 B。分库分表是最后手段 —— 只有当单表数据量过大、且索引优化 / 缓存 / 读写分离都已用尽仍不达标时,才值得付出分布式复杂度。
【逐项辨析】
- A 表刚建好时就分、一劳永逸:错。过早分片会凭空引入分布式事务、跨分片查询、全局 ID 等复杂度,而此时数据量根本不需要,属于“用大炮打蚊子”。
- B 单表过大导致性能显著下降,且已通过索引优化、缓存、读写分离仍无法满足:正确。 它同时给出了“数据量判据”与“前置手段已穷尽”两个条件,是唯一完整的表述。
- C 只要建了索引就不需要分库分表:错。过于绝对 —— 索引解决的是“查询定位”,解决不了单机写入热点、单表存储容量上限与 DDL 的维护代价。
- D 任何表都应该尽早分库分表:错。与 A 同病,“任何”“尽早”两个绝对化词直接排除;分片是按需引入的重架构变更。
【知识点】 分库分表的定位是“优化阶梯的最后一级”,顺序不可跳。
| 阶段 | 手段 | 解决什么问题 | 代价 |
|---|---|---|---|
| ① | SQL 与索引优化 | 慢查询、全表扫描 | 低(改代码 / 加索引) |
| ② | 加缓存(Redis) | 热点读、减轻库压力 | 中(一致性维护) |
| ③ | 读写分离 | 读扩展 | 中(主从延迟,见 M29) |
| ④ | 归档冷数据 | 单表行数下降 | 低(需业务配合) |
| ⑤ | 垂直拆分 | 按业务拆库、大字段独立成表 | 中(跨库 JOIN 变多) |
| ⑥ | 水平分库分表 | 突破单机容量与写入上限 | 高(分布式事务、跨片查询、全局 ID、扩容重分片) |
判断标准要落到指标而不是拍脑袋:单表行数、慢查询占比、写入延迟、磁盘占用、DDL 耗时。经验口径是“单表千万级开始评估”,但具体阈值取决于列宽、索引数量与访问模式 —— 一行 200 字节的表到 5000 万行可能依然健康,而一行 5 KB 的表 1000 万行就已吃力。真正的触发条件永远是“性能指标确实恶化了”,而不是某个固定行数。
水平分片引入的额外问题必须提前想清楚:一是分片键选择(决定能否避免跨片查询,选错会导致全片扫描);二是全局唯一 ID(自增主键失效,需雪花 ID / 号段模式,与 M22 主键选型呼应);三是跨片查询与分页(ORDER BY ... LIMIT 需各片各取再归并);四是分布式事务(跨片写要用 TCC / 本地消息表等);五是扩容重分片(从 4 库扩到 8 库要迁移数据,需预留平滑方案)。这些正是它必须“最后才用”的原因。
【记忆锚点】 “先优化、再缓存、后读写分离,实在不行才分库分表” —— 分片是“最后手段”,不是“最优手段”。
【易混对比】
- 垂直拆分 vs 水平拆分:垂直是“按业务 / 按列切”(用户库、订单库;大字段挪到附表),水平是“按行切”(同结构表按分片键分散到多个库表)。前者复杂度低、更常见,后者才是“分库分表”通常所指。
- 分库分表 vs 分区表:分区表是单机内由数据库管理的物理划分,应用无感知、无需改代码,但仍在同一实例,突破不了单机上限;分库分表是应用层方案,能真正扩展。
- 分表 vs 分库:分表解决单表过大;分库解决单实例连接数 / IO 上限。通常先分表,压力仍大再分库。
- 换问法:若题目问“分库分表后自增主键为什么不能用了”,答案就回到 M22 的“需改用雪花 ID / 号段模式生成全局唯一 ID”。
【自测】 某订单表 2000 万行、单行约 300 字节,按 create_time 排序的查询变慢,已加联合索引仍有慢查询,且缓存命中率低(订单查询按用户分散)。按优化阶梯,下一步最该做什么?
答:先做冷数据归档(把历史订单迁到归档表,把单表行数压下来),再评估是否值得水平分片 —— 分片是第 ⑥ 级,不该在 ④ 之前上。与 M24 深分页、M22 主键选型连考;架构岗与后端社招高频。
【知识关联】
- 补题关联:无直接补题;可与补-24(表设计)连读。分库分表是“设计无法再水平扩展时的最后手段”。
- 面试/工程:单表行数经验阈值(如千万级)不是铁律,更该看:索引树高、Buffer Pool 命中率、慢查询、主从延迟、备份窗口。优先顺序通常是:优化 SQL/索引 → 读写分离 → 缓存 → 垂直拆分 → 水平分库分表。过早分片会把简单问题复杂化(跨片 JOIN、分布式事务、扩容迁移)。
- 面试追问:① 为什么不能只看行数决定分表?(行宽、访问模式、索引大小、硬件都影响;500 万窄表可能仍轻松) ② 分片键怎么选?(按最高频查询维度,尽量让常用查询落在单分片)
【拓展延伸】
- 变式问法:问“什么时候该分库分表”选“容量/性能/可用性瓶颈且其他手段无效”;把“刚过一百万行就分”设为错误项。
- 参数/命令:
EXPLAIN、慢查询日志、information_schema.INNODB_BUFFER_PAGE命中;ShardingSphere/Sharding-JDBC;一致性哈希或范围分片;扩容用双写迁移。
M31. 防止 SQL 注入最有效的方式是?
考点:SQL 注入防护
A. 使用 PreparedStatement / 参数化查询(占位符绑定参数) B. 用字符串拼接构造 SQL C. 手动过滤掉单引号 D. 给相关列加上索引
答案:A
【考点】SQL 注入的原理与参数化查询的防护机制。
【结论】 防止 SQL 注入最有效的手段是参数化查询(PreparedStatement / 占位符绑定) —— 它把 SQL 的“结构”与“数据”彻底分离,选 A。
【逐项辨析】
- A 使用
PreparedStatement/ 参数化查询(占位符绑定参数):正确。 SQL 模板先被数据库预编译,语法结构在参数到达之前就已固定;参数只以“数据”身份填入占位符,无法改变语句结构。 - B 用字符串拼接构造 SQL:错。这正是漏洞的根源 —— 用户输入与 SQL 语法拼在同一条字符串里,输入中的
'会闭合原引号,其后内容被当作语法解析。 - C 手动过滤掉单引号:错。错在“过滤”二字 —— 字符替换属“打补丁”式防护,可用十六进制编码、注释符、宽字节注入(
%bf%27)等方式绕过,是不可靠方案。 - D 给相关列加上索引:错。索引只影响查询性能,与输入是否被当作语法解析完全无关,属答非所问。
【知识点】 SQL 注入的定义:应用程序把用户可控的输入未经分离处理就拼接进 SQL 语句,使输入被数据库当作 SQL 语法解析执行。其成立需两个条件同时满足:
| 成立条件 | 含义 | 参数化查询如何破坏它 |
|---|---|---|
| 输入进入 SQL 字符串 | 输入与语法处在同一段文本里 | 输入不进入 SQL 文本,只走参数通道 |
| 数据库再次解析该文本 | 输入被当成语法 | 模板先预编译,结构已固化 |
推导:SQL 注入的本质是“数据越权升级为语法”。若语句写成 SELECT * FROM user WHERE name = ' + 输入 + ',当输入为 ' OR '1'='1 时,整句变成 WHERE name = '' OR '1'='1',条件恒真 → 全表泄露。参数化查询走的是两段式协议:先 PREPARE 把模板编译成执行计划(此时结构已定),再 EXECUTE 把参数按值传入,数据库不会再对参数做语法解析,于是 ' OR '1'='1 只是一个“名字很怪的普通字符串”。
补充两点工程口径:① MyBatis 中 #{} 是预编译占位符(安全),${} 是字符串拼接(有注入风险);② 参数化查询覆盖不了表名、列名、ORDER BY 方向等结构位置,这些位置必须用白名单校验。
【记忆锚点】 「结构先定死,数据只当值」 —— 参数化查询的防护力来自“SQL 结构在参数到来之前就已经编译完成”。
【易混对比】
- 参数化查询 vs 转义 / 过滤:前者是结构性防护(让输入永远无法成为语法),后者是字符级修补(永远可能漏掉某种编码)。工程上应以前者为主、后者为辅。
#{}vs${}(MyBatis):#{}→?占位符 → 预编译,安全;${}→ 直接文本替换 → 拼接,危险。- SQL 注入 vs XSS:前者攻击数据库(伪造 SQL 语法),后者攻击浏览器(注入脚本)。二者同源(数据与代码未分离),但落点不同。
- 换问法:若题干改成“下列哪种做法不能防止 SQL 注入”,答案就落在“字符串拼接”“手动过滤单引号”上。
【自测】 MyBatis 中 ${} 与 #{} 哪个存在 SQL 注入风险?若确实需要动态指定排序字段,应如何防护?
答:
${}有风险(直接文本替换,可注入);#{}走预编译占位符,安全。动态排序字段无法参数化,必须对字段名做白名单校验。与 R06 的“输入不可信”思路同源;互联网 / 国企笔试高频。
【知识关联】
- 补题关联:补-10(视图可收窄可见列,作为纵深防御)。注:补-07 考 WHERE 与 HAVING 的作用阶段、补-06 考 DDL 语句分类,都与注入无关;本库补题暂无 SQL 注入专项,注入的工程口径见本题【知识点】与【自测】。
- 面试/工程:ORM 默认参数化即可挡住值注入;仍危险的是标识符注入(动态表名、列名、ORDER BY 方向)——必须白名单映射,绝不能拼用户输入。二次注入:恶意数据先“安全地”入库,后续查询拼进语句时才爆发,预编译救不了已入库的脏数据,必须入库前校验/转义。
- 面试追问:① 参数化能否防住所有注入?(不能防表名/列名/关键字位置;只能防值位) ② 二次注入是什么?(恶意数据被安全写入,之后在另一条动态 SQL 中被当成可信片段拼接)
【拓展延伸】
- 变式问法:对比“字符串拼接 vs PreparedStatement 参数绑定 vs 存储过程(不一定更安全)”;给出一段拼接 SQL 选注入点。
- 参数/命令:JDBC
useServerPrepStmts=true走服务端预编译;账号GRANT最小权限,禁止应用账号 DDL;sql_mode加固;WAF/网关做二次拦截;代码评审禁用字符串拼 SQL。
M32. READ COMMITTED(读已提交)能避免下列哪种并发问题?
考点:隔离级别能解决的问题
A. 脏读 B. 不可重复读 C. 幻读 D. 以上全部
答案:A
【考点】四种隔离级别与三类并发问题的对应关系。
【结论】 READ COMMITTED 只能避免脏读,不可重复读与幻读仍会发生 —— 选 A。
【逐项辨析】
- A 脏读:正确。 RC 只读取“已提交”的数据,读不到别的事务未提交的中间状态,因此脏读被杜绝。
- B 不可重复读:错。RC 下每次
SELECT都生成新的 ReadView,同一事务两次读之间其他事务提交的修改会被读到,值发生变化。 - C 幻读:错。RC 既不使用间隙锁、也不复用快照,两次范围查询之间的新增 / 删除行会被看到,幻读同样可能发生。
- D 以上全部:错。错在“全部”二字 —— RC 解决的是三者中最轻的脏读,把不可重复读与幻读也算进去,是夸大了它的能力。
【知识点】 三类并发问题的定义(均以“事务 A 读、事务 B 写”为背景):
| 并发问题 | 现象 | 反例 |
|---|---|---|
| 脏读 | 读到别的事务未提交的数据 | B 把余额 100 改成 200(未提交),A 读到 200;B 回滚,A 读到的是从未存在过的值 |
| 不可重复读 | 同一事务内两次读同一行,值不同 | A 先读余额 100,B 提交改成 200,A 再读变成 200 |
| 幻读 | 同一事务内两次范围查询,行数不同 | A 查“年龄 > 20 共 5 行”,B 插入 1 行并提交,A 再查变成 6 行 |
四级别与三类问题的完整对照:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 标准下可能(MySQL InnoDB 通过间隙锁基本避免) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
推导:脏读的根源是“读到了未提交的数据”,只要规定“只能读已提交版本”即可消除 —— 这正是 RC 做的事,所以 RC 是“解决脏读的最低成本级别”。而不可重复读与幻读的根源是“快照被刷新”:RC 每次查询都重建 ReadView,两次读之间别人一提交,快照就变了。要消除它们,必须让同一事务在多次读之间复用同一个快照(RR),或者加锁串行化(SERIALIZABLE)。
工程口径:RC 是 Oracle / SQL Server 的默认级别,也是不少互联网公司的实际选择 —— 因为它不加间隙锁、锁范围小、并发度更高,幻读交由业务幂等或乐观锁处理;而 MySQL InnoDB 默认是 RR。
【记忆锚点】 「RC 只挡脏读,RR 再挡不可重复读,SERIALIZABLE 全挡」 —— 级别越高,问题越少,并发越差。
【易混对比】
- 不可重复读 vs 幻读:前者是同一行的值变了(
UPDATE),后者是同一个范围的行数变了(INSERT/DELETE)。一个针对“值”,一个针对“集合”。 - RC vs RR:RC 每次读新建 ReadView(读最新已提交),RR 事务内复用第一个 ReadView(读快照)。这是两者一切差异的总根源。
- 隔离级别 vs 锁:隔离级别决定“要不要加锁、加什么锁”;MVCC 提供无锁的快照读。RR 下普通
SELECT走快照读(不加锁),SELECT ... FOR UPDATE走当前读(加间隙锁)。 - 换问法:若题干改成“能避免不可重复读的级别”,答案就变成“REPEATABLE READ 及以上”;若问“哪个级别不加间隙锁”,答案是“READ COMMITTED 及以下”。
【自测】 MySQL InnoDB 的默认隔离级别是什么?在该级别下,普通 SELECT(快照读)还会出现幻读吗?
答:默认 REPEATABLE READ;快照读下不会 —— 事务内复用同一个 ReadView,B 新插入的行不在快照里,A 看不到。但当前读(
SELECT ... FOR UPDATE/UPDATE)下,InnoDB 靠间隙锁(Gap Lock)+ Next-Key Lock 阻止范围内插入,从而基本避免幻读。与 M31 连考;大厂面试高频。
【知识关联】
- 补题关联:补-17(ACID 与隔离性)、补-18(2PL)、补-19(S/X 锁相容矩阵)——原理层的隔离能力矩阵。
- 面试/工程:不少公司把默认级别改成 RC:锁竞争更少、复制在 ROW 模式下更安全、死锁形态更可预期;幻读交给业务唯一约束/防重表兜底。银行/国企笔试常考“哪个级别解决哪个问题”的矩阵,必须背死。
- 面试追问:① RC 完全不能防幻读吗?(RC 无普通间隙锁,范围读两次结果可能变;但唯一索引等值插入冲突仍能挡住“重复键”类插入) ② 为什么互联网偏 RC?(并发更高、锁更少,且多数业务用唯一键防重即可)
【拓展延伸】
- 变式问法:矩阵填空:RR 能否避免幻读 → InnoDB RR + Next-Key 可“很大程度避免”,严格串行需 SERIALIZABLE;或给出四个级别选项问“哪个能避免不可重复读”。
- 参数/命令:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;;tx_isolation/transaction_isolation;注意 RC 下无普通 Gap Lock(外键/唯一检查除外),死锁与阻塞形态会变;压测对比 RC vs RR 的 TPS 与锁等待。