链接
1.介绍
优化方面
2.如何定位慢查询
方案1:开源工具
追踪功能
方案2:Mysql自带慢日志
将慢sql记录到日志文件中
总结
生产一般不开启 损耗mysql性能
3.SQL执行慢-分析方法
获取查询语句信息
type字段
总结
4.索引概念+底层数据结构
B+树是B树的优化
2:是因为数据都在叶子节点,查找路径差不多所以稳定
3:双向指针连接,所以范围查询方便
5.聚簇索引,非聚簇索引,回表查询
回表查询(二次)
总结
单独给字段创建的索引,一般多个
6.覆盖索引,超大分页优化
基于二级索引
二级索引的叶子,一定会包含索引键 (name) + 主键 id
总结1
超大分页-覆盖索引+子查询优化
总结2
先分页查询获取表中的id,然后对表的id进行排序,能筛选出分页之后的id集合,
因为id是覆盖索引,操作id效率高,最后通过id集合去到原来的表中做关联查询
效率就可以得到提升
7.索引创建的原则
总结
一般是数据量大的基础上,然后还需要查询
三个复杂查询
联合索引
索引数量
8.索引失效情况
不满足最左前缀
不正常情况,只是命中name
范围索引右边的列使用索引
在‘索引列’进行运算
字符串不加单引号
总结
最左前缀,模糊,范围,字符串计算和单引号
9.SQL优化经验
表设计的优化
SQL语句的优化
主从复制、读写分离
总结
事务相关
1.事务特性
2.并发事务带来的问题、隔离级别
并发事务带来的问题
这个幻读的情况 是在解决了不可重复读问题的情况下 就是可以重复读取了 且重复读取时一致的数据
假设表里只有 id=1、2 两个账户,事务 A 要统计余额 > 0 的账户总数:
- 事务 A 第一次执行:
SELECT * FROM account WHERE money > 0,查到 2 条数据;- 此时事务 B 新开事务,执行
INSERT INTO account(id,money) VALUES(3,1000),并commit 提交;- 事务 A 没有结束,再次执行一模一样的
SELECT * FROM account WHERE money > 0,这次查到 3 条;事务 A 在同一个事务里,同样的查询语句,前后结果行数变多了,仿佛凭空多出一行,这就是幻读。
反向场景:事务 B 删除了一条符合条件的数据并提交,A 第二次查询少了一行,也属于幻读。
即:
- UPDATE 改已有行 → 不可重复读
- INSERT/DELETE 增减行 → 幻读
| 问题 | 操作对象 | 触发 SQL 类型 | 现象 |
|---|---|---|---|
| 不可重复读 | 已存在的单行数据 | 其他事务 UPDATE 更新 | 同一行的值前后不一样 |
| 幻读 | 整个范围的数据集 | 其他事务 INSERT/DELETE 增删行 | 查询到的总行数变多 / 变少,出现 / 消失新记录 |
解决以上问题:事务隔离
隔离级别
串行化是强制加锁串行执行解决所有并发问题,一般不用,性能差
总结
并发事务带来的问题
| 问题 | 一句话 | 根因 | 解决思路 | 举例 |
|---|---|---|---|---|
| 脏读 | 读到了别人还没提交的中间数据 | 读没有隔离,能读到未提交数据 | 只允许读已提交 | 事务A改 salary=9000(未提交),事务B读到 9000,A回滚了,B读到的是脏数据 |
| 不可重复读 | 同一条数据,两次读结果不一致(被别人 UPDATE) | 读未加锁,两次读之间被别人改了并提交 | 事务内读用快照(MVCC) | 事务A第一次读 salary=5000,事务B改成 8000 并提交,事务A再读变成 8000 |
| 幻读 | 同一个范围查询,两次读行数不一致(被别人 INSERT/DELETE) | 没加间隙锁,范围查询被别人插了或删了并提交 | MVCC + Next-Key Lock | 事务A查 age>30 有 5 条,事务B插入一条并提交,事务A再查变成 6 条 |
| 丢失更新 | 两个事务同时修改同一行,一个的更新被覆盖 | 写没加锁,后提交的覆盖了先提交的 | SELECT ... FOR UPDATE 加行锁 | 事务A读 salary=5000,事务B也读 5000,A改成 6000 提交,B改成 7000 提交,A 的更新丢失 |
隔离级别与问题的对应关系
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 怎么做到的 |
|---|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ✅ 存在 | ✅ 存在 | ✅ 存在 | 什么都不做,直接读最新值 |
| READ COMMITTED(读已提交) | ❌ 解决 | ✅ 存在 | ✅ 存在 | 每次 SELECT 生成新快照 |
| REPEATABLE READ(可重复读)⭐ | ❌ 解决 | ❌ 解决 | ⚠️ InnoDB 通过 MVCC + 间隙锁基本解决 | 事务第一次 SELECT 生成快照,后续复用;当前读加 Next-Key Lock |
| SERIALIZABLE(串行化) | ❌ 解决 | ❌ 解决 | ❌ 解决 | 所有读加共享锁,写加排他锁,事务完全串行 |
MySQL 默认隔离级别:REPEATABLE READ
RC 和 RR 的核心区别(最关键)
RC(READ COMMITTED)—— 每次读都看最新已提交
SELECT ① → 生成快照 A 别人提交了新数据 SELECT ② → 生成快照 B(能看到新数据)← 所以会有不可重复读RR(REPEATABLE READ)—— 进事务时拍照,之后只看这张照片
SELECT ① → 生成快照 A 别人提交了新数据 SELECT ② → 还是快照 A(看不到新数据)← 所以解决了不可重复读⚠️ 标准 SQL 中 RR 级别不解决幻读,但 InnoDB 用MVCC(快照读)+ Next-Key Lock(当前读)两种机制配合,基本消除了幻读。
一句话速记口诀
脏读是读了没提交的,不可重复读是读了被改的,幻读是读了被插的,丢失更新是写了被覆盖的。RC每次读最新快照解决脏读,RR锁住第一次快照解决不可重复读,InnoDB再加间隙锁解决幻读,串行化全部排队。
3.undo log和redo log区别
都是mysql的日志文件
redo log
内存操作完的数据没同步到磁盘,属于脏页,此时如果服务器宕机,会导致数据同步失败,内存的数据可能会消失,违背事务持久化的特性
mysql引入了undo log日志文件
机制:先写日志,延迟刷磁盘数据文件
undo log
总结
| undo log | redo log | |
|---|---|---|
| 一句话 | 回滚日志,记录修改前的旧数据 | 重做日志,记录修改后的新值 |
| 解决问题 | 事务回滚 + MVCC(多版本并发控制) | 崩溃恢复(crash recovery) |
| 记录内容 | 修改前的数据(旧值) | 修改后的数据(新值) |
| 作用阶段 | 事务执行过程中 | 事务执行过程中 |
| 保证特性 | 原子性(Atomicity) | 持久性(Durability) |
| 生命周期 | 事务提交后可被清理(purge 线程) | 数据刷盘后可被覆盖(循环写入) |
| 存储位置 | 共享表空间(ibdata)或独立 undo 表空间 | redo log 文件(ib_logfile0 / ib_logfile1) |
| 文件大小 | 动态增长 | 固定大小,循环写入 |
| 关联机制 | MVCC(通过版本链提供一致性快照) | WAL(先写日志再写磁盘) |
| 回滚应用 | ROLLBACK 时根据 undo log 恢复旧值 | — |
| 崩溃恢复 | — | 重启后重放 redo log 恢复未刷盘的数据 |
核心记忆口诀:
undo 存旧值,用于回滚 + MVCC;redo 存新值,用于崩溃恢复。一个是往回看(回到过去),一个是往前追(恢复未来)。
细节:
redo log 细节
核心思想:WAL
Write-Ahead Logging,先写日志,再写磁盘。
为什么?因为随机 IO 慢。直接把修改写到数据页是随机写,而写 redo log 是顺序写,快得多。
组成
- redo log buffer(内存)
- redo log file(磁盘,ib_logfile0、ib_logfile1...)
写入时机
事务执行过程中就开始写 buffer,不是提交时才写。
刷盘策略(innodb_flush_log_at_trx_commit)
这个参数控制 buffer 什么时候真正写到磁盘:
| 值 | 行为 | 安全性 |
|---|---|---|
| 0 | 每秒由后台线程刷盘 | 丢1秒数据 |
| 1 | 每次提交时刷盘 | 不丢数据(最安全) |
| 2 | 提交时写到 OS 缓存,每秒刷盘 | OS 挂才丢 |
默认是 1,面试直接说就行。
特点
- 物理日志:记录的是"某页某偏移处改成什么值"
- 循环写:写满后回到开头覆盖旧的
- 由checkpoint机制控制"哪些部分已经可以被覆盖"——就是对应数据页已经刷到磁盘了
undo log 细节
作用有两个,不只是回滚
- 1.事务回滚
- 2.MVCC(多版本并发控制)
两种类型
| 类型 | 对应操作 | 何时清理 |
|---|---|---|
| insert undo log | INSERT 操作 | 事务提交后立刻可以删 |
| update undo log | UPDATE / DELETE 操作 | 不能立刻删,要等没有事务再读旧版本 |
为什么 insert 的可以立刻删?因为插入的数据其他事务根本没读到过,不需要旧版本。
update/delete 的为什么不能删?因为其他事务可能正在通过 MVCC 读那个旧版本。
旧版本链
每行数据被修改时,旧版本通过 undo log 串成一条链:
当前数据 → undo log(版本3) → undo log(版本2) → undo log(版本1)事务读数据时,沿着这条链找到自己"看得见"的那个版本。
判断依据:ReadView
ReadView 里有四个关键东西:
- m_ids:生成 ReadView 时,还在活跃的事务 ID 列表
- min_trx_id:活跃事务中最小的 ID
- max_trx_id:下一个要分配的事务 ID(当前最大 + 1)
- creator_trx_id:创建这个 ReadView 的事务 ID
判断规则:
- 1.数据的 trx_id等于creator_trx_id → 自己改的,可见
- 2.数据的 trx_id小于min_trx_id → 在 ReadView 之前就提交了,可见
- 3.数据的 trx_id大于等于max_trx_id → 在 ReadView 之后才开始的事务,不可见
- 4.数据的 trx_id在 min 和 max 之间→ 看是否在 m_ids 里:
- 在 → 没提交,不可见
- 不在 → 已提交,可见
不可见就沿 undo log 链继续找上一个版本。
Purge 线程
后台有个 purge 线程专门负责清理已经没有任何事务需要的 undo log。
对比总结
| redo log | undo log | |
|---|---|---|
| 类型 | 物理日志 | 逻辑日志 |
| 内容 | 数据页修改后 | 数据修改前的逆操作 |
| 保证 | 持久性(D) | 原子性(A)+ MVCC |
| 写入时机 | 事务执行中 | 事务执行中 |
| 什么时候不删 | checkpoint 推进后可覆盖 | 无事务读旧版本后 purge 清理 |
| 存储位置 | 磁盘上的 ib_logfile | 表空间里的回滚段(rollback segment) |
面试能说清 WAL、刷盘策略、undo log 的两种类型、MVCC 的 ReadView 四个判断规则,这块就稳了。
4.mvcc多版本并发控制
实现快照读,读不阻塞写、写不阻塞读,高并发性能强,同时解决脏读、不可重复读;配合间隙锁在 RR 隔离级别消除幻读
ReadView,它只管快照读。当前读根本不走 ReadView,走的是另一条路——加锁
概述:
一个版本的 trx_id 比快照里未提交事务的最小ID还小,或者不在未提交事务列表里,就可见;否则就沿 undo log 往回找。
总结:
MVCC 速记总结
一句话
读旧版本不加锁,写最新版本才加锁。
依赖三件套
隐藏列 undo log版本链 ReadView (trx_id + (旧版本串成链, (判断哪些版本 roll_ptr) 回滚用) 对我可见)版本链长什么样
当前行 [TRX_ID=4] ──→ undo [TRX_ID=3] ──→ undo [TRX_ID=2] ──→ undo [TRX_ID=1] 最新版本 旧版本 更旧 最早每次修改:旧值存 undo → 当前行指向新值
ReadView 四个字段
| 字段 | 一句话 |
|---|---|
| m_ids | 此刻还没提交的事务有哪些 |
| min_trx_id | 这些活跃事务里最小的 ID |
| max_trx_id | 下一个要分配的新事务 ID |
| creator_trx_id | 我自己的 ID |
可见性判断(按顺序)
trx_id == 我自己? → 可见 ✅(自己改的) trx_id < min? → 可见 ✅(早就提交了) trx_id >= max? → 不可见 ❌(是未来事务) trx_id 在 min 和 max 之间? ├─ 在 m_ids 里? → 不可见 ❌(没提交) └─ 不在 m_ids 里? → 可见 ✅(已经提交了)不可见就沿 roll_ptr 往回找,找到可见版本为止。
RC vs RR 的唯一区别
RC(读已提交):每次 SELECT → 新建 ReadView RR(可重复读):第一次 SELECT → 建 ReadView,后面复用这就是 RR 能"可重复读"的原因——不是加锁,是复用快照。
快照读 vs 当前读
| 快照读(走 MVCC) | 当前读(不走 MVCC) |
|---|---|
| 普通 SELECT | SELECT ... FOR UPDATE(排它锁) |
| SELECT ... LOCK IN SHARE MODE(共享锁) | |
| INSERT / UPDATE / DELETE(排它锁) |
背下来的关键口诀
存旧值靠 undo log,判断能不能看靠 ReadView,RC 每次建 RR 建一次,不可见就沿链往回追。
主从同步原理
总结
分库分表
目前主库和从库存的数据一样,那此时是解决访问的压力,但读写分开来进行访问时解决不了海量数据存储的问题
分库分表策略
垂直拆分
就是按模块分
水平拆分
分摊单机压力,缩小单次检索的数据范围,路由规则很多
| 拆分类型 | 拆分依据 | 部署位置 | 表结构特点 | 核心解决问题 |
|---|---|---|---|---|
| 垂直分库 | 业务功能模块 | 多个独立数据库实例 | 各库存放不同种类数据表 | 业务解耦,不同业务分摊服务器压力 |
| 水平分库 | 分片路由(取模/ 范围 / 哈希等) | 多个独立数据库实例 | 各个库内部的表结构完全一致 | 突破单机数据库并发、硬件瓶颈 |
| 垂直分表 | 字段冷热、字段大小 | 同一个数据库当中 | 拆分出多张字段不同的子表 | 单表字段过多、大字段拖慢查询的问题 |
| 水平分表 | 分片路由(取模/ 范围 / 哈希等) | 同一个数据库当中 | 拆分出多张结构一致的子表 | 单张表行数太多、索引臃肿的问题 |
总结
| 垂直分表 | 垂直分库 | 水平分表 | 水平分库 | |
|---|---|---|---|---|
| 一句话 | 把一张大表的列拆成多张小表 | 不同业务的表拆到不同数据库 | 同一张表的数据按行拆到多张表 | 同一张表的数据按行拆到多个数据库 |
| 拆的是什么 | 列(字段) | 整个业务模块 | 行(记录) | 行(记录) |
| 拆分前 | 1 张宽表 | 1 个库放所有业务表 | 1 张大表 | 1 张大表在 1 个库 |
| 拆分后 | 多张窄表(同库) | 多个库(不同机器) | 多张结构相同的表(同库) | 多个库,每个库结构相同(不同机器) |
| 核心驱动 | 单表字段太多,冷热数据分离 | 不同业务耦合在一起,互相影响 | 单表数据量太大(千万/亿级) | 单库存不下或扛不住读写压力 |
| 关联方式 | 同库直接 JOIN | 应用层代码分别查询后组装 | 同库直接查/UNION ALL | 中间件(ShardingSphere)路由 + 聚合 |
| 举例 | user 表拆成 user_base + user_profile | 用户库 / 订单库 / 商品库拆开 | orders 表拆成 orders_0 ~ orders_3 | orders_0 在机器A,orders_1 在机器B |
| 分片键 | 不需要 | 不需要 | 需要(如 user_id % 4) | 需要(如 user_id % 2) |