← 学习门户 每日面试题 学习看板

2026-10-13

MySQL 深入 —— 事务 / 索引 / 日志

10 题 · 平均 3.3/5 · low 2.67 · medium 3.5 · high 3.67 · 第 1 轮

#1 InnoDB的数据库事务机制是什么? low mysql

题目

InnoDB的数据库事务机制是什么?

我的回答

因为 DB 的是一个事务机制主要是两个层面的,一方面是基于 MVCC 的,通过类似于视图版本快照的模式来执行事务的区分和并行度。另外就是基于锁实现的串行化的一些更新的相关处理,包括行锁、表锁、间隙锁一系列操作。二者是 InnoDB 主要的事务机制支持。摸索保证更新、修改不出现相互排斥的问题。通过 MVCC 的版本快照,提升多事务的并行度,提高效率。

点评

评分:⭐⭐⭐☆☆(3/5)
优点:抓住两大支柱(MVCC + 锁),方向对;明确意识到 MVCC 提升并发、锁负责互斥
不足:未讲 ACID 拆解、MVCC 三要素、锁体系分类、隔离级别实现、持久化机制
补充:事务 = ACID 目标 + 4 类机制(MVCC、锁、undo log、redo log);MVCC 关键三要素:隐藏列 + undo 版本链 + ReadView ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 InnoDB 事务"目标 - 机制"双层的完整认知,能否把 ACID 拆到具体组件上。

**知识点拆解**:

1. **目标层:ACID 各由什么保证**
- **Atomicity**:undo log 支持事务回滚;binlog 保证事务原子性写入(一个事务的 binlog 事件要么全写要么不写)
- **Consistency**:业务层 + 约束(主键、外键、CHECK)+ 隔离级别
- **Isolation**:锁(悲观)+ MVCC(乐观读)+ 隔离级别四档
- **Durability**:redo log(InnoDB 内部)+ binlog(Server 层)+ 两阶段提交

2. **MVCC 三大件**
- **隐藏列**:每行有 `trx_id`(最近修改的事务 ID)和 `roll_pointer`(指向 undo log 中上一版本)
- **Undo Log 版本链**:每次修改写一条 undo,通过 roll_pointer 串成版本链
- **ReadView**:事务第一次一致性读时创建,含 `m_ids`(活跃事务列表)、`min_trx_id`、`max_trx_id`、`creator_trx_id`;根据当前行的 trx_id 判断可见性,不可见则沿 undo 链回溯到可见版本

3. **一致性读 vs 当前读**
- **一致性读**(快照读):普通 `SELECT`,走 MVCC,不加锁
- **当前读**:`SELECT ... FOR UPDATE / LOCK IN SHARE MODE`、`UPDATE`、`DELETE`、`INSERT`,读最新已提交数据并加锁

4. **锁体系**
- **按粒度**:表锁 / 行锁 / 页锁
- **按模式**:S(共享)锁 / X(排他)锁
- **InnoDB 行锁细分**:
- **Record Lock**:锁单行
- **Gap Lock**:锁索引区间(不含记录),防幻读
- **Next-Key Lock**:Record + Gap 组合,RR 默认加
- **Insert Intention Lock**:insert 前的特殊 gap lock,多个 insert 可并发
- **Auto-inc Lock**:自增锁,表级,控制自增值分配

5. **隔离级别实现**
- **READ UNCOMMITTED**:读不加锁、不走 MVCC,直接读当前值
- **READ COMMITTED**(默认 Spring 也是这个):每次快照读都建新 ReadView,行锁,不加间隙锁
- **REPEATABLE READ**(InnoDB 默认):事务内首次快照读建 ReadView 后复用;加 Next-Key Lock 防幻读
- **SERIALIZABLE**:所有读加 S 锁,串行化

6. **WAL 与两阶段提交(D 的关键)**
- WAL:先写日志(内存缓冲 + 顺序落盘)再写数据页,避免随机 IO
- 事务提交流程:写 binlog prepare → 写 redo log commit → 写 binlog commit
- 崩溃恢复:根据 binlog prepare 状态 + redo log 完整性决定重做或回滚

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否把 MVCC 讲到底(ReadView + undo 链)、把锁体系讲全(Next-Key Lock、Insert Intention Lock)
- **广度**:能否关联隔离级别、redo/undo/binlog、WAL
- **应用**:能否说清"为什么 InnoDB 默认 RR 也能保证大部分场景不出现幻读"

**延伸递进面试题**:
1. **RC 和 RR 的区别,MVCC 层和锁层各自的差异?**
- 解答要点:ReadView 创建时机(RC 每次读新建、RR 首次读后复用);锁 RR 加 Next-Key、RC 只加 Record Lock
2. **InnoDB 的 ReadView 可见性判断规则?**
- 解答要点:`trx_id < min_trx_id` 可见;`= max_trx_id` 不可见;`min_trx_id <= trx_id < max_trx_id` 若不在 m_ids 中则可见(已提交);`= creator_trx_id` 自己可见
3. **undo log 和 redo log 的区别?**
- 解答要点:undo 记录修改前(用于回滚 + MVCC)、redo 记录修改后(用于崩溃恢复);undo 事务提交后就清理,redo 循环写

第三部分:面试者参考回答(口语化)

面试官您好,InnoDB 的事务机制我分两层来讲:目标层的 ACID,和支撑 ACID 的四类机制——MVCC、锁、undo log、redo log。

**第一,ACID 对应到具体组件**。原子性靠 undo log 回滚;一致性靠约束加隔离级别;隔离性靠锁加 MVCC 加隔离级别;持久性靠 redo log 加 binlog 加两阶段提交。这样拆下来每一件事都有具体机制兜底,不是空谈。

**第二,MVCC 是实现隔离性读的主要手段**。三个核心要素:一是每行有两个隐藏列,trx_id 记录最后修改的事务 ID,roll_pointer 指向上一个版本;二是每次修改会写一条 undo log,通过 roll_pointer 串成版本链;三是 ReadView 是事务做快照读时创建的可见性视图,含 m_ids、min_trx_id、max_trx_id、creator_trx_id 四个字段,通过这套规则判断当前版本是否可见,不可见就沿 undo 链回溯到可见版本。RC 每次快照读都新建 ReadView,RR 首次读之后复用,所以 RR 能重复读到同一版本。

**第三,锁是写路径的串行化手段**。InnoDB 的行锁不只是行锁,细分有 Record Lock、Gap Lock、Next-Key Lock、Insert Intention Lock、Auto-inc Lock。RR 级别下更新加的是 Next-Key Lock(记录加间隙),能防幻读;RC 只加 Record Lock 不防幻读。SELECT 默认一致性读不加锁,SELECT FOR UPDATE 才是当前读会加锁。

**第四,持久性靠 redo log 和 binlog 的两阶段提交**。写事务时先写 binlog prepare,再写 InnoDB 的 redo log commit,最后写 binlog commit。崩溃恢复时靠 binlog prepare 状态加 redo log 是否完整来决定重做或回滚,这也是 MySQL 默认 `innodb_flush_log_at_trx_commit=1` 的原因,保证不丢数据。

当然,这套机制在极端场景也有边界——比如 SERIALIZABLE 才能真串行化,RR 的间隙锁在某些 DML 下也可能出现类似幻读的现象(如间隙内插入加间隙外已有数据的 update)。实际业务里我们会根据读写比选择隔离级别,读多写少用 RR 靠 MVCC,写多则降级到 RC 减少锁冲突。

#2 什么是死锁?如何避免和解决 low mysql

题目

什么是死锁?如何避免和解决

我的回答

死锁就是一个彻底解不开的锁,表示这个动作被卡顿停止,没有人工介入是无法解除的。构成死锁核心有这么几个原因。首先是资源没有一次性申请,不同的线程获取了不同的资源,又相互造成循环等待。最后我申请资源,你也申请资源,大家申请的,你申请的是我的,我申请的是你的,我不能抢你的,你也不能抢我的,这里边就涉及一个不能抢占其他人资源,也是导致死锁的一个核心原因。总结一下死锁构成的原因有,没有一次性申请所有资源,相互等待了资源,资源不能强制争抢。这 3 个任意破坏一个都可以解除这个死锁。

点评

评分:⭐⭐☆☆☆(2/5)
优点:覆盖部分死锁必要条件;符合破坏条件防死锁思路
不足:完全脱离数据库语境;未讲 InnoDB 死锁检测(`innodb_deadlock_detect`)、监控(`SHOW ENGINE INNODB STATUS`)、业务侧规避方案
补充:InnoDB 死锁本质 = 锁交叉形成 wait-for graph 环 → 检测器主动选牺牲者回滚;业务防死锁第一原则 = 固定加锁顺序(主键升序) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:既要有操作系统层死锁的理论理解,更要掌握 InnoDB 死锁的检测、监控与业务侧规避手段。

**知识点拆解**:

1. **死锁理论(四必要条件)**:
- **互斥**:一次只能被一个事务持有
- **占有且等待**:持有 A 的同时等 B
- **不可剥夺**:事务持有的锁不能被动回收,必须主动释放
- **循环等待**:A 等 B、B 等 C、C 等 A 形成环
- 破坏任一条件即可防止死锁

2. **InnoDB 的死锁机制**:
- InnoDB 是**自动检测**型:默认 `innodb_deadlock_detect=ON`,事务在等锁时会持续构建 wait-for graph,一旦形成环就立刻识别出死锁
- **牺牲者选择**:回滚 undo log 少(改动小)的事务,让它抛出 `ERROR 1213`,另一事务继续执行
- 死锁检测成本:O(N²) 检查当前所有等锁事务,事务数很多时开销大
- 超时兜底:`innodb_lock_wait_timeout`(默认 50 秒),超过直接抛 `ERROR 1205: Lock wait timeout exceeded`,事务回滚

3. **InnoDB 常见死锁场景**:
- **间隙锁交叉**:两个事务在 RR 隔离级别下分别用 `INSERT` 或 `SELECT FOR UPDATE` 请求不同的间隙锁,最终循环等待
- **不同顺序更新多行**:事务 A 先锁 id=1 再锁 id=2,事务 B 先锁 id=2 再锁 id=1
- **`INSERT ... ON DUPLICATE KEY UPDATE`**:并发插入相同主键,一个 INSERT 一个 UPDATE 触发死锁
- **索引缺失导致的锁升级**:本该走索引却全表扫描,锁大量行放大冲突面
- **外键约束**:父子表同时更新时的加锁顺序
- **`UPDATE` 无 WHERE 条件**:锁全表

4. **死锁监控与排查**:
- `SHOW ENGINE INNODB STATUS`:看 `LATEST DETECTED DEADLOCK` 段,包含两个事务的 SQL、持有的锁、等待的锁
- `information_schema.innodb_trx`:当前运行中的事务
- `information_schema.innodb_locks`(5.7)/ `performance_schema.data_locks`(8.0):当前持有的锁
- `performance_schema.data_lock_waits`(8.0):锁等待链,看谁等谁
- 慢查询日志里 `Error 1213` 频次监控

