Skip to content

MySQL 8.0 面试题精选 ​

生成时间:2026-06-07 22:22 | 共 10 题


1. InnoDB 架构:Buffer Pool 的多实例拆分机制 ​

难度:⭐⭐

题目: MySQL 8.0 中 InnoDB Buffer Pool 支持多实例拆分,请说明其原理、配置方式及为什么要引入这个机制。

标准答案:

InnoDB Buffer Pool 是一块用于缓存数据和索引的内存区域。在 MySQL 5.5 之前只有单实例,并发访问时需要争抢一把全局 mutex,高并发下成为瓶颈。MySQL 5.6 引入了多实例拆分机制(innodb_buffer_pool_instances),将 Buffer Pool 拆分为多个独立实例,每个实例有自己的锁、LRU 链表和 flush 链表,从而减少锁竞争。

配置方式:

  • innodb_buffer_pool_size:总 Buffer Pool 大小
  • innodb_buffer_pool_instances:实例数(默认 1,当 Buffer Pool ≥ 1GB 时建议设为实例数)
  • 每个实例大小 = innodb_buffer_pool_size / innodb_buffer_pool_instances

关键要点:

  • 只有当 Buffer Pool 大于 1GB 时,设置多个实例才有意义
  • MySQL 8.0 还支持在线调整 Buffer Pool 大小(innodb_buffer_pool_chunk_size 控制增量步长),无需重启
  • 每个实例内部维护独立的 LRU(old sublist + young sublist)和 flush 链表

延伸思考: 如果一个热点数据页恰好被哈希到同一个实例,是否会形成新的瓶颈?如何通过 innodb_buffer_pool_size 和 chunk_size 的配合来优化在线缩放的性能?


2. 索引优化:覆盖索引与索引下推(ICP) ​

难度:⭐⭐

题目: 请解释覆盖索引和索引下推(Index Condition Pushdown, ICP)的原理,并说明两者在查询优化中的协同关系。

标准答案:

覆盖索引:当查询所需的所有列都包含在索引中时,InnoDB 可以直接从索引树返回数据,无需回表查询聚簇索引。通过 EXPLAIN 中 Extra 列显示 Using index 来判断。

索引下推(ICP):MySQL 5.6 引入,用于优化联合索引的"最左前缀失效"场景。正常情况下,如果 WHERE 条件中有索引列无法利用最左前缀(如联合索引 (a, b) 查询 a LIKE 'xxx%' AND b = 2),存储引擎只会用 a LIKE 'xxx%' 从索引中定位记录,然后回表判断 b = 2。ICP 将 b = 2 这个条件下推到存储引擎层,在索引遍历时就进行过滤,减少回表次数。

协同关系:

  • 覆盖索引消除了回表,ICP 减少了回表次数
  • 当覆盖索引生效时,ICP 不会触发(因为根本不需要回表)
  • 两者是互补的优化手段:覆盖索引是"不用回表",ICP 是"少回表"

EXPLAIN 判断:

  • 覆盖索引:Extra: Using index
  • ICP:Extra: Using index condition

延伸思考: 在联合索引 (a, b, c) 中,查询 WHERE a > 1 AND b = 2,ICP 如何工作?如果把索引改为 (a, c, b) 呢?


3. 事务隔离级别与 MVCC 机制 ​

难度:⭐⭐⭐

题目: MySQL 8.0 默认隔离级别是什么?请详细描述 InnoDB 在 RR(可重复读)级别下 MVCC 的实现机制,包括 Read View 的创建时机、undo log 版本链的构造方式,以及 RR 和 RC 级别的核心区别。

标准答案:

MySQL 8.0 默认隔离级别为 REPEATABLE READ(RR)。

