Skip to content

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 能否命中覆盖索引?为什么?

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):

  1. 版本的 trx_id == creator_trx_id → 自身事务修改,可见
  2. trx_id < min_trx_id → 事务在 Read View 创建前已提交,可见
  3. trx_id >= max_trx_id → 事务在 Read View 创建后才开始,不可见
  4. 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 后,哪些范围会被锁住?其他事务能插入哪些值?

sql
BEGIN;
SELECT * FROM t WHERE c = 20 FOR UPDATE;

标准答案 ​

在 RR 级别下,InnoDB 使用 Next-Key Lock(间隙锁 + 行锁)来防止幻读。

对 c = 20 的查询走索引 idx_c,加锁分析:

  1. Next-Key Lock on idx_c:锁住 (10, 20](左开右闭)
  2. Gap Lock on idx_c:锁住 (20, 30)(防止在 20 之后插入新记录)
  3. 最终锁定索引 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 复制改进:

  1. Writeset 并行复制:通过 binlog_transaction_dependency_tracking = WRITESET,基于行级别的修改冲突检测(而非库级别),实现更细粒度的并行回放。两个事务修改的行无交集 → 可并行执行
  2. GTID 持久化增强:gtid_executed 在事务提交时同步更新到 mysql.gtid_executed 表,崩溃恢复更可靠
  3. JSON 部分更新复制:binlog_row_image = MINIMAL 配合 JSON 部分更新,减少 binlog 体积
  4. 复制过滤器改进:支持在线修改复制过滤器,无需重启

Writeset 并行原理:

  • 每个事务的 Writeset = 该事务修改的所有行的哈希集合
  • 如果两个事务的 Writeset 无交集(无行冲突),则可以并行回放
  • 相比 5.7 的库级并行(DATABASE 模式),极大提升了单库场景的并行度

延伸思考 ​

  • Writeset 模式下,如何处理存储过程和触发器引起的隐式依赖?
  • 主从延迟如何监控和优化?pt-heartbeat 的原理是什么?

7. 分库分表:方案选型与跨分片查询 ​

难度:⭐⭐⭐

在日订单量千万级的电商系统中,如何设计分库分表方案?请对比 Range 分片和 Hash 分片的优缺点,并说明跨分片 JOIN 和分页查询的解决方案。

标准答案 ​

分片策略对比:

维度Range 分片(按时间/ID范围)Hash 分片(取模)
扩容简单,新增分片即可复杂,需要数据迁移(rehash)
热点易产生热点(近期数据访问集中)数据分布均匀
范围查询高效,可定位到少量分片差,需扫描所有分片
典型场景日志、订单按时间归档用户数据按用户ID打散

推荐方案:订单系统采用 Range + Hash 混合——按月 Range 分区(方便归档和范围查询),月内按 user_id Hash 分表(均匀分布)。

跨分片问题与方案:

  1. 跨分片 JOIN:

    • 冗余字段(将关联数据冗余到分片内)
    • 全局表(小表广播到所有分片)
    • 应用层组装(先查各分片,内存中 JOIN)
    • 中间件支持(ShardingSphere 的绑定表)
  2. 跨分片分页:

    • 各分片取 Top N → 内存归并排序 → 全局分页(深度翻页性能差)
    • 禁止深度翻页,改用游标翻页(基于上次最大 ID)
    • 使用 ES 做二级索引,先从 ES 获取目标分片和主键,再回分片取详情

延伸思考 ​

  • 分库分表后如何保证分布式事务?Seata AT 模式与 TCC 模式各适用什么场景?
  • 何时应该用分布式数据库(TiDB/OceanBase)替代分库分表?

8. 窗口函数:排名与累计计算 ​

难度:⭐⭐

MySQL 8.0 新增了窗口函数,请用窗口函数实现以下需求:

对员工表 emp(dept, name, salary) 查询每个部门薪资排名前三的员工,并计算每个部门的薪资累计占比(累计薪资/部门总薪资)。

标准答案 ​

sql
-- 每个部门薪资前三
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 查询组织架构:

sql
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 功能:

sql
-- 创建含 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 关键监控指标。

标准答案 ​

排查流程:

第一步:确认问题范围

sql
-- 查看当前活跃连接和状态
SELECT * FROM sys.processlist WHERE command != 'Daemon';

-- 查看最近执行的语句事件
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile;

第二步:Performance Schema 定位瓶颈

sql
-- 开启等待事件采集
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;

第三步:慢查询分析

sql
-- 开启慢查询日志
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 关键指标

sql
-- 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 PoolLRU 冷热比例异常优化查询,增大 Buffer Pool
统计信息更新Innodb_stats_persistent手动 ANALYZE TABLE,低峰期自动

延伸思考 ​

  • MySQL 8.0 的 performance_schema 默认开启,对性能有多大影响?如何选择性开启?
  • 如何利用 sys 库的视图快速定位问题?sys.innodb_buffer_stats_by_table 能告诉你什么?