📖 相关阅读

本文是 MySQL 深度指南完整版(架构→InnoDB→索引→事务→MVCC→锁→优化→主从→35道面试题)。

04-MySQL高级面试-图书馆类比.html 是本文件的精简面试版,用图书馆类比讲解核心面试题(B+树→索引→MVCC→锁→SQL优化→日志),适合快速突击;本文适合深度理解。

MySQL 深度指南

从架构原理到生产实战 · 面试全覆盖 · 含 8 张 CSS 架构图

💼 想要看真实工作场景?推荐搭配实战排查

本页侧重面试考点速答。若需看真实生产问题的排查过程和修复代码,请看以下实战页面:

🔧 MySQL原理深度讲解 🔧 MySQL问题驱动排查
🏗 1. MySQL 架构总览
📐 MySQL 分层架构图
客户端
Java/Python/Go
MySQL CLI
Navicat/DBeaver
⬇ TCP/IP 连接
连接层
连接管理
连接池/线程池
权限认证
用户名/密码/主机
安全
SSL/TLS
SQL 层
查询缓存
8.0已移除
解析器
词法/语法分析
优化器
执行计划/索引选择
执行器
调用存储引擎API
⬇ 存储引擎 API
存储引擎
InnoDB
默认 ★
MyISAM
Memory
其他...
磁盘
Redo Log
Undo Log
数据文件 (.ibd)
Binlog
📦 MySQL 架构分层
连接层 = 连接管理接收客户端连接、认证权限、分配线程处理请求
SQL 层 = 解析与优化SQL 词法语法解析、选择索引/JOIN顺序(优化器)、调用执行器
存储引擎 = 数据操作负责数据的实际存储和读取(InnoDB/MyISAM),可插拔设计
磁盘 = 持久化数据文件真正写入磁盘,引擎通过文件系统读写数据
一条 SQL 的执行流程
SQL 语句
连接器
解析器
词法+语法
优化器
选索引/JOIN顺序
执行器
权限检查+调用引擎
返回结果

关键细节:执行器在调用存储引擎之前会先做权限校验。 优化器是整个流程的"大脑",它决定使用哪个索引、JOIN 表的顺序——如果选错了索引,查询可能慢 100 倍。

📊 Excel 对比

Excel 没有存储引擎的概念——只有一种"存储方式"(.xlsx 文件)。所有数据操作都通过同一套引擎完成。
MySQL 的强大在于可插拔存储引擎——同一台服务器上不同的表可以使用不同的引擎。 这就像 MySQL 支持多种存储引擎,事务型用 InnoDB,只读型用 MyISAM,各有专长。

CREATE TABLE t1 (...) ENGINE=InnoDB; — 指定引擎
SHOW ENGINES; — 查看所有支持的引擎

2. InnoDB vs MyISAM
核心对比 (12 个维度)
维度 InnoDB MyISAM
事务支持✓ ACID
锁粒度行级锁表级锁
崩溃恢复Redo Log 自动恢复需 REPAIR TABLE
外键✓ 支持
MVCC✓ 多版本并发
COUNT(*)需遍历索引计数直接存储行数
全文索引✓ (5.6+)
存储文件.ibd (数据+索引).MYD + .MYI 分离
聚簇索引✓ 主键即数据✗ 非聚簇
哈希索引自适应哈希(内部)
压缩支持表压缩支持 MyISAM Pack
适用场景几乎一切场景只读/读多写少/不需事务
选择指南
✓ 选 InnoDB(99% 场景)
  • 需要事务(借书单/支付/库存)
  • 高并发读写
  • 需要外键约束
  • 需要崩溃安全恢复
  • MySQL 5.7+ 默认引擎
◇ 选 MyISAM(极少数场景)
  • 纯读场景(如日志分析表)
  • 不需要事务
  • COUNT(*) 查询极频繁
  • 历史遗留系统兼容
📐 InnoDB 内存架构
内存 (In-Memory)
Buffer Pool (缓冲池)
• 数据页缓存
• 索引页缓存
• 自适应哈希索引
• 占总内存 60-80%
Log Buffer (日志缓冲)
• Redo Log 内存缓冲
• 定期刷到磁盘
Additional Memory
• Undo 页缓存
• 内部临时表
刷盘
读取
磁盘 (On-Disk)
系统表空间 (ibdata1)
• 数据字典
• Doublewrite Buffer
• Change Buffer
独立表空间 (*.ibd)
• 每表一个文件
• 数据 + 索引
• innodb_file_per_table
Redo Log (ib_logfile*)
• WAL 机制
• 顺序写,高性能
• 崩溃恢复核心
🌳 3. B+ 树索引原理
📐 B+ 树结构图(以主键索引为例)
根节点 (Root)
30
60
┌──────────┼──────────┐
分支节点 (Branch)
10
20
40
50
70
80
┌────┼────┐ ┌────┼────┐ ┌────┼────┐
叶子节点 (Leaf) — 存储全部数据 + 双向链表
1
5
10
15
20
25
30
35
40
45
50
55
60
65
70
75
80
85
⇠ 双向链表:1⇄5⇄10 ⇄ 15⇄20⇄25 ⇄ 30⇄35⇄40 ⇄ 45⇄50⇄55 ⇄ ... ⇢
三层 B+ 树可以存储约 2000万+ 行数据(假设每页16KB,指针6字节)
为什么是 B+ 树?—— 数据结构对比
数据结构 查找 范围查询 磁盘友好 面试结论
二叉搜索树 O(logN) 可能退化为 O(N) ✗ 差 ✗ 树太深 数据量大时树高太大,IO 次数多
Hash 索引 O(1) 等值极快 ✗ 不支持 不支持范围查询和排序
B 树 O(logN) 中 etc. △ 数据在所有节点 非叶子节点也存数据,扇出小,树更高
B+ 树 ★ O(logN) 稳定 ✓ 叶子链表 ✓ 扇出大,树矮 磁盘IO最少 → 最优选择
B+ 树的三大优势:
非叶子节点只存 key,单个页能容纳更多 key → 扇出更大 → 树更矮 → IO 更少
叶子节点双向链表,范围查询只需找到起点,沿链表顺序扫描
查询性能稳定,所有数据都在叶子节点,每次查询路径长度相同
📚 类比:B+ 树 = 图书馆书架系统