MVCC 核心机制:

  1. 隐藏列:每行记录包含 DB_TRX_ID(最近修改的事务ID)、DB_ROLL_PTR(回滚指针指向 undo log)、DB_ROW_ID(隐藏自增ID)

  2. Undo Log 版本链:每次更新操作会将旧值写入 undo log,通过 DB_ROLL_PTR 串成版本链。版本链从最新版本指向最老版本,形成 当前数据 → undo log v2 → undo log v1 → ... 的链式结构。

  3. Read View:事务进行快照读时创建的一致性视图,包含四个关键字段:

    • m_ids:生成 Read View 时当前所有活跃(未提交)事务的 ID 列表
    • min_trx_id:m_ids 中的最小值
    • max_trx_id:下一个即将分配的事务 ID(即当前最大事务 ID + 1)
    • creator_trx_id:创建该 Read View 的事务 ID
  4. 可见性判断规则(沿版本链遍历):

    • DB_TRX_ID == creator_trx_id → 自己修改的,可见
    • DB_TRX_ID < min_trx_id → 事务已提交,可见
    • DB_TRX_ID >= max_trx_id → 事务在 Read View 之后开始,不可见
    • min_trx_id <= DB_TRX_ID < max_trx_id 且不在 m_ids 中 → 已提交,可见;在 m_ids 中 → 未提交,不可见,沿版本链继续找

RR vs RC 的核心区别:

  • RR:Read View 在事务第一次执行 SELECT 时创建,整个事务期间复用同一个 Read View,所以所有快照读结果一致
  • RC:每次 SELECT 都创建新的 Read View,所以同一事务内可能读到其他事务已提交的新数据

延伸思考: RR 级别下,事务 A 先快照读后当前读(SELECT ... FOR UPDATE),会读到什么数据?为什么 RR 不能完全防止幻读?InnoDB 是如何通过 Next-Key Lock 补充解决幻读问题的?


4. 锁机制:行锁、间隙锁、临键锁与意向锁 ​

难度:⭐⭐⭐

题目: 请详细说明 InnoDB 中行锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock)和意向锁(Intention Lock)的作用、加锁规则,以及在 RR 级别下的加锁场景分析。

标准答案:

四种锁类型:

锁类型作用粒度
Record Lock锁定索引上的单条记录行级
Gap Lock锁定索引记录之间的间隙,防止其他事务在间隙中插入间隙级
Next-Key LockRecord Lock + Gap Lock 的组合(左开右闭区间)行+间隙
Intention Lock表级锁,表示事务即将对表中的行加行锁表级

意向锁:

  • 意向共享锁(IS):事务打算加行共享锁前,先加表级 IS 锁
  • 意向排他锁(IX):事务打算加行排他锁前,先加表级 IX 锁
  • 目的:快速判断表级锁与行级锁是否冲突,无需逐行检查
  • 意向锁之间互不冲突,只与表级锁(S/X)冲突

RR 级别下加锁规则(丁奇总结的两大原则 + 两个优化 + 一个 bug):

两大原则:

  1. 加锁的基本单位是 Next-Key Lock
  2. 查找过程中访问到的对象才会加锁

两个优化:

  1. 等值查询,唯一索引,Next-Key Lock 退化为 Record Lock
  2. 等值查询,向右遍历到最后一个不满足条件的值时,Next-Key Lock 退化为 Gap Lock

场景分析(索引有值 5, 10, 15):

  • SELECT * FROM t WHERE id = 10 FOR UPDATE(id 是主键)→ 只加 Record Lock 在 id=10
  • SELECT * FROM t WHERE id = 10 FOR UPDATE(id 是普通索引)→ 加 Next-Key Lock (5,10] + Gap Lock (10,15)
  • SELECT * FROM t WHERE id > 10 FOR UPDATE → 加 Next-Key Lock (10,15] + (15,+∞)

延伸思考: 在 RC 级别下,Gap Lock 是否生效?为什么 RC 级别不需要 Gap Lock?如果业务上不会出现幻读问题,是否应该选择 RC 级别以减少锁冲突?


5. SQL 优化:EXPLAIN 执行计划深度解读 ​