5. **业务侧防死锁方案(优先级从高到低)**:
- **固定加锁顺序**:所有事务按主键升序加锁,如统一先锁 id 小的行
- **缩小事务粒度**:事务内越少锁越好,避免长事务
- **减少事务内 SQL 数量**:一条 SQL 完成就一条,避免多次 SELECT FOR UPDATE + UPDATE
- **加锁前先用 SELECT 判断**:能用 `SELECT ... FOR UPDATE NOWAIT`(8.0)就别阻塞
- **走乐观锁**:用 `version` 列 CAS 更新,避免悲观锁冲突
- **合理建索引**:无索引的 UPDATE/DELETE 会锁大量行
- **控制并发**:热点行加应用层分布式锁(Redis、Zookeeper)串行化
- **调大 timeout**:`innodb_lock_wait_timeout` 从 50s 提到 300s,让慢事务有机会完成

6. **参数调整场景**:
- 默认 `innodb_deadlock_detect=ON`:适合事务短、数量少
- 大量长事务:检测开销大,可 `innodb_deadlock_detect=OFF` + 依赖 timeout
- 高并发写入:可能需 `innodb_lock_wait_timeout` 减小到 5-10 秒快速失败

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲清 InnoDB 的自动死锁检测机制与牺牲者选择逻辑
- **广度**:能否覆盖监控工具链、参数调优、业务侧规避
- **应用**:能否在真实线上死锁事故中给出一套定位与修复方案

**延伸递进面试题**:
1. **为什么两个相同 SQL 并发执行会死锁,同一个 SQL 单独跑不会?**
- 解答要点:SQL 相同不代表访问顺序相同。RR 隔离下两条 UPDATE 可能锁不同的间隙/行,如果扫描顺序受 B+ 树遍历影响,可能形成交叉等待。
2. **`innodb_deadlock_detect=OFF` 有什么后果?**
- 解答要点:不再自动检测,只能依赖 `innodb_lock_wait_timeout` 让等锁方超时回滚。优点:省 CPU(大事务场景明显);缺点:恢复慢、等待时间长。
3. **怎么在压测环境主动触发死锁?**
- 解答要点:两个会话,会话 1 `BEGIN; UPDATE t SET c=c+1 WHERE id=1;`(不 commit),会话 2 `BEGIN; UPDATE t SET c=c+1 WHERE id=2;`,然后会话 1 `UPDATE t SET c=c+1 WHERE id=2;`,会话 2 `UPDATE t SET c=c+1 WHERE id=1;`,会话 1 立刻收到 1213 报错。

第三部分:面试者参考回答(口语化)

面试官您好,死锁我先从理论上讲清楚,再落到 InnoDB 的具体表现。

理论上死锁有四个必要条件:互斥、占有且等待、不可剥夺、循环等待。破坏任意一个就能防止死锁。经典例子是两个事务,A 拿了资源 1 等 2,B 拿了资源 2 等 1,就形成循环等待,谁都动不了。

但在 MySQL InnoDB 里,死锁的表现和检测方式跟理论不太一样,我主要讲几个点。

**InnoDB 的默认行为是自动检测**。`innodb_deadlock_detect` 默认开启,事务在等锁的时候会持续构建 wait-for graph,一旦发现环,就立刻选出牺牲者——通常是 undo log 少、改动少的那个事务,回滚它并抛 `ERROR 1213: Deadlock found`,另一事务继续执行。所以开发者看到的错误是 1213,业务代码要做重试。检测本身有 O(N²) 开销,事务特别多的场景可以考虑关闭检测,改用 `innodb_lock_wait_timeout`(默认 50 秒)来兜底——超时会抛 1205 错误。

**常见死锁场景**有几个。第一是间隙锁交叉,RR 隔离级别下两个事务用不同的 WHERE 条件触发不同间隙锁,最终循环等待。第二是多行更新顺序不同,A 先锁 id=1 再锁 id=2,B 反过来。第三是 `INSERT ... ON DUPLICATE KEY UPDATE`,并发插相同主键时一个走 INSERT 一个走 UPDATE 触发死锁。第四是 UPDATE 没写 WHERE 条件锁全表。第五是索引缺失导致锁大量行放大冲突面。

**排查线上死锁**我会按这个链路走:先跑 `SHOW ENGINE INNODB STATUS` 看 `LATEST DETECTED DEADLOCK` 段,里面会显示两个事务的 SQL、各自持有的锁、等待的锁;再看 `performance_schema.data_lock_waits` 找锁等待链;最后看 `innodb_trx` 找超长事务。慢查询日志里频繁出现 1213 错误就是死锁高发信号。

**预防方案**我按优先级列一下。第一是**固定加锁顺序**,所有事务按主键升序加锁,这是最有效的办法,业务侧只要约定统一顺序就能消掉大部分死锁。第二是**缩小事务粒度**,一条 SQL 能完成就别拆多条,事务内减少锁数量。第三是**避免长事务**,长事务持有锁越久,越容易和其他事务撞。第四是**加索引**,无索引的 UPDATE 会锁大量行。第五是**用乐观锁绕过**,`version` 列 CAS 更新,冲突就重试,不用悲观锁串行化。第六是**8.0 的 NOWAIT / SKIP LOCKED**,`SELECT FOR UPDATE NOWAIT` 拿不到锁立即返回,避免阻塞等待成环。

参数层面,默认配置基本够用,只是长事务多、检测开销大的场景可以 `innodb_deadlock_detect=OFF`,靠 `innodb_lock_wait_timeout` 兜底。反过来热点行死锁频发的场景,把 `innodb_lock_wait_timeout` 调小让失败快一点,业务侧快速重试。

https://chat.deepseek.com/share/wy9ppt0dw6b7uhgyct

---

#3 MySQL中字典锁是什么? low mysql

题目

MySQL中字典锁是什么?

我的回答

字典锁指的应该是 MySQL 数据库更新数据字典,比如它的索引、它的表结构、增减字段之类的操作。当发生字典锁的时候,应该会锁整张表。如果是类似于结构的重建,应该会锁整张表。比如当我们加索引的时候,大表加索引会导致整张表无法进行正常的查询、插入等操作,这就是字典锁的一个体现。所以线上尽量不可能,不要对大表进行 MDL 操作,不要更新它的表结构,不要加在峰值的时候加索引。要加的话,尽量选择平缓时段,业务量没那么大的时段,以及选择可以先建新表,再进行修改表名。然后进行更新的操作。

点评

评分:⭐⭐⭐☆☆(3/5,二次回答)
优点:抓到了真实业务场景(大表加索引锁表);提出了正确的规避方案(错峰、Ghost Table 思路)
不足:概念混淆严重——字典锁 ≠ MDL;未讲数据字典本身是什么;未讲粒度演进;规避方案不全面(未提 gh-ost、Instant DDL)
补充:字典锁 = InnoDB 内部 mutex 保护 dd cache;MDL = Server 层元数据锁;两者层级完全不同。大厂规避 DDL 锁表三大武器:`ALGORITHM=INPLACE LOCK=NONE`、`gh-ost`、Instant DDL ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察是否清楚 InnoDB 内部锁 vs MySQL Server 层锁的层级差异,避免混淆。

**知识点拆解**:

1. **数据字典(Data Dictionary)本身**
- 内容:所有 schema / table / index / column / constraint 的元信息
- 存储位置演进:
- MySQL 5.6 及以前:`.frm` 文件 + `.ibd` + 部分在系统表
- MySQL 5.7:`.frm` 文件为主
- MySQL 8.0:全部迁到 `mysql.*` 数据字典表(dd_*),并加 `data_dictionary` 内存缓存
- 启动时 InnoDB 会把 dd 表读入内存的 data dictionary 缓存,运行时通过字典锁保护这个缓存

2. **InnoDB 字典锁(data dictionary latch)**
- 本质:**InnoDB 内部的 mutex / rw-lock**,C++ 实现,业务不可见
- 保护对象:表对象、索引对象、页描述符、事务系统、redo 相关的元数据结构
- 粒度演进:
- MySQL 5.7:粗粒度(一把大字典锁保护整个 dd cache)
- MySQL 8.0:细粒度拆分(表级、索引级、页级各一把),大幅降低争用
- 面试重点:**这不是业务层需要关心的锁,也不会写进 SQL 语义**

3. **MDL(Metadata Lock)—— 常被混淆**
- Server 层的元数据锁,`performance_schema.metadata_locks` 可见
- 规则:DML(SELECT/INSERT/UPDATE/DELETE)拿共享 MDL,DDL(ALTER/CREATE/DROP)拿排他 MDL
- 常见事故:**长事务持共享 MDL 阻塞 DDL**(`ALTER TABLE` 一直等)
- 排查:`performance_schema.metadata_locks`、`information_schema.innodb_trx` 找长事务
- 解决:`metadata_locks_timeout` 参数(8.0)、kill 长事务、DDL 用 `ALGORITHM=INPLACE LOCK=NONE`

4. **其他可能混淆的锁**
- **表锁(Table Lock)**:SQL 层的 `LOCK TABLES`,DML 显式锁,业务可见
- **意向锁(IS / IX)**:InnoDB 的意向锁,表级,用于快速判断"这张表是否有行锁"
- **Auto-inc Lock**:InnoDB 自增锁,控制自增值分配,表级
- **Adaptive Hash Index Lock**:自适应哈希索引的锁,内部实现

5. **面试应对策略**
- 先反问:"您问的是 InnoDB 内部的 data dictionary lock,还是 Server 层的 MDL?"
- 如果指内部字典锁:讲保护对象、粒度、不可见
- 如果指 MDL:讲 Server 层规则、长事务阻塞 DDL 场景、参数调优

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清字典锁是 InnoDB 内部实现细节,粒度演进如何
- **广度**:能否区分字典锁 vs MDL vs 表锁 vs 意向锁
- **应用**:能否说清 MDL 导致的长事务阻塞 DDL 事故如何排查

**延伸递进面试题**:
1. **长事务阻塞 DDL 怎么排查和解决?**
- 解答要点:`SELECT * FROM performance_schema.metadata_locks` 看锁等待;`innodb_trx` 找长事务;`metadata_locks_timeout` 加超时;kill 长事务
2. **InnoDB 8.0 数据字典做了哪些优化?**
- 解答要点:从 `.frm` 迁到数据字典表,内存缓存 + 细粒度锁拆分,DDL 并发能力大幅提升
3. **意向锁和 MDL 的关系?**
- 解答要点:意向锁是 InnoDB 行锁到表锁的"传递"标记(表上有行锁前先在表上打 IX),MDL 是元数据保护;两者层级不同

第三部分:面试者参考回答(口语化)

面试官您好,字典锁这个词其实要分两种理解,我先说清楚差异。

**第一种是 InnoDB 内部的 data dictionary lock**。这是存储引擎内部实现细节,本质是 C++ 层的 mutex 或 rw-lock,业务层完全感知不到,也控制不了。它保护的是内存中的 data dictionary 缓存——里面存着表元信息、索引元信息、页描述符、事务系统这些结构。MySQL 8.0 之前粒度比较粗,基本是一把大字典锁保护整个 dd cache;8.0 之后拆成了更细粒度的锁,比如表级、索引级、页级各自一把,并发争用大幅下降。这个字典锁面试里主要是讲清"它是内部实现,不是 SQL 语义"就够了。

