MySQL 8.0 面试题精选
生成时间:2026-06-07 22:43 | 共 10 题
1. InnoDB 架构:Buffer Pool 的核心机制
难度:⭐⭐
请详细描述 InnoDB Buffer Pool 的架构设计,包括 LRU List、Flush List、Free List 的作用,以及 InnoDB 对传统 LRU 算法做了什么改进?为什么?
标准答案
InnoDB Buffer Pool 由多个 Instance 组成(innodb_buffer_pool_instances),每个 Instance 管理自己的链表:
- Free List:管理空闲页,启动时所有页都在 Free List 上
- LRU List:管理已使用的页,采用改进型 LRU算法
- Flush List:管理脏页(已修改但未刷盘的页),按修改时间排序
LRU 改进——冷热分区(Midpoint Insertion Strategy):
InnoDB 将 LRU List 分为 Young Region(热端,默认 5/8) 和 Old Region(冷端,默认 3/8)。新读取的页不直接进入热端,而是插入到冷端的头部(midpoint)。只有当冷端的页在 innodb_old_blocks_time(默认 1 秒)内再次被访问,才会被提升到热端头部。
这样设计的核心目的是防止全表扫描等一次性操作将热数据挤出 Buffer Pool,避免缓存污染。
延伸思考
- 如何通过
innodb_buffer_pool_size和innodb_buffer_pool_instances合理配置 Buffer Pool? - 为什么 MySQL 8.0 支持 Buffer Pool 在线缩容(5.7 只支持在线扩容)?
2. 索引优化:覆盖索引与最左前缀原则
难度:⭐⭐
假设有一张表 orders(id, user_id, status, created_at, amount),存在联合索引 idx_user_status(user_id, status)。以下 SQL 能否命中覆盖索引?为什么?
SELECT user_id, status, created_at FROM orders WHERE user_id = 100;标准答案
不能命中覆盖索引。虽然 WHERE user_id = 100 可以走 idx_user_status 索引,但查询需要返回 created_at 字段,而该字段不在联合索引中,必须回表获取。覆盖索引要求 SELECT 的所有列都在索引中,无需回表。
要实现覆盖索引,可创建:idx_user_status_created(user_id, status, created_at)。
最左前缀原则:联合索引 (A, B, C) 可支持 A、A,B、A,B,C 的查询条件,但不能跳过最左列直接用 B 或 C。但 MySQL 8.0 的索引跳跃扫描(Index Skip Scan)在特定条件下可以跳过最左列。
延伸思考
- Index Skip Scan 的适用条件和限制是什么?
- 什么情况下应该用覆盖索引而不是回表?如何权衡索引维护成本?
3. 事务隔离级别与 MVCC 机制
难度:⭐⭐⭐
MySQL 8.0 默认隔离级别是什么?请详细说明 InnoDB 在 RR(可重复读)隔离级别下如何通过 MVCC 实现一致性非锁定读,以及它与 RC(读已提交)级别的 MVCC 实现有何关键差异?
标准答案
MySQL 8.0 默认隔离级别为 REPEATABLE READ(RR)。
MVCC 核心机制:
每行数据包含两个隐藏列:DB_TRX_ID(最后修改的事务 ID)和 DB_ROLL_PTR(回滚指针,指向 undo log 中的前版本)。通过 undo log 版本链,可以找到数据的所有历史版本。
Read View(读视图)决定事务能看到哪个版本:
- RR 级别:Read View 在事务第一次执行 SELECT 时创建,整个事务期间复用同一个 Read View → 保证可重复读
- RC 级别:Read View 在每次 SELECT 时都重新创建 → 可以看到其他事务已提交的最新数据
可见性判断规则(基于 Read View 的 m_ids、min_trx_id、max_trx_id、creator_trx_id):
- 版本的
trx_id == creator_trx_id→ 自身事务修改,可见 trx_id < min_trx_id→ 事务在 Read View 创建前已提交,可见trx_id >= max_trx_id→ 事务在 Read View 创建后才开始,不可见min_trx_id <= trx_id < max_trx_id且trx_id not in m_ids→ 已提交,可见;否则不可见
延伸思考
- RR 级别下为什么还会出现幻读?什么场景下会触发?
- MVCC 能否完全替代锁机制?哪些场景 MVCC 无法解决?
4. 锁机制:行锁、间隙锁与 Next-Key Lock
难度:⭐⭐⭐
在 RR 隔离级别下,假设表 t(id INT PRIMARY KEY, c INT, KEY idx_c(c)) 中有数据 (5,10), (10,20), (15,30)。执行以下 SQL 后,哪些范围会被锁住?其他事务能插入哪些值?
BEGIN;
SELECT * FROM t WHERE c = 20 FOR UPDATE;标准答案
在 RR 级别下,InnoDB 使用 Next-Key Lock(间隙锁 + 行锁)来防止幻读。
对 c = 20 的查询走索引 idx_c,加锁分析:
- Next-Key Lock on idx_c:锁住
(10, 20](左开右闭) - Gap Lock on idx_c:锁住
(20, 30)(防止在 20 之后插入新记录) - 最终锁定索引 c 的范围:(10, 30)(间隙部分)+ 行锁在 c=20 上
其他事务的插入情况:
c = 15→ 在 (10, 20) 间隙内 → 阻塞c = 25→ 在 (20, 30) 间隙内 → 阻塞c = 10→ 等于左边界 → Next-Key Lock (10,20] 不包含 10 的间隙 → 但 c=10 本身有行锁?实际 c=10 不在锁定行上 → 需要看 c=10 行是否有锁 → c=10 行本身未被 FOR UPDATE 锁定 → 但插入新行 c=10 需要看间隙 → (10,20] 间隙锁覆盖 → 新插入 c=10 会阻塞(因为插入位置在 (10,20) 之间)c = 5→ 不在任何锁定范围 → 允许c = 30→ 等于右边界 → 间隙锁 (20,30) 不包含 30 → 允许(30 行本身未被锁)c = 35→ 不在任何锁定范围 → 允许
延伸思考
- RC 级别下间隙锁还会生效吗?为什么?
- 意向锁(IS/IX)的作用是什么?它和行锁/表锁是什么关系?
5. SQL 优化:EXPLAIN 执行计划深度解读
难度:⭐⭐
请解读以下 EXPLAIN 输出中每个关键字段的含义,并指出可能的性能问题:
id: 1
select_type: SIMPLE
table: orders
type: ref
possible_keys: idx_user_status,idx_created
key: idx_user_status
key_len: 8
ref: const
rows: 15230
filtered: 10.00
Extra: Using where; Using filesort标准答案
- type: ref:使用非唯一索引等值匹配,性能尚可(全表扫描为 ALL,最优为 const/system)
- possible_keys: idx_user_status, idx_created:优化器考虑了两个索引
- key: idx_user_status:最终选择了
idx_user_status - key_len: 8:索引使用了 8 字节(user_id BIGINT = 8B,说明只用了联合索引的第一列)
- ref: const:与常量等值比较
- rows: 15230:预估扫描 15230 行
- filtered: 10.00:只有 10% 的行满足 WHERE 其余条件 → 实际满足条件的行约 1523 行
- Extra: Using where:回表后还需在 Server 层过滤(说明有非索引列条件)
- Extra: Using filesort:需要额外排序,这是性能瓶颈所在
性能问题:使用了 idx_user_status 但仍需 filesort,说明 ORDER BY 的列不在该索引中。可能需要创建包含排序列的联合索引来消除 filesort。
延伸思考
- 如何通过调整联合索引的列顺序同时满足 WHERE 过滤和 ORDER BY 排序?
filtered字段的值是估算的,优化器如何获取这个值?
6. 主从复制:GTID 与并行复制
难度:⭐⭐
MySQL 8.0 的主从复制相比 5.7 有哪些重要改进?请说明 GTID 复制的原理,以及基于 Writeset 的并行复制机制。
标准答案
GTID(Global Transaction Identifier):
格式 GTID = source_id:transaction_id,每个事务有全局唯一标识。核心优势:
- 主从切换时无需手动定位 binlog 位点
- 自动跳过已执行的事务,避免重复执行
- 简化复制拓扑管理
MySQL 8.0 复制改进:
- Writeset 并行复制:通过
binlog_transaction_dependency_tracking = WRITESET,基于行级别的修改冲突检测(而非库级别),实现更细粒度的并行回放。两个事务修改的行无交集 → 可并行执行 - GTID 持久化增强:
gtid_executed在事务提交时同步更新到 mysql.gtid_executed 表,崩溃恢复更可靠 - JSON 部分更新复制:
binlog_row_image = MINIMAL配合 JSON 部分更新,减少 binlog 体积 - 复制过滤器改进:支持在线修改复制过滤器,无需重启
Writeset 并行原理:
- 每个事务的 Writeset = 该事务修改的所有行的哈希集合
- 如果两个事务的 Writeset 无交集(无行冲突),则可以并行回放
- 相比 5.7 的库级并行(
DATABASE模式),极大提升了单库场景的并行度
延伸思考
- Writeset 模式下,如何处理存储过程和触发器引起的隐式依赖?
- 主从延迟如何监控和优化?
pt-heartbeat的原理是什么?
7. 分库分表:方案选型与跨分片查询
难度:⭐⭐⭐
在日订单量千万级的电商系统中,如何设计分库分表方案?请对比 Range 分片和 Hash 分片的优缺点,并说明跨分片 JOIN 和分页查询的解决方案。
标准答案
分片策略对比:
| 维度 | Range 分片(按时间/ID范围) | Hash 分片(取模) |
|---|---|---|
| 扩容 | 简单,新增分片即可 | 复杂,需要数据迁移(rehash) |
| 热点 | 易产生热点(近期数据访问集中) | 数据分布均匀 |
| 范围查询 | 高效,可定位到少量分片 | 差,需扫描所有分片 |
| 典型场景 | 日志、订单按时间归档 | 用户数据按用户ID打散 |
推荐方案:订单系统采用 Range + Hash 混合——按月 Range 分区(方便归档和范围查询),月内按 user_id Hash 分表(均匀分布)。
跨分片问题与方案:
跨分片 JOIN:
- 冗余字段(将关联数据冗余到分片内)
- 全局表(小表广播到所有分片)
- 应用层组装(先查各分片,内存中 JOIN)
- 中间件支持(ShardingSphere 的绑定表)
跨分片分页:
- 各分片取 Top N → 内存归并排序 → 全局分页(深度翻页性能差)
- 禁止深度翻页,改用游标翻页(基于上次最大 ID)
- 使用 ES 做二级索引,先从 ES 获取目标分片和主键,再回分片取详情
延伸思考
- 分库分表后如何保证分布式事务?Seata AT 模式与 TCC 模式各适用什么场景?
- 何时应该用分布式数据库(TiDB/OceanBase)替代分库分表?
8. 窗口函数:排名与累计计算
难度:⭐⭐
MySQL 8.0 新增了窗口函数,请用窗口函数实现以下需求:
对员工表
emp(dept, name, salary)查询每个部门薪资排名前三的员工,并计算每个部门的薪资累计占比(累计薪资/部门总薪资)。
标准答案
-- 每个部门薪资前三
WITH ranked AS (
SELECT dept, name, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM emp
)
SELECT dept, name, salary
FROM ranked
WHERE rn <= 3;
-- 部门薪资累计占比
SELECT dept, name, salary,
SUM(salary) OVER (PARTITION BY dept ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum_salary,
SUM(salary) OVER (PARTITION BY dept) AS dept_total,
ROUND(
SUM(salary) OVER (PARTITION BY dept ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
/ SUM(salary) OVER (PARTITION BY dept) * 100, 2
) AS cum_pct
FROM emp;关键点:
ROW_NUMBER():严格排名(1,2,3),RANK()有并列跳号(1,1,3),DENSE_RANK()不跳号(1,1,2)ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:定义窗口范围为从第一行到当前行,实现累计PARTITION BY:分组计算
延伸思考
ROWS和RANGE窗口框有什么区别?在处理相同值时行为如何不同?- 窗口函数的执行顺序在 SQL 查询生命周期中的哪个阶段?为什么 WHERE 不能过滤窗口函数结果?
9. CTE 与 JSON 支持
难度:⭐⭐
MySQL 8.0 的 CTE(Common Table Expression)相比子查询有什么优势?请用递归 CTE 实现一个组织架构树查询,并说明 MySQL 8.0 的 JSON 功能如何处理半结构化数据。
标准答案
CTE 优势:
- 可读性更强,逻辑分层清晰
- 递归 CTE 可处理层级/图结构数据(子查询无法递归)
- 同一 CTE 可在查询中多次引用(子查询需重复书写)
- 优化器可能对 CTE 做物化优化
递归 CTE 查询组织架构:
WITH RECURSIVE org_tree AS (
-- 锚点:顶层管理者
SELECT id, name, manager_id, 1 AS level, CAST(name AS CHAR(500)) AS path
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- 递归:逐层查找下属
SELECT e.id, e.name, e.manager_id, ot.level + 1,
CONCAT(ot.path, ' -> ', e.name)
FROM employees e
INNER JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT * FROM org_tree ORDER BY level, path;JSON 功能:
-- 创建含 JSON 列的表
CREATE TABLE products (
id INT PRIMARY KEY,
attrs JSON,
INDEX idx_color ((CAST(attrs->'$.color' AS CHAR(20))))
);
-- 插入与查询
INSERT INTO products VALUES (1, '{"color":"red","size":"L","tags":["new","hot"]}');
SELECT attrs->>'$.color' AS color FROM products WHERE attrs->'$.color' = '"red"';
-- JSON 多值索引(8.0.17+)
ALTER TABLE products ADD INDEX idx_tags (
(CAST(attrs->'$.tags' AS CHAR(20) ARRAY))
);
SELECT * FROM products WHERE 'hot' MEMBER OF (attrs->'$.tags');
-- JSON 部分更新(减少 binlog 体积)
UPDATE products SET attrs = JSON_SET(attrs, '$.color', 'blue') WHERE id = 1;延伸思考
- 递归 CTE 如何防止无限递归?
cte_max_recursion_depth的作用? - JSON 列上建索引有哪些方式?函数索引和多值索引各适用什么场景?
10. 性能调优:Performance Schema 与慢查询优化
难度:⭐⭐⭐
线上 MySQL 出现间歇性性能抖动,请描述一套完整的排查流程,包括 Performance Schema、Sys Schema、慢查询日志的综合使用,以及 InnoDB 关键监控指标。
标准答案
排查流程:
第一步:确认问题范围
-- 查看当前活跃连接和状态
SELECT * FROM sys.processlist WHERE command != 'Daemon';
-- 查看最近执行的语句事件
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile;第二步:Performance Schema 定位瓶颈
-- 开启等待事件采集
UPDATE performance_schema.setup_instruments SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'wait/%';
-- 查看顶级等待事件
SELECT event_name, count_star, sum_timer_wait/1000000000 AS total_sec
FROM performance_schema.events_waits_summary_global_by_event_name
ORDER BY sum_timer_wait DESC LIMIT 10;
-- 查看文件 I/O 热点
SELECT file_name, count_read, count_write,
sum_number_of_bytes_read/1024/1024 AS read_mb
FROM performance_schema.file_summary_by_instance
ORDER BY sum_number_of_bytes_read DESC LIMIT 10;第三步:慢查询分析
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 1秒阈值
-- 用 mysqldumpslow 或 pt-query-digest 分析
-- pt-query-digest /var/lib/mysql/slow.log第四步:InnoDB 关键指标
-- InnoDB 状态
SHOW ENGINE INNODB STATUS\G
-- 关键指标解读:
-- BUFFER POOL:hit rate(命中率应 > 99%),free wait(等空闲页)
-- ROW OPERATIONS:每秒读写行数
-- SEMAPHORES:有无长时间等待的自旋锁
-- LOG:log flush 频率,checkpoint age第五步:常见抖动原因与对策
| 原因 | 排查方法 | 解决方案 |
|---|---|---|
| 脏页刷盘 | Innodb_buffer_pool_wait_dirty | 调整 innodb_io_capacity,平滑刷脏 |
| Redo Log 写满 | Innodb_log_waits | 增大 innodb_log_file_size |
| 自适应哈希锁争用 | rwlock: adaptive_hash 等待高 | 关闭 AHP innodb_adaptive_hash_index=OFF |
| 大查询淘汰 Buffer Pool | LRU 冷热比例异常 | 优化查询,增大 Buffer Pool |
| 统计信息更新 | Innodb_stats_persistent | 手动 ANALYZE TABLE,低峰期自动 |
延伸思考
- MySQL 8.0 的
performance_schema默认开启,对性能有多大影响?如何选择性开启? - 如何利用
sys库的视图快速定位问题?sys.innodb_buffer_stats_by_table能告诉你什么?