MySQL精简面试题

MySQL

基础概念

(1) MySQL 存储引擎有哪些?常用的 InnoDB、MyISAM 区别?

MySQL 常见存储引擎有:InnoDBMyISAMMemoryArchive 等,最常用的是 InnoDB 和 MyISAM。

对比维度 InnoDB MyISAM
事务支持 支持事务(ACID) 不支持事务
锁粒度 行级锁(基于索引) 表级锁
外键 支持外键约束 不支持外键
崩溃恢复 支持(redo log) 不支持,易损坏
全文索引 MySQL 5.6+ 支持 支持
COUNT(*) 需要全表扫描 内部计数器,直接返回
索引结构 B+ 树(聚簇索引) B+ 树(非聚簇索引)
MVCC 支持 不支持
适用场景 高并发、需要事务 读多写少、不需要事务

MySQL 5.5 起默认引擎从 MyISAM 改为 InnoDB,现在绝大多数场景都用 InnoDB。

(2) InnoDB 相比 MyISAM 优势在哪?

  1. 支持事务:InnoDB 支持 ACID 事务,可以保证数据一致性;MyISAM 不支持,一旦崩溃容易出现数据损坏。

  2. 行级锁:InnoDB 支持行级锁,并发性能远优于 MyISAM 的表级锁(写操作锁整张表)。

  3. 外键约束:InnoDB 支持外键,可以在数据库层面保证数据完整性。

  4. 崩溃恢复:InnoDB 通过 redo log 实现崩溃恢复,断电后能自动恢复数据;MyISAM 不行。

  5. MVCC:InnoDB 支持多版本并发控制,读写不冲突,并发性能更强。

  6. 缓冲池:InnoDB 有 Buffer Pool 缓存数据和索引,减少磁盘 IO。

(3) 什么是事务?事务四大特性 ACID 分别是什么?

事务是一组逻辑操作的最小工作单元,要么全部执行成功,要么全部回滚,保证数据的一致性。

ACID 四大特性:

  • A(Atomicity)原子性 :事务中的操作要么全部完成,要么全部不完成。如果某个操作失败,整个事务回滚。由 undo log 实现。

  • C(Consistency)一致性:事务执行前后,数据库从一个一致状态转变到另一个一致状态。一致性是最终目的,由其他三个特性共同保证。

  • I(Isolation)隔离性 :多个并发事务之间互不干扰,一个事务的中间状态对其他事务不可见。由 锁 + MVCC 实现。

  • D(Durability)持久性 :事务一旦提交,对数据的修改就是永久性的,即使系统崩溃也不会丢失。由 redo log 实现。

(4) 数据库三范式是什么?日常开发一定要严格遵守吗?

  • 第一范式(1NF):每个字段都是不可再分的最小原子单元。例如"地址"字段不应包含省、市、区,应拆分。

  • 第二范式(2NF):在满足 1NF 的基础上,非主键字段必须完全依赖于主键,不能只依赖主键的一部分。主要针对联合主键的情况。

  • 第三范式(3NF):在满足 2NF 的基础上,非主键字段之间不能有传递依赖关系。即非主键字段只能依赖于主键,不能依赖于其他非主键字段。

日常开发不一定要严格遵守 。为了查询性能,经常会进行反范式化设计

  • 适当增加冗余字段,减少多表 JOIN 操作

  • 例如订单表中冗余存储用户名、商品名,虽然违反 3NF,但避免了频繁 JOIN

  • 核心原则:在数据一致性和查询性能之间取得平衡

(5) 执行一条 SQL 语句,期间都发生了哪些事情?

以查询语句为例,完整流程:

  1. 连接器:客户端发起连接请求,验证用户名、密码、权限。

  2. 查询缓存(MySQL 8.0 已移除):检查查询缓存,若命中直接返回结果。

  3. 解析器:词法分析 → 语法分析,生成抽象语法树(AST),检查 SQL 语法是否正确。

  4. 预处理器:检查表名、字段名是否存在,处理通配符(SELECT *)。

  5. 优化器:选择最优的执行计划,包括选择使用哪个索引、JOIN 顺序等。

  6. 执行器:调用存储引擎接口,按照执行计划执行查询。

  7. 存储引擎:InnoDB 执行实际的数据读取(先从 Buffer Pool 找,找不到从磁盘读),返回数据给执行器。

  8. 返回结果:执行器将结果集返回给客户端。

对于更新语句,还涉及:WAL 机制(先写 redo log prepare → 写 binlog → 提交 redo log)。

事务(超级高频)

(6) MySQL 四大事务隔离级别分别是什么?