**第二种是常被混淆的 MDL(Metadata Lock)**。这个是 MySQL Server 层的元数据锁,`performance_schema.metadata_locks` 里可见。规则是 DML 拿共享 MDL、DDL 拿排他 MDL。生产上最容易出的事故是——**有个长事务持共享 MDL 没提交,后续想 ALTER TABLE 就会一直等,把整个库的连接池占满**。排查就三步:先跑 `SELECT * FROM performance_schema.metadata_locks` 看锁等待链,再跑 `SELECT * FROM information_schema.innodb_trx` 找长事务,最后 kill 掉那个长事务。8.0 之后可以调 `metadata_locks_timeout` 加超时兜底,避免永久阻塞。

所以面试官如果问"字典锁",我会先反问清一下具体指哪个。如果指 InnoDB 内部的,就讲保护对象和粒度演进;如果指 MDL,就重点讲长事务阻塞 DDL 的排查套路——这是我实际线上踩过最多次的坑。

---

#4 MySQL索引一定遵循最左前缀匹配吗? medium mysql

题目

MySQL索引一定遵循最左前缀匹配吗?

我的回答

这个前缀匹配是 MySQL 基础的索引中使用的基础规则,但并不是一定遵守,因为它还有包括全文索引、位图索引,还有其他的索引方式。比如全文索引,它创建的索引就是对于索引字段进行分词,然后比对,和最左前缀没什么关系。最左前缀只是指包括联合索引和普通的索引中使用的索引规则。其实主要是联合索引,普通索引直接比对字段了,和最左前缀没什么关系。然后 Like 的时候使用的也类似于最左前缀,因为它比对是从左往右。如果是中间或者是右侧区域 Like,在左侧 Like 的时候,使用模糊匹配,它是不能走索引的,这也是最左前缀的一个体现。全文索引就不是这样了

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:抓住"不一定";举了 LIKE 反例;区分单列 vs 联合索引;提到全文索引的分词机制
不足:遗漏 Skip Scan(8.0)、ICP、Index Merge、Functional Index、优化器成本模型
补充:绕开最左前缀 5 类机制 = Skip Scan + Index Merge + ICP + Functional Index + 优化器放弃;加分金句:"最左前缀本质是 B+ 树有序性的必然结果" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:理解最左前缀规则的**底层原因**(B+ 树有序性),以及 MySQL 8.0 引入的多个"绕开"机制。

**知识点拆解**:

1. **最左前缀规则的底层原因**
- B+ 树是多级有序结构,键比较是**左到右逐字段**
- `(a, b, c)` 索引内部:先按 a 排,a 相同时按 b 排,a、b 都相同时按 c 排
- 查询必须"先固定前面的列"才能定位后面的列的有序区间
- 跳过中间列 → 后面的列在这个前缀下无序 → 无法用索引定位

2. **最左前缀规则被打破的场景(5 类)**
- **Skip Scan(8.0)**:当首列基数很低(如性别只有 M/F),且查询条件在首列所有取值上都满足时,优化器会跳过扫描——比如 `(gender, city)` 索引,`WHERE city = 'Beijing'` 会被拆成 `gender='M' AND city='Beijing'` 和 `gender='F' AND city='Beijing'` 两次扫描合并。EXPLAIN 显示 `Using index condition`
- **Index Merge**:`WHERE a=1 OR b=2` 可能走 `(a)` 和 `(b)` 两个索引再 UNION,EXPLAIN 显示 `Using union(idx_a, idx_b)`
- **Functional Index(8.0)**:`INDEX((lower(name)))` 让 `WHERE lower(name)='x'` 也能走索引
- **全文索引 / Hash 索引**:完全不同的索引结构,无最左前缀概念
- **优化器放弃**:即使满足最左前缀,代价过高(如匹配行数占比 > 30%)会选全表扫描

3. **LIKE 与最左前缀的关系**
- `LIKE 'xx%'`:前缀确定,走 B+ 树范围扫描
- `LIKE '%xx'` / `LIKE '%xx%'`:前缀不确定,无法利用 B+ 树有序性 → 全表扫描
- `LIKE 'x_x%'`:中间通配符会中断,但仍比 `%xx` 高效(有前缀 x)

4. **ICP(Index Condition Pushdown)—— MySQL 5.6+**
- 场景:`(a, b, c)` 索引,`WHERE a=1 AND c=3`(c 不是最左前缀)
- 无 ICP:先按 a=1 定位,回表读所有行,再在 Server 层过滤 c=3
- 有 ICP:c=3 条件下推到存储引擎层,在索引层过滤,减少回表
- 注意:ICP **不是**绕过最左前缀,仍然是走 a=1 的索引,只是加速过滤

5. **优化器成本模型(Cost-based Optimizer)**
- MySQL 是成本模型,不是规则模型
- 即使满足最左前缀,如果估算扫描行数 > 全表 30%,可能放弃索引走全表
- `EXPLAIN` 里看 `key=None` + `type=ALL` 就是优化器放弃索引
- `FORCE INDEX` / `IGNORE INDEX` 可以强制干预,但线上慎用(索引被删直接生产报错)

6. **Hash 索引的特殊性**
- **InnoDB Adaptive Hash Index**:InnoDB 自动维护,业务无法控制开关,只读热数据的哈希映射
- **Memory 引擎**:显式支持 Hash,但只支持 `=` 和 `IN` 精确匹配
- 都不涉及最左前缀规则

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清"最左前缀是 B+ 树有序性的必然结果"
- **广度**:能否覆盖 8.0 新增的 Skip Scan、Functional Index
- **应用**:能否根据 EXPLAIN 判断"为什么这里没用上索引"

**延伸递进面试题**:
1. **Skip Scan 跳跃扫描在什么条件下生效?如何验证?**
- 解答要点:首列基数低、查询在首列所有取值上都能匹配、`EXPLAIN` 显示 `Using index condition`。验证:`EXPLAIN FORMAT=JSON` 看 `plan_info` 里的 `skip_scan` 字段
2. **`(a, b, c)` 索引,`WHERE a=1 AND c=3` 会不会因为 c 不满足最左前缀而全表扫描?**
- 解答要点:不会。会走 `(a)` 的索引,然后 c=3 通过 ICP 下推到存储引擎层过滤,减少回表。EXPLAIN `Extra` 显示 `Using index condition`
3. **如果优化器放弃索引走全表,怎么强制走索引?有哪些风险?**
- 解答要点:`FORCE INDEX(idx_xxx)` 或 `USE INDEX`。风险:索引被删或改结构后 SQL 直接生产报错;索引统计信息更新后成本变化可能不如全表快;无法动态调整。生产慎用

第三部分:面试者参考回答(口语化)

面试官您好,最左前缀不是一定遵守的,我按"底层原理 + 绕开机制"两层来讲。

**底层原理**:最左前缀本质是 B+ 树的有序性决定的。`(a, b, c)` 联合索引在 B+ 树里是先按 a 排,a 相同再按 b 排,所以必须先固定前面的列才能利用后面的列的有序区间定位。跳过中间列 → 后面的列在这个前缀下无序 → 索引用不上。这个规则只适用于 B+ 树类索引,所以索引类型本身就会打破它。

**能绕开最左前缀的机制,MySQL 8.0 已经比较多了**。第一是 **Skip Scan 跳跃扫描**,8.0 新特性,当首列基数很低(比如性别只有两个值),且查询在后列也能精确匹配时,优化器会跳过首列做一次遍历合并结果——`WHERE city='Beijing'` 在 `(gender, city)` 索引上就能跳过 gender 直接扫 city。第二是 **Index Merge**,`WHERE a=1 OR b=2` 走两个单列索引再 UNION,绕开单个联合索引。第三是 **Functional Index**,8.0 可以建表达式索引,`INDEX((lower(name)))` 让 `WHERE lower(name)='x'` 也能走。第四是**其他索引类型**,比如全文索引是倒排结构、Hash 索引是哈希定位,跟 B+ 树最左前缀规则完全无关。第五是**优化器成本模型**——即使满足最左前缀,如果估算扫描行数占比太高,优化器也可能直接放弃走全表。

**再补充一个容易被误会的点**:ICP(索引条件下推)不是绕开最左前缀,而是让"看起来用不上最左前缀"的列也在索引层过滤。`(a, b, c)` 索引上 `WHERE a=1 AND c=3`,c 虽然不在最左前缀上,但通过 ICP 下推到存储引擎层过滤,能显著减少回表次数。

至于优化器放弃索引的场景,MySQL 5.7 可以 `FORCE INDEX` 强制走,但生产环境慎用——索引被删或改结构 SQL 直接生产报错,而且统计信息变化后可能反而比全表慢。实际线上更推荐通过调整索引设计让优化器自动选对,而不是硬干预。

当然,最左前缀这个规则在 8.0 里被大量优化器机制"松绑"了,理解它的关键还是回到 B+ 树的有序性——**只要索引结构是 B+ 树,规则本质成立;只要索引结构不是 B+ 树,或者优化器判断 B+ 树扫描代价过高,规则就会被打破**。

#5 a,b两个单独索引,where a=xx and b=xx 走哪个索引?为什么? medium mysql

题目

a,b两个单独索引,where a=xx and b=xx 走哪个索引?为什么?

我的回答

具体装哪个索引不一定,MySQL 会根据索引的具体区分度以及使用索引的成本来规划优化选择哪个索引去使用。但是如果这么一个搜索条件的话,并且经常使用,推荐建 A 跟 B 的联合索引,让两个字段都能被使用到,提升效率。这两个字段 A 跟 B,看优化器选择,哪个区分度高,筛选的成本更少,能取到更少,能在更少的数据集中找到目标结果。一般优化器会选择对应的索引,并不会根据直接查看就会知道使用哪个索引。但是如果使用联合索引,将两个数据字段都使用上,这样就可以保证更好的优化,更少的数据集被选择。

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:抓住"优化器基于区分度+成本选择";主动给出联合索引方案;理解无法预判需 EXPLAIN 验证
不足:遗漏 Index Merge;未讲 EXPLAIN 关键字段(`key`/`rows`/`filtered`/`Extra`);未讲优化器可能放弃走全表(占比 > 30%)
补充:优化器枚举所有方案选成本最低;联合索引 (a, b) 最优,理想命中 1 行;加分金句:"回表次数才是成本模型的核心" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:理解 MySQL 优化器的**成本模型**、掌握 `EXPLAIN` 关键字段、能给出合理的索引优化方案。

**知识点拆解**:

1. **MySQL 是成本模型优化器(Cost-based Optimizer)**
- 不是规则模型,不会"永远走区分度高的索引"
- 会枚举所有候选方案(每个索引、全表扫描),估算每个方案的成本,选最低的
- 成本模型核心公式:**成本 ≈ IO 成本 + CPU 成本**,其中 IO 成本主要来自**回表次数**
- 依赖 `information_schema.statistics` 的 `Cardinality`(采样估算,可能不准)

2. **单列索引 a、b 各自的成本估算**
- 走索引 a:`rows_a = total_rows / cardinality_a`,然后回表 rows_a 次过滤 b
- 走索引 b:`rows_b = total_rows / cardinality_b`,然后回表 rows_b 次过滤 a
- 优化器选 `rows_a` 和 `rows_b` 里更小的那个
- **回表成本**通常比索引扫描大得多,所以优化器极度倾向"命中行数少"的方案