找一本书:
先找区(根节点)→ A-F区 / G-M区 / N-Z区
再找架(分支节点)→ 第3排书架
再找层(叶子节点)→ 第2层
拿书(数据)→ 目标书就在这层

每一步都只需要看一个"指示牌"(索引页),不需要翻遍整个图书馆。三层就能定位千万本书。

📊 Excel 对比

Excel:没有索引时,查找需要翻遍整表(全表扫描 = 逐行肉眼找)。
排序后可以用二分查找(类似索引),但 Excel 的"排序"是全局重排。

MySQL:B+ 树索引 = 给表建了一套"排序好的目录"。查找时通过目录(3-4次 IO)直接定位到行,无需翻遍全表。

• 无索引:WHERE name='张三' → 扫描 500 万行
• 有索引:通过 B+ 树 → 3 次 IO 即可定位

📐 聚簇索引 vs 二级索引(回表过程)
二级索引 (name 列)
name (索引key)主键 id (指针)
张三3
李四1
王五5
叶子节点存的是 主键值,不是行数据
回表查询
再查一次主键索引
聚簇索引 (主键 id)
idnameageemail
1李四25li@xx.com
3张三30zhang@xx.com
5王五28wang@xx.com
叶子节点存的是 完整行数据
SELECT * FROM user WHERE name='张三'
① 查二级索引(name) 找到 id=3 → ② 回表查聚簇索引(id) 拿到完整行 → 回表 = 两次 B+ 树查找
覆盖索引 — "不用回表"
覆盖索引原理
❌ 需要回表
SELECT * FROM user WHERE name='张三'

二级索引(name) → 找到 id=3
→ 再查聚簇索引拿全部列
→ 两次 B+ 树查找
VS
✓ 覆盖索引
SELECT name, id FROM user WHERE name='张三'

二级索引(name, id) → name 和 id
都在索引里,直接返回
→ 一次 B+ 树查找就够

判断方法:EXPLAIN 的 Extra 列出现 Using index 就说明用了覆盖索引,无需回表。

🏭 生产案例:查询从 3s 优化到 30ms

场景:借书单表 500 万行,查询 SELECT order_id, user_id, status, create_time FROM orders WHERE user_id = 10086 AND status = 'PAID' ORDER BY create_time DESC LIMIT 20

-- 优化前:只有 user_id 索引,需回表过滤 status,再排序 → 3s
-- 优化后:联合索引 (user_id, status, create_time)
ALTER TABLE orders ADD INDEX idx_user_status_time(user_id, status, create_time);

-- 覆盖索引:order_id 是主键,在二级索引中自带
-- user_id + status 用于筛选 + create_time 用于排序 → 无需 filesort
-- 结果:30ms,提升 100 倍
🔐 4. 事务与 ACID
A — 原子性

定义:事务中的操作要么全部成功,要么全部回滚。
类比:整个借书单要么全成功(扣库存+扣余额+生成借书单),要么全失败。不能只扣了余额但没生成借书单。
实现:Undo Log — 回滚时反向补偿。

C — 一致性

定义:事务前后数据库从一个一致状态转到另一个一致状态。
类比:库存不能出现负数。转账前后总金额不变。
实现:由 A+I+D 共同保证,是最终目标而非单独机制。

I — 隔离性

定义:并发事务之间互不干扰。
类比:两个人同时买最后一件商品,只有一个人能成功。各事务感觉不到别人的存在。
实现:MVCC + 锁机制

D — 持久性

定义:事务一旦提交,数据永久保存,断电不丢失。
类比:借书单确认后,即使服务器立刻断电,重启后借书单数据依然存在。
实现:Redo Log — WAL 先写日志再写数据。

📐 四大隔离级别 — 问题一览

Read Uncommitted

读未提交
✗ 脏读
✗ 不可重复读
✗ 幻读

Read Committed

读已提交 (RC)
✓ 无脏读
✗ 不可重复读
✗ 幻读

Repeatable Read ★

可重复读 (RR) — MySQL默认
✓ 无脏读
✓ 无不可重复读
✗ 幻读(InnoDB通过MVCC+间隙锁解决)

Serializable

串行化
✓ 无脏读
✓ 无不可重复读
✓ 无幻读
⚠ 性能最差
隔离级别由低到高:性能由高到低。MySQL 默认 RR,InnoDB 在 RR 级别通过 MVCC + Gap Lock 基本解决了幻读。
三种并发问题详解
问题场景示例
脏读 事务A读到了事务B未提交的数据 B更新余额为900(未提交)→A读到900→B回滚→A基于错误数据做了操作
不可重复读 同一事务内,两次读同一行结果不同 A第一次读余额=1000→B修改并提交→A再读余额=900(同一事务内数据变了)
幻读 同一事务内,两次读行数不同 A读age>20有5条→B插入一条age=25→A再读有6条(多了一行"幻影")
🔀 5. MVCC 多版本并发控制
🍕 类比:MVCC = 快照副本

你在修改文档的时候,别人看到的还是你的"旧版本快照"。你改完提交后,新读者才能看到新版本。每个人在同一时刻看到的可能是不一样的版本。
核心价值:读写不互相阻塞 — 写操作不需要加读锁,读操作不会被写阻塞。