隔离级别 英文 说明
读未提交 READ UNCOMMITTED 能读到其他事务未提交的数据,最低级别
读已提交 READ COMMITTED 只能读到其他事务已提交的数据(Oracle 默认)
可重复读 REPEATABLE READ 同一事务内多次读取结果一致(MySQL/InnoDB 默认
串行化 SERIALIZABLE 所有事务串行执行,最高级别,性能最差

(7) 脏读、不可重复读、幻读分别是什么?

  • 脏读(Dirty Read) :事务 A 读到了事务 B 还未提交的数据。如果 B 回滚,A 读到的就是脏数据。

  • 不可重复读(Non-Repeatable Read) :事务 A 两次读同一行数据,中间事务 B 修改并提交了 该行,导致 A 两次读到不同的值 。侧重于修改

  • 幻读(Phantom Read) :事务 A 两次用相同条件查询,中间事务 B 插入了新行 ,导致 A 第二次查到了多出来的"幻影行" 。侧重于新增/删除

(8) 四大隔离级别分别能解决哪些问题?

隔离级别 脏读 不可重复读 幻读
读未提交 ✗ 不能解决 ✗ 不能解决 ✗ 不能解决
读已提交 ✓ 解决 ✗ 不能解决 ✗ 不能解决
可重复读 ✓ 解决 ✓ 解决 ✗ 不能完全解决
串行化 ✓ 解决 ✓ 解决 ✓ 解决

(9) InnoDB 默认隔离级别是什么?

InnoDB 默认隔离级别是 可重复读(REPEATABLE READ)

可以通过命令查看和修改:

SELECT @@transaction_isolation; -- 查看当前隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 修改

MySQL 选择 RR 而不是 RC 作为默认级别,主要是因为在 RR 级别下配合 MVCC + Next-Key Lock 可以很大程度上解决幻读问题。

(10) 什么是幻读?怎么解决幻读?

幻读:事务 A 在同一个事务内,用相同条件查询两次,第二次查到了第一次没有的"幻影行",这是因为事务 B 在中间插入了新数据并提交。

解决方案

  1. 快照读(普通 SELECT) :通过 MVCC 解决。事务 A 读取的是事务开始时的快照版本,其他事务插入的数据对 A 不可见。

  2. 当前读(SELECT ... FOR UPDATE / LOCK IN SHARE MODE) :通过 Next-Key Lock(临键锁) 解决。临键锁 = 记录锁 + 间隙锁,锁住索引记录及其前后的间隙,阻止其他事务插入新行。

  3. 串行化隔离级别:直接串行执行所有事务,彻底避免幻读,但性能极差。

(11) 事务的实现原理:MVCC 是什么?

MVCC(Multi-Version Concurrency Control,多版本并发控制) 是 InnoDB 实现高并发读写的核心机制。

核心思想 :每行数据保存多个历史版本,读操作可以读取旧版本,不需要加锁,从而实现读写不冲突

MVCC 依赖的三大组件

  1. 隐式字段:DB_TRX_ID(最近修改的事务ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(隐藏主键)。

  2. undo log 版本链:每次修改数据时,将旧版本写入 undo log,通过回滚指针串联成版本链。

  3. Read View(读视图):事务执行快照读时生成,用于判断版本链中哪个版本对当前事务可见。

MVCC 只在 读已提交(RC)可重复读(RR) 两个隔离级别下工作。

(12) 可重复读隔离级别,完全解决幻读了吗?

没有完全解决

在 RR 隔离级别下:

  • 快照读(普通 SELECT):MVCC 可以避免幻读,因为读的是快照。

  • 当前读 + Next-Key Lock:大部分场景可以避免幻读。

但存在一个经典问题:如果事务 A 先执行快照读,然后事务 B 插入并提交,事务 A 再执行当前读(如 UPDATE),此时 MVCC 快照会"看到"B 插入的行,产生幻读。

所以 RR 级别下只能最大程度减少幻读,不能完全消除。要彻底解决幻读需要串行化隔离级别。

MVCC

(13) MVCC 底层原理是什么?

MVCC 底层由三部分组成:

1. 数据行的隐式字段

  • DB_TRX_ID(6字节):最后一次修改该行的事务 ID。

  • DB_ROLL_PTR(7字节):回滚指针,指向 undo log 中该行的上一个版本。

  • DB_ROW_ID(6字节):如果表没有主键,InnoDB 自动生成的隐藏主键。

2. undo log 版本链

  • 每次 UPDATE/DELETE 操作会将旧版本数据写入 undo log。

  • 通过 DB_ROLL_PTR 将多个历史版本串成链表。

  • 链表头部是最新版本,尾部是最旧版本。

3. Read View(读视图)

  • 快照读时生成 Read View,包含四个关键字段:

  • m_ids:生成 Read View 时所有活跃(未提交)事务 ID 列表

  • min_trx_id:m_ids 中最小值

  • max_trx_id:下一个将分配的事务 ID(即最大事务 ID + 1)

  • creator_trx_id:创建该 Read View 的事务 ID

可见性判断规则

  • DB_TRX_ID < min_trx_id:版本在 Read View 创建前已提交 → 可见

  • DB_TRX_ID >= max_trx_id:版本在 Read View 创建后才出现 → 不可见

  • DB_TRX_ID 在 [min_trx_id, max_trx_id) 之间:

  • 如果在 m_ids 中 → 不可见(事务还未提交)

  • 如果不在 m_ids 中 → 可见(已提交)

  • 如果当前版本不可见,沿版本链找更旧的版本,直到找到可见版本或到链尾。

(14) 什么是快照读、当前读?举例说明

快照读(Snapshot Read)

  • 读取 MVCC 版本链中的历史版本数据,不加锁

  • 普通的 SELECT 语句就是快照读。

SELECT * FROM user WHERE id = 1; -- 快照读

当前读(Current Read)

  • 读取数据库中最新的已提交数据,并加锁

  • 以下语句是当前读:

SELECT * FROM user WHERE id = 1 FOR UPDATE; -- 加排他锁

SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE; -- 加共享锁

INSERT INTO user VALUES (1, 'Tom'); -- 插入操作

UPDATE user SET name = 'Tom' WHERE id = 1; -- 更新操作

DELETE FROM user WHERE id = 1; -- 删除操作

快照读保证可重复读,当前读保证读到最新数据但会产生锁竞争。

(15) 隐式字段 DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID 作用

字段 大小 作用
DB_TRX_ID 6 字节 记录最后一次修改(INSERT/UPDATE)该行的事务 ID。MVCC 判断版本可见性时用到。
DB_ROLL_PTR 7 字节 回滚指针,指向 undo log 中该行的上一个历史版本,用于构建版本链。
DB_ROW_ID 6 字节 隐藏自增行 ID。当表没有定义主键时,InnoDB 自动生成作为聚簇索引的主键。如果有主键则不需要此字段。

(16) undo log 日志作用、版本链是什么?

undo log 的作用

  1. 事务回滚:存储数据修改前的旧值,回滚时用旧值恢复数据(实现原子性)。

  2. MVCC 多版本控制:保存数据的历史版本,供快照读使用。

版本链

  • 每次修改数据时,InnoDB 将旧版本写入 undo log 段。

  • 新版本的 DB_ROLL_PTR 指向旧版本,形成一条从新到旧的链表。

  • 版本链头部是最新版本 (当前数据行),尾部是最旧版本

  • 快照读时,从版本链头部开始,根据 Read View 判断哪个版本可见。

(17) Read View 视图四个字段作用、可见性规则

四个字段

字段 含义
m_ids 创建 Read View 时,所有未提交的事务 ID 列表
min_trx_id m_ids 中的最小值
max_trx_id 系统即将分配的下一个事务 ID(当前最大事务 ID + 1)
creator_trx_id 创建该 Read View 的当前事务 ID

可见性规则(对版本链中某版本的 DB_TRX_ID 判断):

  1. DB_TRX_ID == creator_trx_id :自己修改的版本 → ✅ 可见

  2. DB_TRX_ID < min_trx_id :在 Read View 创建前已提交 → ✅ 可见

  3. DB_TRX_ID >= max_trx_id :在 Read View 创建后才出现 → ❌ 不可见

  4. min_trx_id <= DB_TRX_ID < max_trx_id

  • 若 DB_TRX_ID m_ids 中 → ❌ 不可见(创建时该事务还未提交)

  • 若 DB_TRX_ID 不在 m_ids 中 → ✅ 可见(创建时该事务已提交)

如果当前版本不可见,沿着版本链继续找更早的版本,直到找到可见版本。

(18) MVCC 怎么实现可重复读、读已提交?

两者的核心区别在于 Read View 的生成时机不同

读已提交(RC)

  • 每次 执行快照读(SELECT)都会重新生成 Read View。

  • 所以每次读都能读到最新已提交的数据。

  • 其他事务在两次读之间提交了修改,就能读到不同的值 → 产生不可重复读

可重复读(RR)

  • 只在事务中第一次 执行快照读时生成 Read View,之后复用同一个 Read View。

  • 所以整个事务期间读到的数据都是一样的 → 实现可重复读

  • 其他事务提交的修改对当前事务始终不可见。

简单记忆:RC 每次读都新建 Read View,RR 只在第一次读时新建

索引

(19) InnoDB 索引结构为什么选 B+ 树,不选二叉树、红黑树、B 树?

不选二叉树/红黑树

  • 树的高度太高,每个节点只存一个数据,查询需要大量磁盘 IO。

  • 例如 1000 万条数据,二叉树高度约 23 层,每次查询可能要 23 次磁盘 IO。

不选 B 树

  • B 树每个节点都存数据(key + value),导致非叶子节点能存的索引数量更少,树更高。

  • B 树的范围查询需要中序遍历,效率低。

选 B+ 树的原因

  1. 非叶子节点只存索引,不存数据:一个节点能存更多索引,树更矮。通常 3-4 层的 B+ 树可以存储千万级数据,查询只需 3-4 次磁盘 IO。

  2. 数据全部存在叶子节点:查询性能稳定,每次都走到叶子节点。

  3. 叶子节点之间用双向链表连接:天然支持范围查询,只需遍历链表即可。

  4. 磁盘预读友好:操作系统按页读取,B+ 树节点大小设置为一个页(默认 16KB),一次 IO 读一个节点。

(20) B+ 树和 B 树的区别?

对比 B 树 B+ 树
数据存储 所有节点都存数据 只有叶子节点存数据,非叶子节点只存索引
叶子节点连接 叶子节点之间没有指针 叶子节点通过双向链表连接
范围查询 需要中序遍历整棵树,效率低 只需在叶子节点链表上顺序遍历
查询稳定性 可能在中间节点就找到,不稳定 每次都要走到叶子节点,性能稳定
非叶子节点容量 因为存数据,每个节点索引少 只存索引,每个节点索引多,树更矮

(21) 聚簇索引和非聚簇索引(二级索引)区别?

对比 聚簇索引(主键索引) 非聚簇索引(二级索引)
叶子节点存什么 整行数据 索引列的值 + 主键值
数量 一张表只有一个 可以有多个
数据顺序 数据按聚簇索引顺序物理存储 不影响数据物理存储顺序
查询 直接拿到整行数据 拿到主键后需要回表到聚簇索引查完整行

InnoDB 每张表都有且仅有一个聚簇索引(如果有主键就用主键,没有就用隐藏 ROW_ID)。

(22) 主键索引、唯一索引、普通索引、联合索引区别

索引类型 是否允许重复 是否允许 NULL 说明
主键索引 不允许重复 不允许 NULL 一张表只能有一个,InnoDB 聚簇索引的基础
唯一索引 不允许重复 允许 NULL(多个NULL) 保证列值唯一性
普通索引 允许重复 允许 NULL 最基本的索引,加快查询速度
联合索引 取决于类型 取决于类型 多个列组合的索引,遵循最左前缀原则

(23) 什么是回表查询?怎么避免回表?

回表查询 :通过二级索引查到了主键值,但需要的列数据不在二级索引的叶子节点中,需要拿着主键到聚簇索引中再查一次,这个过程叫回表。

例如:表有 id(主键)、name、age,在 name 上建了二级索引:

SELECT * FROM user WHERE name = 'Tom';

-- 先在 name 索引找到 id → 再回主键索引查整行数据 → 回表

避免回表的方法

  1. 覆盖索引:查询的列都包含在索引中,不需要回表。

> -- 建立联合索引 (name, age)

SELECT name, age FROM user WHERE name = 'Tom'; -- 不需要回表

  1. 减少 SELECT 列:只查需要的列,避免 SELECT *。

(24) 覆盖索引是什么?使用场景?

覆盖索引:查询所需的所有列都能从索引中直接获取,不需要回表到聚簇索引。

判断标志 :EXPLAIN 中 Extra 列显示 Using index

使用场景

  1. 只需要查少数几个字段时,为这些字段建立联合索引。

  2. 统计查询:SELECT COUNT(*) FROM user(利用主键索引扫描比全表扫描快)。

  3. 分页查询优化:

> -- 利用覆盖索引先拿到 id,再 JOIN 原表

SELECT * FROM user t1

INNER JOIN (SELECT id FROM user ORDER BY name LIMIT 1000000, 10) t2

ON t1.id = t2.id;

(25) 最左前缀原则原理、为什么要遵守?

最左前缀原则 :联合索引 (a, b, c) 在 B+ 树中的排列顺序是先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序

因此查询时必须从最左列开始匹配:

  • WHERE a = 1 → ✅ 走索引

  • WHERE a = 1 AND b = 2 → ✅ 走索引

  • WHERE a = 1 AND b = 2 AND c = 3 → ✅ 走索引

  • WHERE b = 2 → ❌ 不走索引(缺少最左列 a)

  • WHERE b = 2 AND c = 3 → ❌ 不走索引

  • WHERE a = 1 AND c = 3 → ⚠️ 只能用到 a 的索引(MySQL 5.0+ 优化器可能会用索引下推)

为什么:因为 B+ 树是按联合索引的列顺序排列的,跳过左边的列,右边的列就不是有序的了,无法利用 B+ 树的有序性进行二分查找。

MySQL 8.0 新特性 :支持索引跳跃扫描(Index Skip Scan),在某些条件下即使不满足最左前缀也能使用索引,但效率不如完整匹配。

(26) 索引下推原理和作用

索引下推(Index Condition Pushdown, ICP) 是 MySQL 5.6 引入的优化。

没有 ICP 时

  • 联合索引 (name, age),查询 WHERE name LIKE '张%' AND age = 10

  • 存储引擎根据 name LIKE '张%' 找到所有匹配的主键 → 全部回表 → Server 层再过滤 age = 10

有 ICP 时

  • 存储引擎在遍历索引时,直接判断索引中 age 字段是否等于 10

  • 不满足条件的记录直接跳过,减少回表次数

作用:减少回表查询的次数,提升查询性能。

EXPLAIN 中 Extra 显示 Using index condition 表示使用了索引下推。

(27) 什么是索引失效?哪些情况会导致索引失效?

索引失效:建立了索引但查询时没有使用索引,而是走全表扫描。

常见索引失效场景

  1. 违反最左前缀原则:联合索引跳过左边的列。

  2. 对索引列使用函数或运算:WHERE YEAR(create_time) = 2024,WHERE id + 1 = 5。

  3. 隐式类型转换:字段是 varchar 类型,查询条件传了数字 WHERE phone = 13800138000。

  4. LIKE** 以通配符开头**:WHERE name LIKE '%张'(全表扫描)。

  5. OR** 条件中有非索引列**:WHERE indexed_col = 1 OR non_indexed_col = 2。

  6. NOT IN **、**NOT EXISTS:在某些场景下优化器放弃索引。

  7. IS NULL ** / **IS NOT NULL:某些情况下优化器认为全表扫描更快。

  8. 范围查询右边的列:WHERE a > 1 AND b = 2,联合索引 (a,b) 中 b 无法使用索引。

  9. !=** 或 ****<>**:优化器可能认为全表扫描更高效。

(28) 模糊查询 like %xxx、like xxx% 哪个走索引?

  • LIKE 'xxx%'(右边模糊)→ ✅ 走索引。因为 B+ 树是按前缀排序的,xxx 开头的数据是连续的一段。

  • LIKE '%xxx'(左边模糊)→ ❌ 不走索引。因为无法确定前缀,需要全表扫描。

  • LIKE '%xxx%'(两边模糊)→ ❌ 不走索引

如果一定要用左模糊查询的优化方案

  1. 使用全文索引(FULLTEXT INDEX)。

  2. 使用 Elasticsearch 等搜索引擎。

  3. 反转字符串存储 + 右模糊查询(适用于特定场景)。

(29) 字段类型隐式转换为什么会导致索引失效?

当查询条件的值类型与字段类型不一致时,MySQL 会进行隐式类型转换

例如:phone 字段是 VARCHAR 类型,查询 WHERE phone = 13800138000(数字类型)。

MySQL 会将字符串转为数字进行比较,等价于:

WHERE CAST(phone AS SIGNED) = 13800138000

这就相当于对索引列使用了函数,破坏了 B+ 树的有序性,导致索引失效,只能全表扫描。

解决方案

  • 保证查询条件类型与字段类型一致。

  • WHERE phone = '13800138000'(加引号)。

字符串转数字 会导致索引失效;但数字转字符串(字段是数字类型,查询传字符串)通常不影响索引。

(30) 为什么不建议用 SELECT *?

  1. 无法利用覆盖索引:SELECT * 查出所有列,二级索引不包含所有列,必须回表,增加 IO 开销。

  2. 网络传输浪费:查出很多不需要的字段,占用更多网络带宽。

  3. 内存消耗增大:MySQL 需要将更多数据放入 Buffer Pool / 结果集缓存中。

  4. 代码可读性差:不明确需要哪些字段,不利于代码维护。

  5. 字段变更风险:表结构变动(新增大字段)可能导致 SELECT * 查询变慢甚至出错。

(31) 联合索引创建顺序原则:区分度高、长度小、经常查询

创建联合索引时列的顺序建议遵循:

  1. 区分度高的列放左边:区分度 = 不重复值的比例。区分度越高,过滤效果越好。
  • 例如身份证号区分度 > 性别区分度。

  • 可以用 SELECT COUNT(DISTINCT col) / COUNT(*) FROM table 计算。

  1. 字段长度小的放左边:索引页能存更多索引项,减少树的高度,降低 IO 次数。

  2. 经常用于查询条件的列优先:最常用的查询条件放最左,满足最左前缀原则。

示例:(user_id, create_time) 比 (create_time, user_id) 更优,因为 user_id 区分度高且更常用于查询。

(32) 什么时候不适合建索引?

  1. 数据量很小的表:几百条数据,全表扫描比走索引还快。

  2. 频繁增删改的列:索引维护成本高,每次 DML 都要更新索引。

  3. 区分度低的列:如性别(男/女)、状态(0/1),过滤效果差,建索引意义不大。

  4. 很少出现在 WHERE 中的列:不用于查询条件的列建索引是浪费。

  5. TEXT、BLOB 等大字段:索引体积太大,效率低(可以用前缀索引)。

  6. 表更新频率远大于查询频率:索引带来的查询收益不如维护成本。

(33) InnoDB 有哪些锁?行锁、表锁、意向锁

按粒度分类

锁类型 说明
表级锁 锁住整张表。包括表共享读锁(S)、表排他写锁(X)、意向锁
行级锁 锁住具体行。包括记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock)
页级锁 介于表锁和行锁之间,锁住一页数据(BDB 引擎,InnoDB 不用)

按功能分类

  • 共享锁(S 锁 / 读锁):允许事务读取一行,多个事务可同时持有。

  • 排他锁(X 锁 / 写锁):允许事务更新或删除一行,独占访问。

意向锁(Intention Lock)

  • 意向共享锁(IS):事务打算给行加共享锁前,先给表加 IS 锁。

  • 意向排他锁(IX):事务打算给行加排他锁前,先给表加 IX 锁。

  • 作用:快速判断表中是否有行锁,避免逐行检查,提升加表锁的效率。

(34) 行锁什么时候变表锁?

InnoDB 的行锁是加在索引上的,不是加在行数据上的。以下情况行锁会退化为表锁:

  1. 没有走索引 :如果 UPDATE/DELETE 的 WHERE 条件没有使用索引,InnoDB 会扫描全表,对每一行加锁,等效于表锁

> UPDATE user SET name = 'Tom' WHERE age = 18; -- age 没有索引 → 表锁

  1. 使用了全表扫描:即使有索引,但优化器判断全表扫描更高效时,也会退化为表锁。

解决方案:确保 WHERE 条件中的列有索引,避免行锁变表锁。

(35) 记录锁、间隙锁、临键锁分别是什么?

这三种锁都是 InnoDB 在可重复读(RR)隔离级别下用于解决幻读问题的行锁类型:

锁类型 英文名 锁住范围 说明
记录锁 Record Lock 只锁索引记录本身 精确锁住某一行,如 id = 5
间隙锁 Gap Lock 锁住索引记录之间的间隙 不包含记录本身,如 (5, 10)
临键锁 Next-Key Lock 记录锁 + 间隙锁 左开右闭区间,如 (5, 10]。InnoDB RR 下默认使用

示例:索引中有 5、10 两个值:

  • 记录锁:锁住 5 或 10

  • 间隙锁:锁住 (5, 10) 之间的间隙

  • 临键锁:锁住 (0, 5]、(5, 10] 这样的区间

读已提交(RC) 隔离级别下,InnoDB 只有记录锁,没有间隙锁和临键锁。

(36) 临键锁怎么解决幻读?

场景:事务 A 执行 SELECT * FROM t WHERE id > 5 FOR UPDATE(当前读)。

临键锁的工作过程

  1. 对 id = 10 加记录锁 + (5, 10] 的临键锁。

  2. 对 id > 10 到正无穷的间隙也加间隙锁 (10, +∞)。

  3. 事务 B 如果想 INSERT INTO t VALUES (8),会落在 (5, 10] 的临键锁范围内 → 被阻塞

  4. 事务 B 如果想 INSERT INTO t VALUES (15),落在 (10, +∞) 间隙锁范围 → 被阻塞

这样,在当前读的场景下,临键锁锁住了查询范围 + 间隙,阻止其他事务在范围内插入新数据,从而解决了幻读问题。

(37) 什么是死锁?产生条件、怎么排查和避免死锁?

死锁:两个或多个事务互相持有对方需要的锁,形成循环等待,谁都无法继续执行。

产生条件(四个必要条件)

  1. 互斥条件:资源不能共享,一个事务占用的资源其他事务不能访问。

  2. 持有并等待:事务持有某些资源的同时还在等待其他资源。

  3. 不可剥夺:事务已获得的资源不能被强制剥夺。

  4. 循环等待:多个事务形成首尾相接的等待环。

排查方法

  1. 查看最近一次死锁信息:

> SHOW ENGINE INNODB STATUS;

  1. 开启死锁日志,记录每次死锁:

> SET GLOBAL innodb_print_all_deadlocks = ON;

  1. 查看当前锁等待情况:

> SELECT * FROM information_schema.INNODB_LOCKS;

SELECT * FROM information_schema.INNODB_LOCK_WAITS;

避免方法

  1. 按固定顺序访问表和行:避免循环等待。

  2. 缩短事务:事务越短,持锁时间越少。

  3. 降低隔离级别:使用 RC 级别,没有间隙锁,减少死锁概率。

  4. 合理使用索引:确保走索引加行锁,避免退化为表锁。

  5. 设置锁等待超时:innodb_lock_wait_timeout(默认 50 秒)。

  6. InnoDB 自带死锁检测:检测到死锁时自动回滚持有最少排他锁的事务。

日志

(38) MySQL 三大日志:redo log、undo log、binlog 各自作用

日志 所属层 作用
redo log InnoDB 引擎层 崩溃恢复。记录数据页的物理修改("在某个数据页做了什么"),保证事务持久性(D)。
undo log InnoDB 引擎层 事务回滚 + MVCC。记录数据的逻辑反向操作("修改前的旧值"),保证事务原子性(A)。
binlog MySQL Server 层 主从复制 + 数据归档。记录所有修改数据的逻辑语句,用于从库同步和数据恢复。

redo log 是 InnoDB 特有的;binlog 是所有存储引擎共有的(Server 层)。

(39) redo log 为什么能保证事务崩溃恢复?WAL 机制

WAL(Write-Ahead Logging,预写式日志):事务提交时,先把修改写入 redo log 文件,再慢慢刷到磁盘数据页。

redo log 保证崩溃恢复的原理

  1. 事务执行时,数据修改先写入 Buffer Pool(内存)。

  2. 同时将修改操作写入 redo log buffer(内存)。

  3. 事务提交时,redo log 刷入磁盘(fsync),此时即使数据页还没刷盘也没关系。

  4. 如果 MySQL 崩溃,重启时会读取 redo log,**重做(redo)**已提交但未刷盘的数据页修改。

redo log 的结构

  • 固定大小的循环文件(如 2 个文件,每个 1GB)。

  • 有 write pos(写入位置)和 checkpoint(已刷盘位置)两个指针。

  • write pos 追上 checkpoint 时,说明日志满了,需要阻塞,强制刷盘推进 checkpoint。

核心思想:用顺序写日志代替随机写数据页,IO 效率更高。

(40) binlog 是什么?格式 statement/row/mixed 区别

binlog(归档日志) 是 MySQL Server 层的逻辑日志,记录所有修改数据的 SQL 语句,用于主从复制数据恢复

格式 记录内容 优点 缺点
STATEMENT 原始 SQL 语句 日志量小 NOW()、UUID() 等函数导致主从不一致
ROW 每行数据修改的前后值 不会主从不一致 批量操作时日志量巨大
MIXED 混合模式:普通 SQL 用 STATEMENT,不安全的用 ROW 兼顾两者 仍可能有少量不一致场景

生产环境一般用 ROW 格式,保证数据一致性,配合 binlog 压缩策略控制日志大小。

(41) redo log 和 binlog 区别?

对比 redo log binlog
所属层 InnoDB 引擎层 MySQL Server 层
日志内容 物理日志("某个数据页的某个偏移做了什么修改") 逻辑日志(SQL 语句或行数据变更)
文件大小 固定大小,循环写入 追加写入,一个文件写满后新建
用途 崩溃恢复 主从复制、数据归档恢复
写入时机 事务执行过程中持续写入 事务提交时写入
格式 循环文件 追加文件

(42) 事务提交时 redo log、binlog 两阶段提交原理

为什么需要两阶段提交:redo log 和 binlog 是两个独立的日志,如果不用两阶段提交,崩溃恢复时可能出现两个日志不一致的情况。

两阶段提交流程

  1. prepare 阶段:
  • InnoDB 将 redo log 写入磁盘,标记为 prepare 状态
  1. 写 binlog:
  • Server 层将 binlog 写入磁盘
  1. commit 阶段:
  • InnoDB 将 redo log 标记为 commit 状态

崩溃恢复时的判断逻辑

  • redo log = commit → 直接提交事务 ✅

  • redo log = prepare + binlog 完整 → 补全 commit,提交事务 ✅

  • redo log = prepare + binlog 不完整 → 回滚事务 ❌

  • redo log 不完整 → 回滚事务 ❌

两阶段提交保证了 redo log 和 binlog 的一致性,是 MySQL 崩溃恢复 + 主从复制双重保障的关键。

SQL 优化

(43) 一条 SQL 执行完整流程?

查询语句(SELECT)

客户端 → 连接器 → 查询缓存(8.0前) → 解析器 → 预处理器 → 优化器 → 执行器 → 存储引擎 → 返回结果

更新语句(UPDATE)

客户端 → 连接器 → 解析器 → 优化器 → 执行器 → 存储引擎

存储引擎:将数据页读入 Buffer Pool → 修改数据 → 写 redo log (prepare)

Server 层:写 binlog

存储引擎:提交 redo log (commit) → 后台异步刷脏页到磁盘

→ 返回客户端

更新语句不是直接写磁盘,而是先改内存(Buffer Pool),通过 WAL 机制保证持久性。

(44) explain 执行计划每个字段含义

EXPLAIN SELECT * FROM user WHERE name = 'Tom' 的结果:

字段 含义
id 查询序号。id 相同从上往下执行;id 不同,id 越大优先级越高
select_type 查询类型:SIMPLE(简单查询)、PRIMARY(最外层)、SUBQUERY(子查询)、DERIVED(派生表)、UNION
table 访问的表名
type(重要) 访问类型,从好到差:system > const > eq_ref > ref > range > index > ALL
possible_keys 可能使用的索引
key(重要) 实际使用的索引
key_len 索引使用的字节数,可判断联合索引用到了哪些列
rows 预估扫描行数,越小越好
Extra(重要) Using index(覆盖索引)、Using where(Server层过滤)、Using filesort(需优化)、Using temporary(需优化)、Using index condition(索引下推)

(45) type 执行效率级别

从优到劣排列:

type 说明 示例
system 表只有一行记录(系统表) SELECT * FROM mysql.user WHERE ...
const 通过主键或唯一索引等值查询,最多返回一行 WHERE id = 1
eq_ref 多表 JOIN 时,被驱动表通过主键/唯一索引等值匹配 JOIN ON t1.id = t2.idt2.id 是主键)
ref 通过普通索引等值查询,可能返回多行 WHERE name = 'Tom'(name 有索引)
range 索引范围扫描 WHERE id > 5 AND id < 10
index 全索引扫描(扫描整棵索引树) SELECT id FROM user(覆盖索引)
ALL 全表扫描 SELECT * FROM user WHERE ...(无索引可用)