3. **Index Merge(索引合并,MySQL 5.6+)**
- 对 `WHERE a=xx AND b=xx`,优化器可以考虑两个单列索引各自扫描后再合并结果
- EXPLAIN `Extra` 显示:
- `Using intersect(idx_a, idx_b)`:结果取交集(AND 场景)
- `Using union(idx_a, idx_b)`:结果取并集(OR 场景)
- `Using sort_union(...)`:结果排序后合并
- **实际很少被选中**:需要扫描两个索引 + 合并结果集,成本通常高于单个联合索引
- 只有当两个索引都覆盖查询所有列(覆盖索引)时,优化器才可能选

4. **为什么联合索引 (a, b) 通常是最好的**
- 一次索引扫描能同时定位 a 和 b
- 理想情况:命中就 1 行,回表 1 次
- 相比之下,单索引最坏情况要回表成千上万行过滤另一列
- **结论**:经常一起查的字段,必须建联合索引

5. **优化器可能放弃索引走全表**
- 场景:两个索引区分度都低(如 `WHERE gender='M' AND status=1`,扫描 40% + 40% 都超过阈值)
- MySQL 有个经验阈值:预估扫描行数占比 > 30% 时倾向全表扫描(B+ 树扫描不如顺序扫描快)
- EXPLAIN 显示:`key=None` + `type=ALL`
- **对策**:改查询、改索引、或建复合条件索引

6. **优化器选错索引的常见原因与解决**
- **统计信息不准**:`ANALYZE TABLE t` 刷新
- **优化器 bug**:MySQL 5.7 之前成本模型有 bug,8.0 大幅改进
- **强制干预**:
- `FORCE INDEX(idx_xxx)`:强制走
- `USE INDEX(idx_xxx)`:提示但优化器可以忽略
- `IGNORE INDEX(idx_xxx)`:忽略某个索引
- **风险**:索引被删或改结构,SQL 直接生产报错;生产慎用

7. **EXPLAIN 关键字段速查**
- `id`:查询序号,同一 id 从上到下执行,不同 id 从左到右
- `select_type`:SIMPLE / PRIMARY / SUBQUERY / DERIVED 等
- `table`:查询的表
- `type`:访问类型,从好到坏:`system > const > eq_ref > ref > range > index > ALL`
- `possible_keys`:可能用到的索引
- `key`:实际使用的索引
- `key_len`:使用索引的字节数(判断联合索引用到哪几列)
- `rows`:预估扫描行数
- `filtered`:预估过滤后剩下的百分比
- `Extra`:额外信息(`Using index` 覆盖索引 / `Using where` 需过滤 / `Using index condition` ICP / `Using filesort` 需排序 / `Using temporary` 需临时表 / `Using join buffer` 用 join buffer)

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清优化器成本模型的核心是"回表次数"权衡
- **广度**:能否覆盖 Index Merge、FORCE INDEX、EXPLAIN 关键字段
- **应用**:能否给出线上的索引优化方案和选错索引的排查流程

**延伸递进面试题**:
1. **为什么 Index Merge 很少被优化器选中?**
- 解答要点:需要扫描多个索引 + 合并结果集,成本远高于单个联合索引;只有覆盖索引场景才可能划算
2. **优化器选错索引怎么排查?**
- 解答要点:先看 `EXPLAIN` 的 `rows` 和 `filtered`,`ANALYZE TABLE` 刷新统计信息,对比其他索引的实际命中行数,必要时 `FORCE INDEX` 验证
3. **联合索引 `(a, b)` 和 `(b, a)` 有什么区别?**
- 解答要点:完全等价的两个索引在 B+ 树层面是不同的物理结构;`(a, b)` 对 `WHERE a=? AND b=?` 最优,对 `WHERE b=?` 也能用(跳过 a);`(b, a)` 反过来。通常按查询频率最高的组合建一个就够,第二个查询频率低的话用不上

第三部分:面试者参考回答(口语化)

面试官您好,这个问题我按"优化器怎么选 + 为什么这样设计"来答。

**先说优化器怎么选**。MySQL 是成本模型优化器,它会枚举所有候选方案——走索引 a、走索引 b、走 Index Merge、走全表扫描——然后估算每个方案的成本,选最低的。成本模型的核心是**回表次数**:走哪个索引,回表过滤另一列的次数最少,优化器就选哪个。所以两个索引区分度差得多时,优化器会选区分度高、扫描行数少的那个;区分度差不多时可能选谁都不一定,具体要看统计信息和当前负载。

**Index Merge 是一种理论上的方案**,5.6+ 支持。对 `WHERE a=xx AND b=xx` 优化器可以考虑走两个单列索引分别扫描再取交集(`Using intersect`)。但实际很少被选中——需要扫两个索引 + 合并结果集,成本通常远高于单个联合索引。除非两个索引都是覆盖索引才可能划算。

**推荐建 (a, b) 联合索引**是标准答案。因为联合索引一次扫描就能同时定位 a 和 b,理想情况命中就 1 行,回表 1 次;相比之下走单索引最坏要回表成千上万行过滤另一列。频繁一起查的字段一定要建联合索引,这是最本质的优化。

**优化器可能完全放弃索引走全表**。如果两个索引区分度都低(比如 `WHERE gender='M' AND status=1`,两个字段各自筛出 40%),扫描占比超过 30% 时优化器会倾向全表扫描,因为 B+ 树随机 IO 不如顺序扫描快。EXPLAIN 显示 `key=None type=ALL` 就是这种情况。

**排查线上优化器选错索引**我会按这个流程:先看 `EXPLAIN` 的 `rows` 和 `filtered` 判断是不是选错了;跑 `ANALYZE TABLE` 刷新统计信息看有没有变化;如果还是选错,`FORCE INDEX` 强制走指定索引验证效果。`FORCE INDEX` 生产慎用——索引被删或改结构 SQL 直接生产报错。8.0 的优化器成本模型比 5.7 精准很多,能少踩坑。

当然,具体走哪个索引不能只看索引就断言,一定要用 `EXPLAIN` 验证。EXPLAIN 里 `key` 是实际选择的索引、`rows` 是预估扫描行数、`Extra` 里的 `Using index` 说明是覆盖索引、`Using filesort` 说明需要额外排序——这几个字段基本能定位大部分优化器决策问题。

#6 联合索引是越多越好吗? medium mysql

题目

联合索引是越多越好吗?

我的回答

首先明确一点,索引并不是越多越好,索引创建要符合业务需求,因为索引创建跟使用都是需要成本的。每建立一个索引,都要在插入更新数据的时候进行索引更新,插入跟更新的数据,相应的效率就会降低。所以我们使用索引的时候,或者说使用联合索引,使用索引的时候,要控制我们索引的创建量。在主业务中去考虑使用哪些索引,而不是所有字段都去建立索引。并且尽可能地选择区分度高或者是有价值的字段去增加设置索引。另外可以考虑联合索引方面的设计是在覆盖索引中使用,减少回表。通过覆盖索引来实现不做回表,一次性把所需的数据展现出来。但是如果涉及的字段太多,并且没有什么查询意义,可能就不考虑这方面的使用。

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:方向正确;抓住写放大核心;业务驱动;提到区分度和覆盖索引
不足:未讲空间成本、优化器成本、冗余索引治理、覆盖索引边界、读写比场景、8.0 新特性
补充:索引三张账单 = 写放大 + 空间 + 优化器;治理黄金手段 = `sys.schema_unused_indexes` + `INVISIBLE INDEX` 灰度下线;加分金句:"索引不是能不能建,而是值不值得建" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察索引设计中的**成本收益权衡**思维——不是能不能建,而是值不值得建。

**知识点拆解**:

1. **索引的三张账单**
- **写放大账单**:每次 INSERT/UPDATE/DELETE 都要维护每个索引的 B+ 树;一个表 5 个索引,写入成本 ≈ 数据本身 + 5 份索引维护
- **空间账单**:每个索引一份完整的 B+ 树副本,磁盘占用 = 数据大小 × (1 + 索引数);Buffer Pool 命中率 = 有效数据页 / 总页,索引挤占会导致数据页被淘汰
- **优化器账单**:优化器枚举所有候选方案估算成本,索引越多评估越慢;百万级大表可能明显影响 QPS

2. **联合索引 vs 多个单列索引的取舍**
- **`(a, b)` 一个联合索引**:空间省一半、写放大减半、查询 `WHERE a=? AND b=?` 最优;但只覆盖以 a 打头的查询
- **`(a)` + `(b)` 两个单列索引**:能覆盖 `WHERE a=?` 和 `WHERE b=?` 两种独立模式;但空间翻倍、写放大翻倍
- **决策原则**:查询模式合并成联合索引;独立模式用单列;避免过度建模

3. **覆盖索引的设计权衡**
- 覆盖索引 = SELECT 列全部命中索引,`EXPLAIN` 显示 `Using index`
- 优点:省回表,性能提升数倍
- 代价:索引宽度增加,Buffer Pool 命中率下降
- **权衡策略**:
- 高频查询 + 结果集小:值得覆盖
- 低频查询 + 结果集大:不值得(回表次数少也扛得住)
- 索引宽度 > 数据行宽的一半:慎建(索引比数据还大)

4. **冗余索引治理(关键工程实践)**
- **前缀冗余**:`(a, b, c)` 存在时 `(a, b)`、`(a)` 冗余,因为 `(a, b, c)` 已能覆盖前缀查询
- **完全冗余**:`(a)` 和 `(a)` 重复
- **反向冗余**:`(a, b)` 和 `(b, a)` 都能用但用途不同,通常保留一个
- **治理工具**:
- `sys.schema_unused_indexes`:`SELECT * FROM sys.schema_unused_indexes` 找 `count_star=0`
- `performance_schema.table_io_waits_summary_by_index_usage`:细粒度统计
- **灰度下线(8.0)**:`ALTER TABLE t ALTER INDEX idx INVISIBLE` → 观察 1-2 周无 SQL 走这个索引 → `DROP INDEX`
- **不要直接 DROP**:可能有低频 SQL 依赖,直接删生产报错

5. **不同业务场景的索引策略**
- **读多写少(报表库、维表、日志查询表)**:多建索引,覆盖常见查询
- **写多读少(订单写入、事件流、日志采集表)**:少建索引,能不加就不加,靠分区、归档策略代替
- **读写均衡(核心业务表)**:精细设计联合索引,冗余治理必须做

6. **8.0 相关新特性**
- **`INVISIBLE INDEX`**:灰度下线首选
- **`FUNCTIONAL INDEX`**:表达式索引,如 `INDEX((lower(email)))`,让 `WHERE lower(email)=?` 也能走
- **`DESCENDING INDEX`**:降序索引,让 `ORDER BY a DESC` 直接走索引
- **优化器改进**:成本模型比 5.7 精准,能少踩选错索引的坑

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲清"写放大 + 空间 + 优化器"三张账单
- **广度**:能否覆盖冗余治理、8.0 新特性、不同场景策略
- **应用**:能否给出一套线上索引治理的完整流程