📊 Excel 对比

MVCC ≈ Excel 的"共享工作簿"(多人同时编辑不同区域)。
但比共享工作簿更强:每个人看到的是自己打开时刻的"快照",其他人修改了也不会影响你看到的版本。只有当你刷新(重新开启事务)后才看到最新数据。

📐 MVCC 版本链 — Read View + Undo Log
当前版本 (数据页中)
id=3, name='王五', age=28
trx_id = 103 (最后修改的事务ID) | roll_pointer →
⬇ roll_pointer
Undo Log 版本 2
id=3, name='王五', age=25
trx_id = 102 | roll_pointer →
⬇ roll_pointer
Undo Log 版本 1 (最早)
id=3, name='张三', age=25
trx_id = 101 | roll_pointer = NULL
Read View (读视图) — 决定你能看到哪个版本
m_ids:生成 Read View 时,所有活跃(未提交)的事务 ID 列表
min_trx_id:m_ids 中最小的值
max_trx_id:下一个将分配的事务 ID
creator_trx_id:创建该 Read View 的事务 ID

可见性判断规则:
① trx_id < min_trx_id → 可见(事务已提交)
② trx_id ≥ max_trx_id → 不可见(在 Read View 创建后才开始的)
③ min_trx_id ≤ trx_id < max_trx_id → 看 m_ids:
  在 m_ids 中 → 不可见(事务还在跑)
  不在 m_ids 中 → 可见(事务已提交)
RC vs RR:MVCC 的区别
Read Committed (RC)Repeatable Read (RR)
Read View 生成时机每次 SELECT 都生成新的 Read View事务第一次 SELECT 时生成,后续复用
效果能看到其他事务已提交的新数据 → 不可重复读始终看到事务开始时的快照 → 可重复读
类比每次刷新页面看到最新数据一直看的是你打开时的快照
🔒 6. 锁机制
📐 InnoDB 锁类型层次结构
全局锁
FTWRL (备份用)
表级锁
Table Lock / MDL / IS/IX
⬇ InnoDB 特有
Record Lock
锁单行记录
Gap Lock
锁间隙(不含记录)
Next-Key Lock
Record + Gap
Next-Key Lock = Record Lock + Gap Lock (左开右闭区间)
InnoDB 在 RR 级别下通过 Next-Key Lock 解决幻读
共享锁 (S) vs 排他锁 (X)
共享锁 (读锁/S)排他锁 (写锁/X)
加锁方式SELECT ... LOCK IN SHARE MODE
SELECT ... FOR SHARE (8.0)
SELECT ... FOR UPDATE
UPDATE/DELETE
兼容性S 与 S 兼容 (可多人同时读)X 与任何锁都不兼容 (独占)
类比图书馆里多人同时看同一本书借走了这本书,别人既不能看也不能借
📐 Gap Lock — 防止幻读
假设 id 列有记录:5, 10, 15, 20
Gap (-∞, 5)
5
Gap (5, 10)
10
Gap (10, 15)
← 被锁住
15
Gap (15, 20)
20
Gap (20, +∞)
事务A执行 SELECT * FROM t WHERE id=12 FOR UPDATE → 找不到 id=12 → 加 Gap Lock (10, 15)
事务B尝试 INSERT INTO t VALUES (12)被阻塞 → 防止了幻读
死锁:检测与避免
死锁产生条件
-- 事务A
BEGIN;
UPDATE account SET balance=balance-100 WHERE id=1; -- 锁住 id=1
UPDATE account SET balance=balance+100 WHERE id=2; -- 等待 id=2 ← 阻塞

-- 事务B (同时)
BEGIN;
UPDATE account SET balance=balance-100 WHERE id=2; -- 锁住 id=2
UPDATE account SET balance=balance+100 WHERE id=1; -- 等待 id=1 ← 死锁!
解决方案
  • InnoDB 自动检测死锁,回滚代价最小的事务
  • innodb_deadlock_detect = ON (默认开启)
  • SHOW ENGINE INNODB STATUS 查看最近死锁
  • 预防:按固定顺序操作(如都先锁 id 小的)
  • 预防:大事务拆小,减少持锁时间
  • 预防:innodb_lock_wait_timeout
🏭 生产案例:UPDATE 导致死锁

场景:电商扣库存,高并发下多条 UPDATE 交叉锁行导致死锁。
解决:统一按 id ASC 排序后再执行 UPDATE,避免环路等待。同时将大事务拆成小事务,缩短锁持有时间。
监控:设置告警监控 SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks',死锁频率突增立即排查。