优化目标:至少达到 range 级别,最好能达到 ref

(46) 怎么看懂慢查询日志?

开启慢查询日志

SET GLOBAL slow_query_log = ON;

SET GLOBAL long_query_time = 1; -- 超过1秒的查询记录下来

慢查询日志关键字段

Time: 2024-01-01T10:00:00.000000Z

User@Host: rootroot @ localhost 127.0.0.1

Query_time: 3.200000 Lock_time: 0.000100 Rows_sent: 1 Rows_examined: 1000000

SELECT * FROM user WHERE age = 18;

  • Query_time:查询耗时(超过 long_query_time 才记录)。

  • Lock_time:等待锁的时间。

  • Rows_sent:返回给客户端的行数。

  • Rows_examined:扫描的行数(越大说明效率越低)。

分析工具:使用 mysqldumpslow 或 pt-query-digest 进行聚合分析,找出 TOP N 慢 SQL。

(47) 慢查询优化整体思路?

  1. 开启慢查询日志:先定位哪些 SQL 慢。

  2. 使用 EXPLAIN 分析执行计划:查看 type、key、rows、Extra,确认是否走了预期的索引。

  3. 优化索引

  • 缺少索引 → 添加合适的索引

  • 索引失效 → 改写 SQL 让索引生效

  • 回表太多 → 利用覆盖索引

  1. 优化 SQL 语句
  • 避免 SELECT *,只查需要的列

  • 避免子查询,改写为 JOIN

  • 优化深分页(limit 大偏移量)

  1. 优化表结构
  • 适当反范式化,减少 JOIN

  • 大表考虑分库分表

  1. 优化配置参数:Buffer Pool 大小、排序缓冲区等。