**延伸递进面试题**:
1. **`(a, b, c)` 已存在时,要不要再建 `(a, b)`?**
- 解答要点:通常不用,前缀冗余。但特殊场景例外:如果业务经常单独查 `(a, b)` 而不查 c,且 `(a, b)` 索引更小更省内存,可以保留;否则治理掉
2. **索引太多导致写入变慢,怎么定位是哪个索引拖后腿?**
- 解答要点:`performance_schema.table_io_waits_summary_by_index_usage` 看每个索引的 `count_write`;结合 `SHOW INDEX FROM t` 看索引列表;把低使用率的索引 `INVISIBLE` 灰度观察
3. **覆盖索引 vs 回表索引怎么选?**
- 解答要点:命中率高 + 结果集小 + 列少 → 覆盖;命中率低或列多 → 不覆盖,靠 ICP 加速

第三部分:面试者参考回答(口语化)

面试官您好,联合索引绝对不是越多越好,我按"三张账单 + 三个策略"来讲。

**先说三张账单**。第一是**写放大**,每次 INSERT/UPDATE/DELETE 都要维护所有索引的 B+ 树,5 个索引就是 5 倍维护成本。第二是**空间**,每个索引一份完整 B+ 树副本,索引多了不但磁盘爆,Buffer Pool 也被索引挤占,命中率下降。第三是**优化器成本**,候选方案多了优化器估算更慢,大表上 QPS 会有影响。这三笔账加起来,索引是奢侈品,不是必需品。

**再说三个策略**。第一个是**联合索引 vs 单列索引的取舍**——常用查询模式合并成联合索引(比如 `WHERE a=? AND b=?` 建 `(a, b)` 就够),独立的查询模式才用单列索引。避免把每个字段都单建。第二个是**覆盖索引的边界**——覆盖索引能省回表是好事,但为了覆盖把所有 SELECT 列都塞进一个索引,索引宽度会爆炸,反而挤压 Buffer Pool。经验值是索引宽度别超过数据行宽的一半。第三个是**冗余治理**,这块特别容易忽略——`(a, b, c)` 存在时 `(a, b)`、`(a)` 就是冗余,应该删掉。治理工具用 `sys.schema_unused_indexes` 找 `count_star=0` 的索引;MySQL 8.0 有 `ALTER TABLE t ALTER INDEX idx INVISIBLE`,先设为不可见观察 1-2 周,确认没有 SQL 依赖再真删。

**最后是分场景策略**。读多写少的报表库、维表可以多建;写多读少的订单写入、日志表要克制,能用分区和归档策略就少建索引;核心业务表要精细设计联合索引,冗余治理必须做。

8.0 还有几个特性值得知道:`INVISIBLE INDEX` 灰度下线、`FUNCTIONAL INDEX` 表达式建索引(`INDEX((lower(email)))` 让函数查询也能走)、`DESCENDING INDEX` 让 `ORDER BY a DESC` 直接走索引。

当然,我的经验是——**索引不是能不能建的问题,而是值不值得建**。业务有真实查询模式才建,没查询模式的索引都是浪费,删掉能省资源、少踩坑。

#7 一次insert操作,MySQL的几种log的写入顺序? medium mysql

题目

一次insert操作,MySQL的几种log的写入顺序?

我的回答

几种日志的写入顺序,首先会涉及事务,创建重试日志,也是 Redo log。Redo log 进行创建,然后进行提交,防止事务的事件出现服务器崩溃,用来做重试。然后创建 Undo log,Undo log 去确保整个事务进行,如果事务出现问题进行回滚。然后这些都完成,最终要写 binlog 用来进行数据同步。

点评

评分:⭐⭐☆☆☆(2/5)
优点:覆盖三种日志;基本定位对(redo 恢复、undo 回滚、binlog 复制)
不足:顺序完全错(用户说 redo→undo→binlog);完全没提两阶段提交(2PC);未讲刷盘参数;未讲 undo 持续存在(Purge 线程)
补充:2PC 核心 = 保证 redo commit 和 binlog commit 的原子性;刷盘策略双 1 最安全;undo 膨胀常因长事务阻塞 Purge;加分金句:"跨层协调需要 2PC,否则崩溃后主从一定不一致" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:理解 MySQL 三种日志的分工与两阶段提交协议,掌握事务提交时的完整执行链路。

**知识点拆解**:

1. **三种日志的分工**
- **Undo Log**(InnoDB 内部):记录修改**前**的状态,用于**事务回滚**和 **MVCC**(版本链)
- **Redo Log**(InnoDB 内部):记录修改**后**的状态,用于**崩溃恢复**(WAL 机制)
- **Binlog**(Server 层):记录所有 DML/DDL 事件,用于**主从复制**和**PITR 时间点恢复**
- 关键:**undo 和 redo 都在 InnoDB 内部**,binlog 在 Server 层,跨层协调需要 2PC

2. **一次 insert 的完整 10 步**
- **准备阶段**:
1. Server 层 SQL 解析、权限检查
2. 生成执行计划,路由到 InnoDB
3. InnoDB 生成 undo log(记录修改前)
4. 修改 Buffer Pool 中的内存页(**注意此时未落盘**)
5. 写 redo log buffer(记录修改后)
- **提交阶段(2PC)**:
6. 事务提交触发:写 binlog buffer
7. binlog write → binlog cache(内存)
8. binlog flush → OS 文件缓存,**prepare 状态**
9. InnoDB 写 redo log commit 标记 → **redo log commit** → fsync 到磁盘
10. binlog commit → fsync binlog 到磁盘

3. **为什么需要 2PC**
- 没有 2PC 的问题:redo 和 binlog 各自独立落盘,可能出现"redo 已 commit 但 binlog 未落盘"→ 崩溃后数据在数据页但 binlog 缺失,从库无法同步这段数据
- 2PC 保证:redo commit 和 binlog commit 是**原子语义**——要么都成功要么都失败
- 崩溃恢复决策:
- 看 binlog 状态:如果 prepare → 看 redo log 是否 commit,如果 commit 则补写 binlog commit;如果未 commit 则回滚
- 如果 binlog 已 commit → 直接确认事务完成
- MySQL 术语:**crash safe**

4. **刷盘时机参数**
- **`innodb_flush_log_at_trx_commit`**:控制 redo log 刷盘策略
- `0`:事务提交只写 redo buffer,每秒由后台线程刷到磁盘(最快,可能丢 1 秒数据)
- `1`:每次事务提交都 fsync redo log 到磁盘(最安全)
- `2`:每次事务提交写 OS 文件缓存,每秒 fsync(折中,OS 崩溃可能丢)
- **`sync_binlog`**:控制 binlog 刷盘策略
- `0`:依赖 OS 周期刷盘(最快,可能丢)
- `1`:每次事务提交都 fsync binlog 到磁盘(最安全)
- 其他数字 N:每 N 次事务提交 fsync 一次
- **生产组合**:
- **双 1**(1, 1):金融、交易类,性能开销最大但不丢数据
- **(1, 0)** 或 **(2, 1)**:日志类、埋点类,容忍极小数据丢失换性能
- **(0, 0)**:测试环境,生产禁用

5. **Undo Log 的持续存在与清理**
- 事务提交后 undo log **不会立即清理**
- InnoDB 的 **Purge 线程**(`srv_purge_workers`,8.0 多 worker)异步清理:条件是 undo 版本**没有被任何活跃事务的 ReadView 使用**
- **长事务的隐患**:一个长事务持有 ReadView,会阻塞 purge 清理 undo log,导致 undo tablespace 膨胀、表空间碎片增加
- **常见事故**:`ibdata1` 或 `undo_001` 无限增长,排查:`information_schema.innodb_trx` 找长事务
- **参数**:`innodb_max_undo_log_size`(8.0.22+)超过阈值会尝试清理

6. **Redo Log 的循环写特性**
- Redo log 是**固定大小、循环写**(一组 log file),不是追加
- 默认 4.0+ 是 `innodb_log_files_in_group` + `innodb_log_file_size`(如 2 个 5GB)
- MySQL 8.0.30+ 使用 `innodb_redo_log_capacity` 统一管理,支持动态调整
- **注意**:如果 redo log 快写满而 checkpoint 来不及推进,MySQL 会强制 flush Buffer Pool 到磁盘(性能骤降,`InnoDB: Log scan aborted`)
- **排查**:`SHOW ENGINE INNODB STATUS` 看 `Log sequence number` 和 `Last checkpoint at` 差值

7. **Binlog 的三种格式**
- **ROW**:记录每一行的变更前后镜像(默认,最安全、最占空间)
- **STATEMENT**:记录 SQL 语句(占空间少,可能不一致,如 `NOW()`、`UUID()`)
- **MIXED**:默认 STATEMENT,遇到不确定函数自动切 ROW
- **生产建议**:一律用 ROW 格式,避免主从不一致

8. **常见事故场景**
- **`innodb_flush_log_at_trx_commit=2` + 宕机**:redo 只写 OS cache 未 fsync,OS 缓存丢失导致数据丢失
- **`sync_binlog=0` + kill -9**:binlog 依赖 OS 刷盘,进程被 kill 时 OS 缓冲区可能丢
- **redo log 太小 + 写入压力大**:checkpoint 跟不上,触发强制 flush,性能骤降
- **长事务不提交 + undo 膨胀**:purge 阻塞,表空间膨胀到爆盘

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲清 2PC 的原子语义和崩溃恢复决策流程
- **广度**:能否覆盖刷盘参数、undo 清理、redo 循环写、binlog 格式
- **应用**:能否针对线上事故(如 undo 膨胀、redo 写满)给出排查方案

**延伸递进面试题**:
1. **为什么 binlog 和 redo log 不能自己独立提交?**
- 解答要点:跨层数据一致性——如果 redo commit 但 binlog 未 commit,从库无法同步这段数据,PITR 也丢失。2PC 保证两者原子。
2. **`innodb_flush_log_at_trx_commit=1` 的性能开销有多大?**
- 解答要点:每次事务都要 fsync,SSD 上单次 fsync 约 1ms 级,写入 IOPS 上限受 fsync 频率限制。可以用组提交(binlog group commit + redo group commit)合并 fsync,提升 5-10 倍。
3. **undo tablespace 无限膨胀怎么排查?**
- 解答要点:`information_schema.innodb_trx` 找长事务;`SHOW ENGINE INNODB STATUS` 看 history list length;kill 长事务或提交/回滚;检查 `innodb_purge_threads` 是否够。

第三部分:面试者参考回答(口语化)

面试官您好,我按"三种日志的分工 + 两阶段提交"来讲。

**三种日志分工**。redo log 是 InnoDB 内部的重做日志,记录修改后的状态,用于崩溃恢复;undo log 是 InnoDB 内部的回滚日志,记录修改前的状态,用于事务回滚和 MVCC;binlog 是 Server 层的日志,记录所有 DML/DDL 事件,用于主从复制和 PITR 时间点恢复。关键点:redo 和 undo 都在 InnoDB 内部,binlog 在 Server 层,跨层协调需要 2PC。

**一次 insert 的完整顺序**。第一步,SQL 到 Server 层解析、走执行计划。第二步,进入 InnoDB,先生成 undo log 记录修改前状态。第三步,改 Buffer Pool 里的内存页,注意此时未落盘。第四步,写 redo log buffer 记录修改后状态。第五步,事务提交时触发 2PC——先写 binlog buffer,然后 binlog write 到 binlog cache 文件,再 binlog flush 到 OS 文件缓存,此时 binlog 处于 **prepare 状态**。第六步,InnoDB 写 redo log 的 commit 标记并 fsync 到磁盘,这是 **redo log commit**。第七步,binlog 才真正 commit,fsync 到磁盘。