7. 慢查询优化实战
📐 EXPLAIN 输出字段详解
列名含义重点值 / 说明
id 查询序号 越大越先执行;相同 id 从上往下
select_type 查询类型 SIMPLE(简单) / PRIMARY(主查询) / SUBQUERY / DERIVED(派生表)
type 访问类型 (最重要)
system const eq_ref ref range index ALL
性能:好 → 坏 | 至少达到 range 级别,避免 ALL
possible_keys 可能用的索引 显示 MySQL 考虑了哪些索引
key 实际用的索引 NULL = 没用索引(全表扫描)
key_len 索引使用长度 判断联合索引用了几个列
ref 与索引比较的列 const / 字段名
rows 预估扫描行数 越少越好,差距大说明统计信息不准
Extra 附加信息 Using index 覆盖索引
Using where 在存储引擎检索后过滤
Using filesort 需要额外排序(坏)
Using temporary 用了临时表(坏)
Using index condition 索引下推(ICP)
📐 type 字段 — 从最优到最差
system
const (主键/唯一)
eq_ref (JOIN主键)
ref (非唯一索引等值)
range (索引范围扫描)
index (全索引扫描)
ALL (全表扫描) ⚠
面试结论:SQL 优化目标 — type 至少达到 range,最好 ref 以上
常见优化模式
① 最左前缀原则
联合索引 (a, b, c) 的命中规则
✓ 命中
WHERE a=1
WHERE a=1 AND b=2
WHERE a=1 AND b=2 AND c=3
WHERE a=1 AND c=3 (仅用a)
✗ 不命中
WHERE b=2
WHERE c=3
WHERE b=2 AND c=3
原因:B+ 树按 a→b→c 排序,跳过 a 就无法利用索引的有序性
② 索引下推 (Index Condition Pushdown, ICP)
❌ 无 ICP (MySQL 5.6 之前)
联合索引 (name, age)
WHERE name LIKE '张%' AND age=25
存储引擎只按 name 找到所有姓张的 → 回表逐行判断 age
✓ 有 ICP
存储引擎在索引中就判断 age=25 → 只对满足条件的行回表
减少了回表次数
EXPLAIN 中 Extra 显示 Using index condition 即启用了 ICP。
③ 分页优化 — Deferred Join
-- ❌ 慢:扫描 100万 行后丢弃前 999990 行
SELECT * FROM orders ORDER BY id LIMIT 999990, 10;

-- ✓ 快:先通过索引查 id,再 JOIN 回表拿数据
SELECT t1.* FROM orders t1
INNER JOIN (
  SELECT id FROM orders ORDER BY id LIMIT 999990, 10
) t2 ON t1.id = t2.id;

-- 子查询只走索引(覆盖索引),只有10行回表
④ ORDER BY 使用索引

如果 ORDER BY 的列有索引且顺序一致,MySQL 可以利用索引的有序性直接返回,无需 filesort
EXPLAIN 中 Extra 没有 Using filesort 就说明用了索引排序。
注意:ORDER BY 的方向必须一致(不能 ASC + DESC 混用,8.0 之前)。

⑤ COUNT 优化

COUNT(*)COUNT(1) 等价,InnoDB 都会选最小的索引来遍历(不一定是全表扫描)。
COUNT(列) 不统计 NULL。
优化思路:用 Redis 维护计数、汇总表、估算 SHOW TABLE STATUS

🏭 生产案例:EXPLAIN 看到 type=ALL 扫描 500 万行

场景:SELECT * FROM user WHERE phone='13800138000',phone 列无索引。

-- EXPLAIN 结果:
-- type: ALL | rows: 5,000,000 | Extra: Using where

-- 优化 1: 加索引
ALTER TABLE user ADD INDEX idx_phone(phone);

-- 优化后 EXPLAIN:
-- type: ref | key: idx_phone | rows: 1 | Extra: NULL

-- 优化 2: 如果只需要部分列,用覆盖索引
SELECT id, phone, name FROM user WHERE phone='13800138000';
-- Extra: Using index
🔄 8. 主从复制与读写分离
📐 主从复制流程 — Binlog 复制
Master
① 执行 SQL
② 写 Redo Log
③ 写 Binlog
Binlog
→→→
推送/拉取
Slave
④ IO Thread 拉取
⑤ 写 Relay Log
⑥ SQL Thread 回放
执行
→→→
从库数据
与主库一致
三线程模型:Master Binlog Dump Thread + Slave IO Thread + Slave SQL Thread
复制模式对比
模式原理数据安全性能适用场景
异步复制 主库提交后立即返回,不等从库 主库宕机可能丢数据 最高 默认模式,对数据一致性要求不高
半同步复制 主库提交后等至少一个从库收到 Binlog 至少一个从库有数据 略低 金融场景,平衡性能与安全
组复制 (MGR) Paxos 协议,多数节点确认 强一致 最低 高可用集群,自动故障转移
读写分离架构
应用
读写请求
代理层
ProxySQL / ShardingSphere / MyCat
→ 写
Master
写操作
→ 读
Slave 1
Slave 2
Slave N
🏭 生产案例:主从延迟导致读到旧数据

场景:用户下单后立即刷新页面,查询走了从库,但 Binlog 还没同步过来 → 看不到刚下的借书单。

解决方案:
强制走主库:写操作后的 N 秒内读请求强制路由到主库
中间件感知延迟:ProxySQL 判断 Seconds_Behind_Master,延迟大时不路由到该从库
半同步复制:降低数据丢失概率,但仍有延迟
业务妥协:前端提示"数据同步中,请稍后刷新"

🎯 9. 面试题精选 35 道

点击题目展开答案。难度标签:基础 中级 高级

1
MySQL 一条 SQL 语句的执行流程是什么?
基础架构

答案:连接器(认证/分配线程) → 解析器(词法分析→语法分析→AST) → 优化器(选择执行计划/索引) → 执行器(权限检查→调用存储引擎API) → 存储引擎(读写数据) → 返回结果。

MySQL 8.0 移除了查询缓存功能,因为命中率低且维护成本高。

追问:优化器做了哪些事?→ 选择索引、决定 JOIN 顺序、子查询改写等。
2
MySQL 为什么使用 B+ 树作为索引结构?
中级索引

答案:三个核心原因:

① 磁盘 IO 友好:非叶子节点只存 key,单个 16KB 页容纳更多 key,扇出大(约 1200),三层可存 2000 万行,每次查询只需 3 次 IO。

② 范围查询高效:叶子节点通过双向链表串联,找到起点后沿链表顺序扫描。

③ 查询性能稳定:所有数据都在叶子节点,每次查询路径长度相同。

追问:为什么不用 B 树?→ B 树非叶子节点也存数据,扇出小,树更高,IO 更多。
3
什么是聚簇索引?和非聚簇索引有什么区别?
基础索引