(48) limit 分页深偏移量怎么优化?(limit 1000000, 10)

问题:LIMIT 1000000, 10 实际上扫描了 1000010 行,然后丢弃前 100 万行,只返回 10 行。效率极低。

优化方案

方案一:延迟关联(推荐)

-- 利用覆盖索引先快速定位 id,再 JOIN 原表

SELECT * FROM user t1

INNER JOIN (

SELECT id FROM user ORDER BY id LIMIT 1000000, 10

) t2 ON t1.id = t2.id;

子查询只扫描主键索引(覆盖索引),速度远快于扫描整行数据。

方案二:记录上次位置

-- 记住上次最后一条的 id,下次从该 id 开始查

SELECT * FROM user WHERE id > 上次最大id ORDER BY id LIMIT 10;

适用于顺序翻页场景,每次都是 O(10) 的扫描量。

方案三:业务限制

  • 限制最大翻页深度,如只允许翻前 100 页。

  • 搜索引擎(如百度)也是只显示前几十页。

(49) order by 排序原理、什么时候会 Using filesort?怎么优化?

MySQL 两种排序方式

  1. 全字段排序(全字段在 sort_buffer 中排序):把需要的字段都放入 sort_buffer,在内存中排序后直接返回。

  2. rowid 排序:只把排序字段 + 主键 id 放入 sort_buffer,排序后根据 id 回表取完整数据。