**为什么需要 2PC**。redo 和 binlog 各自独立落盘的话,会出现"redo 已 commit 但 binlog 未落盘"的中间态,崩溃后从库无法同步、PITR 也丢数据。2PC 保证两者原子——要么都成功要么都失败。崩溃恢复决策很简单:看 binlog 状态,prepare 就看 redo log 是否 commit,commit 就补写 binlog commit,未 commit 就回滚。

**刷盘参数**这块特别关键。`innodb_flush_log_at_trx_commit` 控制 redo,`sync_binlog` 控制 binlog,两个都设 1 就是双 1,最安全但性能最低。生产上金融、交易类坚持双 1;日志、埋点类可以放宽到 (1, 0) 或 (2, 1) 换性能。SSD 上单次 fsync 约 1ms,写入 IOPS 上限受 fsync 频率限制,所以 MySQL 用组提交(binlog group commit + redo group commit)合并 fsync,能提升 5-10 倍吞吐。

**再补两个容易忽略的点**。undo log 提交后不会立即清理,靠 Purge 线程异步清理,条件是没有活跃事务的 ReadView 使用它;一旦有个长事务持 ReadView,purge 就阻塞,undo tablespace 会膨胀到爆盘——这个坑我踩过,排查方法是 `innodb_trx` 找长事务,然后 kill 掉。redo log 是循环写的,默认两个 5GB,如果写入太快 checkpoint 跟不上,会强制 flush Buffer Pool 到磁盘,性能骤降,`InnoDB: Log scan aborted` 就是这个信号,需要调大 `innodb_log_file_size`。

当然,binlog 格式生产一律用 ROW 格式,避免 STATEMENT 里的 `NOW()`、`UUID()` 之类函数导致主从不一致。

#8 InnoDB在RR隔离级别下如何解决幻读? high mysql

题目

InnoDB在RR隔离级别下如何解决幻读?

我的回答

InnoDB 在可重复读下解决幻读使用的是 MVCC,解决了部分幻读,并没有完全解决幻读。它解决的方式是基于版本控制,当事务开启时,第一次进行 SELECT 的时候,创建对应的事务版本号,然后依据 MVCC 的机制读取事务,根据事务的 ID 去读取当前可读到的事务 ID 信息,来进行读取数据的控制,当进行了数据删除的时候,还是会导致 MVCC 中事务数据出现丢失,发生 SELECT 的返回结果不一致的问题。但是它还是能处理一部分幻读问题的。

点评

评分:⭐⭐⭐☆☆(3/5)
优点:意识到 RR 没有完全解决幻读;提到 MVCC;能想到边界情况
不足:完全遗漏 Gap Lock / Next-Key Lock;未区分快照读 vs 当前读;未讲 MVCC 三要素和 Next-Key Lock 构成
补充:RR 防幻读双机制 = 快照读靠 MVCC(ReadView 复用)+ 当前读靠 Next-Key Lock(RC 只加 Record Lock 不加 Gap Lock);RR 不 100% 防,混用快照读和当前读会看到"看似幻读" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察能否讲清 RR 隔离级别下"快照读靠 MVCC、当前读靠 Next-Key Lock"的双机制,并识别出"InnoDB 其实不 100% 防幻读"的边界。

**知识点拆解**:

1. **幻读的定义**
- 事务 A 两次相同查询返回结果**行数不同**——即看到"新插入的行"或"消失的行"
- 幻读 ≠ 脏读(读到未提交)≠ 不可重复读(同一行值变了)
- 隔离级别防幻读能力:RC 不防、RR 大部分防、SERIALIZABLE 完全防

2. **RR 防幻读的两条路径**
- **路径 1:快照读(普通 SELECT)—— 靠 MVCC**
- 事务首次一致性读时创建 ReadView
- ReadView 复用,读到的是"首次读时刻"的快照
- 其他事务的 INSERT / UPDATE / DELETE 都不可见
- **效果**:快照读路径完全不出现幻读
- **路径 2:当前读(SELECT FOR UPDATE / UPDATE / DELETE)—— 靠 Next-Key Lock**
- Next-Key Lock = Record Lock + Gap Lock
- 加锁范围:索引上匹配的记录 + 记录之间的间隙
- 其他事务想在间隙内插入 → 被阻塞,直到持锁事务提交或回滚
- **效果**:当前读路径大部分不出现幻读

3. **MVCC 三要素**
- **隐藏列**:每行有 `trx_id`(最近修改的事务 ID)和 `roll_pointer`(指向 undo 上一版本)
- **Undo Log 版本链**:每次修改写一条 undo,通过 roll_pointer 串成版本链
- **ReadView**:事务做快照读时创建,含 `m_ids`、`min_trx_id`、`max_trx_id`、`creator_trx_id`
- **可见性判断规则**:
- `trx_id < min_trx_id` → 可见(事务已提交)
- `trx_id = max_trx_id` → 不可见(还没分配 ID 的事务)
- `min_trx_id <= trx_id < max_trx_id` → 不在 m_ids 中可见(已提交),在 m_ids 中不可见(活跃)
- `trx_id = creator_trx_id` → 自己可见
- **RC vs RR 差异**:RC 每次快照读新建 ReadView、RR 首次读后复用

4. **Next-Key Lock 的构成与作用**
- **Record Lock**:锁索引上的具体记录
- **Gap Lock**:锁索引上的区间(不含记录),比如锁 (id=3, id=7) 之间的区间
- **Next-Key Lock**:Record + Gap 的组合,锁 (左端区间, 记录]
- **Insert Intention Lock**:insert 前的特殊 gap lock,多个 insert 可并发
- **锁范围示例**:`(a, b, c)` 索引,`WHERE c=5 FOR UPDATE` 若命中 c=5 的记录,会锁住 c=5 的记录 + c=5 之后到下一记录的间隙
- **RC 差异**:RC 只加 Record Lock,不加 Gap Lock,所以 RC 下间隙内可以并发插入

5. **RR 不 100% 防幻读的边界**
- **官方文档承认**:InnoDB 大部分场景防幻读,但不保证完全
- **经典反例场景 1**:混用快照读和当前读
- 事务 A 先用普通 SELECT 读一次走 ReadView
- 事务 A 再用 SELECT FOR UPDATE 当前读,如果其他事务在间隙内插入并提交了,A 的当前读能看到新插入的行
- 这不是 MVCC 的锅,是当前读本身读的是最新已提交数据
- **经典反例场景 2**:间隙锁的锁范围陷阱
- 假设索引是 `(a, b)` 联合索引
- 事务 A:`SELECT * FROM t WHERE a=1 AND b=2 FOR UPDATE`(加 Next-Key Lock)
- 事务 B:`INSERT INTO t VALUES (1, 2, 100)`(走 gap lock 插入,被 A 阻塞)
- 事务 A 提交后 B 插入成功
- 事务 A 再 SELECT FOR UPDATE 能看到 B 插入的行——这是"当前读看到当前读插入",不是幻读
- **核心结论**:只要事务内混用快照读和当前读,就可能看到"看似幻读"的现象

6. **常见误区辨析**
- **误区 1**:RR 完全防幻读 —— 错,官方不保证 100%
- **误区 2**:MVCC 是 RR 唯一手段 —— 错,只是快照读的手段
- **误区 3**:RR 靠行锁防幻读 —— 错,靠的是 Next-Key Lock(含间隙锁)
- **误区 4**:RC 也能防幻读 —— 错,RC 只加 Record Lock 不防间隙插入

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否说清 MVCC 和 Next-Key Lock 各自的触发场景
- **广度**:能否覆盖 ReadView 可见性规则、锁体系全貌
- **应用**:能否识别线上"看似幻读"的真实场景,理解 InnoDB 不完全防幻读的边界

**延伸递进面试题**:
1. **RC 和 RR 在锁行为上有哪些差异?**
- 解答要点:ReadView 创建时机(RC 每次、RR 首次复用);锁 RR 加 Next-Key Lock、RC 只加 Record Lock;RC 允许间隙插入,RR 阻塞
2. **RR 下"当前事务能看到自己提交后又看到别的行"是什么情况?**
- 解答要点:混用快照读和当前读。快照读走 ReadView 不看新插入,当前读会看到已提交的插入
3. **如何在不影响业务的前提下测试 RR 是否真的防幻读?**
- 解答要点:两个会话,会话 A `BEGIN; SELECT id FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE;`(不 commit);会话 B `INSERT (15)` 被阻塞;A commit;B 提交成功;A 再 SELECT FOR UPDATE 才看得到 15——这是"当前读看到当前读插入",不是幻读;改成快照读 A 始终看不到 15

第三部分:面试者参考回答(口语化)

面试官您好,InnoDB 在 RR 隔离级别下解决幻读我用**双机制**来答,同时也要说清它并不 100% 防。

**第一个机制是快照读靠 MVCC**。普通 SELECT 是快照读,事务做第一次快照读时会创建 ReadView,含 m_ids、min_trx_id、max_trx_id、creator_trx_id 四个字段,用这套规则判断当前行版本是否可见。RR 的关键是**首次读后 ReadView 复用**——不像 RC 每次都新建。所以事务后续再读,还是那个快照时刻的数据,其他事务的 INSERT、UPDATE、DELETE 都看不到。快照读这条路径完全不出现幻读。

**第二个机制是当前读靠 Next-Key Lock**。SELECT FOR UPDATE、UPDATE、DELETE 都是当前读,会加 Next-Key Lock,也就是 Record Lock + Gap Lock 的组合。Gap Lock 锁的是索引上的间隙区间(不含记录),比如锁 (id=3, id=7) 之间的间隙。其他事务想在这个间隙内插入,会被阻塞直到持锁事务提交或回滚。RR 默认加 Next-Key Lock,RC 只加 Record Lock 不加 Gap Lock——所以 RC 不防间隙插入导致的幻读,RR 大部分能防。

**但要承认 InnoDB 并不 100% 防幻读**,官方文档也是这么说的。典型反例是**事务内混用快照读和当前读**:事务 A 先用普通 SELECT 读一次走 ReadView,再用 SELECT FOR UPDATE 当前读,如果其他事务在间隙内插入并提交了,A 的当前读就能看到这个新插入的行——这不是 MVCC 的锅,是当前读本身读的是最新已提交数据。另外还有一些间隙锁范围陷阱,比如锁的是索引列的间隙而不是主键的间隙,也会让"看似幻读"的现象出现。

**再补充个 RC vs RR 的差异**。RC 每次快照读都新建 ReadView、RR 首次读后复用;RC 只加 Record Lock、RR 加 Next-Key Lock。所以 RC 完全不防幻读,RR 大部分防但不完美。生产上 Spring Boot 默认其实是 RC,MySQL 官方默认是 RR,两者选择看业务——读多写少、要防幻读选 RR;写多、有间隙锁性能顾虑选 RC。

当然,如果真的想 100% 防幻读,只能上 SERIALIZABLE 隔离级别,但代价是所有读都加 S 锁,串行化执行,业务基本会崩。所以工程上都是接受 RR 的"大部分防"这个折中。

