MySQL 学习笔记
目录
- 核心基础概念
- 底层数据结构
- 索引分类与物理存储
- 核心机制与高级特性
- 索引失效场景
- 性能分析与调优实战
- 设计规范与最佳实践
- 日志系统
- MySQL 三层全局架构
- 事务机制
- SQL 实战高频场景
- 表设计与字段设计规范
- 联合索引设计方法论
- 锁死锁与事务排查
- 主从复制与高可用基础
- Java 开发中的 MySQL 常见问题
- EXPLAIN 深度解读
- 慢查询日志与性能排查流程
- 字符集排序规则与存储细节
- 线上 DDL变更与大表治理
- 备份恢复与数据安全
- 分库分表与分区表基础
- MySQL 常见线上故障场景
- MySQL 8.0 重要特性
- 分布式事务与一致性设计
- 典型排障案例
- 面试高频问题速查
- 学习路线与阅读顺序
- 章节检查清单
- 术语速查表
- 故障排查速查表
1. 核心基础概念【基础认知】
1.1 什么是索引?它的本质是什么?
索引是帮助 MySQL 高效获取数据的排好序的数据结构。数据库在存储数据之外,还额外维护着满足特定查找算法的数据结构(在 InnoDB 中主要是 B+ 树)。这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。
类比理解:就像一本新华字典的"拼音目录"或"偏旁部首目录"。
- 没有目录:查一个字需要从第一页翻到最后一页(全表扫描)
- 有了目录:几次定位就能找到目标页
1.2 索引的优缺点是什么?为什么不给每个字段建索引?
优点
- 减少扫描数据量:大大减少服务器需要扫描的数据量,极大提高检索速度(降低磁盘 I/O 成本)
- 避免排序和临时表:索引天然有序,对 ORDER BY 和 GROUP BY 操作显著降低 CPU 消耗
缺点
- 占用磁盘空间:索引是真实存在于磁盘上的文件
- 降低写操作性能:每次 INSERT、UPDATE、DELETE 时,MySQL 不仅要更新数据,还要动态维护索引结构
为什么不给所有列建索引?
数据库的查询和写入是一把双刃剑。如果给所有列建索引:
- 读虽然快了,但空间开销极其庞大(索引文件可能比数据文件还大)
- 每次数据更新会引发大量索引树重新排序和节点分裂,导致写入性能崩溃
结论:建立索引需要寻找"读写平衡点"。
1.3 索引的开销体现在哪里?
空间开销(磁盘侧):索引需要存储在磁盘上(InnoDB 中数据和聚簇索引存在同一个 .ibd 文件里)。索引越多,表体积越大
时间开销(CPU/内存/磁盘IO侧):主要体现在数据变更(DML操作)时。B+ 树为保持平衡和有序,插入和删除时会频繁发生"页分裂(Page Split)"和"页合并(Page Merge)",消耗大量系统资源
2. 底层数据结构【重中之重】
2.1 二叉查找树 (BST) 与红黑树
问题:二叉树在极端情况下(如顺序插入主键)会退化成链表,查询时间复杂度从 O(log₂N) 退化成 O(N)
红黑树/AVL树:虽然通过自旋解决了"退化"问题,但本质还是"二叉"的,每个节点最多只有两个分支
致命缺陷:如果表有 1000 万条数据,树的高度会达到 20 多层。数据库中每一层通常对应一次磁盘 I/O,20 多次磁盘 I/O 的延迟极其可怕(可能需要几十到几百毫秒)
2.2 B 树 (B-Tree)
改进:把"二叉"变成"多叉"(通常叫 m 阶),树变"矮胖"了,有效降低树的高度
致命缺点(为什么 InnoDB 不用它):
数据库读取数据的基本单位是"磁盘页"(InnoDB 默认一页 16KB)。在 B 树中,非叶子节点不仅存索引 Key,还存整行真实数据(Data)。这导致一个 16KB 的页里存不了几个索引,节点数量变多,树的高度又被拉高,最终增加了磁盘 I/O 次数
2.3 B+ 树 (B+Tree) ⭐
InnoDB 的选择
特点
- 非叶子节点极其纯粹:只存索引 Key 和指针(路标),不存 Data。这样 16KB 的节点可以存下上千个路标
- 高度控制:能把高度控制在 3~4 层,支撑千万级别甚至上亿的数据量。查一次数据最多只需 3~4 次磁盘 I/O
- 叶子节点双向链表:所有数据都在叶子节点,用指针串联成双向链表。这让 B+ 树不仅单次点查快,范围查询(Range Query)更是无敌
范围查询优势:比如查 id > 100,只要找到 100 这个节点,顺着链表往后拿数据就行,不需要回溯到上层节点
2.4 Hash 索引
特点:Hash 算出的值是散列的、无序的
不支持的操作:
- 范围查询(
>,<,BETWEEN) - 排序(
ORDER BY) - 模糊查询(
LIKE 'abc%')
自适应哈希索引(AHI):InnoDB 默认用 B+ 树,但内部有自动优化机制。如果发现某个索引被非常频繁访问,会在内存中基于 B+ 树自动构建 Hash 索引来加速等值查询
3. 索引的分类与物理存储【逻辑与物理层面】
分类维度
| 分类方式 | 类型 |
|---|---|
| 按数据结构 | B+Tree 索引、Hash 索引、Full-text 全文索引 |
| 按物理存储 | 聚簇索引、非聚簇索引/二级索引 |
| 按字段特性 | 主键索引、唯一索引、普通索引、全文索引 |
| 按字段个数 | 单列索引、联合索引(复合索引) |
3.1 聚簇索引 vs 非聚簇索引
聚簇索引
在 InnoDB 中,"索引即数据,数据即索引"。整张表的数据依附在聚簇索引的 B+ 树叶子节点上
- 一张表有且只能有一个聚簇索引(通常是主键)
- 数据文件本身就是索引文件
非聚簇索引(二级索引)
存的是当前索引的字段值 + 对应的主键 ID,绝对不存整行完整数据
为什么这么设计?
为了节省空间!如果每个二级索引的叶子节点都存一份完整的行数据,硬盘会爆掉,且数据一致性极难维护
3.2 什么是回表?
假设表有 id(主键)、name(二级索引)、age 三个字段
执行:SELECT * FROM table WHERE name = '张三';
- MySQL 先去
name的二级索引树里找,找到 '张三' 的叶子节点,拿到主键ID = 5 - 但
SELECT *需要所有字段(包括age),二级索引里没有age - 于是 MySQL 拿着
ID = 5,再回到聚簇索引(主键)树里,查出整行完整数据
这个"跑回去再查一次"的动作,就叫回表
3.3 什么是覆盖索引?
执行:SELECT id, name FROM table WHERE name = '张三';
- MySQL 同样去
name的二级索引树里找,找到 '张三' 的叶子节点,拿到ID = 5 - 这时 MySQL 发现,
SELECT只要id和name,而当前这棵二级索引树的叶子节点里正好包含了id和name - MySQL 直接把数据返回,不用回表。这就是覆盖索引,极大提升查询性能!
4. 核心机制与高级特性【高阶理论】
4.1 最左前缀匹配原则 (Leftmost Prefixing)
在联合索引 (a, b, c) 的 B+ 树中:
- 数据首先按
a全局排序 - 在
a值相同的情况下,再按b局部排序 b相同再按c局部排序
为什么 b=1 AND c=2 失效?
脱离了 a,b 的值在整棵树里是无序散落的,MySQL 没法二分查找,只能放弃索引去全表扫描(或扫全索引)
关于 a=1 AND c=2:
- MySQL 5.6 之前:只有
a能走索引定位,c用不上 - MySQL 5.6 及之后:通过"索引下推"优化
4.2 索引下推 (Index Condition Pushdown, ICP) ⭐
没有 ICP 时代(MySQL 5.6 之前)
InnoDB 引擎通过索引找到所有 a=1 的叶子节点后,因为二级索引里"不能用" c 的条件:
- 只能拿着这些节点里的主键 ID,疯狂"回表"去聚簇索引查出完整行数据
- 把数据扔给 MySQL 的 Server 层
- Server 层拿到数据后,再判断
c=2是否成立
问题:如果 a=1 有 10 万条数据,就要回表 10 万次,非常慢!
有 ICP 时代(MySQL 5.6 及之后)
既然联合索引 (a, b, c) 的叶子节点里本来就存了 c 的值,MySQL 变聪明了:
- Server 层把
c=2这个条件"下推"给 InnoDB 引擎 - InnoDB 在遍历
a=1的二级索引节点时,顺手检查里面的c是不是等于 2 - 只有满足
c=2的,才去"回表"查整行
核心目的:尽早在二级索引中过滤数据,大幅减少"回表"次数
4.3 前缀索引 (Prefix Index)
对于很长的字符串(如邮箱、URL),建立完整索引太占空间。可以只取前 n 个字符建索引
选择性(Selectivity)公式
选择性 = 不重复的索引值数量 / 表记录总数SQL 公式:
SELECT COUNT(DISTINCT LEFT(column_name, n)) / COUNT(*) FROM table_name;如何确定最佳前缀长度?
- 不断调整
n的大小(如n=5, 6, 7) - 当算出来的比例越接近 1,说明区分度越高
- 找到一个"长度最短,且区分度足够高(比如 > 0.9)"的
n值
5. 索引失效场景【避坑指南】
5.1 给数据"毁容"(破坏原本模样)
- 使用函数
- 类型转换
- 隐式类型转换
- 运算表达式
5.2 蒙住左眼找东西(破坏最左前缀)
- LIKE 前缀模糊查询(如
LIKE '%张'或LIKE '%张%')
5.3 寻找"大红海"(成本计算器觉得不划算)
NOT/!=(不等于、不为空)
5.4 一颗老鼠屎坏了一锅粥(逻辑短路)
OR条件(只要有一个字段没索引,整个查询都可能失效)
6. 性能分析与调优实战【排查工具】
使用 EXPLAIN
在 SQL 前加 EXPLAIN 分析执行计划
四个核心指标
6.1 possible_keys 和 key(数据库的抉择)
| 指标 | 含义 |
|---|---|
| possible_keys | 候选人:可能用上的索引 |
| key | 最终赢家:优化器实际跑的索引 |
关键判断:如果 possible_keys 有值,但 key 是 NULL,说明数据库算了一笔账,觉得索引还要回表,不如直接全表扫描
6.2 type(性能晴雨表)
| type | 性能 | 说明 |
|---|---|---|
| const / system | 极快 | 能够快速锁定唯一一行 |
| eq_ref / ref | 极快 | 用了普通索引,精准定位 |
| range | 正常 | 用了索引做范围查询 |
| index | 勉强 | 全索引扫描 |
| ALL | 灾难 | 全表扫描 ⚠️ |
6.3 Extra(附加信息)
| Extra 值 | 含义 |
|---|---|
| Using index | 触发了覆盖索引 ✅ |
| Using index condition | 触发了索引下推 (ICP) ✅ |
| Using where | Server 层在内存中进行额外条件过滤 |
| Using filesort | ORDER BY 无法利用索引顺序,需要重新排序 ⚠️ |
| Using temporary | GROUP BY 需要临时表,消耗大 ⚠️ |
注意:Using filesort 或 Using temporary 说明你的排序/分组无法利用现有 B+ 树顺序,数据库被迫在内存或临时文件里重新排序,必须优化索引!
7. 设计规范与最佳实践【建库建表经验】
7.1 应该在哪些列上建立索引?
- 频繁作为 WHERE 查询条件的字段
- 经常需要 ORDER BY 和 GROUP BY 的字段
- 多表关联(JOIN)的 ON 字段
7.2 什么是索引的选择性(基数 Cardinality)?
定义:不重复的索引值(基数)与表记录总数的比值
选择性 = COUNT(DISTINCT column_name) / COUNT(*)取值范围:0 到 1 之间
| 选择性 | 含义 | 适合建索引 |
|---|---|---|
| 接近 1 | 值几乎都不一样(如身份证号、手机号、UUID) | ✅ 适合 |
| 接近 0 | 大量重复值(如状态码 0/1/2、性别男/女) | ❌ 不适合 |
7.3 哪些列不适合建立索引?
- 致命雷区 1:选择性极低的字段
- 致命雷区 2:频繁更新的字段
- 致命雷区 3:数据量太少的表
8. 日志系统
8.1 Undo Log【回滚/撤销日志】
定义
通俗理解:后悔药、历史快照,所属层级在 InnoDB 存储引擎特有
日志类型:逻辑日志,记录的是反向操作(如执行 INSERT 则记录 DELETE)
核心职责
保证事务的原子性(Atomicity):当事务回滚或异常中断时,通过执行反向操作将数据恢复到修改前状态
实现 MVCC(多版本并发控制):解决读写冲突,实现"写不阻塞读"
底层机制(版本链)
InnoDB 为每行数据维护两个隐藏字段:
- trx_id(事务ID)
- roll_pointer(回滚指针)
每次修改数据时,旧数据存入 Undo Log,通过回滚指针将这些历史记录串联成版本链(Version Chain)。查询时配合 Read View(读视图)算法,找到对当前事务可见的历史版本
8.2 Redo Log【重做日志】
定义
通俗理解:防灾账本,断电不丢数据的底牌,所属层级在 InnoDB 存储引擎特有
日志类型:物理日志,记录的是"在某个数据页的某个偏移量修改了什么字节"
核心职责
保证事务的持久性(Durability),实现 Crash-Safe(崩溃恢复) 能力
底层机制(WAL 技术)
WAL (Write-Ahead Logging,日志先行):
- 修改数据时,先在内存(Buffer Pool)中修改
- 立刻将物理变更追加写入 Redo Log 文件
- 后台线程异步将内存脏页刷入磁盘真实数据文件
性能奥秘:
- 直接写真实数据是极慢的随机 I/O
- 写 Redo Log 是极快的顺序 I/O(追加写)
写入方式:
- 大小固定,循环写(Circular Write)
- 内存数据安全刷盘后,对应 Redo Log 空间可被覆盖重用
8.3 Binlog【归档日志】
定义
通俗理解:监控录像带,记录一切逻辑变更
所属层级:MySQL 服务层(Server Layer)共有,任何引擎都可用
日志类型:逻辑日志,记录的是 SQL 语句的原始逻辑或基于行的变更记录
核心职责
数据备份与恢复:用于灾难后的增量数据恢复(PITR,将数据库恢复到过去的任意一秒,防删库跑路)
主从复制(Replication):主库将 Binlog 同步给从库,从库进行回放,保证主从架构的数据一致
写入方式
文件大小动态增长,追加写(Append Only),绝不覆盖历史数据
8.4 两阶段提交【2PC - Two-Phase Commit】
定义
通俗理解:将 Redo Log 和 Binlog 绑在一条船上,同生共死
核心目的:解决断电等异常情况下,Redo Log(引擎侧)与 Binlog(Server 侧)数据不一致的问题,最终保障主从集群的数据一致性
执行流程
阶段一(Prepare)
↓
InnoDB 将修改操作写入 Redo Log,状态标记为 Prepare
↓
阶段二(写 Binlog)
↓
Server 层将修改逻辑完整写入 Binlog 文件
↓
阶段三(Commit)
↓
InnoDB 接收到 Binlog 写入成功信号,将 Redo Log 状态修改为 Commit
↓
事务正式完成崩溃恢复的审判逻辑(极其重要)
当 MySQL 异常宕机重启时,根据 Redo Log 的状态判断:
| Redo Log 状态 | 判断逻辑 | 处理方式 |
|---|---|---|
| Commit | 事务已完美提交 | 直接恢复数据 |
| Prepare | 需查验 Binlog | |
| → Binlog 完整 | 从库能收到这笔修改 | 强行提交(Commit) |
| → Binlog 不完整 | 从库收不到这笔修改 | 回滚(Rollback) |
核心原则:以 Binlog 是否写入完整为最高裁决标准
9. MySQL 三层全局架构【宏观认知】
MySQL 采用客户端/服务器(C/S)架构,内部实现了极其优雅的分层与插件式设计:
┌─────────────────────────────────────┐
│ 客户端(Client) │
└──────────────┬──────────────────────┘
│ TCP/IP 连接
┌──────────────▼──────────────────────┐
│ 连接层 (Connection Layer) │ ← 网络通信、身份认证、连接管理
├─────────────────────────────────────┤
│ 服务层 (Server Layer) │ ← SQL 解析、优化、执行调度
│ - 连接管理 - 查询缓存(已移除) │
│ - 解析器 - 优化器 - 执行器 │
├─────────────────────────────────────┤
│ 存储引擎层 (Storage Engine Layer) │ ← 数据落地存储、内存缓冲、事务锁
│ - InnoDB - MyISAM - Memory ... │
└─────────────────────────────────────┘9.1 连接层【通信与安全】
当客户端与 MySQL 交互时,连接层完成以下核心动作:
1. 网络建连
- 基于 TCP/IP 协议进行三次握手建立连接
- 断开时进行四次挥手
- 高并发项目通常使用数据库连接池(Connection Pool)复用连接
2. 鉴权与上下文绑定
- 校验账号密码及 IP 白名单
- 关键特性:权限在连接建立瞬间读取并绑定在当前连接上下文中
- 后续修改权限不会影响已建立的连接(除非 FLUSH PRIVILEGES 且重新建连,或 KILL 线程)
3. 线程管理
- 为每个合法连接分配专属工作线程(Thread)
max_connections控制最大并发数- 线程缓存(Thread Cache)复用销毁的线程
wait_timeout机制剔除长时间休眠的空闲连接
9.2 服务层【四大核心组件】
1. 查询缓存 (Query Cache)
⚠️ 在 MySQL 8.0 中已被官方彻底移除
原因:
- 基于 SQL 文本的绝对哈希匹配,命中率极低
- 任何 DML 操作都会引发该表所有缓存大面积失效
- 维护成本远大于收益
现代架构通常将缓存层上浮至 Redis 等专业中间件
2. 解析器 (Parser)
(1)词法分析(Lexical Analysis)
- 将 SQL 字符串切分为独立的 Token(关键字、标识符)
(2)语法分析(Syntax Analysis)
- 校验语法规则(捕获语法错误)
- 将 Token 组装成抽象语法树 (AST)
(3)预处理器(Preprocessor/语义分析)
- 校验 AST 的语义真实性
- 查验数据字典核对表名和列名是否存在
3. 优化器 (Optimizer) ⭐
核心是 CBO (基于成本的优化器, Cost-Based Optimizer)
- 分析底层统计信息
- 计算各种索引组合及全表扫描的 I/O 与 CPU 成本
- 生成成本最低的执行计划 (Execution Plan)
注意:统计信息不是实时的。如果短时间大量修改数据,可以主动刷新统计信息或指定索引
4. 执行器 (Executor)
根据优化器生成的执行计划开展工作:
- 细粒度的操作权限校验
- 调用存储引擎的统一接口(Handler API)
- 以循环方式向存储引擎索要数据
- 在 Server 层完成额外条件过滤(如 Using where)
- 将结果集返回给客户端
9.3 存储引擎层【底层基石】
InnoDB vs MyISAM 核心对比
| 维度 | InnoDB | MyISAM |
|---|---|---|
| 事务支持 | ✅ 完整支持 ACID | ❌ 不支持 |
| 锁粒度 | 行级锁 (Row Lock) | 表级锁 |
| 崩溃恢复 | ✅ Crash-Safe | ❌ 容易损坏 |
| 物理存储 | 聚簇索引 | 非聚簇索引 |
| 外键 | ✅ 支持 | ❌ 不支持 |
自 MySQL 5.5 起,InnoDB 成为默认引擎
InnoDB 的 Buffer Pool 机制
InnoDB 并非直接对磁盘进行高频读写,而是引入缓冲池 (Buffer Pool) 技术:
- 所有增删改查优先在内存中的缓冲池完成
- 修改后的数据页被称为"脏页 (Dirty Page)"
- 后台 I/O 线程通过异步刷盘机制慢慢写入磁盘
- 通过 Redo Log 保障数据一致性和持久性
优势:在保证极高并发性能的同时,确保数据安全
10. 事务机制
10.1 事务的基础 ACID 特性
| 特性 | 全称 | 含义 |
|---|---|---|
| A | Atomicity(原子性) | 事务内的操作要么全部成功,要么全部回滚失败 |
| C | Consistency(一致性) | 数据库状态从一个合法状态变为另一个合法状态(如转账前后总金额不变) |
| I | Isolation(隔离性) | 多个并发事务同时执行时,互相不能看到对方的"半成品"或被互相干扰 |
| D | Durability(持久性) | 只要事务提交,数据绝对不会因为宕机断电而丢失 |
10.2 并发世界的三大灾难
| 问题 | 描述 | 例子 |
|---|---|---|
| 脏读 (Dirty Read) | 读到了别人还没提交(甚至最后回滚了)的修改 | 读到了根本不存在的数据 |
| 不可重复读 (Non-repeatable Read) | 同一事务内,两次读取同一行的"值"不一样 | 别人在两次读取之间执行了 UPDATE 并提交 |
| 幻读 (Phantom Read) | 同一事务内,两次查询的"数据行数"不一样 | 别人在两次读取之间执行了 INSERT/DELETE 并提交 |
10.3 四大隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 (Read Uncommitted) | ✅ 可能 | ✅ 可能 | ✅ 可能 |
| 读已提交 (Read Committed) | ❌ 避免 | ✅ 可能 | ✅ 可能 |
| 可重复读 (Repeatable Read) | ❌ 避免 | ❌ 避免 | ✅ 可能 |
| 串行化 (Serializable) | ❌ 避免 | ❌ 避免 | ❌ 避免 |
MySQL InnoDB 的默认事务隔离级别是:可重复读 (RR)
10.4 MVCC(多版本并发控制)
什么是 MVCC?
MVCC (Multi-Version Concurrency Control) 是一种多版本并发控制机制,用于解决读写冲突,实现"写不阻塞读"。
核心原理
MVCC 通过 Undo Log 的版本链 + Read View(读视图) 实现:
- 版本链:每行数据的隐藏字段
roll_pointer将所有历史版本串联成链 - Read View:事务启动时生成的一个"快照视图",记录当时活跃的事务 ID 列表
- 可见性判断:查询时根据 Read View 判断版本链中的哪个版本对当前事务可见
优势
- 读操作不加锁,不阻塞写操作
- 写操作也不阻塞读操作
- 极大提升了并发性能
10.5 锁机制
锁的分类
| 分类维度 | 类型 |
|---|---|
| 按锁粒度 | 全局锁、表级锁、行级锁 |
| 按锁类型 | 共享锁(S锁)、排他锁(X锁) |
| 按算法 | 记录锁、间隙锁、临键锁(Next-Key) |
InnoDB 的行锁
| 锁类型 | 说明 |
|---|---|
| 记录锁(Record Lock) | 锁定某一条具体记录 |
| 间隙锁(Gap Lock) | 锁定索引记录之间的间隙,防止幻读 |
| 临键锁(Next-Key Lock) | 记录锁 + 间隙锁的组合,RR 级别默认使用 |
锁的兼容性
| S锁(共享) | X锁(排他) | |
|---|---|---|
| S锁(共享) | ✅ 兼容 | ❌ 不兼容 |
| X锁(排他) | ❌ 不兼容 | ❌ 不兼容 |
11. SQL 实战高频场景
11.1 COUNT(*)、COUNT(1)、COUNT(column) 的区别
| 写法 | 含义 | 注意点 |
|---|---|---|
COUNT(*) | 统计行数 | 推荐,语义最清晰 |
COUNT(1) | 统计行数 | 大多数场景与 COUNT(*) 等价 |
COUNT(column) | 统计该列非 NULL 的数量 | 会忽略 NULL |
结论:
- 在 InnoDB 中,
COUNT(*)和COUNT(1)通常没有本质差别 - 如果你要统计表总行数,优先写
COUNT(*) - 如果业务字段允许 NULL,
COUNT(column)很容易统计少了
11.2 EXISTS vs IN
IN
适合:
- 子查询结果集较小
- 语义偏向"某字段属于某个集合"
SELECT *
FROM orders
WHERE user_id IN (
SELECT id FROM users WHERE status = 1
);EXISTS
适合:
- 外层结果集较小,但子查询可利用索引快速判断是否存在
- 语义偏向"是否存在满足条件的关联记录"
SELECT *
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);经验原则:
- 小表驱动大表、能用索引快速命中的场景,
EXISTS往往更稳 - 不要死记硬背,一定看
EXPLAIN
11.3 JOIN 的基本原则
常见 JOIN
INNER JOIN:只保留两边都匹配的数据LEFT JOIN:保留左表全部数据,右表匹配不到补 NULLRIGHT JOIN:语义对称,但工程中通常不推荐,统一改写成LEFT JOIN
工程实践原则
- 小结果集驱动大结果集
- 被驱动表的关联字段必须有索引
- 避免
SELECT * - 多表 JOIN 前先确认业务是否真的需要一次查全
11.4 分页查询与深分页问题
普通分页
SELECT id, name
FROM user
ORDER BY id
LIMIT 0, 20;深分页问题
SELECT id, name
FROM user
ORDER BY id
LIMIT 100000, 20;问题:
- MySQL 需要先扫描并丢弃前 100000 行,再取后 20 行
- 偏移量越大,性能越差
优化思路 1:基于主键游标翻页
SELECT id, name
FROM user
WHERE id > 100000
ORDER BY id
LIMIT 20;优化思路 2:先定位主键,再回表
SELECT *
FROM user
WHERE id >= (
SELECT id
FROM user
ORDER BY id
LIMIT 100000, 1
)
ORDER BY id
LIMIT 20;11.5 批量操作注意事项
- 批量
INSERT要分批,避免单次事务过大 - 批量
UPDATE/DELETE要带条件、分批提交 - 线上删除大表数据优先考虑按主键区间分段删除
- 大事务会带来:锁持有时间变长、Undo 膨胀、主从延迟增加
12. 表设计与字段设计规范
12.1 主键设计原则
推荐:
- 使用短、小、稳定、递增的主键
- 常见做法:自增 ID、雪花 ID、业务无意义 Long 型主键
不推荐:
- 使用很长的字符串做主键
- 使用频繁变更的业务字段做主键
原因:
- InnoDB 的聚簇索引叶子节点就是整行数据
- 二级索引叶子节点存的是二级索引列 + 主键值
- 主键越大,所有二级索引越大
12.2 常见字段类型选择
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 金额 | DECIMAL | 避免浮点精度问题 |
| 时间点 | DATETIME / TIMESTAMP | 看范围与时区需求 |
| 状态值 | TINYINT | 节省空间 |
| 长文本 | TEXT | 不适合频繁排序过滤 |
| 主键/关联ID | BIGINT | 兼容长期增长 |
12.3 varchar vs char
| 类型 | 特点 | 适用场景 |
|---|---|---|
CHAR | 定长 | 长度固定、更新少的短字段 |
VARCHAR | 变长 | 大多数字符串场景 |
经验:
- 手机号、身份证号这类"看起来定长"的字段,在很多系统里仍常用
VARCHAR - 因为它们本质是字符串,不参与数值计算,且未来规则可能变化
12.4 datetime vs timestamp
| 类型 | 特点 |
|---|---|
DATETIME | 范围大,和时区转换关系较弱 |
TIMESTAMP | 占用更小,受时区影响,更适合记录系统时间 |
建议:
- 业务创建时间、更新时间常用
DATETIME - 如果非常明确依赖服务器时区和自动更新时间,可考虑
TIMESTAMP
12.5 建表规范建议
- 所有表都要有主键
- 尽量设置
NOT NULL - 重要字段要有默认值,避免业务侧空指针和三值逻辑混乱
- 字段名、表名要有统一规范,不使用保留字
- 业务系统中谨慎使用外键,很多团队更倾向应用层保证一致性
12.6 为什么很多互联网项目少用外键
优点:
- 数据库层强约束,数据一致性更直观
缺点:
- 跨表耦合增强
- DDL 和扩容迁移复杂
- 高并发场景下排查问题不够灵活
结论:
- 核心不是"外键好不好"
- 而是是否符合你的系统规模、团队规范和治理能力
13. 联合索引设计方法论
13.1 联合索引不是字段的简单拼接
联合索引 (a, b, c) 设计时,顺序极其重要。它影响:
- 哪些查询能命中索引
- 是否能利用索引排序
- 是否能形成覆盖索引
13.2 设计顺序的一般原则
常见排序思路:
- 等值查询字段优先
- 区分度高的字段适当前置
- 排序字段尽量放到可以继续利用索引的位置
- 尽量让常用查询形成覆盖索引
例如业务查询:
SELECT id, user_id, status, create_time
FROM orders
WHERE user_id = ?
AND status = ?
ORDER BY create_time DESC;可优先考虑:
(user_id, status, create_time)13.3 范围条件对索引使用的影响
如果联合索引是 (a, b, c):
a = ? AND b = ? AND c = ?:通常可充分利用a = ? AND b > ? AND c = ?:c往往无法继续用于有序检索- 一旦遇到范围条件,后续列的利用能力通常会下降
13.4 覆盖索引优先于回表
很多高频查询并不需要整行字段。
例如:
SELECT id, status
FROM orders
WHERE user_id = ?
AND create_time >= ?;如果索引能覆盖查询所需字段,通常能显著减少回表成本。
13.5 不要重复建索引
例如已经有:
KEY idx_user_status_time (user_id, status, create_time)那就要重新评估是否还需要:
KEY idx_user (user_id)因为联合索引可能已经覆盖前缀能力。索引不是越多越好,重复索引只会增加写入成本和维护成本。
13.6 一个实用设计流程
- 列出系统 Top N 高频 SQL
- 按
WHERE、JOIN、ORDER BY、SELECT字段拆解 - 找出可合并的索引需求
- 用
EXPLAIN验证 - 关注慢查询日志和线上写入开销,持续调整
14. 锁死锁与事务排查
14.1 什么情况下会发生死锁
死锁本质:两个或多个事务互相等待对方持有的资源,形成环路。
典型场景:
- 事务 A 先锁记录 1,再锁记录 2
- 事务 B 先锁记录 2,再锁记录 1
- 双方都在等待,最终触发死锁检测
14.2 常见诱因
- 更新多行时访问顺序不一致
- 没走索引,行锁扩大为大量记录扫描锁定
- 大事务持锁时间过长
SELECT ... FOR UPDATE使用不当
14.3 排查思路
先看现象
- 接口超时
- 数据库锁等待变长
- 事务执行时间异常增加
再看数据库信息
SHOW ENGINE INNODB STATUS;SHOW PROCESSLIST;- 慢查询日志
- 事务执行 SQL 与索引命中情况
最后定位根因
重点看:
- 哪两条 SQL 互相等待
- 锁住了哪张表、哪个索引、哪些记录
- 是否因为缺失索引导致扫描范围过大
14.4 降低死锁概率的原则
- 多条记录更新时保持固定顺序
- 尽量通过索引精确命中记录
- 缩短事务时间,避免事务里做远程调用
- 批量操作拆小批次
- 对热点资源做好重试机制
14.5 长事务的危害
- 长时间持锁,影响并发
- Undo Log 无法及时清理
- 主从复制延迟加剧
- 回滚成本极高
经验:
- 事务中只做必要的数据库操作
- 不要把 RPC、发消息、复杂计算都塞进事务里
15. 主从复制与高可用基础
15.1 为什么需要主从复制
常见目标:
- 读写分离
- 数据备份
- 故障切换
- 扩展读取能力
15.2 主从复制的基本流程
- 主库写入 Binlog
- 从库 I/O 线程拉取主库 Binlog
- 写入从库 Relay Log
- 从库 SQL 线程 / Worker 线程回放日志
- 从库完成数据同步
15.3 主从延迟的常见原因
- 主库写入压力太大
- 从库执行能力不足
- 大事务导致回放缓慢
- 从库上存在慢 SQL、锁冲突
15.4 读写分离的注意事项
核心问题:主从延迟导致读到旧数据
常见解决思路:
- 写后读走主库
- 对一致性要求高的接口强制读主
- 业务接受最终一致时再读从
15.5 Binlog 的三种格式
| 格式 | 特点 |
|---|---|
STATEMENT | 记录 SQL 语句,体积小,但某些场景可能不够安全 |
ROW | 记录行变化,最稳,复制更准确 |
MIXED | 两者混合 |
工程上通常更偏向 ROW,因为一致性更稳。
15.6 GTID 简介
GTID(Global Transaction Identifier)可以理解为给每个事务分配一个全局唯一 ID。
作用:
- 简化主从切换
- 让复制位点管理更清晰
- 更方便故障恢复和拓扑调整
16. Java 开发中的 MySQL 常见问题
16.1 Spring 事务常见误区
1. 方法不是 public
- 默认基于代理的事务增强可能失效
2. 同类方法内部自调用
this.xxx()不经过代理,事务可能不生效
3. 异常被吞掉
- 事务感知不到异常,导致没有按预期回滚
4. 回滚规则理解错误
- 默认对运行时异常回滚
- 受检异常是否回滚要看配置
16.2 事务传播行为要理解到什么程度
Java 开发至少要理解:
REQUIRED:默认加入当前事务REQUIRES_NEW:挂起当前事务,开启新事务SUPPORTS:有事务就加入,没有就非事务执行
最常见坑:
- 外层大事务套内层
REQUIRES_NEW,结果和预期不一致 - 事务边界设计不清,导致锁持有时间过长
16.3 MyBatis / JPA 常见 SQL 坑
SELECT *滥用- 动态 SQL 生成了没有索引支撑的条件
- 批量更新没有分批
- N+1 查询问题
- 把本可一次 JOIN/批量查询的逻辑拆成循环查库
16.4 连接池相关问题
常见连接池如 HikariCP、Druid 需要关注:
- 最大连接数不能乱配
- 连接获取超时要和业务超时协调
- 长时间空闲连接要有回收机制
- 必须配置连接有效性检测
16.5 如何写出更适合 MySQL 的业务代码
- 先设计好索引,再写核心 SQL
- 事务尽量短小
- 不在循环里反复查数据库
- 避免把数据库当缓存使用
- 对热点更新、库存扣减、幂等处理要有专门方案
16.6 一个 Java 开发者的 MySQL 成长路径
初级
- 会写增删改查
- 理解索引、事务、隔离级别的基本概念
- 能看懂简单
EXPLAIN
中级
- 能设计联合索引
- 能优化慢 SQL
- 能分析锁等待、回表、覆盖索引
- 能处理分页、批量操作、读写分离等常见问题
高级
- 能从架构层面设计分库分表、复制、高可用方案
- 能结合 Java 事务模型和数据库机制做系统设计
- 能处理线上性能瓶颈、主从延迟、死锁、热点数据问题
17. EXPLAIN 深度解读
17.1 为什么只看 type 不够
很多人看 EXPLAIN 只盯着 type,这是不够的。真正要结合这些字段一起看:
idselect_typetabletypepossible_keyskeykey_lenrowsfilteredExtra
17.2 id 和执行顺序
一般规律:
id越大,越先执行id相同,按从上到下顺序执行- 如果是子查询或派生表,要结合
select_type一起判断
17.3 select_type 常见值
| 值 | 含义 |
|---|---|
SIMPLE | 简单查询,不包含子查询或 UNION |
PRIMARY | 最外层查询 |
SUBQUERY | 子查询 |
DERIVED | 派生表(通常来自 FROM 子查询) |
UNION | UNION 后面的查询 |
UNION RESULT | UNION 结果集 |
17.4 key_len 能看出什么
key_len 表示 MySQL 实际使用的索引长度。
它可以帮助你判断:
- 联合索引到底用了几列
- 是否因为范围查询、类型不匹配等原因没用满索引
- 是否可能存在隐式转换
经验:
- 如果你预期用了
(a, b, c)三列,但key_len明显偏短,就要怀疑后续列没用上
17.5 rows 和 filtered
| 字段 | 含义 |
|---|---|
rows | 预估要扫描的行数 |
filtered | 经过条件过滤后预计保留的比例 |
判断思路:
rows很大,通常意味着扫描范围大filtered很低,说明扫描了很多无效数据
17.6 Extra 里值得重点关注的值
除了前面提到的 Using index、Using filesort、Using temporary,还常见:
Using join buffer:通常说明 JOIN 没很好利用索引Impossible WHERE:条件恒不成立Backward index scan:MySQL 8.0 中可能用于倒序扫描
17.7 一套实用的 EXPLAIN 分析顺序
- 先看是否全表扫描:
type=ALL - 再看用了哪个索引:
key - 看联合索引用了几列:
key_len - 看扫描量大不大:
rows - 看有没有额外排序和临时表:
Extra - 最后结合 SQL 语义判断是否还有优化空间
18. 慢查询日志与性能排查流程
18.1 慢查询日志是什么
慢查询日志记录执行时间超过阈值的 SQL,是性能排查的第一入口。
常见价值:
- 找出最耗时 SQL
- 找出扫描行数过多的 SQL
- 找出频率高但单次不一定很慢的热点 SQL
18.2 排查慢 SQL 的标准流程
- 先确认是不是数据库慢
- 找到具体慢 SQL
- 查看执行计划
EXPLAIN - 检查索引设计
- 检查数据量、统计信息、SQL 写法
- 回归验证
18.3 判断是不是数据库问题
不要一上来就怪数据库。先排除:
- 应用线程池阻塞
- 连接池拿连接超时
- 下游 RPC 太慢
- 网络抖动
18.4 慢 SQL 常见根因
- 没有索引
- 索引建了但没用上
- 联合索引顺序不合理
SELECT *导致回表成本大- 深分页
- 排序/分组无法利用索引
- 大事务、锁等待
18.5 一个排查模板
看到一条慢 SQL 时,至少回答这几个问题:
- 它慢在扫描,还是慢在锁等待?
- 它是偶发慢,还是持续慢?
- 数据量变化前后是否有明显差异?
- 执行计划最近是否发生变化?
- 是否和上线、DDL、索引变更有关?
19. 字符集排序规则与存储细节
19.1 为什么字符集问题很容易被忽略
因为很多系统在开发初期看起来都"能用",但到了跨系统对接、模糊查询、排序、表情符号、国际化场景时就容易出问题。
19.2 推荐字符集
推荐统一使用 utf8mb4
原因:
- 能完整支持 Unicode
- 能支持 Emoji
- 避免老
utf8实际只支持最多 3 字节的问题
19.3 排序规则(Collation)是什么
排序规则决定:
- 字符串比较方式
- 大小写是否敏感
- 排序规则
- 索引比较规则
例如常见差异:
- 大小写敏感 vs 不敏感
- 重音符号是否区分
19.4 工程上的建议
- 库、表、字段字符集尽量统一
- 避免应用层和数据库层字符集不一致
- 新项目优先统一为
utf8mb4 - 涉及唯一索引时,要确认排序规则是否会影响唯一性判断
19.5 文本字段的注意点
- 长文本不适合频繁参与排序、分组、范围过滤
- 大文本字段不应该轻易出现在联合索引中
TEXT/BLOB类字段要谨慎放进热点表
20. 线上 DDL变更与大表治理
20.1 为什么 DDL 很危险
ALTER TABLE 看起来只是改个字段,但在生产环境里可能带来:
- 表锁或元数据锁
- 长时间阻塞业务 SQL
- 主从延迟
- 磁盘空间暴涨
20.2 元数据锁(MDL)
MySQL 对表结构操作会涉及 MDL。
典型问题:
- 某个长事务一直持有表的 MDL 读锁
- 你执行
ALTER TABLE ALTER等待 MDL 写锁- 后续新的查询也被堵住
- 最终看起来像"数据库突然卡死"
20.3 线上 DDL 的基本原则
- 避开业务高峰期
- 先评估表大小、索引数量、磁盘空间
- 关注是否会阻塞主业务 SQL
- 优先使用支持 Online DDL 的方式
- 必要时使用成熟工具,如
pt-online-schema-change、gh-ost
20.4 大表治理思路
大表本身不是原罪,问题在于:
- 热点访问集中
- 索引过多
- 冷热数据混在一起
- 历史数据无限堆积
常见治理手段:
- 归档历史数据
- 垂直拆分冷热字段
- 水平拆分热点表
- 减少冗余索引
20.5 删除大批量数据的正确姿势
不推荐:
DELETE FROM orders WHERE create_time < '2023-01-01';更推荐分批:
DELETE FROM orders
WHERE id >= ? AND id < ?
LIMIT 1000;核心原因:
- 缩短事务时间
- 减少锁冲突
- 降低主从延迟
- 方便失败重试
21. 备份恢复与数据安全
21.1 为什么备份不是“有就行”
很多团队有备份,但没有真正做过恢复演练。没有恢复验证的备份,不算真正可靠。
21.2 常见备份类型
| 类型 | 特点 |
|---|---|
| 全量备份 | 完整备份整个数据集,恢复简单 |
| 增量备份 | 只备份变化数据,节省空间 |
| 逻辑备份 | 如 mysqldump,可读性强,恢复较慢 |
| 物理备份 | 如 XtraBackup,恢复通常更快 |
21.3 恢复能力比备份文件更重要
你要明确自己需要哪种能力:
- 误删表能否恢复
- 误删部分数据能否恢复
- 能否恢复到某个时间点
- 故障恢复需要多久
21.4 PITR(时间点恢复)
时间点恢复通常依赖:
- 全量备份
- 备份之后的 Binlog
- 按时间点回放 Binlog
21.5 数据安全最重要的几条原则
- 生产库必须限制高危权限
DELETE/UPDATE必须带条件- 重要操作先
SELECT再执行 - 定期恢复演练
- 备份文件要有异地或多副本
22. 分库分表与分区表基础
22.1 什么时候要分库分表
不是数据量一大就必须分库分表,而是当单机数据库在这些方面遇到瓶颈时:
- 存储容量
- 单表性能
- 写入吞吐
- 热点争抢
- 运维窗口过长
22.2 分库分表带来的代价
好处:
- 扩展容量
- 分摊压力
代价:
- 跨库 JOIN 复杂
- 跨分片排序、分页、聚合困难
- 全局唯一 ID 问题
- 运维复杂度上升
22.3 常见拆分方式
垂直拆分
- 按业务域拆库
- 按字段冷热拆表
水平拆分
- 按用户 ID、订单 ID、时间等维度分片
22.4 分区表和分表不是一回事
分区表:
- 还是一张逻辑表
- MySQL 内部按规则拆成多个分区
分库分表:
- 通常需要应用层、中间件或基础设施配合
- 属于系统架构层面的拆分
22.5 分片键选择原则
好的分片键应该尽量满足:
- 分布均匀
- 访问路径稳定
- 常用查询能带上分片键
- 不容易形成热点
23. MySQL 常见线上故障场景
23.1 CPU 飙高
常见原因:
- 大量全表扫描
- 排序/分组过多
- 热点 SQL 激增
- 统计信息失真导致错误执行计划
排查方向:
- 慢 SQL
EXPLAIN- 是否新增了大查询
- 是否有索引失效
23.2 连接数打满
常见原因:
- 应用连接泄漏
- 慢 SQL 太多,连接长时间不释放
- 突发流量
- 连接池配置不合理
处理思路:
- 先看应用连接池
- 再看 MySQL 当前活跃连接
- 排查阻塞 SQL 和慢 SQL
- 必要时限流降级
23.3 突然大量锁等待
常见原因:
- 大事务
- 热点更新
- DDL 阻塞
- 批量更新/删除
23.4 主从延迟突然变大
优先排查:
- 是否有大事务
- 是否刚做了 DDL
- 从库是否有慢 SQL
- 从库机器资源是否不足
23.5 执行计划突变
常见诱因:
- 数据分布变化
- 统计信息变化
- 索引新增/删除
- MySQL 版本差异
应对思路:
- 对比变更前后执行计划
- 检查统计信息
- 检查 SQL 参数分布
- 必要时临时干预执行计划
23.6 一套通用故障处理方法
- 先止血,保护主流程
- 先确认范围,是单 SQL、单表、单库还是整站问题
- 优先保留现场信息
- 不要在没有把握时直接做高风险操作
- 事后沉淀复盘:现象、原因、处理、预防
24. MySQL 8.0 重要特性
24.1 为什么 Java 开发要关心 8.0
因为 MySQL 8.0 不只是“版本升级”,它在这些方面都有明显变化:
- 数据字典
- 默认字符集
- SQL 能力增强
- 执行计划优化
- 索引与统计信息能力
24.2 默认字符集变化
MySQL 8.0 默认字符集更偏向 utf8mb4,这对国际化、Emoji、跨系统兼容更友好。
24.3 窗口函数
窗口函数适合:
- 排名
- 分组内排序
- 环比、同比
- TopN 问题
例如:
SELECT
user_id,
amount,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rn
FROM orders;优点:
- 很多以前依赖子查询、自连接的写法可以大幅简化
24.4 CTE(公用表表达式)
WITH recent_orders AS (
SELECT *
FROM orders
WHERE create_time >= '2026-01-01'
)
SELECT *
FROM recent_orders
WHERE status = 1;适合:
- 提高复杂 SQL 可读性
- 分阶段组织查询逻辑
24.5 函数索引
在一些场景下,查询条件里会对字段做函数处理。MySQL 8.0 对表达式索引支持更友好。
价值:
- 避免因为函数处理导致索引失效
但原则不变:
- 能直接改写 SQL 避免函数,通常还是优先改 SQL
24.6 降序索引与执行计划改进
MySQL 8.0 在排序、倒序扫描、优化器能力方面更成熟。
意味着:
- 某些
ORDER BY ... DESC场景可以更好利用索引 - 但最终仍要以
EXPLAIN结果为准
24.7 隐藏索引(Invisible Index)
隐藏索引可以先让某个索引对优化器“不可见”,但不真正删除。
价值:
- 评估删除索引的影响
- 做索引治理时更安全
24.8 Java 项目升级到 8.0 要注意什么
- 驱动版本要匹配
- 字符集、时区配置要核对
- 部分 SQL 执行计划可能变化
- 保留回归测试,尤其是核心查询和报表 SQL
25. 分布式事务与一致性设计
25.1 为什么单机事务不够了
在单体应用里,一个数据库本地事务通常够用。但到了微服务或多系统协作场景,就会出现:
- 一个业务跨多个库
- 一个业务既写数据库又发消息
- 一个业务要调用多个远程服务
这时就不能只靠单机 MySQL 事务。
25.2 一致性问题的典型场景
例如下单:
- 写订单表
- 扣库存
- 发优惠券消息
- 写支付流水
只要其中一步失败,就会出现数据不一致问题。
25.3 常见方案
方案 1:强一致分布式事务
典型代表:
- 2PC
- XA
优点:
- 理论上更强一致
缺点:
- 性能差
- 实现复杂
- 对系统可用性影响大
方案 2:最终一致性
更符合互联网高并发系统。
常见做法:
- 本地消息表
- 事务消息
- 补偿机制
- 重试机制
25.4 本地消息表模式
核心思路:
- 在本地事务里同时写业务数据和消息表
- 提交成功后,由后台任务投递消息
- 消费端做好幂等
优点:
- 实现相对简单
- 和 MySQL 本地事务结合自然
25.5 幂等是最终一致性的基础
为什么必须幂等:
- 消息可能重复投递
- 接口可能超时重试
- 消费者可能重复消费
常见幂等手段:
- 唯一业务单号
- 去重表
- 状态机控制
- 唯一索引约束
25.6 补偿思想
最终一致性不是“不一致也没关系”,而是:
- 允许短时间不一致
- 必须设计恢复手段
补偿方式常见有:
- 定时扫描补单
- 死信队列重试
- 人工介入修复
25.7 Java 开发应该掌握到什么程度
至少要理解:
- 什么场景该用本地事务,什么场景不够
- 什么是最终一致性
- 为什么消息系统一定要配幂等
- 为什么不能迷信“分布式事务框架一键解决”
26. 典型排障案例
26.1 案例一:慢 SQL
现象
- 某接口 RT 从 50ms 飙到 3s
- 应用日志显示数据库查询耗时显著增加
排查
- 查慢查询日志,发现某条订单查询异常慢
EXPLAIN发现type=ALL- SQL 中按
status和create_time查询,但缺少合适联合索引
处理
- 新增
(status, create_time)联合索引 - 回归验证执行计划
复盘
- 原因不是数据库“突然变差”,而是数据量增长后原本凑合能跑的 SQL 撑不住了
26.2 案例二:死锁
现象
- 订单取消和库存回滚接口偶发失败
- 日志出现死锁异常
排查
- 查看
SHOW ENGINE INNODB STATUS - 发现两个事务更新资源顺序不一致
- 一个先改订单表再改库存表,另一个反过来
处理
- 统一资源访问顺序
- 对热点操作增加有限重试
26.3 案例三:主从延迟
现象
- 用户刚修改昵称,刷新后仍看到旧值
- 读写分离架构下,从库延迟明显
排查
- 检查复制延迟指标
- 发现主库刚执行大批量更新
- 从库回放压力增大
处理
- 写后读临时切主库
- 批量操作改成分批
- 优化从库资源和回放能力
26.4 案例四:连接数打满
现象
- 服务大量报数据库连接获取超时
- MySQL 活跃连接暴涨
排查
- 看应用连接池监控
- 发现某批接口慢 SQL 激增
- 连接长时间不释放,连接池被耗尽
处理
- 先限流
- 再处理慢 SQL
- 调整连接池阈值和超时参数
26.5 案例五:线上 DDL 阻塞
现象
- 执行
ALTER TABLE后,大量查询卡住
排查
- 检查发现前面有一个长事务未提交
- 该事务持有 MDL 读锁
- DDL 等待写锁,后续 SQL 全被堵住
处理
- 终止异常长事务
- 重新选择业务低峰执行 DDL
- 建立 DDL 前检查流程
27. 面试高频问题速查
27.1 初级高频
1. 为什么索引能加速查询
- 因为索引本质是有序数据结构,能减少扫描范围
2. 为什么 InnoDB 用 B+ 树
- 磁盘 I/O 友好
- 高度低
- 范围查询强
3. 聚簇索引和二级索引有什么区别
- 聚簇索引叶子节点存整行数据
- 二级索引叶子节点存索引列和主键值
4. 什么是回表
- 先查二级索引,再根据主键回到聚簇索引查整行
5. 什么是覆盖索引
- 查询所需字段都在索引里,直接返回,不用回表
27.2 中级高频
1. 联合索引为什么要遵循最左前缀
- 因为联合索引在 B+ 树中的排序是按列顺序构造的
2. 索引什么时候会失效
- 函数处理
- 隐式转换
- 违背最左前缀
- 选择性太差
- 优化器判断全表扫更划算
3. EXPLAIN 重点看哪些字段
typekeykey_lenrowsExtra
4. MVCC 是什么
- 基于多版本数据和可见性规则实现非阻塞读
5. 为什么会发生死锁
- 多个事务以不同顺序持有并等待彼此资源
27.3 高级高频
1. Redo Log 和 Binlog 的区别
- Redo Log:InnoDB 层,物理日志,保证持久性
- Binlog:Server 层,逻辑日志,用于复制和恢复
2. 两阶段提交为什么有必要
- 保证 Redo Log 和 Binlog 在异常情况下的一致性
3. 如何设计一个高并发订单系统的 MySQL 层
- 合理分库分表
- 热点隔离
- 索引设计
- 读写分离
- 幂等与最终一致性
4. 为什么很多互联网项目不用外键
- 不是不能用,而是更倾向应用层治理一致性,以换取扩展性和灵活性
5. 主从延迟怎么处理
- 写后读主库
- 控制大事务
- 提升从库能力
- 优化复制链路
27.4 一句话复习法
- 索引解决的是“怎么更快找到数据”
- 事务解决的是“并发下如何保证正确性”
- 日志解决的是“异常下如何保证不丢数据”
- 复制解决的是“如何扩展读和做容灾”
- 分库分表解决的是“单机数据库的容量与吞吐极限”
28. 学习路线与阅读顺序
28.1 初级 Java 开发者阅读顺序
建议顺序:
- 第 1 章 核心基础概念
- 第 2 章 底层数据结构
- 第 3 章 索引分类与物理存储
- 第 10 章 事务机制
- 第 11 章 SQL 实战高频场景
- 第 12 章 表设计与字段设计规范
- 第 16 章 Java 开发中的 MySQL 常见问题
- 第 27 章 面试高频问题速查
目标:
- 能独立写常见 SQL
- 理解索引、事务、回表、覆盖索引
- 避免最常见的开发坑
28.2 中级 Java 开发者阅读顺序
建议顺序:
- 第 4 章 核心机制与高级特性
- 第 5 章 索引失效场景
- 第 6 章 性能分析与调优实战
- 第 13 章 联合索引设计方法论
- 第 14 章 锁死锁与事务排查
- 第 17 章 EXPLAIN 深度解读
- 第 18 章 慢查询日志与性能排查流程
- 第 26 章 典型排障案例
目标:
- 能独立优化慢 SQL
- 能看懂执行计划
- 能分析锁等待、回表、索引失效
28.3 高级 Java 开发者阅读顺序
建议顺序:
- 第 8 章 日志系统
- 第 9 章 MySQL 三层全局架构
- 第 15 章 主从复制与高可用基础
- 第 20 章 线上 DDL变更与大表治理
- 第 21 章 备份恢复与数据安全
- 第 22 章 分库分表与分区表基础
- 第 23 章 MySQL 常见线上故障场景
- 第 24 章 MySQL 8.0 重要特性
- 第 25 章 分布式事务与一致性设计
目标:
- 能参与数据库架构设计
- 能处理线上故障与容量扩展
- 能把 MySQL 机制和系统设计连起来
28.4 一条推荐的总路线
先建立概念框架,再补 SQL 实战,然后学执行计划和索引设计,最后进入线上治理、架构扩展和一致性设计。
29. 章节检查清单
29.1 索引检查清单
做索引设计前,至少问自己:
- 这条 SQL 的高频过滤条件是什么
- 有没有排序、分组、分页需求
- 能否形成联合索引
- 能否形成覆盖索引
- 会不会和现有索引重复
- 写入成本是否能接受
29.2 SQL 优化检查清单
优化一条慢 SQL 时,至少检查:
- 有没有
SELECT * - 是否命中了正确索引
- 是否存在函数、隐式转换、OR 等问题
- 是否有深分页
- 是否触发
Using filesort或Using temporary - 是慢在扫描还是慢在锁等待
29.3 事务检查清单
设计事务时,至少确认:
- 事务边界是否足够小
- 事务里是否混入 RPC、发消息、复杂计算
- 是否可能形成长事务
- 是否有死锁风险
- 是否需要幂等和重试
29.4 建表检查清单
新建表前,至少确认:
- 主键是否合理
- 字段类型是否合适
- 是否设置了
NOT NULL和默认值 - 是否提前设计了核心索引
- 是否考虑未来数据量增长
29.5 线上变更检查清单
做 DDL 或大批量数据操作前,至少确认:
- 是否在业务低峰
- 是否评估过表大小与影响范围
- 是否会造成 MDL 阻塞
- 是否会导致主从延迟
- 是否有回滚和兜底方案
30. 术语速查表
| 术语 | 一句话解释 |
|---|---|
| 聚簇索引 | 叶子节点存整行数据的索引组织方式 |
| 二级索引 | 叶子节点存索引列和主键值的索引 |
| 回表 | 先查二级索引,再回主键索引查整行 |
| 覆盖索引 | 查询字段都在索引里,不用回表 |
| 最左前缀 | 联合索引从最左列开始才能更好利用 |
| ICP | 索引下推,尽量在索引层过滤数据 |
| MVCC | 多版本并发控制,核心目标是非阻塞读 |
| Read View | 决定当前事务能看到哪些版本的可见性快照 |
| Undo Log | 用于回滚和 MVCC 的历史版本记录 |
| Redo Log | 用于崩溃恢复的物理日志 |
| Binlog | 用于复制和恢复的逻辑日志 |
| WAL | 先写日志再刷数据页的机制 |
| Gap Lock | 锁定索引间隙,防止幻读 |
| Next-Key Lock | 记录锁 + 间隙锁 |
| AHI | InnoDB 的自适应哈希索引机制 |
| MDL | 元数据锁,涉及表结构操作时很关键 |
| GTID | 全局事务 ID,方便复制和切换 |
| PITR | 时间点恢复,恢复到某个具体时刻 |
| CTE | 公用表表达式,提升复杂 SQL 可读性 |
| 幂等 | 同一个请求执行多次,结果保持一致 |
30.1 最容易混淆的几组概念
COUNT(*) vs COUNT(column)
COUNT(*)统计总行数COUNT(column)只统计该列非 NULL 数量
DATETIME vs TIMESTAMP
DATETIME更偏业务时间表达TIMESTAMP更偏系统时间与时区处理
分区表 vs 分库分表
- 分区表还是一张逻辑表
- 分库分表是系统层面的拆分
31. 故障排查速查表
31.1 慢 SQL
优先看:
- 慢查询日志
EXPLAIN- 索引是否命中
- 是否深分页
- 是否排序/分组引发额外开销
31.2 锁等待
优先看:
- 是否有大事务
- 是否有热点更新
- 是否存在缺失索引
SHOW ENGINE INNODB STATUS- 当前阻塞链路
31.3 连接数打满
优先看:
- 连接池配置
- 活跃 SQL 是否变慢
- 是否有连接泄漏
- 是否有阻塞 SQL
31.4 主从延迟
优先看:
- 是否有大事务
- 是否有 DDL
- 从库资源是否足够
- 从库是否有慢 SQL
31.5 DDL 阻塞
优先看:
- 是否有长事务
- 是否被 MDL 卡住
- 是否在高峰期执行
- 是否可改用在线变更工具
31.6 数据误删
优先做:
- 先止损,避免继续写入扩大影响
- 确认影响范围
- 判断是否能通过备份 + Binlog 做 PITR
- 保留现场,谨慎操作
31.7 一个统一排查框架
所有故障都可以先按这 5 步走:
- 先看现象
- 再定范围
- 保留现场
- 先止血再根因分析
- 复盘并沉淀为 checklist
📌 总结
MySQL 学习的核心路径:
- 索引原理 → B+ 树结构、聚簇/非聚簇索引、回表与覆盖索引
- 执行分析 → EXPLAIN、最左前缀、索引下推、索引失效场景
- 事务机制 → ACID、隔离级别、MVCC、锁机制
- 日志系统 → Undo Log、Redo Log、Binlog、两阶段提交
- 架构认知 → 三层架构、连接层、服务层、存储引擎层
- 工程实战 → SQL 写法、分页、JOIN、批量操作、建表规范
- 性能优化 → 联合索引设计、覆盖索引、慢 SQL 分析、锁与死锁排查
- 架构进阶 → 主从复制、读写分离、高可用、GTID
- Java 结合 → Spring 事务、ORM 常见问题、连接池配置、业务代码规范
- 排查工具 → EXPLAIN 全字段、慢查询日志、性能定位方法
- 生产治理 → 字符集、线上 DDL、大表治理、备份恢复
- 架构扩展 → 分库分表、分区表、常见线上故障处理
- 版本进阶 → MySQL 8.0 新能力、执行计划变化、函数索引、窗口函数
- 一致性设计 → 分布式事务、消息最终一致性、幂等、补偿
- 案例与面试 → 典型故障案例、面试高频题、速查复习
- 知识管理 → 学习路线、检查清单、术语表、故障速查表
持续更新中...