📖 本篇简介:百度华为系面试的 MySQL 题偏"原理深挖与严谨"——不只要你会用,还要你讲清 B+ 树、MVCC、锁算法、两阶段提交、Buffer Pool 这些底层为什么这么设计。本篇精选 20 道公开高频原理题,覆盖索引结构、事务日志、隔离级别、优化器、复制原理、8.0 新特性。题目均为公开面经常见类型,解析讲通用原理,不涉及任何公司未公开信息。前置知识:第 1、3、6、7、15 章;本系列示例基于 MySQL 8.0。
目录
- 1 本篇考情与知识地图
- 题目 1:InnoDB为什么用B+树做索引
- 题目 2:聚簇索引和二级索引结构有何不同
- 题目 3:MVCC怎么实现的
- 题目 4:事务ACID分别靠什么保证
- 题目 5:redo undo binlog三种日志区别
- 题目 6:为什么用B+树不用B树哈希
- 题目 7:RC和RR怎么实现的
- 题目 8:间隙锁临键锁什么时候触发
- 题目 9:redo和binlog两阶段提交
- 题目 10:change buffer原理与适用场景
- 题目 11:查询优化器怎么选索引
- 题目 12:主从复制原理binlog格式
- 题目 13:为什么主键推荐自增不推荐UUID
- 题目 14:页分裂和页合并怎么发生
- 题目 15:Buffer Pool的LRU怎么改良
- 题目 16:一条SQL执行的完整流程
- 题目 17:索引下推ICP原理
- 题目 18:字符集排序规则影响什么
- 题目 19:MySQL80有哪些重要新特性
- 题目 20:checkpoint机制解决什么问题
- 速查速记
- 常见坑与注意事项
- 复习清单
- 练习题
1 本篇考情与知识地图
百度华为系面试爱追"为什么",答题要从结构讲起。先建一张底层知识地图:
| 层 | 关键结构 | 对应题目 |
|---|---|---|
| 索引 | B+ 树、聚簇/二级 | 1、2、6 |
| 事务 | MVCC、undo、隔离级别 | 3、4、7 |
| 日志 | redo/undo/binlog、两阶段 | 5、9、20 |
| 锁 | 记录/间隙/临键 | 8 |
| 内存 | Buffer Pool、change buffer | 10、15 |
| 优化 | 优化器、ICP | 11、17 |
主线:从磁盘结构 → 内存结构 → 日志 → 锁 → 并发,串成一条链。
题目 1:InnoDB为什么用B+树做索引
考点
- B+ 树结构;磁盘 IO。
参考答案
1. 矮胖:B+ 树非叶子节点不存数据,扇出大,3~4 层就能存千万行,树高低意味着 IO 少;
2. 叶子链表:叶子节点双向链表相连,范围查询顺链表扫描快;
3. 稳定查询代价:所有查询都到叶子,路径长度一致;
4. 对比哈希:哈希不支持范围/排序;对比 B 树:B 树非叶子也存数据,扇出小、树高;
5. 适配磁盘:按页(16KB)读写,B+ 树一次读一页带很多索引项。
-- B+树结构示意,本示例基于 MySQL 8.0
非叶子节点: 只存key不存数据 -> 扇出大 -> 树矮(3~4层存千万行)
叶子节点 : 存整行/主键, 双向链表相连 -> 范围查询顺链扫描
追问
- 问:树高怎么算?答:千万行约 3~4 层,每次查询 3~4 次 IO。
- 问:为什么不用哈希索引做主索引?答:哈希只支持等值,不支持范围/排序/前缀。
题目 2:聚簇索引和二级索引结构有何不同
考点
- 叶子节点存什么;回表。
参考答案
1. 聚簇索引(主键):叶子节点存整行数据,一张表只有一个;
2. 二级索引:叶子节点存主键值,查到后要回表取整行;
3. 回表:二级索引查到主键 → 再到聚簇索引取行;
4. 覆盖索引:查询列都在二级索引里,免回表;
5. 没主键:InnoDB 会选唯一非空索引,都没有就生成隐藏主键。
追问
- 问:为什么回表慢?答:多一次 B+ 树查找 + 随机 IO。
- 问:怎么避免回表?答:覆盖索引,把查询列包进二级索引。
题目 3:MVCC怎么实现的
考点
- undo 版本链;ReadView。
参考答案
1. 隐藏列:每行有 trx_id(最近修改事务)、roll_pointer(指向 undo);
2. undo 版本链:每次修改在 undo 里留旧版本,串成链;
3. ReadView:事务开始时生成"活跃事务列表",判断哪个版本对当前可见;
4. 可见性规则:遍历版本链,找到第一个满足可见性的版本;
5. 效果:读不加锁、读写不阻塞,RC 每次 SELECT 生成新 ReadView,RR 只在第一次生成。
追问
- 问:RC 和 RR 区别?答:ReadView 生成时机不同,RC 每次生成(可见最新已提交),RR 复用(可重复读)。
- 问:MVCC 能解决幻读吗?答:快照读解决,当前读(for update)靠临键锁。
题目 4:事务ACID分别靠什么保证
考点
- 每种特性的实现机制。
参考答案
1. 原子性 A:undo log,回滚时恢复;
2. 一致性 C:是目标,由 AID + 业务约束共同保证;
3. 隔离性 I:锁 + MVCC;
4. 持久性 D:redo log,crash 后重放恢复;
5. 两阶段提交:redo 与 binlog 协调,保证崩溃恢复后主从一致。
追问
- 问:持久性只靠 redo?答:redo 先于数据页刷盘,crash 后重放,保证已提交不丢。
- 问:一致性怎么理解?答:事务把数据库从一个合法状态带到另一个合法状态。
题目 5:redo undo binlog三种日志区别
考点
- 三种日志职责。
参考答案
1. redo log(InnoDB):物理日志,记录"某页改了什么",保证 crash-safe 持久性;
2. undo log(InnoDB):逻辑日志,记录反向操作,用于回滚和 MVCC;
3. binlog(Server 层):逻辑日志,记录所有变更,用于主从复制和恢复;
4. 写入时机:redo 循环写、binlog 追加写;
5. 两阶段提交:保证 redo 与 binlog 一致。
-- 三种日志对比
redo : InnoDB物理日志, 崩溃恢复, WAL先写日志
undo : InnoDB逻辑日志, 回滚+MVCC版本链
binlog: Server层逻辑日志, 主从复制+point-in-time恢复
追问
- 问:为什么需要两种日志?答:最早只有 binlog,InnoDB 为 crash-safe 加 redo,两阶段提交协调。
- 问:redo 写哪?答:ib_logfile,循环复用。
题目 6:为什么用B+树不用B树哈希
考点
- 对比 B 树、哈希、二叉树。
参考答案
1. 比 B 树:B+ 树叶子链表适合范围查询,非叶子不存数据扇出大树低;
2. 比哈希:哈希只等值,不支持范围/排序/前缀,且哈希冲突;
3. 比二叉树:二叉树太高,磁盘 IO 次数多;
4. 范围查询是数据库刚需:B+ 树叶子链表天然友好;
5. 顺序 IO:叶子链表顺序扫描,比随机 IO 快。
追问
- 问:Memory 引擎支持哈希索引?答:内存表可用哈希,但 InnoDB 自适应哈希是内部优化。
- 问:等值查询哈希不是更快?答:单次等值哈希快,但数据库要范围/排序,B+ 更通用。
题目 7:RC和RR怎么实现的
考点
- ReadView 时机;锁差异。
参考答案
1. RC(读提交):每次 SELECT 都生成新 ReadView,能看到最新已提交,解决不可重复读之外的问题;
2. RR(可重复读):只在事务第一次 SELECT 生成 ReadView,之后复用,保证可重复读;
3. 当前读:SELECT ... FOR UPDATE 读最新,靠临键锁防幻读;
4. RR 下幻读:快照读防不住的幻读,当前读用临键锁;
5. 8.0:RR 默认下 MVCC + 临键锁基本防幻读。
追问
- 问:RR 还会幻读吗?答:快照读不会;当前读 + 临键锁也防住,除非锁范围没覆盖。
- 问:RC 有什么好处?答:锁范围小、并发高,互联网常用 RC。
题目 8:间隙锁临键锁什么时候触发
考点
- 锁算法;触发条件。
参考答案
1. 记录锁:锁单条索引记录;
2. 间隙锁(Gap):锁索引记录之间的间隙,防插入;
3. 临键锁(Next-Key):记录锁 + 间隙锁,左开右闭区间;
4. 触发:RR 隔离级别下,范围查询/当前读会加临键锁;
5. 唯一索引等值命中:退化为记录锁;未命中则间隙锁。
-- InnoDB行锁算法,本示例基于 MySQL 8.0
记录锁 : 锁单条索引记录
间隙锁 : 锁记录之间的间隙, 防插入
临键锁 : 记录锁+间隙锁, 左开右闭(RR范围/当前读触发)
追问
- 问:间隙锁解决什么?答:RR 下防幻读(别的事务在间隙插入)。
- 问:RC 有间隙锁吗?答:RC 没有间隙锁,只有记录锁,并发更高。
题目 9:redo和binlog两阶段提交
考点
- 两阶段提交流程;崩溃恢复。
参考答案
1. 阶段一 prepare:写 redo log 并置 prepare 状态;
2. 阶段二 commit:写 binlog,再把 redo 置 commit;
3. 崩溃恢复:重启时扫描 redo——prepare 状态则看 binlog 是否完整,完整则提交,否则回滚;
4. 目的:保证 redo 与 binlog 一致,主从数据不脱节;
5. 否则:只写 redo 没写 binlog,从库就少了这次变更。
追问
- 问:为什么必须两阶段?答:让"提交"这个动作在 redo 和 binlog 间有一致的判定点。
- 问:崩溃在 prepare?答:拿 binlog 判断,完整就提交,不完整回滚。
题目 10:change buffer原理与适用场景
考点
- 二级索引写优化;适用条件。
参考答案
1. 作用:对不在 Buffer Pool 的二级索引页的写(insert/delete/change),先记在 change buffer;
2. 合并:后续读该页时再把 change buffer 合并进页,避免立即随机 IO;
3. 收益:减少二级索引写的随机磁盘 IO;
4. 适用:写多读少、二级索引不常被立刻读的场景;
5. 不适用:唯一索引(要立即校验唯一性)、聚簇索引。
追问
- 问:唯一索引用不用 change buffer?答:不用,唯一性校验必须立即读页。
- 问:change buffer 占内存?答:
innodb_change_buffer_max_size控制,占 Buffer Pool 比例。
题目 11:查询优化器怎么选索引
考点
- 成本估算;统计信息。
参考答案
1. 基于成本:估算各方案扫描行数、回表代价、排序代价,选最小;
2. 统计信息:索引基数、数据分布(直方图);
3. 可选方案:全表扫、走某索引、索引合并;
4. 误判:统计信息过期、数据分布倾斜会选错;
5. 干预:FORCE INDEX、ANALYZE TABLE 更新统计信息。
追问
- 问:统计信息怎么更新?答:
ANALYZE TABLE,或自动采样。 - 问:选错索引怎么办?答:FORCE INDEX 临时救,根治是更新统计信息/改写 SQL。
题目 12:主从复制原理binlog格式
考点
- 复制三线程;binlog 格式。
参考答案
1. 主库写 binlog;从库 IO 线程拉 binlog 写 relay log;SQL 线程重放;
2. binlog 格式:STATEMENT(记 SQL)、ROW(记行变更,最常用)、MIXED;
3. 异步复制:主库不等从库;半同步:等至少一个 ACK;
4. 并行复制:8.0 基于组提交并行回放,降低延迟;
5. GTID:全局事务 ID,简化主从切换定位。
-- 查看主从复制状态,本示例基于 MySQL 8.0
SHOW MASTER STATUS; -- 主库当前binlog位点
SHOW REPLICA STATUS\G -- 从库复制状态, 看Seconds_Behind_Source延迟
追问
- 问:STATEMENT 有什么坑?答:
NOW()、UUID() 等不确定函数主从不一致,所以多用 ROW。 - 问:从库延迟大?答:并行复制 + 大事务拆分 + 监控。
题目 13:为什么主键推荐自增不推荐UUID
考点
- 顺序写;页分裂。
参考答案
1. 自增主键顺序插入:B+ 树尾部追加,不页分裂;
2. UUID 随机:插入位置随机,频繁页分裂、磁盘碎片;
3. 二级索引体积:二级索引存主键,主键越短索引越小;
4. 自增 BIGINT 8 字节:比 UUID 16/36 字节省空间;
5. 分库后:用趋势递增的分布式 ID,仍避免随机。
追问
- 问:UUID 完全不能用?答:可做业务展示,但别当聚簇主键。
- 问:有序 UUID?答:UUIDv7 含时间戳,可缓解页分裂。
题目 14:页分裂和页合并怎么发生
考点
- 页满;插入与删除。
参考答案
1. 页分裂:插入新键时数据页满了,InnoDB 拆出新页、搬数据,代价高;
2. 随机主键插入:每次都可能落在中间满页,频繁分裂;
3. 顺序主键:永远插尾部,几乎不分裂;
4. 页合并:删除导致页太空,相邻页合并;
5. 影响:分裂导致写入抖动、空间碎片。
追问
- 问:怎么减少分裂?答:自增/趋势递增主键,顺序写尾部。
- 问:碎片怎么整理?答:OPTIMIZE TABLE 重建(大表要在线做)。
题目 15:Buffer Pool的LRU怎么改良
考点
- 冷热分离;防全表扫。
参考答案
1. 普通 LRU 问题:一次全表扫描会把热点页挤出;
2. InnoDB 改良:LRU 分 young(年轻)和 old(老生代)两段;
3. 新页先入 old:只有在 old 停留超过时间阈值再访问才晋升 young;
4. 效果:全表扫的页停在 old,不会冲掉 young 热点;
5. 参数:innodb_old_blocks_pct、innodb_old_blocks_time。
追问
- 问:Buffer Pool 设多大?答:物理内存 50%~70%,尽量把热数据放内存。
- 问:冷数据进来会污染?答:old 段 + 时间阈值挡住。
题目 16:一条SQL执行的完整流程
考点
- 连接器 → 解析 → 优化 → 执行 → 引擎。
参考答案
1. 连接器:建立连接、鉴权;
2. 分析器:词法/语法解析,生成解析树;
3. 优化器:选索引、定执行计划;
4. 执行器:调用 InnoDB 接口;
5. 引擎层:Buffer Pool 找页,没有则从磁盘读,按事务/锁/MVCC 返回数据;
6. 更新类:还涉及 redo/undo/binlog 两阶段提交。
追问
- 问:查询缓存呢?答:8.0 已移除查询缓存。
- 问:慢在哪?答:优化器选错索引、引擎层磁盘 IO、锁等待。
题目 17:索引下推ICP原理
考点
- 把过滤下推到存储引擎层。
参考答案
1. 无 ICP:(a,b) 索引,WHERE a=1 AND b LIKE '%x%'——先按 a 查到回表行,再 server 层过滤 b;
2. 有 ICP:在索引层就用 b 条件过滤,减少回表次数;
3. 收益:减少回表 IO;
4. 前提:条件列在索引里;
5. EXPLAIN:Extra 显示 Using index condition。
追问
- 问:ICP 解决什么?答:联合索引里范围后列的过滤,提前在引擎层过滤少回表。
- 问:开启方式?答:
optimizer_switch='index_condition_pushdown=on',默认开。
题目 18:字符集排序规则影响什么
考点
- utf8mb4;排序规则与索引。
参考答案
1. 字符集:utf8mb4 才能存 emoji,别用旧 utf8(3 字节);
2. 排序规则 collation:决定比较/排序是否大小写敏感、重音敏感;
3. 影响索引:不同 collation 的列比较可能无法用索引;
4. JOIN 列要同字符集 collation:否则隐式转换导致索引失效;
5. 乱码根因:客户端、连接、库表字符集不一致。
追问
- 问:utf8 和 utf8mb4 区别?答:utf8 是 3 字节伪 utf8,utf8mb4 才完整。
- 问:
utf8mb4_general_ci和utf8mb4_bin?答:ci 不区分大小写,bin 二进制精确。
题目 19:MySQL80有哪些重要新特性
考点
- 8.0 关键改进。
参考答案
1. 原子 DDL:DDL 崩溃自动回滚;
2. 窗口函数、CTE(WITH);
3. 隐藏索引、降序索引、函数索引、隐藏列;
4. 性能:内部临时表、优化器、自持久化自增;
5. JSON 增强、GIS 增强;
6. 移除查询缓存。
追问
- 问:隐藏索引干嘛?答:先把索引设不可见,观察影响后再决定删/留,安全下线索引。
- 问:8.0 数据字典?答:事务化数据字典,不再是 .frm 文件。
题目 20:checkpoint机制解决什么问题
考点
- 刷脏页;恢复速度。
参考答案
1. 问题:修改先在 Buffer Pool(脏页),不能每次都刷盘;
2. redo log:保证 crash 不丢;checkpoint 把脏页刷盘推进 redo 可复用点;
3. 作用:控制 redo 写满前刷页,缩短 crash 恢复时间;
4. 触发:redo 快满、脏页比例高、空闲时;
5. 影响性能:刷脏页太多会突感卡顿,要限流。
追问
- 问:刷脏为什么会卡?答:同步刷盘占 IO,大促前要把刷页调顺。
- 问:恢复快靠什么?答:checkpoint 之后的页不用重放,只重放增量。
速查速记
| 主题 | 结论 | 关键词 |
|---|---|---|
| 索引结构 | 矮胖+叶子链表 | 3~4层千万行 |
| 聚簇vs二级 | 叶子存整行vs存主键 | 回表 |
| MVCC | undo链+ReadView | RC/RR时机 |
| 持久性 | redo WAL | crash重放 |
| 两阶段 | prepare→commit | redo/binlog一致 |
| change buffer | 二级索引写先缓存 | 唯一索引用不上 |
| Buffer Pool | young/old两段 | 防全表扫污染 |
| 优化器 | 成本+统计信息 | 过期会选错 |
常见坑与注意事项
- 坑 1:以为 InnoDB 的 count(*) 有计数——MyISAM 才有,InnoDB 要数可见行;
- 坑 2:把 UUID 当主键——随机写导致页分裂,写入性能差;
- 坑 3:忽略 RC/RR 的 ReadView 时机——以为 RR 下一定不幻读,当前读仍要临键锁;
- 坑 4:JOIN 两列字符集 collation 不同——隐式转换索引失效;
- 坑 5:统计信息过期不更新——优化器选错索引,
ANALYZE TABLE; - 注意:本篇均为公开原理题,解析为通用 InnoDB 机制,不涉及任何公司内部实现。
复习清单
| 题目 | 核心结论 | 易错点 |
|---|---|---|
| B+树 | 矮胖+叶子链 | 别和B树混 |
| 聚簇 | 叶子存整行 | 二级索引要回表 |
| MVCC | undo链+ReadView | RC/RR时机不同 |
| 三日志 | redo物理/undo逻辑/binlog逻辑 | 职责别混 |
| 两阶段 | prepare/commit | 崩溃看binlog |
| 间隙锁 | RR范围当前读触发 | RC没有 |
| change buffer | 二级索引写缓存 | 唯一索引不用 |
| BufferPool | young/old两段 | 防全表扫 |
练习题
练 1:用一句话说清聚簇索引和二级索引的区别,以及什么叫回表。
<details>
<summary>参考答案</summary>
聚簇索引(主键)的 B+ 树叶子节点存整行数据;二级索引叶子节点只存主键值。用二级索引查到主键后,再拿主键回聚簇索引取整行的过程叫"回表"。若查询列都在二级索引里(覆盖索引),则免回表。
</details>
练 2:RR 隔离级别下,事务 A 两次范围 SELECT 之间,事务 B 插入一行并提交,事务 A 的快照读和当前读结果分别怎样?
<details>
<summary>参考答案</summary>
快照读(普通 SELECT):复用第一次的 ReadView,看不到 B 新插入的行,不幻读;当前读(SELECT ... FOR UPDATE / UPDATE):读最新已提交,能看到 B 的行——这就是 RR 下快照读与当前读的差异,当前读靠临键锁防幻读。
</details>
下一篇预告:第 35 章 快手滴滴小米系真题精讲
文章回复
0 条公开回复