#9 设计索引的时候有哪些原则(考虑哪些因素)? high mysql

题目

设计索引的时候有哪些原则(考虑哪些因素)?

我的回答

设计索引要考虑的核心机制,首先是业务第一。基于什么样的业务去设计什么样的索引,不能创建一个没有意义的索引。你创建了一个特别好的索引,但这个索引没有意义,创建的是没有用的。首先创建索引要在区分度比较高的字段上进行创建。如果一个字段区分度特别低,各种数据一样占一半,或者占的比例都很多,没有什么区分度,那么这个索引的意义也不大。要解决的核心目标是什么?是为了快速的找到我们目标的数据。前置说的区分度高的,就是为了这个;然后整个索引的过程会涉及整个搜索的过程会涉及几个方面。首先是通过索引找到数据,然后如果你要显示的数据在索引中不存在,就会涉及一个回表。这里边要考虑的就是你的索引是否要进行覆盖,覆盖索引的所有字段。如果能覆盖,就会减少一次搜索。另外就是查询的时候会涉及排序,排序的字段是否要包含在索引中?尽可能的其实要把排序字段在索引中体现出来,这样可以更好的利用索引提前进行排序,而不是搜索出来之后再进行外部的文件排序,那样会浪费更多的时间和资源。还有就是对于数据量的考量,数据量级具体在什么量级,使用什么样的索引,不同量级的话可能要考虑提前做分库分表,这里不仅仅是索引方面的问题,还有对数据量评估,以什么维度去进行数据库的拆分。这就是我考虑的创建索引时的主要原则。

点评

评分:⭐⭐⭐⭐☆(4/5,第 2 次,从 3/5 提升)
优点:业务第一;区分度;覆盖索引减少回表;排序字段放索引避免 filesort;数据量级考量
不足:未讲联合索引列顺序、写放大、唯一索引、主键选择、前缀索引、索引长度限制、JOIN 场景、NOT NULL、冗余治理、8.0 新特性
补充:联合索引列顺序速查 `WHERE a=? AND b>? ORDER BY c` → `(a, b, c)`;索引设计 5 问 = 谁查、查多少、多频繁、要不要排序、写多还是读多 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:以业务查询模式为锚点,系统掌握索引设计的全维度决策框架,覆盖数据、访问、性能、治理四条主线。

**知识点拆解**:

1. **业务维度:先看查询,再定索引**
- **查询频率**:一个索引每月只被用几次,收益远不如维护成本
- **查询模式**:等值 / 范围 / 排序 / 分组 / JOIN,每个模式对索引要求不同
- **数据量**:小表(< 1 万行)优化器倾向全表;大表必须靠索引
- **读写比**:读多写少 → 多建索引;写多读少 → 少建但建好

2. **基数(Cardinality)与选择率**
- 基数 = `distinct_count / total_rows`
- 基数接近 1(如 ID、订单号)→ 单列索引最有效
- 基数很低(如 gender、status 只有 2-3 值)→ 单列索引几乎无用,考虑放联合索引前缀或 Skip Scan
- 中基数(如 city、user_id)→ 单列索引常见
- `SHOW INDEX FROM t` 里的 `Cardinality` 是采样估算,用 `SELECT COUNT(DISTINCT col), COUNT(*) FROM t` 拿真实值

3. **联合索引的列顺序原则(关键)**
- **等值条件列放前**:`WHERE a=? AND b>?` → 索引顺序 `(a,b)` 才对
- **范围条件列放后**:范围列后面的列无法用索引定位(跨范围无序)
- **排序列放最后**:`ORDER BY` 的列如果放在联合索引最后,且前面都是等值,可以省 filesort
- **高基数列尽量靠前**:能更快收敛匹配行
- **避免低基数列开头**:如 `(status, user_id)`,status 基数 3 会导致扫描大量重复行

4. **覆盖索引优先**
- `SELECT` 的列全部命中某个索引 → 走覆盖索引,`Extra=Using index`
- 覆盖索引能省掉回表,性能提升数倍
- 权衡:索引宽度 vs 覆盖收益。避免为了覆盖把所有列都塞进一个索引,那样索引本身会膨胀

5. **回表数量控制**
- 回表次数 = 索引扫描到的行数,不是最终 LIMIT 数量
- `LIMIT 10` 的查询可能回表几百万次
- 优化:延迟关联、覆盖索引、ICP

6. **索引键长度限制**
- InnoDB 默认:`innodb_large_prefix=ON` 时 3072 字节(`COMPRESSED`/`DYNAMIC` 行格式)
- 旧版本 767 字节(`COMPACT`/`REDUNDANT` 行格式)
- TEXT/BLOB 不能整列建索引,必须用**前缀索引** `INDEX(name(20))`
- 前缀长度选择:`SELECT COUNT(DISTINCT LEFT(name, N)) / COUNT(DISTINCT name) FROM t`,达到 0.99+ 停止

7. **索引类型选择**
- B+ 树索引(默认):等值、范围、排序都能用
- 全文索引:中文模糊搜索场景,需要 `ngram` 分词器
- 空间索引:GIS 场景
- HASH 索引:仅 Memory 引擎支持;InnoDB 的 adaptive hash index 是自动的,无法手动控制
- MySQL 8.0 新增:`FUNCTIONAL INDEX`(表达式索引),`DESCENDING INDEX`(降序索引)

8. **冗余索引治理**
- **前缀冗余**:`(a,b,c)` 已存在时 `(a,b)`、`(a)` 是冗余
- **完全冗余**:`(a)` 和 `(a)` 重复
- **反向冗余**:`(a,b)` 和 `(b,a)` 都能用,但用途不同,通常保留一个
- 治理手段:
- `sys.schema_unused_indexes` 找 `count_star=0` 的索引
- `performance_schema.table_io_waits_summary_by_index_usage` 精确统计
- MySQL 8.0 `ALTER TABLE t ALTER INDEX idx INVISIBLE` 灰度下线,观察 1-2 周再 DELETE

9. **特殊场景**
- **JOIN 场景**:外键关联字段必须建索引,否则 join 时右表全表扫描
- **分库分表**:索引设计要考虑 shard key 是否在索引里,跨分片查询尤其要谨慎
- **唯一约束**:业务上唯一的字段必须建唯一索引,同时能防止重复插入,还能加速等值查询
- **主键选择**:优先自增 bigint,避免 UUID(无序、体积大、页分裂)
- **NULL 值**:索引列允许 NULL 会增加 NULL bitmap 开销,优先 NOT NULL + 默认值

10. **工程化治理**
- 索引变更走 CR:谁能加、谁能删要有流程
- `FORCE INDEX` 硬编码是雷区:索引被删业务立即崩
- 定期审计:每月扫一次 `schema_unused_indexes` 和慢查询
- 生产变更前必跑 `EXPLAIN ANALYZE`(8.0)看真实代价

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲清联合索引列顺序、覆盖索引、基数选择背后的原理
- **广度**:能否覆盖索引类型、长度限制、8.0 新特性、分库分表
- **应用**:能否针对真实业务给出索引设计 + 治理的完整方案

**延伸递进面试题**:
1. **`(status, user_id, create_time)` 是常见设计,如果查询是 `WHERE status=1 ORDER BY create_time DESC`,这个索引能用吗?**
- 解答要点:能。status 等值命中前缀,`user_id` 未知但因为是联合索引的中间列——不对,中间列未知则 create_time 无法走排序。所以要改成 `(status, create_time)` 或直接建 `(status, create_time DESC)`(8.0 支持)。
2. **TEXT 字段做前缀索引怎么选长度?**
- 解答要点:跑 `SELECT COUNT(DISTINCT LEFT(col, N)), COUNT(DISTINCT col) FROM t` 让比值达到 0.99 或以上,同时兼顾空间。经验值:邮箱 20、手机号 11、用户名 10-15。
3. **MySQL 8.0 的 `INVISIBLE INDEX` 相比直接删除有什么好处?**
- 解答要点:可以先设置为不可见观察 1-2 周,确认没有 SQL 走这个索引后,再执行真正的 `DROP INDEX`。避免"删了才发现在用"的事故。DDL 也是在线的。

第三部分:面试者参考回答(口语化)

面试官您好,索引设计我按四条主线来讲:业务、数据、性能、治理。

**第一条,业务维度**。索引不是越多越好,一定要业务驱动——有查询模式才建索引。我会先摸清几个问题:谁在查、查多频繁、单条查询返回多少行、要不要排序、表是读多还是写多。小表几千行不用建,优化器自己全表扫;大表必须靠索引。写多读少的表要克制,每个索引都是 INSERT/UPDATE/DELETE 的额外维护成本。

**第二条,数据维度,重点是基数和联合索引列顺序**。基数就是 `distinct_count / total_rows`,越接近 1 越适合建单列索引,比如 ID、订单号;基数很低(如 status 只有 2-3 值)单独建索引基本没用,但可以放联合索引前面做前缀。联合索引的列顺序有讲究:等值条件列放前面、范围条件列放后面、`ORDER BY` 列放最后——这样能省 filesort。反过来如果排序列前面还有个未知列,排序就废了。

**第三条,性能维度,重点是覆盖索引和回表控制**。如果能把 SELECT 列全部塞进一个索引里,就走覆盖索引,EXPLAIN 显示 `Using index`,性能能提升好几倍。但索引宽度也有代价,不能什么都往里塞。回表次数取决于索引扫描行数而不是 LIMIT,所以要用延迟关联或 ICP 减少。索引键长度也有硬限制,InnoDB 默认 3072 字节,TEXT 字段必须做前缀索引,用 `SELECT COUNT(DISTINCT LEFT(col, N)) / COUNT(DISTINCT col)` 找最优长度。

**第四条,索引治理**。这块特别重要,很多线上事故出在这里。三个点:一是冗余治理,`(a,b,c)` 存在时 `(a,b)`、`(a)` 就是冗余,应该删掉;二是无索引治理,用 `sys.schema_unused_indexes` 或者 `performance_schema.table_io_waits_summary_by_index_usage.count_star=0` 找长期没人用的索引;三是灰度下线,MySQL 8.0 有 `ALTER TABLE t ALTER INDEX idx INVISIBLE`,可以先设为不可见观察一两周,确认没有 SQL 依赖后再真删。

特殊场景还有几个:JOIN 关联字段必须建索引;分库分表要考虑 shard key 是否在索引里;唯一约束优先用唯一索引既能防重又能加速;主键选自增 bigint 避免 UUID 打散 B+ 树导致页分裂和随机 IO;索引列最好 NOT NULL,避免 NULL bitmap 的额外开销。

最后工程化流程:任何索引变更走 CR,生产变更前跑 `EXPLAIN ANALYZE` 看真实代价,每月审计一次 `schema_unused_indexes`。特别警惕 `FORCE INDEX` 硬编码——团队有人删了这个索引而代码里还强推,业务直接生产报错,我之前踩过这个坑。

---

#10 MySQL大表在线加索引会阻塞业务吗?如何规避? high mysql

题目

MySQL大表在线加索引会阻塞业务吗?如何规避?

我的回答

