explain
type
最关键的指标,反映了 MySQL 查找数据的方式
system / const 主键或唯一索引的等值查询,速度最快
eq_ref / ref 多表关联(主键/唯一索引关联)或非唯一索引等值查询
range 索引范围扫描
index 全索引扫描
all 全表扫描
possible_keys 与 key
possible_keys 表示可能用到的索引
key 表示实际用到的索引
如果 possible_keys 有值而 key 为 NULL,说明 MySQL 觉得走索引成本更高(比如回表成本过大),这时需要考虑建立更合适的联合索引。
rows 与 filtered
rows 是优化器估计需要读取的行数,这个值越小越好。
filtered 表示返回行数占需读取行数的百分比。如果 rows 很大,但 filtered 只有 1% 甚至更低,说明 MySQL 扫了大量无用数据,通常需要通过联合索引把过滤条件下推。
extra
Using filesort:说明无法利用索引完成排序,使用了 CPU 内存/磁盘进行外部排序,极耗资源。优化手段:将排序字段加入联合索引。
Using temporary:使用了临时表(常见于 group by 或 distinct)。优化手段:同样需要索引优化。
Using index:好现象,说明触发了覆盖索引,不需要回表。
Using index condition:触发了 ICP(索引下推),在索引遍历时就过滤掉了不满足条件的记录,减少了回表次数。
EXPLAIN ANALYZE
它会真正执行这条 SQL,并输出一个树状结构的执行计划,里面包含了实际的执行时间(Actual time)、实际返回的行数(Actual rows)和循环次数。
通过对比优化器的估算 rows 和实际 rows,如果差距过大,说明索引统计信息过期了,可以通过 ANALYZE TABLE 来更新统计信息。
MySQL事务
事务特性
原子性:当前事务操作要么同时成功要么同时失败,原子性通过 undo 日志实现。
隔离性:多个事务并发执行操作相同数据时,他们内部的操作不能相互影响
持久性:一旦提交了事务,它对数据库的改变是持久性的。持久性由 redo 日志实现
一致性:事务的执行必须使数据库从一个一致性状态,转变到另一个一致性状态
事务隔离级别
read uncommit(读未提交):脏读
事务A读到了事务B尚未提交的数据,如果程序出现问题导致了事务A的回滚,但此时事务B已经拿到了数据开始进行其他逻辑
read commit(读已提交):不可重复读
事务A和事务B同时操作同一条数据时,当一个事务未提交之前,另外一个事务的查询语句读取的数据还是修改之前的数据
如果多个事务对一条数据进行了修改,另外一个事务在未提交前在事务内多次查询获取到的数据可能不一致
repeatable read(可重复读):幻读
在一个事务中查询了某条数据之后,无论其他的事务如何对这条数据进行修改。在这个事务里的查询结果始终都会是第一次查询的结果。
这个状态是对于整个数据库的数据生效的,在一个事务开始时会对这个数据库生成一份快照数据,后续其他事务如果修改了数据,那么在后续的查询过程中的结果还是修改之前的数据
幻读:某个事务在开启后,另一个事务对数据库进行了新增操作。那么这个事务的查询结果是没有这条数据的,但是如果在这个事务中执行了 update 操作,那么再进行查询就可以查询出刚刚新增的数据
serializable(串行):解决上面所有问题,包括脏写
将所有的事务全部串行执行,当一个事物没有提交时其他所有事务都无法执行
读锁与写锁
读锁
在查询语句的最后添加 lock in share mode
serializable 隔离级别会自动的为查询语句添加读锁
写锁
所有的事务隔离级别都会对 insert update delete 添加写锁
事务优化原则
将查询操作放在事务外,但是需要 RR 事务级别的查询除外
避免在一个事务中进行太多次的数据操作
更新等涉及加锁的操作尽可能放在事务靠后的位置,如果有多个事务同时对数据进行更新。将更新操作放在事务靠后的位置可以减少其他事务等待的时间
锁机制
乐观锁与悲观锁
悲观锁
适用场景: 写多读少、并发竞争极其激烈、或者绝对不允许产生脏数据
数据被并发修改的概率极高,所以每次查询或修改数据时,都默认其他事务会修改它。因此,在整个数据处理流程中,必须先把数据锁死
依赖 MySQL 数据库原生的锁机制(如行锁、排他锁)
实现方式
在查询语句后加上 FOR UPDATE 其他事务如果想修改或用 FOR UPDATE 查这一行,必须排队等待
在更新语句中通过运算符对数据进行修改:
UPDATE products SET stock = stock - 1 WHERE id = 10;
在一个事务中的 SELECT 语句后面加上 LOCK IN SHARE MODE 时,不允许其他数据进行修改
乐观锁
适用场景:读多写少、并发冲突较少的场景
数据并发修改的概率很低,所以查询、修改数据的时候都不加锁。只有在最后一步提交更新时,才会去检查在当前事务处理数据的过程中有没有其他事务对这条数据进行了修改
数据库不加锁,完全由开发人员在业务代码和 SQL 语句中通过版本号(version)或时间戳(timestamp)来实现。
表锁
表锁主要分为两大类:读锁、写锁
读锁:共享表锁,所有人都能读,所有人都不能写
写锁:排他表锁,只有持锁线程能读,只有持锁线程能写
意向锁
当一张表中已经存在了行锁后,会对这张表添加一个意向锁。有了意向锁的表不能添加表锁。
间隙锁
间隙锁只在可重复读(RR)级别下生效,并且只有在执行 当前读(加锁查询,如 FOR UPDATE 或 LOCK IN SHARE MODE / FOR SHARE)且扫描到了不存在的区间时才会触发。
间隙锁(以及 InnoDB 的所有行级锁)是完全基于索引(Index)进行锁定的。如果 WHERE 条件中没有索引,MySQL 根本无法建立精准的间隙,它为了确保安全,会直接升级为“全表锁”的效果。
假设 age 字段上建了普通索引,索引树里有 [5, 9, 11, 15] 这几个节点。
1. MySQL 会先去 age 索引树上查找 7
2. 发现没有 7,但是找到了它两边的邻居 5 和 9
3. 于是,MySQL 精准地在 age 索引树的 5 到 9 之间的这个间隙上挂了一把锁
4. 此时,插入 age = 6 会被堵住;但由于锁很精准,插入 age = 20 或者操作其他和 (5, 9) 无关的行,完全不受影响。
问题总结
为什么 MySQL 选择 B+树,而不是二叉树、红黑树或 B 树?
对比二叉树/红黑树: 当数据量特别大时,树的高度(Height)会非常高。因为每次寻找节点都对应一次磁盘 I/O,树太高会导致 I/O 次数过多,
B 树的每个节点(包括叶子和非叶子节点)都存储 key(索引键)和 data(实际数据/行指针)。这导致每个节点能容纳的 key 数量变少,树会变高。
B+ 树的叶子节点会使用双向链表进行连接,这会提升范围查询时的效率
什么是聚簇索引和二级索引?
聚簇索引(主键索引): 叶子节点存放的是整行完整的实际数据。一张表必须有且仅有一个聚簇索引(默认是主键;没主键选第一个唯一非空索引;再没有 InnoDB 会隐式生成一个 row_id)。
二级索引(辅助索引): 叶子节点存放的是索引键位 + 主键值。例如你给 age 字段建索引,B+树的叶子节点存的就是 (age, id)。
什么是回表?如何避免回表?
回表: 当你通过二级索引查询数据时(例如 SELECT * FROM user WHERE age = 18),MySQL 会先在 age 的 B+树中找到对应的 id,然后再拿着 id 去聚簇索引的 B+树里查整行数据。经历了两个 B+树的查找过程,这就叫回表。
避免方法(覆盖索引): 只查询索引中已经包含的字段。比如改写为 SELECT id, age FROM user WHERE age = 18。因为 age 索引的叶子节点本身就包含了 id 和 age,直接就能返回结果,不需要再去聚簇索引里查,从而免去了回表开销。
看到 EXPLAIN 的结果,你最先看哪几个字段?
type(访问类型): 显示单表查询的访问方式。这是判断 SQL 性能好坏最直接的指标。
possible_keys 与 key: 预测可能用到的索引 vs. 实际真正用到的索引。如果 key 为 NULL,说明走的是全表扫描。
rows: MySQL 优化器预估需要扫描的行数。这个值越小越好
extra: 查看extra中是否包含了 Using filesort、Using temporary 等信息
当 possible_keys 有值,但 key 是 NULL,可能是什么原因?
回表成本过高(最常见): 如果查询条件筛选出的行数非常多(比如占了全表数据的 20%~30% 以上),MySQL 优化器计算发现“先查二级索引再频繁回表”的成本,比“直接全表扫描”还要高,它就会放弃索引选择全表扫描(ALL)。
表中数据量太少: 如果表里一共就十几条数据,全表扫描比读索引还要快,优化器会直接放弃索引。
EXPLAIN 和 MySQL 8.0 引入的 EXPLAIN ANALYZE 有什么区别?
EXPLAIN ANALYZE:会真正执行这条 SQL(注意:如果是修改语句,会真的修改数据!通常用于 SELECT),并精准记录下每一步的实际耗时(Actual time)、实际返回行数(Actual rows)和循环次数。
什么是脏读
一个事务读取了另一个事务还没有提交的数据
什么是不可重复读
在一个事务中前后读同一行,其他事物提交了修改/删除,导致读到的值不一样
什么是幻读
在同一个事务中,你用相同的条件前后查询了两次,结果第二次查询时,多出了几行之前没有的数据
如何确保更新语句锁住的是“行”而不是“表”?
UPDATE 想要精准触发“行级”悲观锁,更新条件的字段必须建立索引(最好是主键或唯一索引)。
repeatable read 级别的事务会升级为表锁
因为在RR隔离级别下,需要解决不可重复读和幻读可题,所以在遍历扫描聚集索引记录时,为了防止扫描过的索引被其它事务修改(不可重复读问题)或间隙被其它事务插入记录(幻读问题),从而导致数据不一致,所以MySQL的解决方案就是把所有扫描过的索引己录和间隙都锁上。
为什么在 RR 的隔离级别下,在当前事务中执行了 update 操作之后,可以看到其他并发事务对于数据的修改
假设事务 ID 是 100,另一个并发事务 200 插入了一条 id = 9, name = '张三' 的数据并提交了
此时执行:UPDATE users SET name = '李四' WHERE id = 9;
1. UPDATE 语句在修改数据之前,必须先在数据库里把这条数据找出来。在寻找数据时 InnoDB 不会理会 Read View 快照,而是执行当前读。它直接去内存缓冲区(Buffer Pool)中抓取最新已提交的、真正的物理记录。于是,UPDATE 顺理成章地找到了事务 200 刚刚提交的那条 id = 9 的记录。
2. 既然找到了,MySQL 就会执行修改,把 name 改为 '李四'。只要数据被修改,这行数据的隐藏列 trx_id 就会立刻被数据库改写为当前事务的 ID,也就是 100。 并且,这条记录的旧版本(name='张三', trx_id=200)会被丢进 Undo Log(回滚日志)中。
MVCC 多版本并发控制
MVCC 的本质是让数据库中同一条记录可以存在多个历史版本
隐藏字段
DB_TRX_ID:最近一次修改或插入这行数据的事务 ID
DB_ROLL_PTR:回滚指针,指向这条记录在上一个版本的数据(存在 Undo Log 里)
回滚日志(Undo Log)
当一个事务修改某行数据时,InnoDB 不会直接把旧数据覆盖抹去,而是把旧数据打包放入 Undo Log 中。
利用隐藏字段中的 DB_ROLL_PTR 指针,这些存在于 Undo Log 中的旧版本数据会像一条链表一样串联起来,这就是版本链。
一致性视图
Read View 内部包含四个核心字段
m_ids:生成 Read View 时,系统中当前活跃且未提交的事务 ID 列表
min_trx_id:m_ids 中的最小值
max_trx_id:系统准备分配给下一个新事务的 ID
creator_trx_id:创建这个 Read View 的当前事务 ID
当你的事务执行 SELECT 时,MVCC 会生成一个快照(Read View)。这个快照通过数组记录了当前所有活跃的(还未提交的)事务 ID 列表,以及已提交的最大事务ID(不在数组中)
所有小于 min_trx_id 的事务都是已提交的事务
所有大于 max_trx_id 的事务都是未开始的事务
在大于等于 min_trx_id 和 小于等于 max_trx_id 的区间范围内是未提交的活跃事务或已提交的事务
分支主题