聚簇索引:叶子节点存储完整行数据。InnoDB 的主键索引就是聚簇索引,一张表只有一个。数据物理上按主键顺序存储。

非聚簇索引(二级索引):叶子节点存储主键值。查询时先查二级索引拿到主键,再回表查聚簇索引拿到完整行数据("回表"过程)。

为什么 InnoDB 建议用自增主键?顺序插入不会导致页分裂,写入性能好。UUID 做主键会导致随机写入、频繁页分裂。

追问:如果没有主键怎么办?→ InnoDB 自动选第一个唯一非空索引,都没有则生成隐藏的 row_id。
4
什么是回表?如何避免?
中级索引

回表:通过二级索引查到主键值后,再回到聚簇索引查完整行数据的过程。每回一次表就是一次 B+ 树查找。

避免方法(覆盖索引):让查询所需的所有列都在索引中,这样二级索引就能直接返回数据,无需回表。EXPLAIN 中 Extra 显示 Using index

实战:SELECT id, name FROM user WHERE name='张三',如果 (name) 索引包含了 name 和 id,就是覆盖索引。

追问:联合索引 (a,b,c),查询 SELECT a,b WHERE a=1 能用覆盖索引吗?→ 能,a 和 b 都在索引中。
5
最左前缀原则是什么?
基础索引

答案:联合索引 (a, b, c) 按 a→b→c 的顺序构建 B+ 树。查询条件必须从最左列开始才能命中索引:

• WHERE a=1 ✓ (命中 a)
• WHERE a=1 AND b=2 ✓ (命中 a,b)
• WHERE a=1 AND b=2 AND c=3 ✓ (全部命中)
• WHERE b=2 ✗ (跳过了 a,无法利用有序性)
• WHERE a=1 AND c=3 △ (仅命中 a,c 无法利用索引)

注意:MySQL 优化器会自动调整 WHERE 条件的顺序,WHERE b=2 AND a=1 也能命中 (a,b),不需要手写顺序和索引顺序一致。

追问:WHERE a > 1 AND b=2 能用索引吗?→ a 可以走 range,但 b 无法利用索引(a 是范围查询后 b 的有序性被打破)。
6
什么是索引下推 (ICP)?
中级索引

答案:Index Condition Pushdown (MySQL 5.6+)。在联合索引中,存储引擎在索引遍历过程中就对 WHERE 条件进行过滤,减少回表次数。

例子:索引 (name, age),查询 WHERE name LIKE '张%' AND age=25

• 无 ICP:存储引擎按 name 找到所有姓张的 → 全部回表 → Server 层判断 age
• 有 ICP:存储引擎在索引中直接判断 age=25 → 只对满足条件的行回表

EXPLAIN 中 Extra 显示 Using index condition

追问:ICP 的限制?→ 只适用于二级索引,不适用于聚簇索引。子查询条件不能下推。
7
MySQL 的四种事务隔离级别分别是什么?
基础事务

从低到高:

1. Read Uncommitted:可读到未提交数据,有脏读、不可重复读、幻读。
2. Read Committed (RC):只读已提交数据,Oracle/PostgreSQL 默认。有不可重复读、幻读。
3. Repeatable Read (RR):MySQL 默认,同一事务内多次读结果一致。InnoDB 通过 MVCC + Gap Lock 基本解决幻读。
4. Serializable:完全串行化,性能最差,无并发问题。

追问:MySQL 为什么默认 RR 而不是 RC?→ 历史原因+binlog 格式(statement 格式在 RC 下可能导致主从不一致)。
8
MVCC 的实现原理是什么?
高级MVCC

核心组件:

隐藏列:每行数据有 trx_id(最后修改的事务ID) 和 roll_pointer(指向 undo log)。
Undo Log 版本链:通过 roll_pointer 串联同一行的所有历史版本。
Read View:事务开启时创建的"快照",记录当前活跃事务列表,用来判断哪个版本可见。

可见性规则:从最新版本沿版本链向前遍历,找到第一个对当前 Read View 可见的版本(trx_id 已提交且在 Read View 创建之前)。

追问:RC 和 RR 的区别?→ RC 每次 SELECT 创建新 Read View;RR 只在第一次 SELECT 创建,后续复用。
9
什么是 Gap Lock?它是如何解决幻读的?
高级

Gap Lock 锁住索引记录之间的间隙,防止其他事务在该间隙中插入新记录。

解决幻读:事务A查询 WHERE age > 20,InnoDB 会对 20 之后的间隙加 Gap Lock。事务B尝试 INSERT age=25 会被阻塞。这样事务A再次查询时行数不会变。

注意:Gap Lock 在 RC 级别下不生效(RC 没有 Gap Lock),只在 RR 级别生效。且 Gap Lock 之间不冲突(多个事务可以同时持有同一间隙的 Gap Lock),只和 Insert 冲突。

追问:Next-Key Lock 是什么?→ Record Lock + Gap Lock 的组合,锁住记录本身及其前面的间隙(左开右闭)。
10
InnoDB 和 MyISAM 的核心区别?
基础引擎

最核心的四个区别:

事务:InnoDB 支持ACID事务,MyISAM 不支持。
:InnoDB 行级锁,MyISAM 表级锁。高并发下 InnoDB 性能远超 MyISAM。
崩溃恢复:InnoDB 通过 Redo Log 自动恢复,MyISAM 需要 REPAIR TABLE。
聚簇索引:InnoDB 主键索引就是数据(聚簇),MyISAM 索引和数据分开存储。

结论:MySQL 5.7+ 默认 InnoDB,99% 场景用 InnoDB。除非纯只读日志表等极端场景才考虑 MyISAM。