难度:⭐⭐

题目: 请解读 EXPLAIN 输出中 type、key、rows、filtered、Extra 五个关键字段的含义,并给出三种典型的"看似用了索引实则性能很差"的场景。

标准答案:

字段含义
type访问类型,从优到差:system > const > eq_ref > ref > range > index > ALL
key实际使用的索引名,NULL 表示未用索引
rows预估需要扫描的行数
filtered存储引擎返回的数据在 Server 层过滤后剩余的百分比(100% 最好)
Extra额外信息:Using index(覆盖索引)、Using filesort(额外排序)、Using temporary(临时表)等

三种"看似用索引实则性能差"的场景:

  1. 索引列参与函数/运算:WHERE YEAR(create_time) = 2026 → 索引失效,全表扫描。应改为 WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01'

  2. 隐式类型转换:WHERE varchar_col = 123 → MySQL 会将 varchar 列转换为数字进行比较,索引失效。应保持类型一致:WHERE varchar_col = '123'

  3. 索引选择性低但回表代价高:联合索引 (status, name) 查询 WHERE status = 1,如果 status=1 占 90% 的数据,即使用了索引,也需要大量回表,rows × (1 - filtered/100) 的实际回表数可能比全表扫描还慢

延伸思考: Using filesort 是否一定意味着性能差?什么情况下 filesort 比索引排序更优?filtered 字段在什么场景下参考价值有限?


6. 主从复制:GTID 复制与并行复制 ​

难度:⭐⭐

题目: 请对比 MySQL 传统复制和 GTID 复制的原理差异,并说明 MySQL 8.0 中基于 Writeset 的并行复制机制如何提升从库回放性能。

标准答案:

传统复制 vs GTID 复制:

对比项传统复制GTID 复制
定位方式binlog 文件名 + position全局唯一事务ID(server_uuid:transaction_id)
主从切换需手动定位位点,易出错自动寻找复制位点,自动故障恢复
数据一致性依赖 DBA 经验从库可自动跳过已执行事务
配置复杂度需要同步位点信息MASTER_AUTO_POSITION=1 即可

GTID 格式:server_uuid:1-100(来源于某个实例的第1到第100个事务)

MySQL 8.0 Writeset 并行复制:

传统并行复制的局限:

  • Database 并行:只有不同库的事务才能并行,单库场景退化为串行
  • Group Commit 并行:同一组提交的事务可并行,但粒度粗

Writeset 并行复制原理:

  1. 主库在 binlog 中记录每行修改的 writeset(基于行数据的哈希值)
  2. 从库在回放时,检查事务的 writeset 是否与已提交事务的 writeset 有交集
  3. 无交集的事务可以安全并行回放
  4. 核心参数:
    • binlog_transaction_dependency_tracking = WRITESET(8.0 默认)
    • transaction_write_set_extraction = XXHASH64
    • binlog_transaction_dependency_history_size:writeset 历史窗口大小

延伸思考: Writeset 并行在什么场景下会退化为串行?如果两张表有外键关系,Writeset 能否正确判断依赖?


7. 分库分表:分片策略与跨分片查询 ​

难度:⭐⭐⭐

题目: 请对比水平分表和垂直分表的适用场景,说明常见的分片键选择策略,以及跨分片 JOIN、排序和聚合查询的解决方案。

标准答案:

水平 vs 垂直分表:

维度垂直分表水平分表
切分方式按列拆分(冷热分离)按行拆分(数据分散)
适用场景宽表、大字段低频访问单表数据量过大
典型案例商品详情拆为 base + detail订单按 user_id 分到 16 张表
跨表问题主要在 JOINJOIN + 聚合 + 排序 + 分页

分片键选择策略:

  • Range 分片:按时间/ID 范围,易产生热点(新数据集中写入)
  • Hash 分片:shard = hash(key) % N,数据分布均匀但扩容困难
  • 一致性 Hash:扩缩容只影响相邻节点,数据迁移量小
  • 基因法:将分片键信息"编码"进主键,便于反查分片位置