Mysql 大表在线加索引是会阻塞核心业务的哪怕是现在 MySQL 8 说支持 online DDL,也是不行的。它的核心流程还是会导致业务线程阻塞。我们当时考虑规避,首先是建立子表。建立子表的时候,这个子表是不使用的,然后把数据迁移过去,然后进行目标的更新。更新了之后,再把表的名称进行更换。表名称的更换效率会更高一些,影响业务也就更小。还有就是做索引、做表操作的时候,要选择业务低峰期,或者如果可以停机业务,要进行停机,进行发通告,我们进行某些更新,业务暂时停机,然后进行这方面的操作。毕竟执行过程中如果出现了什么特殊情况,会产生不可预料的各种问题,到时候你想找都没有办法找。核心原则首先是规避业务封期,然后是使用通告的形式告知会产生业务变动,进行业务暂停,然后进行相关的操作。再有就是使用复制的方式,创建临时表,把数据复制过去,然后再进行更新,先更新结构,把表复制过去,然后再进行相应的处理。另外 MySQL 好像有单独的 online DDL 的加索引的方式,但是也是会有影响的。只是影响度比原本的操作方式要低一些。这具体要看数据量是多少。我们之前数据量大概是 3 个亿还是 5 个亿?三五个亿的数据更新了很久。中断了大概几分钟。

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:抓到核心问题(8.0 Online DDL 也可能阻塞);提出 Ghost Table 方案;有真实生产经验(3-5 亿数据、中断几分钟)
不足:未讲 MDL 阻塞链;未讲 gh-ost / pt-osc 工具;未讲 Instant DDL;未讲 INPLACE/INSTANT/COPY 三种算法;未讲长事务排查
补充:加索引三种 DDL 算法对比;MDL 阻塞链是生产事故最大原因(长事务持 MDL 共享锁 → ALTER 排队 → 后续 DML 排队 → 连接池爆);生产推荐 gh-ost;加分金句:"ALTER 前必查 `innodb_trx`" ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察是否清楚 Online DDL 的真实机制(INPLACE/INSTANT vs COPY)、MDL 阻塞链、大厂规避工具链。

**知识点拆解**:

1. **DDL 的三种算法(MySQL 5.6+)**
- **COPY**:重建整表,全程锁表读写阻塞,业务完全停顿
- **INPLACE**:不重建表,直接在原 B+ 树追加或修改索引,仅拿短暂 MDL;支持大部分 `ADD INDEX`、`DROP INDEX`、`ADD COLUMN`、`MODIFY COLUMN`
- **INSTANT**(8.0 新增):只修改元信息,不重建数据,亚秒级完成;支持加列、改列名、改 DEFAULT 值、`ALTER TABLE ... CONVERT TO CHARACTER SET` 等

2. **加索引的具体阻塞点**
- **老式 ALTER(无 `ALGORITHM=INPLACE`)**:默认走 COPY 算法,全程锁表,业务完全阻塞
- **推荐语法**:`ALTER TABLE t ADD INDEX idx_a (a), ALGORITHM=INPLACE, LOCK=NONE`
- INPLACE 只在 DDL 开始和结束各拿一次短暂 MDL 排他锁
- 中间过程只拿 MDL 共享锁,不阻塞 DML
- **8.0 INSTANT 适用场景**:加普通列、改列名、改 DEFAULT 值;加索引仍需 INPLACE

3. **MDL(Metadata Lock)阻塞链——生产事故最大原因**
- **机制**:所有 DML 拿 MDL 共享锁,DDL 拿 MDL 排他锁;拿排他锁前要等所有共享锁释放
- **事故场景**:
1. 长事务 T1 执行 SELECT,持 MDL 共享锁不提交
2. ALTER TABLE 想拿 MDL 排他锁,等待 T1
3. 之后新事务 T2、T3... 想拿 MDL 共享锁,排队等 ALTER
4. ALTER 拿不到排他锁就一直等,排队事务全阻塞 → 连接池爆 → 服务雪崩
- **排查**:`SELECT * FROM performance_schema.metadata_locks` 看锁等待;`information_schema.innodb_trx` 找长事务
- **解决**:
- `metadata_locks_timeout`(8.0):ALTER 等不到 MDL 直接超时失败,不再阻塞后续事务
- Kill 长事务
- DDL 用 `ALGORITHM=INPLACE LOCK=NONE`,拿不到锁立即失败

4. **Ghost Table 手工方案(经典但易错)**
- **流程**:
1. `CREATE TABLE t_new LIKE t` 建影子表
2. `INSERT INTO t_new SELECT * FROM t` 全量同步
3. 触发器方式增量同步(`INSERT/UPDATE/DELETE` 触发器)
4. 等全量+增量追平
5. `RENAME TABLE t TO t_old, t_new TO t` 原子切换
6. 观察一段时间后 DROP `t_old`
- **风险**:
- 触发器方式对写入性能有影响
- 切换瞬间可能丢数据(如果切换后有新写入到 t_old)
- 手工流程容易出错
- **优势**:可控性强,切换瞬间业务影响最小

5. **gh-ost / pt-online-schema-change(大厂标配)**
- **gh-ost**:
- GitHub 开源,Facebook、Google 生产方案
- 读 binlog 做增量同步,不用触发器
- `--cut-over=atomic`:切换时用事务原子操作,避免数据丢失
- 支持限流(`--max-lag-msec`)避免影响主库
- 支持中断恢复
- **pt-online-schema-change**:
- Percona Toolkit
- 触发器方式增量同步
- 支持限流
- **两者对比**:gh-ost 更成熟,binlog 方式更安全;pt-osc 依赖触发器,对写入有影响
- **生产演练**:必须先在从库演练,评估数据量、耗时、影响

6. **MySQL 8.0 Instant DDL 支持的操作**
- 加列(非 FIRST/AFTER 位置的普通列)
- 改列名
- 改 DEFAULT 值
- 表级选项变更
- `ALTER TABLE ... CONVERT TO CHARACTER SET`
- **注意**:加索引仍需 INPLACE,不是 INSTANT

7. **生产规避策略清单**
- **首选**:`ALGORITHM=INPLACE, LOCK=NONE`,加超时保护
- **次选**:gh-ost 或 pt-osc 工具自动执行
- **错峰**:低峰期执行,避免和业务高峰叠加
- **通告**:提前告知业务方,准备好停机预案
- **长事务排查**:ALTER 前先查 `innodb_trx`,kill 掉长事务
- **从库演练**:先用从库数据量评估耗时
- **可回滚**:准备回滚脚本,出问题能快速恢复
- **限流**:gh-ost 用 `--max-lag-msec=1000` 限制复制延迟

8. **数据量级参考**
- 100 万行:秒级完成,基本无感
- 1000 万行:几十秒到几分钟,低峰执行
- 1 亿行:几分钟到几十分钟,必须用 gh-ost 或 pt-osc
- 10 亿行以上:几小时到几天,必须分阶段执行、错峰、限流

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:能否讲清 INPLACE/INSTANT/COPY 三种算法的差异和适用场景
- **广度**:能否覆盖 MDL 阻塞链、gh-ost/pt-osc 工具、Instant DDL
- **应用**:能否给出生产大表加索引的完整流程(含风险评估、演练、切换)

**延伸递进面试题**:
1. **MySQL 8.0 的 INSTANT DDL 能加索引吗?**
- 解答要点:不能。INSTANT 只支持修改元信息(加列、改列名、改 DEFAULT 等),加索引仍需 INPLACE 算法
2. **MDL 阻塞导致连接池爆,如何快速恢复?**
- 解答要点:先 `SELECT * FROM performance_schema.metadata_locks` 看是谁持锁;`information_schema.innodb_trx` 找长事务;kill 掉长事务后 MDL 排他锁释放,ALTER 继续;后续事务能拿到 MDL 共享锁,业务恢复
3. **gh-ost 和 pt-osc 怎么选?**
- 解答要点:gh-ost 用 binlog 增量同步,不用触发器,对主库写入影响小;pt-osc 用触发器,对写入有影响;生产推荐 gh-ost

第三部分:面试者参考回答(口语化)

面试官您好,大表加索引确实会阻塞业务,即使 MySQL 8.0 支持 Online DDL 也不能完全放心,我按"机制 + 规避方案 + 生产经验"来讲。

**先讲机制**。MySQL 5.6+ 的 DDL 有三种算法:**COPY**(老式,重建整表全程锁表)、**INPLACE**(不重建表,B+ 树追加索引,只拿短暂 MDL)、**INSTANT**(8.0 新增,只改元信息,亚秒级,支持加列、改列名、改 DEFAULT,但不支持加索引)。生产加索引正确姿势是 `ALTER TABLE t ADD INDEX idx_a(a), ALGORITHM=INPLACE, LOCK=NONE`——加索引走 INPLACE,拿不到锁立即失败而不是排队阻塞。

**再讲真正的坑:MDL 阻塞链**。这是生产事故最大原因。长事务 T1 持 MDL 共享锁不提交 → ALTER 拿排他锁等待 T1 → 后续所有 DML 拿共享锁排队等 ALTER → 连接池爆、服务雪崩。破解三招:`metadata_locks_timeout`(8.0)让 ALTER 等不到 MDL 就超时;ALTER 前先查 `information_schema.innodb_trx` kill 掉长事务;DDL 一定用 `LOCK=NONE` 拿不到锁立即失败。

**规避方案三档**。第一档是**手工 Ghost Table**:建影子表 → 全量同步 → 触发器增量同步 → `RENAME TABLE` 原子切换 → 观察后 DROP 旧表。优点是可控,缺点是触发器影响写入、切换瞬间可能丢数据。第二档是**gh-ost**:GitHub 开源,Facebook 生产方案,读 binlog 增量同步,不用触发器,`--cut-over=atomic` 平滑切换,支持 `--max-lag-msec` 限流。第三档是 **pt-online-schema-change**:Percona 出品,触发器方式,成熟但写入有影响。生产推荐 gh-ost,因为不用触发器对主库压力最小。

**生产流程我按六步走**。第一步,先在从库演练评估耗时和数据量影响。第二步,错峰执行,选业务低峰。第三步,发通告告知业务方。第四步,ALTER 前先 `innodb_trx` 排查长事务并 kill。第五步,工具执行加索引(gh-ost 或 pt-osc),限流避免影响复制延迟。第六步,切换后观察复制延迟和业务指标,出问题能立刻回滚。

**数据量级参考**:百万行秒级无感,千万行低峰执行,亿级必须用 gh-ost 或 pt-osc,十亿级以上要分阶段错峰。我之前遇到过 3-5 亿行的加索引场景,全程跑了几十分钟,中间有几秒业务中断——主要是切换瞬间的 MDL 排他锁拿了片刻。

**MySQL 8.0 的 Instant DDL 不能加索引**,只支持加列、改列名、改 DEFAULT 这类元信息操作,加索引仍需 INPLACE。这个知识点特别容易混。

当然,任何生产 DDL 都是风险操作,我的经验是——**能不在线上做就不在线上做,能在从库演练就在从库演练,必须在线上做的用 gh-ost 工具 + 错峰 + 通告 + 限流,把风险降到最低**。我踩过一次坑,长事务没 kill 干净导致 ALTER 排队把连接池打爆了,从那以后 ALTER 前必查 `innodb_trx` 已经成为肌肉记忆。

📚 查看全部面试记录