追问:MyISAM 有什么优势?→ COUNT(*) 不需遍历(直接存储行数)、表压缩后占用空间小。
11
Redo Log 和 Undo Log 的区别?
中级日志

Redo Log:记录"做了什么改动",用于崩溃恢复(持久性)。WAL 机制,顺序写,先写日志再写数据文件。
Undo Log:记录"改之前的数据",用于回滚(原子性)和 MVCC 版本链。存在于系统表空间或 undo 表空间中。

类比:Redo = 施工记录(重建用),Undo = 撤销记录(回退用)。

追问:Binlog 和 Redo Log 的区别?→ Binlog 是 Server 层的逻辑日志(Statement/Row),用于主从复制和数据恢复;Redo Log 是 InnoDB 层的物理日志(某个页的某个偏移改了什么),用于崩溃恢复。
12
什么是 WAL (Write-Ahead Logging)?
中级日志

WAL 原则:先写日志(Redo Log),再写数据文件。确保即使数据页还没刷到磁盘就崩溃了,也能通过 Redo Log 恢复。

流程:修改数据 → 写入 Buffer Pool(内存) → 写 Redo Log(顺序写,很快) → 返回成功 → 后台线程择机将 Buffer Pool 脏页刷到数据文件。

为什么快?Redo Log 是顺序写(append only),每次写入只需 seek 到末尾。而数据页是随机写,需要找到对应位置。顺序写性能远高于随机写。

追问:Redo Log 写满了怎么办?→ 是一组固定文件循环写,写满时需要暂停更新,强制刷脏页腾出空间。
13
EXPLAIN 中 type 字段有哪些值?含义是什么?
基础优化

从好到差:

system:表中只有一行(MyISAM 引擎的 const 特例)
const:主键/唯一索引等值查询,最多一行匹配
eq_ref:JOIN 时对每行都通过主键/唯一索引等值查询
ref:非唯一索引等值查询,可能多行匹配
range:索引范围扫描 (BETWEEN, >, <, IN)
index:全索引扫描(比 ALL 好,但仍然扫描全部索引)
ALL:全表扫描,最差

面试结论:至少达到 range 级别,最好 ref 以上。

追问:Extra 出现 Using filesort 说明什么?→ 没法用索引排序,需要额外排序操作,通常需要优化。
14
如何优化慢查询?
中级优化

优化步骤:

1. 开启慢查询日志:slow_query_log=ON, long_query_time=1
2. 用 mysqldumpslowpt-query-digest 分析 TOP N 慢查询
3. EXPLAIN 分析执行计划,关注 type/key/rows/Extra
4. 常见优化手段:
  • 添加合适的索引(联合索引/覆盖索引)
  • 避免 SELECT *
  • 分页优化(Deferred Join)
  • 避免 %开头的 LIKE
  • 避免在索引列上使用函数
  • 小表驱动大表( EXISTS vs IN)

追问:怎么看当前正在执行的慢查询?→ SHOW PROCESSLISTinformation_schema.PROCESSLIST
15
什么是覆盖索引?
基础索引

覆盖索引:查询所需的所有列都在索引中,不需要回表。EXPLAIN 中 Extra 显示 Using index

原理:二级索引的叶子节点存储了索引列的值 + 主键值。如果查询只需要索引列和主键,就无需回聚簇索引。

实战:SELECT user_id, order_id FROM orders WHERE user_id=100,索引 (user_id) 已经包含 user_id 和 order_id(主键),自动覆盖。

追问:覆盖索引和联合索引的关系?→ 联合索引天然是覆盖索引的候选,因为索引列更多。
16
MySQL 主从复制的原理?
基础复制

流程:

1. Master 执行 SQL,写入 Binlog
2. Slave 的 IO Thread 连接 Master,请求 Binlog
3. Master 的 Dump Thread 发送 Binlog 给 Slave
4. Slave 的 IO Thread 将 Binlog 写入本地 Relay Log
5. Slave 的 SQL Thread 读取 Relay Log 并回放 SQL

Binlog 格式:Statement(记录SQL)、Row(记录行变更,推荐)、Mixed(混合)。

追问:主从延迟怎么排查?→ SHOW SLAVE STATUS 看 Seconds_Behind_Master。
17
什么是死锁?如何避免?
中级

死锁:两个事务互相持有对方需要的锁,导致双方都无法继续执行。

避免方法:

1. 按固定顺序访问表和行(如按 id 升序)
2. 大事务拆成小事务,减少锁持有时间
3. 合理使用索引,避免行锁升级为表锁
4. 设置合理的 innodb_lock_wait_timeout

InnoDB 自动检测死锁(wait-for graph 算法),回滚代价最小的事务。

追问:如何查看最近的死锁?→ SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 部分。
18
什么是 Binlog?有几种格式?
基础日志

Binlog 是 MySQL Server 层的二进制日志,记录所有DDL和DML操作,主要用于主从复制和数据恢复。

三种格式:

Statement:记录 SQL 语句。日志量小,但某些函数(如 NOW(), UUID())在从库执行结果可能不一致。
Row:记录行数据的变更(修改前/修改后)。日志量大,但数据一致性最好,推荐使用。
Mixed:默认 Statement,不确定的用 Row。

追问:为什么推荐 Row 格式?→ 数据一致性最好,不会出现主从不一致。
19
MySQL 为什么建议用自增主键?
基础索引

两个核心原因:

1. 避免页分裂:自增主键顺序插入,数据追加到 B+ 树末尾,不会导致已有页面分裂。UUID 随机插入会导致频繁页分裂,产生大量碎片。
2. 二级索引占用空间更小:二级索引叶子节点存主键值,int 占 4 字节,UUID 占 36 字节。所有二级索引都会大很多。