Using filesort 出现条件

  • 排序字段没有索引,或排序字段与查询条件不是同一个索引。

  • 使用了 ORDER BY 但无法利用索引的有序性。

-- name 没有索引 → Using filesort

SELECT * FROM user ORDER BY name;

优化方法

  1. 给排序字段加索引

> CREATE INDEX idx_name ON user(name);

  1. 利用联合索引:如果 WHERE 和 ORDER BY 的字段可以组成联合索引,排序可以利用索引有序性。

  2. 增大 sort_buffer_size:减少磁盘临时文件的使用。

  3. 减少排序数据量:加 WHERE 条件缩小范围。

(50) group by 原理与优化思路

GROUP BY 原理

  • MySQL 对 GROUP BY 字段进行排序(或利用索引有序性),然后对相同值的行进行聚合计算。

  • 如果没有索引,MySQL 会使用临时表 + filesort 来完成分组,性能较差。

优化思路

  1. 给 GROUP BY 字段加索引:利用索引的有序性避免额外排序。

  2. 使用联合索引:(a, b) 索引可以支持 WHERE a = 1 GROUP BY b。

  3. 关闭隐式排序:GROUP BY col ORDER BY NULL,如果不需要排序,避免额外排序开销。

  4. 增大 tmp_table_size 和 max_heap_table_size:让临时表尽量在内存中完成。

  5. 减少 GROUP BY 的列数:分组的列越少,性能越好。