跨分片查询方案:

  1. 跨分片 JOIN:

    • 冗余字段(反范式),避免 JOIN
    • 全局表(广播表),每个分片都有一份副本
    • 应用层组装:分别查询后在内存中 JOIN
  2. 跨分片排序/分页:

    • 各分片取 TOP N → 内存归并排序
    • 禁止深分页(LIMIT 100000, 10),改用游标分页
  3. 跨分片聚合:

    • SUM/COUNT:各分片聚合后汇总
    • AVG:需同时聚合 SUM 和 COUNT 再计算,不能简单求平均值
    • DISTINCT:需去重合并

延伸思考: 分库分表后如何实现分布式事务?Seata AT 模式和 TCC 模式各自的优缺点?在什么业务量级下才应该考虑分库分表?


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

难度:⭐⭐

题目: 请说明 MySQL 8.0 窗口函数的语法结构,对比 ROW_NUMBER()、RANK()、DENSE_RANK() 三种排名函数的差异,并给出一个累计求和的实战示例。

标准答案:

窗口函数语法:

sql
窗口函数名() OVER (
  [PARTITION BY 分组列]
  [ORDER BY 排序列 [ASC|DESC]]
  [ROWS/RANGE 窗口帧定义]
)

三种排名函数对比:

函数说明示例(分数 90,90,85)
ROW_NUMBER()连续编号,不重复1, 2, 3
RANK()并列跳号1, 1, 3
DENSE_RANK()并列不跳号1, 1, 2

累计求和示例:

sql
-- 按部门分组,按日期排序,计算薪资累计求和
SELECT
  dept_id,
  emp_name,
  salary,
  pay_date,
  SUM(salary) OVER (
    PARTITION BY dept_id
    ORDER BY pay_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS cumulative_salary
FROM employee_salary;

窗口帧关键字:

  • UNBOUNDED PRECEDING:分区第一行
  • CURRENT ROW:当前行
  • N PRECEDING / N FOLLOWING:前/后 N 行
  • 默认帧:RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW(含同级行)

延伸思考: ROWS 和 RANGE 窗口帧在处理相同排序值时有什么区别?如果要在窗口函数中实现"环比增长率",SQL 应该怎么写?


9. CTE(公用表表达式)与递归查询 ​

难度:⭐⭐

题目: 请说明 MySQL 8.0 中 CTE(WITH 子句)的语法,对比非递归 CTE 和递归 CTE 的区别,并给出一个使用递归 CTE 查询组织树形结构的完整示例。

标准答案:

非递归 CTE:

sql
WITH dept_avg AS (
  SELECT dept_id, AVG(salary) AS avg_sal
  FROM employees
  GROUP BY dept_id
)
SELECT e.name, e.salary, d.avg_sal
FROM employees e
JOIN dept_avg d ON e.dept_id = d.dept_id
WHERE e.salary > d.avg_sal;
  • 类似派生表(子查询),但可引用多次、可读性更好
  • 作用域仅限当前语句

递归 CTE:

sql
WITH RECURSIVE org_tree AS (
  -- 锚点成员:根节点
  SELECT id, name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- 递归成员:逐层展开
  SELECT e.id, e.name, e.manager_id, t.level + 1
  FROM employees e
  JOIN org_tree t ON e.manager_id = t.id
)
SELECT * FROM org_tree ORDER BY level, id;

递归 CTE 结构:

  1. 锚点成员:非递归的初始查询(种子数据)
  2. UNION ALL:连接锚点和递归部分(必须用 UNION ALL,不能用 UNION)
  3. 递归成员:引用 CTE 自身,每次迭代基于上一次结果
  4. 终止条件:递归成员返回空结果集时自动停止

安全限制:

  • cte_max_recursion_depth:最大递归深度(默认 1000)
  • 递归成员不能含 GROUP BY、聚合函数、DISTINCT、LIMIT、ORDER BY、窗口函数

延伸思考: 递归 CTE 能否实现"向上追溯"(从叶节点查到根节点)?如何控制递归深度避免无限循环?在性能上,递归 CTE 和应用层递归相比各有什么优劣?


10. JSON 支持与性能调优实战 ​

难度:⭐⭐⭐

题目: 请说明 MySQL 8.0 对 JSON 的原生支持能力(数据类型、索引、函数),并解释 JSON_TABLE 函数的使用场景。最后给出一个 InnoDB 性能调优的核心参数清单及调优思路。

标准答案:

JSON 原生支持:

  1. JSON 数据类型:

    • JSON 列自动校验 JSON 格式合法性
    • 存储时转换为二进制格式(部分有序),读取时无需重新解析
    • 8.0 新增 JSON 列的默认值支持、部分更新优化
  2. JSON 索引:

    • JSON 列本身不支持直接建索引
    • 通过 Generated Column + 索引 间接实现:
      sql
      ALTER TABLE t ADD COLUMN name_vc VARCHAR(100)
        GENERATED ALWAYS AS (JSON_UNQUOTE(data->'$.name')) STORED,
        ADD INDEX idx_name(name_vc);
    • MySQL 8.0.17+ 支持 Multi-Valued Index(多值索引):
      sql
      CREATE INDEX idx_tags ON t(
        (CAST(data->'$.tags' AS CHAR(32) ARRAY))
      );
  3. 常用 JSON 函数:

    • 提取:JSON_EXTRACT()、->、->>
    • 修改:JSON_SET()、JSON_REPLACE()、JSON_INSERT()、JSON_REMOVE()
    • 查询:JSON_CONTAINS()、JSON_SEARCH()、JSON_KEYS()
    • 聚合:JSON_ARRAYAGG()、JSON_OBJECTAGG()
  4. JSON_TABLE(8.0 新增):将 JSON 数组展开为关系表

    sql
    SELECT jt.name, jt.age
    FROM orders,
      JSON_TABLE(items, '$[*]' COLUMNS(
        name VARCHAR(100) PATH '$.name',
        age INT PATH '$.age'
      )) AS jt;

InnoDB 性能调优核心参数:

参数建议值说明
innodb_buffer_pool_size物理内存 60-80%最重要的参数,缓存数据和索引
innodb_buffer_pool_instancesBuffer Pool ≥ 1GB 时设为 4-8减少锁竞争
innodb_log_file_size1-4GBredo log 大小,影响检查点频率
innodb_flush_log_at_trx_commit1(安全)/ 2(折中)1=每次提交刷盘,2=每秒刷盘
innodb_io_capacitySSD: 10000-20000后台刷新页的能力
innodb_io_capacity_maxSSD: 20000-40000IO 能力上限
innodb_flush_methodO_DIRECT避免 OS 缓存双重缓冲
innodb_adaptive_hash_index按需开关高并发点查开启,范围扫描关闭
innodb_print_all_deadlocksON记录死锁日志便于排查

调优思路:

  1. 先看慢查询日志(slow_query_log),定位瓶颈 SQL
  2. 用 EXPLAIN 分析执行计划,优先优化索引
  3. 调整 Buffer Pool 确保热数据常驻内存
  4. SSD 场景下提高 IO 能力参数
  5. 监控 Innodb_buffer_pool_wait_free,若 > 0 说明 Buffer Pool 不够

延伸思考: JSON 部分更新(JSON_SET)在什么条件下才能真正实现 inplace 更新而不重写整个 JSON 文档?多值索引在什么查询模式下性能最佳?如何判断业务应该用 JSON 列还是拆分为关系表?


📌 复习建议:第 3、4、7 题为高频高难度题,建议结合实际 SQL 在测试环境中动手验证锁行为和 MVCC 现象。