追问:如果一定要用非自增主键呢?→ 唯一业务键做主键可以省去一列索引,但要接受写入性能下降。折中方案:自增主键 + 业务唯一索引。
20
什么是当前读和快照读?
中级MVCC

快照读:普通 SELECT,读取 MVCC 快照中的数据,不加锁。通过 Read View + Undo Log 版本链实现。
当前读:SELECT ... FOR UPDATE / SELECT ... LOCK IN SHARE MODE / INSERT / UPDATE / DELETE,读取最新已提交数据,并加锁。

关键区别:快照读可能读到旧数据(MVCC),当前读一定读到最新数据且会加锁。

追问:RR 级别下,当前读会触发 Gap Lock 吗?→ 会的,RR 级别下当前读会加 Next-Key Lock (Record + Gap)。
21
Buffer Pool 的作用和优化?
中级架构

作用:缓存数据页和索引页,减少磁盘 IO。通常设置为物理内存的 60-80%。

优化策略:

1. innodb_buffer_pool_size:设置为可用内存的 60-80%
2. innodb_buffer_pool_instances:多实例减少锁竞争(buffer pool ≥ 1GB 时设为 2-8)
3. LRU 优化:InnoDB 对 LRU 做了改进(young 区 + old 区),防止全表扫描一次性把热数据挤出去
4. 监控 Buffer pool hit rate,应 > 95%

追问:改进的 LRU 是怎么做的?→ 将 LRU 链表分为 young(热数据5/8) 和 old(冷数据3/8)两段,新数据先进入 old 区,被访问且超过一定时间后才移入 young 区。
22
什么情况下索引会失效?
中级索引

常见索引失效场景:

1. 对索引列使用函数:WHERE YEAR(create_time)=2024 → 改为范围查询
2. 隐式类型转换:WHERE phone=13800138000 (phone 是 varchar) → 全表扫描
3. LIKE 以 % 开头:WHERE name LIKE '%张'
4. OR 条件中有无索引列:WHERE a=1 OR b=2(b无索引)
5. 不满足最左前缀原则
6. NOT IN / NOT EXISTS / != (部分情况)
7. 优化器认为全表扫描更快(数据量小或选择性差)

追问:如何判断索引是否失效?→ EXPLAIN 看type=ALL或key=NULL。
23
MySQL 的 Doublewrite Buffer 是什么?
高级架构

问题:InnoDB 页大小 16KB,而操作系统磁盘 IO 通常以 4KB 为单位。如果写 16KB 页时只写了一部分就断电,会出现"页撕裂"(部分新数据+部分旧数据)。

解决:Doublewrite Buffer。先将脏页写入共享表空间的 Doublewrite 区域(连续 2MB 空间),写成功后再写到真正的数据文件。

恢复:如果写数据文件时崩溃,从 Doublewrite 区域找到完整的页副本进行恢复,再用 Redo Log 重做。

追问:Doublewrite 会不会导致写放大?→ 会的,每个页写两次。但 Doublewrite 区域是顺序写,开销很小。也可关闭 innodb_doublewrite=0
24
如何设计一个千万级数据量的表?
高级架构

多维度的方案:

1. 索引优化:合理的联合索引、覆盖索引,避免全表扫描
2. 读写分离:主库写、从库读,缓解单库压力
3. 分表:水平分表(按用户ID取模/按时间)或垂直分表(拆冷热列)
4. 缓存:Redis 缓存热点数据,减少 DB 压力
5. 归档:冷数据迁移到历史表
6. 分区表:PARTITION BY RANGE,但限制较多
7. 中间件:ShardingSphere / MyCat 做分库分表路由

追问:分库分表后跨表查询怎么办?→ 映射表、冗余字段、ES宽表、应用层组装。
25
什么是 Change Buffer?
高级架构

Change Buffer:对非唯一二级索引的 DML 操作(INSERT/UPDATE/DELETE),如果索引页不在 Buffer Pool 中,不立即从磁盘加载,而是将变更缓存在 Change Buffer 中。等后续该页被访问时再合并(merge)。

目的:减少随机 IO。二级索引的修改通常是随机的,通过 Change Buffer 将多次修改合并为一次 IO。

适用:写多读少的场景。如果写完立刻读,Change Buffer 反而增加开销(要 merge)。

追问:为什么只适用于非唯一索引?→ 唯一索引插入时必须检查唯一性,必须把页加载到内存,Change Buffer 无意义。
26
半同步复制和异步复制的区别?
中级复制

异步复制:主库提交事务后不等从库确认,直接返回客户端。性能最好,但主库宕机可能丢失未同步的 Binlog。
半同步复制:主库提交事务后,等待至少一个从库收到 Binlog 并写入 Relay Log 后才返回。数据安全性提高,但增加延迟(通常 <1ms)。
增强半同步(After Sync):MySQL 5.7+ 默认,先等从库确认再提交事务,比之前的 After Commit 更安全。

追问:半同步复制退化成异步的条件?→ rpl_semi_sync_master_timeout 超时后自动降级。
27
SELECT 和 UPDATE 的加锁行为有什么区别?
中级

普通 SELECT:不加锁,走 MVCC 快照读。
SELECT ... FOR UPDATE:加排他锁(X锁),当前读。
SELECT ... LOCK IN SHARE MODE:加共享锁(S锁)。
UPDATE/DELETE:当前读,对匹配的行加 X 锁。在 RR 级别下还会加 Gap Lock/Next-Key Lock。

重要:如果 UPDATE/DELETE 的 WHERE 条件没有走索引,InnoDB 会对每一行加锁(实际退化为表级锁效果)!所以 UPDATE 一定要走索引。

追问:RC 级别下 UPDATE 的加锁范围?→ RC 没有 Gap Lock,只对匹配的行加 Record Lock。
28
如何排查 MySQL CPU 100% 的问题?
高级生产