(51) join 连接原理:内连接、左连接、右连接

内连接(INNER JOIN):返回两个表中匹配的行。

SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.t1_id;

-- 只返回 t1 和 t2 都匹配的行

左连接(LEFT JOIN):返回左表所有行 + 右表匹配的行(不匹配补 NULL)。

SELECT * FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id;

-- t1 全部返回,t2 不匹配的填 NULL

右连接(RIGHT JOIN):返回右表所有行 + 左表匹配的行(不匹配补 NULL)。

JOIN 执行算法

  1. Index Nested-Loop Join(NLJ) :被驱动表的关联字段有索引。驱动表逐行扫描,利用索引在被驱动表中查找。最高效

  2. Block Nested-Loop Join(BNL):被驱动表无索引。把驱动表的数据放入 join_buffer,然后全表扫描被驱动表进行匹配。

  3. Hash Join(MySQL 8.0.18+):用驱动表构建 hash 表,扫描被驱动表时通过 hash 查找。

优化原则:小表做驱动表,大表做被驱动表;被驱动表关联字段要有索引。

(52) 大表 join 怎么优化?

  1. 确保被驱动表的 JOIN 字段有索引:让 JOIN 走 Index Nested-Loop Join,避免全表扫描。

  2. 小表驱动大表:MySQL 优化器通常会自动选择小表作为驱动表。可用 STRAIGHT_JOIN 强制指定。

  3. 增大 join_buffer_size:当无法走索引时(BNL),更大的 buffer 可以减少对被驱动表的扫描次数。

  4. 减少驱动表数据量:在 JOIN 前先 WHERE 过滤,减少参与 JOIN 的行数。

  5. 只 SELECT 需要的列:减少内存和网络开销。

  6. 分库分表:数据量实在太大时,考虑水平拆分。

  7. 考虑用宽表 / 冗余字段:减少 JOIN 需求,用空间换时间。

相关推荐
晚安日记wanna1 小时前
四年前端被问 Code Review90人只看得懂空格和命名
前端·面试
中趴菜1 小时前
robots.txt 规则误拦排查实战指南
后端
anxiao_m1 小时前
局域网文件传输工具有哪些?4款主流工具实测对比测评
大数据·网络·数据库
墨香幽梦客1 小时前
API集成中的高可用(HA)设计:如何避免单点故障拖垮业务?
数据库
拓人间精准客1 小时前
全维度大数据:助贷机构破冰数据合规与触达难的5个支点
大数据·数据库·sass·tob
落木萧萧8251 小时前
Wrapper、Criteria、Specification:查询能不能写得好维护一点
数据库·后端·架构
小程序设计1 小时前
基于SQL与漏斗模型的社群App优化研究
数据库·sql
步行cgn1 小时前
构造注入详解:Spring 依赖注入的首选方式
后端
吃饱了得干活1 小时前
Spring Boot + Redis 分布式锁:一个订单重复提交引发的六次迭代
java·redis·后端