排查步骤:

1. SHOW PROCESSLIST 找到耗时长的 SQL
2. EXPLAIN 分析执行计划,通常原因是全表扫描(type=ALL)
3. 检查是否有缺失索引或索引失效
4. 检查是否有大量慢查询同时执行
5. SHOW ENGINE INNODB STATUS 看是否有锁等待
6. top -H -p mysql_pid 看具体线程
7. 检查 Buffer Pool 命中率是否过低

追问:如果 SHOW PROCESSLIST 看到很多 Sending data 状态?→ "Sending data" 不只是发送数据,还包括执行器读取/处理数据的过程,可能是全表扫描。
29
什么是自适应哈希索引 (AHI)?
中级架构

AHI:InnoDB 自动监控对 Buffer Pool 中页的访问模式。如果发现某些页被频繁用等值查询访问,会自动为这些页构建内存中的哈希索引。

特点:

• 完全自动,无法手动配置
• 只对等值查询(=, IN)有效
• 由 innodb_adaptive_hash_index 控制
• 在高并发 OLTP 场景下可能提升性能,但在高并发写入场景下可能因为 AHI 的锁竞争导致性能下降

追问:什么时候应该关闭 AHI?→ 高并发写入导致 AHI 的 btr0sea.c 上的 rw-lock 成为瓶颈时。
30
分页查询很慢怎么优化?
中级优化

问题:LIMIT 1000000, 10 需要扫描 1000010 行后丢弃前 100 万行。

优化方案:

1. Deferred Join:子查询只查 id(走索引),再 JOIN 回表
2. 游标分页WHERE id > last_id ORDER BY id LIMIT 10,适合瀑布流
3. Between:如果 id 连续且有序,用 WHERE id BETWEEN 1000001 AND 1000010
4. 缓存:首页/热门页数据缓存到 Redis

追问:为什么子查询只查 id 更快?→ id 是主键(覆盖索引),不需要回表,子查询走索引扫描很快。
31
COUNT(*)、COUNT(1)、COUNT(列) 的区别?
基础优化

COUNT(*)COUNT(1):等价,统计所有行数(含 NULL)。InnoDB 会选最小的索引来遍历,不是全表扫描。
COUNT(列):统计该列非 NULL 的行数。如果列不允许 NULL 则和 COUNT(*) 等价。
COUNT(DISTINCT 列):统计该列不同值的数量。

为什么 InnoDB 不像 MyISAM 那样直接存储行数?因为 MVCC,不同事务可能看到不同的行数。

追问:大表 COUNT 太慢怎么优化?→ Redis 维护计数器 / 汇总表 / SHOW TABLE STATUS 估算。
32
MySQL 8.0 有哪些重要改进?
中级版本

1. 移除查询缓存:命中率低,维护成本高
2. 窗口函数:ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG 等
3. CTE (公用表表达式):WITH RECURSIVE 递归查询
4. 函数索引CREATE INDEX idx ON t ((UPPER(name)))
5. 降序索引:真正支持 DESC 索引(之前是假的)
6. Invisible Index:隐藏索引,用于测试
7. 更细粒度的权限控制:角色(Role)支持
8. DDL 原子性:DDL 也支持崩溃恢复

追问:窗口函数在什么场景用?→ 排名、TopN、环比/同比计算、累计求和等。
33
如何实现一个分布式唯一 ID?
中级架构

常见方案:

1. UUID:简单但无序,不适合做 MySQL 主键(页分裂)
2. Snowflake(雪花算法):64位 = 1位符号 + 41位时间戳 + 10位机器ID + 12位序列号。有序、高性能、趋势递增。需解决时钟回拨问题。
3. 数据库自增:简单但性能瓶颈。可设置 auto_increment_incrementauto_increment_offset 实现多实例。
4. Redis INCR:高性能,但依赖 Redis 可用性
5. Leaf(美团) / UidGenerator(百度):工业级方案

追问:雪花算法时钟回拨怎么办?→ 记录上次时间戳,回拨时拒绝/等待。或使用闰秒缓冲区。
34
ORDER BY 使用索引的条件是什么?
中级优化

ORDER BY 能使用索引的条件:

1. ORDER BY 的列有索引,且顺序与索引一致
2. 多列排序时,列的顺序和方向必须一致(8.0 之前不能混用 ASC/DESC)
3. 如果同时有 WHERE 和 ORDER BY,WHERE 条件也必须能使用同一索引的前缀
4. 不能跨表 JOIN 排序使用索引

判断:EXPLAIN 的 Extra 没有 Using filesort 就说明用了索引排序。

追问:Using filesort 一定慢吗?→ 不一定,如果排序数据量小(内存排序)可以接受。数据量大才会用到磁盘临时文件。
35
MySQL 如何保证数据不丢失?
高级生产

三层保障:

1. Redo Log (WAL):事务提交时先写 Redo Log 到磁盘(innodb_flush_log_at_trx_commit=1),即使数据页还没刷盘,崩溃后也能通过 Redo Log 恢复。
2. Doublewrite Buffer:防止页撕裂(部分写入)。写数据页前先写一份到 Doublewrite 区域。
3. Binlogsync_binlog=1 确保每次提交都刷到磁盘。用于主从复制和 Point-in-Time 恢复。

生产最佳配置:
innodb_flush_log_at_trx_commit=1 (每次提交都刷 Redo Log)
sync_binlog=1 (每次提交都刷 Binlog)
这两个设为 1 才能保证真正的持久性,但会损失约 10-20% 性能。

追问:设为 0 或 2 会怎样?→ 0=每秒刷(可能丢1秒数据);2=写到OS缓存(OS崩溃可能丢)。金融场景必须设为 1。