thumbnail
MySQL

MySQL 面试宝典

适用对象: 初级 ~ 中级后端开发,适配大厂面试、求职复习、刷题背诵。
使用建议: 先理解”深度理解” → 再记”面试总结” → 口述时用自己的话串起来。


目录

  1. 一、MySQL 基础概念
  2. 二、数据库设计 & 索引
  3. 三、SQL 优化
  4. 四、事务 & 锁机制
  5. 五、高阶特性
  6. 六、运维 & 故障排查

一、MySQL 基础概念

1.1 SQL 基础

Q1:SQL 语句的完整执行顺序是什么?为什么是这个顺序? ⭐⭐⭐

1
FROM → ON → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT

记忆口诀:法(FROM)师(WHERE)哥(GROUP BY)哈(HAVING)选(SELECT)弟(DISTINCT)弟(ORDER BY)了(LIMIT)

深度理解 —— 每一步为什么在这个位置:

这个顺序不是随意规定的,而是由数据流的逻辑依赖决定的——上一步的输出是下一步的输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
┌──────────────────────────────────────────────────────────────┐
│ ① FROM + ON + JOIN — 先确定"数据从哪来" │
│ FROM 产生笛卡尔积 → ON 过滤符合条件的行 → JOIN 决定内/外连接│
│ 为什么在最前?没数据源,后面全是空操作 │
├──────────────────────────────────────────────────────────────┤
│ ② WHERE — 行级过滤,提前瘦身 │
│ 对 FROM 产出的原始行逐行判断,干掉不符合条件的行 │
│ 为什么在 GROUP BY 之前?先过滤再分组 = 计算量小 = 快 │
│ 如果把 WHERE 放到 GROUP BY 后面 → 白白多分了很多组 │
├──────────────────────────────────────────────────────────────┤
│ ③ GROUP BY — 按列折叠成组 │
│ 把 WHERE 筛选后的行,按 GROUP BY 列的值分组 │
│ 分组是聚合的前提:没分组 → SUM/AVG 就没法算 │
├──────────────────────────────────────────────────────────────┤
│ ④ HAVING — 组级过滤,二次瘦身 │
│ 对分组后的聚合结果做过滤,如 HAVING COUNT(*) > 3 │
│ 为什么不是 WHERE 来做?因为 WHERE 执行时还没分组, │
│ 聚合函数 COUNT/SUM/AVG 的结果还不存在 │
├──────────────────────────────────────────────────────────────┤
│ ⑤ SELECT — 决定输出哪些列 │
│ 此时才选列!也解释了三个现象: │
│ · WHERE 不能引用 SELECT 的别名(SELECT 还没执行) │
│ · ORDER BY 可以引用别名(SELECT 已执行完) │
│ · SELECT 中写的聚合函数在 GROUP BY 阶段就已计算好了 │
├──────────────────────────────────────────────────────────────┤
│ ⑥ DISTINCT — 对选出的列做去重 │
│ 必须在 SELECT 之后,因为去重基于的是最终展示的列 │
│ 如果某行被 SELECT 丢弃了,那它也不需要参与去重 │
├──────────────────────────────────────────────────────────────┤
│ ⑦ ORDER BY — 对最终结果排序 │
│ 为什么能用别名?因为 SELECT 在它前面执行完了 │
│ 排序是输出前的倒数第二步——必须先定好列再排序 │
├──────────────────────────────────────────────────────────────┤
│ ⑧ LIMIT — 截取需要的行数 │
│ 为什么在最后?只有排好序的结果,截取"前 N 条"才有意义 │
│ 没排序就截 = 截到的是随机行,不是你想要的前 N 条 │
└──────────────────────────────────────────────────────────────┘

⚠️ 易错点: SELECT 的别名只能在 ORDER BY 中使用,WHERE / HAVING / GROUP BY 中不能用,因为它们都在 SELECT 之前执行。

延伸追问:

追问答案
“为什么 WHERE 不能用聚合函数?”WHEREGROUP BY 之前,还没分组,聚合函数没有计算对象
“为什么 ORDER BY 能用别名?”因为 ORDER BYSELECT 之后执行,别名已经定义好了
ONWHERE 在 JOIN 中有什么区别?”内连接时效果相同;外连接时 ON 决定”哪些行能匹配上”,WHERE 在匹配完后做行级过滤,会去掉外连接补的 NULL 行

🎯 面试总结:

“SQL 执行顺序是 FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT。这个顺序的核心逻辑是先取数据、再逐层过滤、最后输出——每步都依赖上一步的结果。理解这个顺序,就能解释为什么 WHERE 不能用别名、为什么 WHERE 不能用聚合函数、为什么 ORDER BY 能用别名。”


Q2:WHEREHAVING 的区别? ⭐⭐

对比维度WHEREHAVING
执行时机GROUP BY 之前GROUP BY 之后
作用对象原始行数据分组后的聚合结果
能否用聚合函数❌ 不能✅ 可以
能否用列别名❌ 不能(SELECT 未执行)❌ 不能(标准 SQL;MySQL 扩展允许但别用)
性能优先过滤,效率高在分组后过滤,数据量通常较小但也有开销

深度理解 —— 为什么需要两个过滤条件?

WHEREHAVING 分工不同,本质是时间差WHERE 生活在”分组前”的原始世界里,HAVING 生活在”分组后”的聚合世界里。比如你要找”订单数 > 5 的用户”——你得先按用户分组、算出每个用户的订单数(COUNT),然后才能判断 COUNT 是否 > 5。WHERE 活在分组前,它根本没见过 COUNT 这个数字,所以这个条件只能 HAVING 来做。

为什么 MySQL 允许 HAVING 中用别名而标准 SQL 不允许?

MySQL 对 HAVING 做了语法扩展,允许 HAVING cnt > 5(cnt 是 SELECT 别名)。这是非标准行为,跨数据库会出问题,不推荐在生产中使用

🎯 面试总结:

WHERE 过滤行,HAVING 过滤组。能用 WHERE 就别用 HAVING——WHERE 在分组前过滤,减少分组计算量,效率高。只有涉及聚合函数的条件才必须用 HAVING。”


Q3:常用的聚合函数有哪些?有什么要注意的?

函数作用忽略 NULL易错点
COUNT(*)统计所有行数(含 NULL)❌ 不忽略COUNT(1) 性能相同
COUNT(列名)统计该列非 NULL 值行数✅ 忽略结果 ≤ COUNT(*)
COUNT(DISTINCT 列)去重后计数✅ 忽略不能和 COUNT(*) 一起用
SUM()求和✅ 忽略 NULL整列都是 NULL 时返回 NULL(不是 0)
AVG()平均值✅ 忽略 NULL分母不含 NULL 行
MAX() / MIN()最大 / 最小值✅ 忽略 NULL全 NULL 返回 NULL
GROUP_CONCAT()分组拼接字符串✅ 忽略 NULL默认最大长度 1024 字节

深度理解 —— COUNT(*) vs COUNT(列) 的性能差异:

COUNT(*) 在 InnoDB 中不会去读数据行,优化器会选择最小的二级索引来扫描计数。因为二级索引叶子页只存索引列+主键,比聚簇索引(完整行)小得多,扫描更快。COUNT(1)COUNT(*) 完全等价——优化器会将它们统一处理。

COUNT(列名) 则需要检查该列的每一行是否为 NULL,比 COUNT(*) 多做一步判断,并且——如果该列没有索引——可能被迫走聚簇索引全扫。

🎯 面试总结:

“用 COUNT(*) 就行,不用纠结 COUNT(1)COUNT(列名)COUNT(*) 优化器会自动选最小索引扫描,性能最优。唯一要区分的是——当你想统计非 NULL 的行数时用 COUNT(列名),否则一律 COUNT(*)。”


Q4:MySQL 中如何实现分页?深分页为什么慢? ⭐⭐

1
2
3
4
5
-- 基础分页(小数据量)
SELECT * FROM users ORDER BY id LIMIT 20, 10; -- 跳过 20 条,取 10 条

-- 游标分页(大数据量,推荐)
SELECT * FROM users WHERE id > 1000 ORDER BY id LIMIT 10;
方式适用场景缺点
LIMIT offset, size小数据量大 offset 时极慢,会扫描并丢弃前 N 行
游标分页(WHERE id > last_id大数据量、无限滚动需要连续主键,不能跳页
子查询覆盖分页中小数据量写法复杂

深度理解 —— 为什么 LIMIT 1000000, 10 很慢?

MySQL 处理 LIMIT offset, size 时,会先扫描 offset + size 行,然后丢弃前面的 offset 行。这意味着 LIMIT 1000000, 10 实际需要扫描 1000010 行,99.99% 的行被扫描后丢弃。这 100 万行对应的数据页都要从磁盘读到内存——全是无用工。

游标分页为什么快?WHERE id > last_id 直接走主键索引的范围查找,B+ 树定位到 last_id 的位置后,顺序往后读 10 行就结束,跳过的那部分完全不用扫描。

🎯 面试总结:

“深分页慢的核心原因是 LIMIT 需要扫描并丢弃大量行。优化方案有三个:首选游标分页(WHERE id > last_id),其次是用子查询先取主键 ID 再回表,最后是业务层直接禁止跳页太深。游标分页的本质是用索引范围查找替代全量扫描+丢弃。”


Q5:MySQL 中如何定义变量和函数?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 用户变量(会话级别,前缀 @)
SET @row_num := 0;
SELECT @row_num := @row_num + 1 AS rn, name FROM users;

-- 自定义函数(必须有返回值,不能有事务语句)
DELIMITER //
CREATE FUNCTION calc_age(birth DATE) RETURNS INT
DETERMINISTIC
BEGIN
RETURN TIMESTAMPDIFF(YEAR, birth, CURDATE());
END //
DELIMITER ;

-- 存储过程(可无返回值,可含事务,可输出参数)
DELIMITER //
CREATE PROCEDURE top_users(IN limit_n INT)
BEGIN
SELECT * FROM users ORDER BY score DESC LIMIT limit_n;
END //
DELIMITER ;
CALL top_users(10);
对比维度存储过程函数
返回值可选(通过 OUT 参数)必须有返回值
调用方式CALL proc()SELECT func() 或表达式中
事务控制允许 COMMIT/ROLLBACK不允许
适用场景批量操作、复杂业务逻辑计算、转换、格式化

深度理解 —— 为什么函数不能有事务语句?

函数通常在 SELECT 语句中被调用,而 SELECT 本身是一个只读操作。如果函数内部偷偷 COMMIT 或 ROLLBACK,会破坏外层语句的事务语义。MySQL 对此做了硬性限制。

🎯 面试总结:

“存储过程和函数的核心区别:函数必须返回一个值且不能用事务语句,适合做计算和转换;存储过程可以没有返回值、可以控制事务,适合封装批量操作。现代开发中存储过程逐渐被应用层代码替代,面试时讲清楚区别即可。”


Q6:UNIONUNION ALL 的区别? ⭐⭐

对比UNIONUNION ALL
去重✅ 去重(等价于 DISTINCT❌ 不去重
内部处理排序 + 比较去重(临时表)直接拼接
性能慢(O(n log n))快(O(n))

深度理解 —— 为什么 UNION 需要临时表?

UNION 的去重语义要求 MySQL 判断”这行有没有出现过”,而两个结果集是先后返回的,不可能在全部行出来之前完成去重。所以必须先把所有行写入临时表,然后排序、比较相邻行去重、最后输出。临时表 + 排序 = 双重开销。UNION ALL 不需要去重,直接逐行输出拼接结果。

🎯 面试总结:

“百分之九十的场景不需要去重,直接用 UNION ALLUNIONUNION ALL 的性能差距是数量级的——多了一次临时表创建和排序。面试时一定要强调’不需要去重就用 UNION ALL’。”


Q7:MySQL 中 NULL 值如何处理?对性能有什么影响? ⭐⭐

1
2
3
4
5
6
7
-- NULL 判断只能用 IS NULL / IS NOT NULL(不能用 = 或 <>)
SELECT * FROM users WHERE email IS NULL; -- ✅ 正确
SELECT * FROM users WHERE email = NULL; -- ❌ 永远返回空!NULL = NULL → NULL

-- NULL 参与运算,结果都是 NULL
SELECT 1 + NULL; -- → NULL
SELECT CONCAT('a', NULL); -- → NULL
影响维度说明
索引IS NULL 可以使用索引;IS NOT NULL 在高 NULL 比例下可能全表扫描
存储NULL 仅用 bitmap 中的 1 bit 标记,不占实际数据空间
运算任何值与 NULL 做运算结果都是 NULL
唯一约束NULL ≠ NULL,所以唯一索引允许多个 NULL 值
聚合函数COUNT(*) 外,所有聚合函数忽略 NULL 行

深度理解 —— 为什么 NULL = NULL 返回 NULL 而不是 TRUE?

SQL 的 NULL 语义遵循三值逻辑(TRUE / FALSE / UNKNOWN)。NULL 表示”未知值”。两个未知值是否相等?——“不知道”,所以返回 NULL(UNKNOWN)。WHERE 子句只保留条件为 TRUE 的行,FALSE 和 UNKNOWN(NULL)都会被过滤掉。

🎯 面试总结:

“建表时尽量用 NOT NULL DEFAULT,避免三值逻辑带来的隐式 bug。查询 NULL 必须用 IS NULL。NULL 在存储上几乎不占空间,真正的代价在于判断逻辑复杂、IS NOT NULL 可能不走索引。”


Q8:MySQL 的字符集与排序规则?

1
2
3
-- 建库/建表指定
CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE TABLE t (name VARCHAR(50)) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
概念说明
字符集(charset)编码规则:字符 → 二进制
排序规则(collation)比较规则:决定两个字符谁大谁小
_cicase insensitive,大小写不敏感('a' = 'A'
_bin二进制比较,区分大小写和重音
_general_ci vs _unicode_cigeneral 快但不精确,unicode 慢但准

⚠️ MySQL 的 utf8 是阉割版(最大 3 字节),存不了 emoji 和部分中文生僻字。必须用 utf8mb4(最大 4 字节)。

深度理解 —— 为什么 collation 会影响索引?

WHERE name = 'abc'utf8mb4_general_ci(不区分大小写)时,'abc''ABC' 被视为相同,索引可以正常命中。但如果查询 WHERE name = 'abc' COLLATE utf8mb4_bin,而索引是用 _general_ci 建的,索引无法匹配 → 全表扫描。字符集/排序规则不一致是 JOIN 性能问题的常见隐性原因。

🎯 面试总结:

“MySQL 的 UTF-8 不是真正的 UTF-8,必须用 utf8mb4。字符集和排序规则要全局统一,不一致会导致索引失效。排序规则选 _ci 还是 _bin 取决于业务是否需要区分大小写。”


1.2 视图

Q9:视图是什么?有什么作用?底层原理? ⭐⭐

1
2
CREATE VIEW v_active_users AS
SELECT id, name, email FROM users WHERE status = 1;
类型说明
普通视图仅存 SQL 定义,查询时实时从基表计算,不存数据
物化视图存储计算结果,查询快但需刷新,MySQL 不原生支持(需触发器模拟)

深度理解 —— 视图的两种执行算法:

MySQL 执行视图查询时有两种策略:

  • MERGE 算法:把视图的 SQL 和外部查询的 SQL 合并成一个 SQL,然后一起优化执行。这是最优方式——视图和基表在同一个执行计划中。
  • TEMPTABLE 算法:先把视图的结果物化到一个临时表,外部查询再对这个临时表查询。无法做索引优化,性能差。

简单视图(无聚合、无 DISTINCT、无子查询)走 MERGE,复杂视图被迫走 TEMPTABLE。这就是为什么多层嵌套视图性能急剧恶化的原因——每一层都可能产生临时表。

🎯 面试总结:

“视图本质是保存的 SQL 语句,不存数据。作用是简化复杂查询、做安全隔离。但它有两个坑:性能不确定(复杂视图走临时表),和 MySQL 不支持物化视图。生产环境中用视图做权限隔离可以,复杂查询建议直接写 SQL。”


1.3 触发器

Q10:触发器是什么?有哪些分类?为什么慎用? ⭐⭐

1
2
3
4
5
CREATE TRIGGER before_insert_user
BEFORE INSERT ON users FOR EACH ROW
BEGIN
SET NEW.created_at = NOW();
END;
分类说明典型用途
BEFORE INSERT插入前触发自动填充默认值、数据校验
AFTER INSERT插入后触发写入日志、更新统计表
BEFORE UPDATE更新前触发数据校验、自动修改时间
AFTER UPDATE更新后触发记录变更历史、同步缓存
BEFORE DELETE删除前触发防止误删、备份数据
AFTER DELETE删除后触发级联清理关联数据
  • NEW:引用 INSERT/UPDATE 之后的行
  • OLD:引用 UPDATE/DELETE 之前的行

深度理解 —— 为什么大厂普遍禁用触发器?

  1. 隐式开销:每插入一行就额外执行一段逻辑,批量插入 10 万行 = 10 万次触发器调用,性能雪崩
  2. 调试困难:触发器在数据库内部静默执行,应用层看不见,出了问题排查成本极高
  3. 死锁放大器:触发器可能修改其他表,引入未预期的锁竞争
  4. 可移植性差:换数据库(如迁移到 PostgreSQL)时触发器语法完全不同

🎯 面试总结:

“触发器是由 DML 事件自动触发的代码块。面试时坦白说知道它的用法,但在生产环境中倾向于把触发逻辑放到应用层——更可控、更好调试、更好维护。如果面试官追问’什么场景适合用’,答审计日志(与业务无关的旁路逻辑)。”


1.4 临时表

Q11:临时表是什么?与普通表有什么本质区别?

1
2
3
-- 临时表只对当前会话可见
CREATE TEMPORARY TABLE tmp_calc (id INT, val DECIMAL(10,2));
-- 会话结束自动删除
特性临时表普通表
可见性仅当前会话所有会话
生命周期会话断开时自动 DROP永久
同名冲突和普通表同名时,优先访问临时表
存储位置磁盘(InnoDB 临时表空间)或内存磁盘

深度理解 —— 临时表的两种存储引擎切换:

MySQL 优先在内存(TempTable 引擎)中创建临时表,超过 tmp_table_size / max_heap_table_size 后自动转磁盘(InnoDB 临时表空间)。这意味着——临时表不一定”快”,如果数据量超过内存阈值,转为磁盘临时表后会显著变慢。大结果集不要用临时表,用分批处理。

🎯 面试总结:

“临时表最大的好处是会话隔离,会话结束自动清理,适合存储复杂计算的中间结果。注意事项:大临时表会落盘,性能下降;和普通表同名时优先访问临时表可能引起隐蔽 bug。”


1.5 FULLTEXT 全文搜索

Q12:MySQL 的 FULLTEXT 全文搜索原理和用法? ⭐⭐

1
2
3
4
5
6
7
8
9
ALTER TABLE articles ADD FULLTEXT INDEX ft_title_body (title, body);

-- 自然语言模式(的相关性排序)
SELECT *, MATCH(title, body) AGAINST('MySQL 优化') AS relevance
FROM articles WHERE MATCH(title, body) AGAINST('MySQL 优化' IN NATURAL LANGUAGE MODE);

-- 布尔模式(支持 + - 操作符)
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('+MySQL -Oracle' IN BOOLEAN MODE);

深度理解 —— 为什么 FULLTEXT 比 LIKE '%keyword%' 快?

LIKE '%keyword%' 无法使用 B+ 树索引(左模糊失效),必须全表扫描。FULLTEXT 索引使用的是倒排索引:记录了”每个词出现在哪些文档的哪些位置”。搜索时直接查倒排索引就能定位文档,效率是天壤之别。

中文分词问题: InnoDB 默认的分词器是按空格分词的,中文没有空格,所以默认全文索引对中文无效。需要配置 ngram parser(MySQL 5.7.6+):WITH PARSER ngram,或者直接用 Elasticsearch。

🎯 面试总结:

“全文搜索底层是倒排索引,比 LIKE '%x%' 快 N 倍。中文必须配置 ngram 分词器。实际大项目中文搜索都走 Elasticsearch——MySQL 的全文索引只适合简单场景。”


1.6 空间数据类型

Q13:MySQL 的空间数据类型和空间索引?

类型用途
POINT坐标点
LINESTRING线
POLYGON多边形
GEOMETRY通用几何类型(可存任意几何对象)
1
2
3
4
5
-- LBS 附近搜索(查找 5km 内的门店)
SELECT *, ST_Distance_Sphere(location, ST_GeomFromText('POINT(120.15 30.28)')) AS dist
FROM stores
WHERE ST_Distance_Sphere(location, ST_GeomFromText('POINT(120.15 30.28)')) < 5000;
-- 建议给 location 列加 SPATIAL INDEX

深度理解 —— 为什么空间索引用 R-Tree 而不是 B+ 树?

B+ 树是一维排序(按值的线性大小排列),而地理位置是二维的(经纬度)。一维排序无法表达二维空间的”附近”关系——经度接近的不一定纬度也接近。R-Tree(矩形树)将空间划分为嵌套的矩形区域,搜索时只检查相交的矩形,天然适合二维范围查询。

🎯 面试总结:

“地理位置查询建 SPATIAL INDEX(底层 R-Tree),不要用双字段 lat BETWEEN x AND y AND lng BETWEEN a AND b,因为 B+ 树无法同时利用两个独立索引实现二维范围过滤。”


📋 基础概念避坑小结

  1. utf8mb4utf8,MySQL 的 utf8 是阉割版
  2. NULL = NULL 返回 NULL(不是 TRUE),判断 NULL 必须用 IS NULL
  3. COUNT(列名) 不统计 NULL,COUNT(*) 统计所有行,两者结果可能不同
  4. 视图不存数据,复杂视图触发临时表算法,性能差
  5. 触发器隐式执行、不好调试,大厂普遍禁用

二、数据库设计 & 索引

2.1 数据库设计

Q14:数据库三大范式是什么?为什么要反范式化? ⭐⭐⭐

范式核心要求反例解决的问题
1NF字段原子性,列不可再分"张三,李四" 存一个字段数据可操作
2NF非主键列完全依赖主键(消除部分依赖)联合主键下某列只依赖其中一个主键数据冗余、更新异常
3NF非主键列不传递依赖主键(消除传递依赖)学生表存了班主任电话(应通过班级ID关联)数据冗余、删除异常

深度理解 —— 范式解决了什么问题?

1
2
3
4
5
6
7
8
9
10
11
反范式问题示例:
订单表存了 (order_id, user_id, user_name, user_city)
→ user_name 和 user_city 不依赖 order_id,只依赖 user_id(违反 2NF)
→ 张三搬家后 city 变了,你需要更新这个人的所有历史订单记录(更新异常!)
→ 张三退单了,删除这条订单 → 张三这个人的 city 信息也丢了(删除异常!)

规范化后:
订单表只存 (order_id, user_id)
用户表存 (user_id, user_name, user_city)
→ 张三搬家:只改用户表一条记录
→ 退单:用户信息不丢

那为什么实际开发中又允许”反范式化”?

典型的反范式化例子:订单表存冗余的 user_name。为什么这样做?——如果每次都 JOIN 用户表拿名字,高并发下 JOIN 是瓶颈。冗余字段避免了 JOIN,用存储换时间。范式保证数据一致性,反范式化追求查询性能,这是永恒的矛盾。

🎯 面试总结:

“三范式本质是消除数据冗余和操作异常——1NF 保证原子性、2NF 消除部分依赖、3NF 消除传递依赖。实际开发中会根据查询需求适当反范式化冗余字段,用空间换时间。核心原则:知道自己在违反哪个范式、为了什么性能收益、如何弥补(用应用层保证一致性)。”


Q15:MySQL 中 InnoDBMyISAM 的区别?为什么 InnoDB 胜出? ⭐⭐⭐

对比维度InnoDB(默认)MyISAM
事务✅ ACID❌ 不支持
锁粒度行级锁仅表锁
外键✅ 支持❌ 不支持
MVCC✅ 有❌ 无
崩溃恢复redo log 自动恢复手动修复,可能丢数据
索引结构聚簇索引(索引即数据)非聚簇索引(索引和数据分离)
COUNT(*)需扫索引(MVCC 下每事务看到不同行数)O(1)(存了精确行数)
适用场景OLTP(高并发读写)读多写少、日志、报表

深度理解 —— InnoDB 胜出的根本原因:

不是功能多,而是崩溃安全 + 行级并发。MyISAM 的表锁意味着一个写操作会阻塞所有读写,并发能力弱一个量级。而且没有 redo log,宕机直接丢数据——这在生产环境不可接受。MySQL 5.5 起默认引擎切换到 InnoDB,标志着 MySQL 从”轻量查询工具”走向”企业级 OLTP 数据库”。

🎯 面试总结:

“InnoDB 和 MyISAM 的核心差异就三点:事务、行锁、崩溃恢复。MySQL 5.5 起默认就是 InnoDB,现在没有理由用 MyISAM——除非你非常在意 COUNT(*) 那一丁点性能差额。”


Q16:外键(FOREIGN KEY)约束的作用和争议? ⭐⭐

1
2
ALTER TABLE orders ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
级联操作效果
CASCADE主表行被删除时,子表关联行也删除
SET NULL主表行被删除时,子表外键列置 NULL
RESTRICT有子表行时禁止删除主表行

深度理解 —— 为什么阿里开发手册禁止外键?

外键约束会导致隐式加锁。当你插入子表时,MySQL 需要去主表检查外键值是否存在——这就要求对主表相关行加共享锁。高并发下这种隐式锁会引发意想不到的锁等待和死锁,开发者难以从应用层 SQL 中直接看到这些锁。

另一个问题是级联操作的不确定性:ON DELETE CASCADE 一次删除可能会连锁触发多表删除,产生大量 undo log、拖慢事务,而这些连锁反应对开发者是”透明的”——难以预估影响范围。

🎯 面试总结:

“外键保证引用完整性,但带来隐式锁、级联风险。大厂普遍禁止外键,将关系约束放在应用层保证。面试时这样答:’我知道外键的作用,但在高并发场景下倾向于用应用层逻辑 + 对账任务来保证数据一致性’。”


Q17:主键和索引的设计原则?为什么推荐自增主键? ⭐⭐⭐

主键设计核心原则:短 + 有序 + 不变。

原则为什么
自增整型插入有序 → 页分裂最少;4/8 字节 → 短
避免 UUIDUUID 无序 → 插入位置随机 → 频繁页分裂 → 碎片严重
避免业务主键可能变动(如手机号换绑),更新主键代价极大(所有二级索引叶子都要改)
分布式用 Snowflake全局有序 + 唯一,自增在分库分表下无法协调

深度理解 —— UUID 为什么是索引杀手?

InnoDB 的聚簇索引按主键顺序存储数据。自增主键意味着新插入的行总是追加在 B+ 树的最右侧,偶尔的页分裂也是顺序扩展。UUID 的主键是随机的——新行可能插入到 B+ 树的任意位置。如果目标页已经满了,就要页分裂(分配新页、搬移一半数据),频繁的页分裂导致:

  • 插入性能暴跌
  • 数据页填充率低(50%~),空间浪费
  • 大量碎片,范围查询性能差

🎯 面试总结:

“主键设计三原则:短、有序、不变。自增 BIGINT 是最优解。UUID 的无序性导致聚簇索引频繁页分裂,性能差一个数量级。分布式场景用 Snowflake 生成有序 ID 替代自增。”


2.2 索引详解

Q18:索引是什么?为什么用 B+ 树而不是红黑树/Hash? ⭐⭐⭐

定义: 索引是帮助 MySQL 高效定位数据的数据结构

深度理解 —— 为什么 B+ 树是最优解?

数据结构优点为什么 MySQL 不用它
Hash等值查询 O(1)❌ 不支持范围查询(>, BETWEEN),❌ 不支持排序
红黑树平衡二叉树,查找 O(log n)❌ 深度太大(每个节点只存 1 行 → 几百万行 = 几十层高 → 太多磁盘 IO)
B 树矮胖,一个节点存多行⚠️ 非叶子节点也存数据 → 范围查询要在非叶子节点间跳来跳去
B+ 树所有数据在叶子节点,叶子形成有序链表✅ 非叶子只存索引键 → 一个节点能放更多键 → 更矮;✅ 叶子链表 → 范围查询顺序扫

关键洞察: MySQL 的索引存在磁盘上。磁盘 IO 是以”页”为单位的(16KB)。B+ 树一个节点恰好是一个页,节点内部是顺序查找(内存级,极快)。树的高度 = 定位一行需要的磁盘 IO 次数。百万行数据 B+ 树高度通常只有 34 层 → 只 34 次磁盘 IO。

🎯 面试总结:

“B+ 树的核心优势:非叶子节点不存数据,一个节点能放更多索引键,树更矮,减少磁盘 IO;叶子节点构成有序双向链表,天然支持范围查询和排序。Hash 只快在等值查询、不支持范围;红黑树太高、磁盘 IO 太多。”


Q19:索引有哪些类型? ⭐⭐⭐

分类维度类型核心特征
数据结构B+ 树索引默认,支持范围/排序
Hash 索引仅等值查询,Memory 引擎
全文索引倒排索引,文本搜索
R-Tree(空间索引)二维范围查询
功能主键索引聚簇索引,唯一且非 NULL
唯一索引值唯一,允许多个 NULL
普通索引无唯一性约束
前缀索引只索引列的前 N 字符,省空间
复合索引多列组成,遵循最左前缀
物理存储聚簇索引叶子 = 整行数据(InnoDB)
非聚簇索引叶子 = 数据地址(MyISAM)

深度理解 —— Hash 索引的局限性:

Hash 索引把索引键通过哈希函数映射到一个桶号,桶内用链表存实际行指针。等值查询 WHERE key = 100 时:计算 hash(100) → 定位桶 → 遍历桶内链表。O(1) 理论很快。但 WHERE key > 100 时,每个 100~+∞ 的值都要算一遍 hash → 退化成全量扫描。而且哈希表的存储顺序是乱序的,ORDER BY key 无法利用索引有序性,需要额外排序。

🎯 面试总结:

“索引按数据结构分四类:B+ 树(主力)、Hash(等值查询专用)、全文(倒排)、空间(R-Tree)。按功能分有主键、唯一、普通、前缀、复合。InnoDB 的主键索引是聚簇索引——这个点面试官最爱问。”


Q20:聚簇索引 vs 非聚簇索引? ⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
InnoDB(聚簇索引):
┌─────────────────────────────────────┐
│ B+ 树叶子 = 完整的行数据(所有列) │ ← 索引即数据,数据即索引
│ 表的物理存储 = 主键 B+ 树 │
└─────────────────────────────────────┘

MyISAM(非聚簇索引):
┌────────────────────┐ 物理行地址
│ B+ 树叶子 = 指针 ───→ 数据文件中的行 │ ← 索引和数据分离
└────────────────────┘
对比聚簇索引(InnoDB)非聚簇索引(MyISAM)
叶子存什么完整的行数据数据的物理地址(行指针)
每表能建几个只能 1 个多个
二级索引怎么找数据先查二级索引拿到主键 → 回表查聚簇索引直接走物理地址
主键大小的影响主键越长,二级索引就越大(叶子存主键)无影响

深度理解 —— 为什么聚簇索引只能有一个?

因为数据本身只有一个物理存储。聚簇索引的 B+ 树叶子节点就是数据行——你不能把同一行数据存在两个不同的 B+ 树里。所以聚簇索引只有一个,必然是主键索引。

🎯 面试总结:

“聚簇索引的叶子节点存完整行数据,一张表只能有一个。InnoDB 的主键就是聚簇索引,二级索引查完后要回表。这就是为什么主键要短——主键越长,所有二级索引的叶子都越大,占更多磁盘空间。”


Q21:什么是覆盖索引和回表?为什么重要? ⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
假设表有索引 idx_name_age (name, age)

❌ 回表查询(多一次磁盘 IO):
SELECT * FROM users WHERE name = '张三';
→ idx_name_age 叶子有 (name, age, 主键ID),但缺少 email、phone
→ 拿着主键ID 回到聚簇索引再查一次 → 回表

✅ 覆盖索引(不回表):
SELECT id, name, age FROM users WHERE name = '张三';
→ idx_name_age 叶子已经包含 id、name、age 三个字段
→ 不需要回表 → EXPLAIN Extra: Using index

深度理解 —— 为什么回表代价大?

回表 = 多一次随机 IO。二级索引叶子存的主键 ID 是”有序但离散”的——不是连续的行号——所以回表访问聚簇索引时是随机读而非顺序读。随机读在机械硬盘上大约 5~10ms,SSD 上也要 0.1ms。如果一个查询回表扫描 10000 行,累积的随机读延迟是致命的。覆盖索引把这些随机读全部消掉。

🎯 面试总结:

“回表是查询性能的大敌——多一次随机 IO。覆盖索引让查询字段全部落在索引中,不回表。减少回表的方法:不写 SELECT *,按业务查询的字段建联合索引覆盖它们。面试时记住检查清单:EXPLAIN Extra 出现 Using index = 用到了覆盖索引。”


Q22:联合索引的最左前缀原则是什么?为什么? ⭐⭐⭐

1
2
3
4
5
6
7
-- 联合索引 (a, b, c)
WHERE a = 1 -- 用 a
WHERE a = 1 AND b = 2 -- 用 a, b
WHERE a = 1 AND b > 2 AND c = 3 -- 用 a, b(c 失效:范围列之后断)
WHERE a = 1 AND c = 3 -- 只用 a(c 跳过 b,失效)
WHERE b = 1 -- 跳过最左列 a,全表扫描
WHERE c = 1 -- 同上

深度理解 —— 为什么跳过最左列整个索引就用不了?

B+ 树的排列方式是按 (a, b, c) 排序的——先排 a,a 相同时排 b,b 相同时排 c。整个索引中,相同的 b 值分布在不同的 a 下,物理上不连续。你要找 b = 1 的行,它们在索引中的位置是散落的(a=1 下面有几行、a=5 下面有几行、a=100 下面有几行…),无法用 B+ 树的范围扫描定位——只能扫全索引或全表。

类比: 电话簿先按姓排序、再按名排序。你要找所有叫”张三”的人——“张”是连续的,”三”在每个”张”下面也是连续的。但你要找所有名叫”三”的人(不看姓),他们分散在电话簿各个位置——没法翻到一个连续区域一次找齐。

🎯 面试总结:

“最左前缀的本质是 B+ 树的多级排序。(a, b, c) 联合索引 → 先按 a 排、再按 b 排、再按 c 排。跳过最左列 a 直接查 b,b 在 B+ 树中就不连续了,索引失效。口诀:最左优先,遇到范围就断。”


Q23:索引在哪些情况下会失效? ⭐⭐⭐

失效场景示例根本原因
索引列用函数WHERE DATE(create_time) = '2024-01-01'函数改变值,破坏索引有序性
隐式类型转换WHERE phone = 13800138000(phone 是 CHAR)字符串 → 数字 = 调用 CAST 函数
LIKE '%xxx' 左模糊WHERE name LIKE '%三'B+ 树只能按前缀定位
OR 混用索引列和非索引列WHERE id = 1 OR name = '张三'name 没索引 → 必须全表
违背最左前缀WHERE b = 1(索引是 (a,b))跳过最左列
范围条件后的列WHERE a > 1 AND b = 2(索引 (a,b))b 在范围段内不连续
!= / NOT INWHERE status != 0大多数行不满足 → 优化器选全表
高 NULL 比例的 IS NOT NULLWHERE email IS NOT NULL(99% 非空)扫描几乎所有行
表中数据太少几百行的表全表扫描比走索引还快

深度理解 —— 索引失效的本质都是”索引的有序性被破坏或无法利用”:

B+ 树的核心优势是利用有序结构做范围定位。函数、类型转换、左模糊、跳过最左前缀——它们共同的问题是你给的条件和索引存储的值对不上,有序结构白建了。

🎯 面试总结:

“一句话记忆:WHERE 条件里不要对索引列做任何加工——不套函数、不做隐式转换、不左模糊。具体到 8 种失效场景,核心都是’破坏索引有序性’或’结果集太大优化器主动放弃’。”


Q24:自适应哈希索引(AHI)是什么? ⭐⭐

定义: InnoDB 自动检测——如果某个 B+ 树索引页被频繁用相同模式访问(等值查询),就在内存中给它建一个哈希表,后续等值查询直接 O(1) 走哈希。

特性说明
触发条件同一索引页被频繁用相同模式访问
管理方式全自动,无需 DBA 配置
存储位置Buffer Pool 中的额外内存
查看方式SHOW ENGINE INNODB STATUS 中 “Hash table size”

深度理解 —— AHI 和手动 Hash 索引的区别:

AHI 是缓冲池加速器——只在热点页上建、只对等值查询有效、内存中、自动维护。不是你创建的,不能指定哪些列、不能持久化。等值查询密集(如 WHERE user_id = ? 高并发查询)受益最大;范围查询无帮助——因为哈希表天然无序。

🎯 面试总结:

“AHI 是 InnoDB 的自优化机制,在热点 B+ 树页上自动构建哈希索引加速等值查询。DBA 无法干预,只在内存中,等值查询密集时效果最好。面试加分点:提到 AHI 说明你了解 InnoDB 内部机制。”


Q25:什么是分区索引?分区裁剪的原理? ⭐⭐

1
2
3
4
5
6
7
8
CREATE TABLE orders (
id INT, order_date DATE
)
PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025)
);

每个分区是独立的物理文件,可以有自己的索引。查询时优化器根据 WHERE 条件中的分区键,跳过不相关的分区——这就是分区裁剪(Partition Pruning)

🎯 面试总结:

“分区表把一个逻辑表拆成多个物理文件,核心价值是分区裁剪——只访问相关分区。最常用 RANGE 分区(按时间)。注意事项:WHERE 条件必须包含分区键才能触发裁剪,否则扫全部分区。”


📋 数据库设计 & 索引避坑小结

  1. 主键自增 > UUID:UUID 的随机性导致页分裂,索引碎片化
  2. SELECT * 是覆盖索引杀手:多一个字段就多一次回表
  3. 联合索引顺序很重要:区分度高的列放最左
  4. 索引不是免费的:每个索引拖慢 INSERT/UPDATE/DELETE
  5. WHERE 不加工索引列:宁可在程序里预处理参数,也别在 SQL 里用函数

三、SQL 优化

3.1 执行计划

Q26:EXPLAIN 怎么看?各字段的深度含义? ⭐⭐⭐

1
EXPLAIN SELECT * FROM users WHERE name = '张三';

核心字段深度解读:

字段含义判断标准
type访问类型ALL(全表扫)→ 要优化;index(全索引扫)→ 也要优化;range 及以上 → 可接受
key实际使用的索引NULL = 没走索引,排查索引失效原因
rows预估扫描行数核心指标!越小越好;几十万以上要警惕
Extra额外执行细节见下表
key_len索引使用的字节数判断联合索引用了几列:key_len = 4 只用了第一列 INT
possible_keys候选索引列表有候选但没用 → 优化器选错了?
ref与索引比较的列/常量const 是常量条件,列名是关联查询

type 性能从优到差:

1
2
3
system > const > eq_ref > ref > range > index > ALL
常数表 主键=常量 唯一索引JOIN 普通索引 范围扫 全索引扫 全表扫
↑ 红色警戒 ↑

Extra 信号灯:

Extra含义🟢/🔴
Using index覆盖索引,不回表🟢 最优
Using index condition索引下推(ICP),在引擎层用索引列过滤🟢 好
Using where用 WHERE 条件在 Server 层过滤🟡 正常
Using temporary用了临时表🔴 排查:GROUP BY / DISTINCT 没走索引?
Using filesort额外排序🔴 排查:ORDER BY 没走索引?
Using join bufferJOIN 没走索引,用了 JOIN 缓冲区🔴 给 JOIN 列加索引

深度理解 —— rows 为什么是”预估”而不是”精确”?

rows 来自统计信息(innodb_stats_persistent),是基于索引页的采样估算,不是精确计数。它和实际行数的误差通常在 10%~50% 范围内。重点不在于数字精确,而在于数量级——10、1000、100000 是三个完全不同的故事。

🎯 面试总结:

“看 EXPLAIN 三板斧:第一看 type 是不是 ALL/index,第二看 rows 预估扫描量是否合理,第三看 Extra 有没有 Using temporary / Using filesort。三条都健康,SQL 就没大问题。重点关注 type=ALL + Extra=Using filesort + rows 很大的组合——这是优化优先级最高的 SQL。”


3.2 查询优化

Q27:如何优化 MySQL 查询?通用优化框架是什么? ⭐⭐⭐

1
2
3
4
5
6
7
8
9
10
11
12
13
优化四个层次(越往下成本越高,先从上往下走):

① 索引层(成本低,收益高):
加索引 → 覆盖索引 → 联合索引 → 避免索引失效

② SQL 层(成本低,收益高):
避免 SELECT * → EXISTS 替 IN → JOIN 替子查询 → UNION ALL 替 UNION

③ 表结构层(成本中):
反范式化冗余字段 → 汇总表/物化视图 → 大字段拆分

④ 架构层(成本高):
缓存(Redis)→ 读写分离 → 分库分表

深度理解 —— 为什么按这个顺序?

索引是数据库内部的”免费加速器”——加一个索引,不改任何代码,查询瞬间变快。SQL 改写同理,只改 SQL 文本。这两个层面成本最低、见效最快。到了表结构层需要考虑数据迁移,架构层更是涉及部署变更——成本陡然上升。80% 的性能问题在索引 + SQL 层面就能解决。

🎯 面试总结:

“优化遵循’先下药、再动刀、最后换器官’的原则。第一步索引——通过 EXPLAIN 找出缺索引或索引失效的情况;第二步改写 SQL——消除子查询、避免 filesort/temporary;第三步才是重构表结构或上缓存。不要一上来就说加 Redis——先把 SQL 和索引搞定。”


Q28:如何优化 COUNT(*) 操作?为什么 InnoDB 的 COUNT 很慢? ⭐⭐

慢的原因: MyISAM 把表的总行数存在元数据中,COUNT(*) 直接返回。InnoDB 支持 MVCC——不同事务在同一时刻看到的是不同的行数(有些行被其他事务修改了但未提交),所以不能缓存一个全局行数。每次 COUNT(*) 都必须扫描索引来计数。

优化方案(按推荐度排序):

方案做法适用场景
选最小的二级索引扫InnoDB 自动选,但你可以建一个小字段二级索引数据量 < 百万
汇总表触发器或定时任务维护计数表需精确计数、量大
近似值SHOW TABLE STATUS LIKE 't' 中的 Rows 列只需估算(如分页总数)
缓存Redis INCR/DECR 维护计数可接受少量不一致

🎯 面试总结:

“InnoDB 的 COUNT(*) 每次都要扫索引,因为 MVCC 下不存在一个全局行数。优化方向:让优化器扫最小的二级索引(自动优化),或用汇总表/Redis 缓存计数。如果不需要精确值——用 SHOW TABLE STATUS 的估算值。”


Q29:子查询为什么比 JOIN 慢?什么情况下子查询可以被优化? ⭐⭐⭐

1
2
3
4
5
-- ❌ 旧版 MySQL 中的慢写法(每行执行一次子查询)
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);

-- ✅ 改写为 JOIN
SELECT DISTINCT u.* FROM users u INNER JOIN orders o ON u.id = o.user_id;

深度理解 —— 子查询慢的真正原因:

旧版 MySQL(5.5 之前)的 IN 子查询是 dependent subquery:外层每拿到一行,就把 user_id 传给内层,内层重新执行一次 SELECT user_id FROM orders。1000 个 user = 内层执行 1000 次。

MySQL 5.6+ 引入了子查询物化(Materialization):先把子查询结果写入一个临时表,外层和外层做 semi-join,避免了逐行重复执行。所以 5.6+ 的 IN 子查询性能已经大幅改善。

NOT IN 仍有陷阱:

1
2
3
4
-- 如果 orders.user_id 有 NULL 值,NOT IN 返回空!
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders);
-- 等价于: id != NULL AND id != 1 AND id != 2 AND ...
-- id != NULL → 结果是 NULL → WHERE NULL → 一行都不返回!

必须用 NOT EXISTSLEFT JOIN ... IS NULL 替代 NOT IN

🎯 面试总结:

“MySQL 5.6+ 子查询做了物化优化,IN 子查询性能大幅改善。但 NOT IN 仍然是坑——遇到 NULL 值直接返回空结果。永远用 NOT EXISTSLEFT JOIN ... IS NULL 替代 NOT IN。”


Q30:INEXISTS 怎么选?核心原则是什么? ⭐⭐

1
2
3
4
5
-- EXISTS:外表驱动,对内表逐条用索引查
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

-- IN:内表先物化,外表用索引查
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users);
场景推荐原因
外表小,内表大EXISTS循环次数少 × 每次用索引快速查 = 快
外表大,内表小IN子查询结果集小,物化成本低

深层原则:小表驱动大表——让外层循环的次数最少。

🎯 面试总结:

EXISTS 是循环外表逐条对内表做索引查找,IN 是先算出内表结果集再查外层列。选谁看谁小——小表驱动大表。记住:EXISTS 的外层是驱动表。”


Q31:如何优化 ORDER BY 查询? ⭐⭐

1
2
3
4
5
-- 让 WHERE 列和 ORDER BY 列组成联合索引
ALTER TABLE orders ADD INDEX idx_user_ctime (user_id, create_time);

-- 这样 ORDER BY 直接用索引有序性,避免 filesort
SELECT * FROM orders WHERE user_id = 100 ORDER BY create_time;

能用索引排序的前提(缺一不可):

  1. ORDER BY 的列必须在联合索引中且顺序匹配
  2. ORDER BY 不能有 ASC/DESC 混合(8.0 之前)
  3. WHERE 条件不能有范围查询夹在索引中间

深度理解 —— filesort 是什么?为什么它慢?

filesort 不等于”读文件排序”,分两种情况:

  • 排序数据量 ≤ sort_buffer_size内存排序(快速排序),较快
  • 排序数据量 > sort_buffer_size磁盘排序(外排序,写临时文件),极慢

filesort 的代价在于:即使走内存排序,它也需要把数据行从数据页复制到 sort_buffer 再排序——额外内存和 CPU 开销。如果能利用索引已有的有序性,连排序都省了。

🎯 面试总结:

ORDER BY 优化的核心是让排序走索引——建联合索引(WHERE 列在前,ORDER BY 列在后),利用索引天然有序性免排序。EXPLAIN 看到 Using filesort 就是没利用索引排序的信号。”


Q32:如何优化 DISTINCT 查询?

1
2
3
4
5
6
-- DISTINCT 去重
SELECT DISTINCT user_id FROM orders;

-- 更优:建索引后 GROUP BY 可能利用松索引扫描
SELECT user_id FROM orders GROUP BY user_id;
-- 前提:user_id 有索引,或 (user_id, ...) 联合索引

深度理解 —— DISTINCT 和 GROUP BY 在内部实现的差异:

DISTINCT 本质是隐藏的 GROUP BY(对所有 SELECT 列分组)。在 MySQL 优化器中,DISTINCTGROUP BY 经常走到相同的执行路径。但 GROUP BY 可以利用松索引扫描(Loose Index Scan)——当只需要分组列的值而不需要聚合时,直接跳读索引中每个分组的第一行,跳过组内行。DISTINCT 有时不会触发这个优化。

🎯 面试总结:

DISTINCT 的优化:优先建索引覆盖去重列,让它利用索引的有序性去重。如果真的只是要SELECT DISTINCT col FROM t,给 col 建索引,GROUP BY col 有时比 DISTINCT 更好地利用松索引扫描。”


Q33:大批量 UPDATE / DELETE / SELECT 如何安全处理? ⭐⭐

1
2
3
4
5
6
7
8
-- ❌ 致命做法:一条 SQL 锁大量行,阻塞所有并发操作
UPDATE orders SET status = 'expired' WHERE create_time < '2024-01-01';

-- ✅ 分批执行:
UPDATE orders SET status = 'expired'
WHERE create_time < '2024-01-01' AND id BETWEEN 1000 AND 2000
LIMIT 1000;
-- 循环执行,直到 affected_rows = 0,每次间隔 50ms

深度理解 —— 大批量操作的三重危害:

  1. 锁膨胀:InnoDB 对扫描到的所有行加锁(没索引的表直接锁全表)。分批操作每次只锁一小批,不影响其他事务
  2. undo log 膨胀:一个事务修改 100 万行 = 100 万行旧版本存在 undo log 中供 MVCC 使用 → 占用巨大磁盘空间 → 其他事务读这些行时要沿 undo 链回溯
  3. 主从延迟:大事务提交后,从库回放同等时间,复制延迟飙升

🎯 面试总结:

“大批量 DML 的核心原则:分批操作 + 避免长事务。每条 SQL 只操作 1000~10000 行,循环执行,每批之间短暂 SLEEP。一箭三雕——减少锁持有时间、控制 undo log 大小、避免主从延迟。”


Q34:如何预防和清理重复数据?

1
2
3
4
5
6
7
8
9
-- 查找重复行
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;

-- 删除重复行(保留最小 ID)
DELETE u1 FROM users u1
INNER JOIN users u2 ON u1.email = u2.email AND u1.id > u2.id;

-- 预防:建唯一索引(根本解决方案)
ALTER TABLE users ADD UNIQUE INDEX uk_email (email);

🎯 面试总结:

“处理重复数据分两步:先用 GROUP BY + HAVING 找到重复行,再用自 JOIN 删除保留一条。根本解决方法是建唯一索引——防患于未然。”


Q35:优化器提示(Optimizer Hints)什么时候用? ⭐⭐

1
2
3
4
-- 语法
SELECT * FROM users FORCE INDEX(idx_name) WHERE name = '张三'; -- 强制走某索引
SELECT * FROM users IGNORE INDEX(idx_name) WHERE name = '张三'; -- 忽略某索引
SELECT /*+ INDEX(users idx_name) */ * FROM users WHERE name = '张三'; -- 8.0+ 推荐

深度理解 —— 为什么优化器会选错索引?

优化器基于统计信息(索引基数、数据分布)做代价估算,但统计信息可能是过时的或不准确的(ANALYZE TABLE 不及时)。数据倾斜严重时(某个值占了 90% 的行),优化器可能低估/高估某条索引的代价。

🎯 面试总结:

“Hints 是最后手段,不是常规优化。优化器选错索引时先 ANALYZE TABLE 更新统计信息,无效才用 Hints。长期看应优化索引设计,而非靠 Hints 硬纠正。”


📋 SQL 优化避坑小结

  1. EXPLAIN 三看:type → rows → Extra
  2. NOT IN 永远不要用,NULL 值会让结果为空
  3. 深分页用游标分页LIMIT 1000000, 10 = 扫描 1000010 行
  4. 大批量 DML 必须分批,每批 ≤ 10000 行
  5. 覆盖索引 + 避免 filesort = SQL 优化 80% 的答案

四、事务 & 锁机制

4.1 事务基础

Q36:事务是什么?ACID 四特性及 MySQL 实现机制? ⭐⭐⭐

定义: 事务是一组不可分割的 SQL 操作,要么全部成功(COMMIT),要么全部失败回滚(ROLLBACK)。

特性描述MySQL 如何实现
原子性 A要么全做,要么全不做undo log:记录修改前的值,回滚时恢复
一致性 C事务前后数据满足所有约束redo log + undo log + 锁 共同维护
隔离性 I并发事务互不干扰MVCC + 锁(行锁/Gap锁)
持久性 D提交后数据永久保存redo log(WAL 机制),崩溃恢复时重做

深度理解 —— redo log 如何保证持久性(WAL 机制):

事务提交时,MySQL 不是直接写数据文件(随机 IO,慢),而是先写 redo log(顺序 IO,快)。只要 redo log 落盘了,事务就认为提交成功。即使下一秒宕机,重启时用 redo log 重放一次,数据就恢复了。这个策略叫 WAL(Write-Ahead Logging,预写日志)——先记日志再写数据。

🎯 面试总结:

“ACID 的底层实现:A 靠 undo log 回滚,I 靠 MVCC + 锁,D 靠 redo log(WAL 机制)。事务提交时先写 redo log(顺序 IO 快),再异步刷数据页到磁盘——这解释了为什么 MySQL 事务提交很快但数据可能延迟落盘。”


4.2 并发问题与隔离级别

Q37:并发事务会引发哪些问题?如何区分? ⭐⭐⭐

问题现象根因类比
脏读读到其他事务未提交的数据没等对方 COMMIT 就读偷看别人草稿
不可重复读同一事务内两次读同一行,值变了中间其他事务做了 UPDATE + COMMIT读第一遍 100,再读变 200
幻读同一事务内两次查询,行数变了中间其他事务做了 INSERT/DELETE + COMMIT第一次 10 条,第二次 11 条

深度理解 —— 不可重复读 vs 幻读的核心区别:

  • 不可重复读 → 聚焦于同一行数据的内容变化(UPDATE 产生)
  • 幻读 → 聚焦于结果集的行数变化(INSERT/DELETE 产生)

🎯 面试总结:

“脏读是读了未提交的,不可重复读是同一行内容变了(UPDATE),幻读是行数变了(INSERT/DELETE)。不可重复读关注行内的值,幻读关注行的存在性。”


Q38:MySQL 的四种隔离级别?InnoDB 在 RR 级别真的解决幻读了吗? ⭐⭐⭐

隔离级别脏读不可重复读幻读实现机制
READ UNCOMMITTED几乎无锁
READ COMMITTED每次读生成新 ReadView
REPEATABLE READ(默认)⚠️ 大部解决事务开始生成一个 ReadView + 间隙锁
SERIALIZABLE所有读自动加 S 锁

深度理解 —— RR 级别如何”基本解决”幻读?

InnoDB 在 RR 级别用了两个武器:

  1. 快照读(Snapshot Read)SELECT ... 不加锁的普通查询,用 MVCC 的 ReadView 保证读取事务开始时的快照——即使其他事务插入了新行,也看不到(因为新行的 DB_TRX_ID 大于当前 ReadView)→ 避免了快照读的幻读
  2. 当前读(Current Read)SELECT ... FOR UPDATE 会加 Next-Key Lock(Record Lock + Gap Lock)——Gap Lock 锁定索引间隙,阻止其他事务在间隙中插入新行 → 避免了当前读的幻读

为什么说是”基本”而不是”完全”解决?

  • 快照读在 RR 级别确实不会幻读——ReadView 固定了
  • 当前读如果只用 Record Lock 而没有 Gap Lock,仍可能幻读——所以 InnoDB 默认给 RR 加了 Gap Lock
  • 但如果在 RC 级别——没有 Gap Lock,当前读会幻读

🎯 面试总结:

“InnoDB RR 级别通过双重机制解决幻读:快照读靠 MVCC ReadView 不变,当前读靠 Gap Lock 锁间隙。但说’基本解决’而非’完全解决’——因为如果先快照读再当前读,真可能读到别人插入的新行,这是 RR 级别允许的。”


4.3 MVCC

Q39:MVCC 是什么?它的核心组件和工作原理? ⭐⭐⭐

定义: MVCC(Multi-Version Concurrency Control)多版本并发控制。通过对每行数据维护多个历史版本,实现读不阻塞写,写不阻塞读

核心组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
每行数据有三个隐藏列:
┌────────┬───────────┬──────────────┐
│ DB_ROW_ID │ DB_TRX_ID │ DB_ROLL_PTR │
│ (隐含主键) │ (最近修改事务)│ (回滚指针 → undo)│
└────────┴───────────┴──────────────┘

ReadView(快照读时的"可见性快照"):
┌───────────────────────────────────────┐
│ · m_ids[]:当前活跃事务 ID 列表 │
│ · min_trx_id:最小活跃事务 ID │
│ · max_trx_id:下一个要分配的事务 ID │
│ · creator_trx_id:当前事务 ID │
└───────────────────────────────────────┘

版本可见性判断逻辑(一句话版):

1
2
3
4
5
6
1. trx_id == 我 → 我自己改的,可见
2. trx_id < min_trx_id → 已经提交的,可见
3. trx_id >= max_trx_id → 未来的,不可见
4. min_trx_id ≤ trx_id < max_trx_id:
· 在活跃列表 m_ids[] 中 → 未提交,不可见
· 不在活跃列表中 → 已提交,可见

深度理解 —— RC 和 RR 的 ReadView 生成时机差异:

  • RC(读已提交):每次 SELECT 都生成一个新的 ReadView → 能看到其他事务的提交 → 会有不可重复读
  • RR(可重复读):事务第一次 SELECT 时生成一个 ReadView,之后一直复用 → 看不到其他事务的提交 → 避免不可重复读

这就是为什么 RC 和 RR 都用了 MVCC,但 RR 能避免不可重复读——不是因为 RR 用了更强的锁,而只是它”不愿意刷新自己的视野”。

🎯 面试总结:

“MVCC 的核心思想是’数据多版本 + 快照读’。每行存了修改它的事务 ID,通过 undo log 链可以回溯历史版本。查询时通过 ReadView 判断哪些版本对当前事务可见。RR 和 RC 的唯一区别是 ReadView 的生成时机——RR 只生成一次,RC 每次读都重新生成。MVCC 是 InnoDB 读写不互相阻塞的根本原因。”


4.4 锁机制

Q40:MySQL 的锁有哪些种类?InnoDB 的三种行锁算法? ⭐⭐⭐

按粒度分:

粒度加锁方式适用场景
全局锁FLUSH TABLES WITH READ LOCK全库备份
表级锁LOCK TABLES t READ/WRITEMyISAM,或 InnoDB 手动加
行级锁InnoDB 自动(通过索引)高并发 OLTP
元数据锁(MDL)DDL 时自动防止 DDL 和 DML 并发

InnoDB 行锁按算法分(核心!):

1
2
3
4
5
6
7
8
9
10
11
12
假设索引 id 有值:1, 5, 10, 15

Record Lock(记录锁):只锁 id=10 这一行
└── 锁定范围:[10]

Gap Lock(间隙锁):锁索引记录之间的间隙
└── 锁定范围:(-∞,1), (1,5), (5,10), (10,15), (15,+∞)
└── 作用:阻止 INSERT,防止幻读

Next-Key Lock(临键锁)= Record Lock + Gap Lock
└── 锁定范围:(-∞,1], (1,5], (5,10], (10,15], (15,+∞]
└── 这是 InnoDB RR 级别的默认行锁算法

深度理解 —— 为什么行锁必须通过索引?

InnoDB 的行锁不是锁”物理行”,而是锁索引记录WHERE id = 10 FOR UPDATE 时,MySQL 在 id 的索引中找到 id=10 这条索引记录,在上面加锁。如果 WHERE 条件没走索引 → 无法定位到具体的索引记录 → 只能对扫描到的所有行的索引记录加锁 → 实际效果 = 锁全表
这就是面试中”行锁变表锁”的真相。

🎯 面试总结:

“InnoDB 的行锁锁的是索引记录,不是物理行。没索引的 WHERE 条件 → 全表扫描 → 对所有扫描到的索引记录加锁 → 行锁膨胀为表锁。三种行锁算法:Record Lock 锁行、Gap Lock 锁间隙防插入、Next-Key Lock 前两者组合,是 RR 级别的默认锁。”


Q41:乐观锁和悲观锁的区别?如何选择? ⭐⭐⭐

对比乐观锁悲观锁
理念假定冲突少,提交时检查假定冲突多,先锁再操作
实现版本号/时间戳(CAS)SELECT ... FOR UPDATE
数据库参与无(应用层逻辑)有(数据库行锁)
并发性能无锁等待,冲突时重试阻塞等待
适用场景读多写少、冲突率低写多、冲突率高
1
2
3
4
5
6
7
8
9
10
-- 悲观锁:先锁后改
BEGIN;
SELECT stock FROM goods WHERE id = 1 FOR UPDATE; -- 阻塞其他事务
UPDATE goods SET stock = stock - 1 WHERE id = 1;
COMMIT;

-- 乐观锁:先改后检查版本
UPDATE goods SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5; -- CAS
-- affected_rows = 0 → 冲突了,重试

深度理解 —— 乐观锁的 ABA 问题:

乐观锁只用版本号判断”有没有被人改过”,不关心改了几次或改了什么。理论上存在 ABA 问题(版本从 5→6→5,你看不出来),实际中不太可能连续改两次恰好回到同一个版本号。如果在意,用时间戳做版本号。

🎯 面试总结:

“乐观锁是应用层思想,悲观锁是数据库层实现。冲突少用乐观(性能好、无锁等待),冲突多用悲观(减少无效重试)。库存扣减场景:乐观锁的首选是原子 UPDATE SET stock = stock - 1 WHERE stock > 0——利用 InnoDB 行锁的天然串行性,一条 SQL 搞定。”


Q42:死锁如何产生?如何预防?发生后怎么处理? ⭐⭐⭐

产生条件(四个缺一不可):

1
2
3
4
① 互斥 — 资源不能共享
② 持有等待 — 持有一个资源时等待另一个
③ 不可剥夺 — 不能被强制释放
④ 循环等待 — 事务A等B,B等A

经典死锁场景:

1
2
3
4
5
6
-- 事务A                           -- 事务B
BEGIN; BEGIN;
UPDATE t SET a=1 WHERE id=1; -- 持有 id=1 的 X 锁
UPDATE t SET b=1 WHERE id=2; -- 持有 id=2 的 X 锁
UPDATE t SET b=1 WHERE id=2; -- 等 id=2 → 阻塞
UPDATE t SET a=1 WHERE id=1; -- 等 id=1 → 死锁!

预防四板斧:

策略做法原理
统一加锁顺序所有事务按相同顺序访问资源打破循环等待
缩短事务减少锁持有时间,禁用事务中的 RPC/人机交互缩小死锁窗口
使用索引避免行锁膨胀为表锁减少锁范围
降级隔离级别RC 级别无 Gap Lock消除间隙锁导致的死锁

发生后: InnoDB 自动检测死锁(innodb_deadlock_detect),选择 undo log 最小的那个事务回滚。应用层捕获错误 1213重试

🎯 面试总结:

“死锁不可 100% 消灭——只要有并发操作同一资源就有概率死锁。能做两件事:通过统一加锁顺序、缩短事务来降低概率;通过重试机制来兜底。死锁发生后 InnoDB 自动回滚代价最小的那个事务,业务层接住死锁异常并重试。”


Q43:什么是锁升级/锁膨胀?InnoDB 有锁升级吗? ⭐⭐

锁升级(Lock Escalation): 数据库将多个细粒度锁合并为一个粗粒度锁以节省内存。InnoDB 不支持锁升级(它用位图存行锁,内存开销不大)。

锁膨胀(Lock Inflation): 这是 InnoDB 特有的现象——WHERE 条件没走索引时,InnoDB 对所有扫描到的行加锁,实际效果等同锁表。

数据库锁升级
MySQL InnoDB❌ 不支持锁升级(存位图)
SQL Server✅ 行 → 页 → 表
Oracle❌ 不支持

🎯 面试总结:

“InnoDB 没有锁升级机制,但行锁通过索引实现。不走索引的 WHERE 条件导致扫描所有行并加锁,产生等效’锁表’的效果——这不是锁升级,而是索引缺失导致的锁膨胀。”


Q44:高并发下如何安全修改同一行数据(如库存扣减)? ⭐⭐⭐

方案演进(冲突从低到高):

1
2
3
4
5
6
7
冲突率低 → 乐观锁(版本号 CAS)
↓ 冲突上升
冲突率中 → 原子 UPDATE(利用行锁天然串行)
↓ 冲突更高
冲突率高 → 悲观锁(FOR UPDATE)
↓ 极高并发
秒杀场景 → Redis 预扣 + 异步写 MySQL
1
2
3
4
5
6
7
8
9
10
11
12
-- 方案一:原子 UPDATE(一行 SQL,行锁保护,推荐首选)
UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock > 0;
-- affected_rows = 1 → 成功;0 → 库存不足

-- 方案二:悲观锁(冲突率极高时)
BEGIN;
SELECT stock FROM goods WHERE id = 1 FOR UPDATE;
UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock > 0;
COMMIT;

-- 方案三:Redis 原子预扣(秒杀)
-- DECR stock:1 → 在 Redis 中原子扣减 → 异步刷 MySQL

深度理解 —— 为什么原子 UPDATE 比 SELECT FOR UPDATE 更好?

原子 UPDATE 是一条 SQL,InnoDB 对 UPDATE 自动加行锁,锁从获取到释放只在一条语句的执行期间,极短。而 SELECT ... FOR UPDATE 的锁从 SELECT 开始持有直到事务 COMMIT——如果事务中还有业务逻辑、RPC 调用,锁持有时间会被大幅拉长,降低并发吞吐。

🎯 面试总结:

“高并发扣减按冲突程度分级处理:轻冲突用乐观锁,中等冲突用原子 UPDATE(利用行锁自动串行),重冲突用悲观锁,秒杀用 Redis 前置扣减。核心原则:锁持有时间越短越好——原子 UPDATE 的锁只持续一条语句,比 SELECT FOR UPDATE 的跨语句锁优越。”


📋 事务 & 锁机制避坑小结

  1. 行锁必须走索引,否则膨胀为表锁——面试高频必考
  2. 长事务是万恶之源:锁不释放、undo 膨胀、主从延迟
  3. 死锁靠两招:降低概率(统一顺序+短事务)+ 重试兜底
  4. FOR UPDATE 必须在事务中,否则不持有锁
  5. 乐观锁必须处理 affected_rows = 0 的重试逻辑

五、高阶特性

5.1 窗口函数

Q45:MySQL 的窗口函数有哪些?窗口函数和 GROUP BY 的本质区别? ⭐⭐⭐

1
2
3
4
5
6
SELECT name, dept, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rk,
DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS dr,
LAG(salary, 1) OVER (PARTITION BY dept ORDER BY hire_date) AS prev_salary
FROM employees;
函数结果示例说明
ROW_NUMBER()1, 2, 3, 4…不重复不跳号
RANK()1, 2, 2, 4…并列同号,后续跳号
DENSE_RANK()1, 2, 2, 3…并列同号,后续不跳
LAG(col, n)取前 N 行值
LEAD(col, n)取后 N 行值
SUM() OVER(...)窗口内累积/滑动聚合

深度理解 —— 窗口函数和 GROUP BY 的本质区别:

GROUP BY折叠行——分组后每组只剩一行,原始行的信息丢失了。窗口函数不折叠行——每一行都保留,只是额外附带了基于”窗口(一组行)”的计算结果。所以 GROUP BY 回答”每个部门的平均工资是多少”,窗口函数回答”每个人的工资和部门平均工资差多少”。

注意: 窗口函数是 MySQL 8.0 才引入的,5.7 及以下版本需要用变量模拟(非常麻烦)。

🎯 面试总结:

“窗口函数不折叠行,GROUP BY 折叠行——这是根本区别。最常考的是 ROW_NUMBER / RANK / DENSE_RANK 的区别,面试时直接写出 1,2,3,4 / 1,2,2,4 / 1,2,2,3。窗口函数 MySQL 8.0+ 才有,低版本不要用。”


5.2 主从复制

Q46:MySQL 主从复制的原理? ⭐⭐⭐

1
2
3
4
5
6
7
    Master                          Slave
┌─────────────┐ ┌─────────────────┐
│ binlog dump │── binlog ──→ │ I/O Thread │
│ thread │ │ ↓ 写 relay log │
└─────────────┘ │ SQL Thread │
│ ↓ 回放 relay │
└─────────────────┘

三步流程:

  1. Master 的 binlog dump 线程把 binlog 事件推给 Slave
  2. Slave 的 I/O 线程接收 binlog 并写入 relay log(中继日志)
  3. Slave 的 SQL 线程读取 relay log 并回放执行

深度理解 —— 为什么是异步的?

默认异步复制:Master 提交事务后立即返回给客户端,不等 Slave 确认。好处是不影响主库写入性能;代价是如果 Master 宕机且 binlog 没来得及传给 Slave,数据丢失。

三种复制模式:

模式数据安全性性能影响
异步(默认)可能丢数据无影响
半同步(5.5+)至少一个 Slave 确认后才返回有延迟
组复制(5.7.17+)多数派确认(Paxos)更大延迟

🎯 面试总结:

“主从复制有三步:binlog dump 推 → I/O 线程收 → SQL 线程回放。默认异步——主库提交不等从库。半同步复制保证至少一台从库收到 binlog 才返回,用性能换数据安全。”


Q47:复制延迟的原因和解决方案?为什么大事务是延迟元凶? ⭐⭐

原因解决方案
从库硬件配置差从库硬件 ≥ 主库
从库承担大量读请求一主多从,读写分离
大事务(批量 UPDATE/DELETE)分批操作,每批 1000~10000 行
从库 SQL 线程单线程(5.6 前)并行复制(5.7+ slave_parallel_workers
主从网络延迟同机房部署

深度理解 —— 为什么大事务是复制延迟的第一元凶?

Master 上一个事务执行 10 分钟,提交后 binlog 中这个事务就是一条 10 分钟的记录。Slave SQL 线程回放时也必须执行 10 分钟——在这 10 分钟内 SQL 线程被阻塞,后面的事务全部堆积。而且 SQL 线程是单线程(5.6 前),一个长事务拖住整个复制链路。

🎯 面试总结:

“复制延迟首因是大事务——主库跑 10 分钟的事务,从库也得回放 10 分钟,后面堆积的事务全部延迟。解决方案:避免大事务(分批)+ 开并行复制(5.7+ LOGICAL_CLOCK 模式)。”


5.3 分区与分布式

Q48:分区表的作用和注意事项? ⭐⭐

1
2
3
4
5
6
7
8
9
10
CREATE TABLE logs (
id INT, log_date DATE, content TEXT
) PARTITION BY RANGE (YEAR(log_date)) (
PARTITION p_old VALUES LESS THAN (2023),
PARTITION p_cur VALUES LESS THAN (2024),
PARTITION p_future VALUES LESS THAN MAXVALUE
);

-- 快速删除:DROP PARTITION 比 DELETE 快几个数量级
ALTER TABLE logs DROP PARTITION p_old;

核心价值: 分区裁剪(查询只访问匹配的分区)+ 快速清理历史数据。

注意事项: WHERE 条件必须包含分区键才能触发裁剪;分区表不支持外键;分区数量不宜过多(建议 ≤ 1024)。

🎯 面试总结:

“分区表的杀手锏是分区裁剪和 DROP PARTITION 秒删历史数据。按时间 RANGE 分区最常用。但 WHERE 必须带分区键,否则扫所有分区。”


Q49:如何实现和管理分布式数据库? ⭐⭐

拆分策略:

维度方式示例
垂直拆分(按业务)不同业务分不同库用户库 / 订单库 / 商品库
垂直拆分(按字段)热字段和冷字段分表用户基础信息 / 用户详情
水平拆分(按 ID)取模分片id % 16 → 16 张表
水平拆分(按时间)按年/月分表orders_2024_01

分库分表后的新问题(面试亮点):

  • 跨库 JOIN 不可用 → 冗余字段 + 应用层组装
  • 分布式事务 → 最终一致性 + 补偿(不用 XA)
  • 全局唯一 ID → Snowflake / 号段模式
  • 扩容数据迁移 → 一致性哈希 + 双写过渡

🎯 面试总结:

“分库分表先垂直拆分(按业务)再水平拆分(按 ID/时间)。分布式引入的新问题:跨库 JOIN 困难、分布式事务、全局 ID——每个都要在架构设计阶段想好方案。中间件推荐 ShardingSphere。”


5.4 数据处理

Q50:大数据量导入导出的最佳实践?

方式命令速度
逻辑导出mysqldump
逻辑导入mysql < dump.sql
文件导出SELECT INTO OUTFILE '/tmp/data.csv'
文件导入LOAD DATA INFILE快(比 INSERT 快 20 倍)

导入加速四件套:

1
2
3
4
5
6
7
SET autocommit = 0;               -- 关闭自动提交,手动批量 COMMIT
SET unique_checks = 0; -- 关闭唯一性检查(确保数据无重复)
SET foreign_key_checks = 0; -- 关闭外键检查
-- LOAD DATA ...
SET unique_checks = 1;
SET foreign_key_checks = 1;
COMMIT;

🎯 面试总结:

“大数据导入用 LOAD DATA INFILE,比逐行 INSERT 快 20 倍。导入前关闭 autocommit、unique_checks、foreign_key_checks,导入后恢复。”


Q51:MySQL 如何实现数据压缩?

1
2
-- InnoDB 页级压缩
CREATE TABLE t (id INT, data TEXT) ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;
压缩方式级别说明
ROW_FORMAT=COMPRESSED页级InnoDB 5.7+;压缩率 50%~70%(文本/JSON)
透明页压缩文件系统级8.0+,需 OS 支持(如 Linux hole punching)
应用层压缩应用级gzip 后存 BLOB

🎯 面试总结:

“InnoDB 压缩适合 TEXT/JSON 等大字段,压缩率 50%~70%。整形字段压缩效果差。注意 KEY_BLOCK_SIZE 参数——设太大没压缩效果,设太小性能下降。”


Q52:什么是数据脱敏?如何实现?

定义: 对敏感数据(身份证、手机号)做变形处理,用于非生产环境(测试、开发、演示)。

方式做法适用
动态脱敏SQL 中实时替换:CONCAT(LEFT(phone,3), '****', RIGHT(phone,4))生产环境查看
静态脱敏导出数据时批量改写脱敏后给测试库
视图脱敏建脱敏视图,应用查视图权限控制

🎯 面试总结:

“脱敏不是加密——脱敏后的数据无法还原,加密可以解密还原。生产环境做动态脱敏,测试环境做静态脱敏。最低成本方案:建脱敏视图。”


📋 高阶特性避坑小结

  1. 窗口函数 8.0+ 才有,低版本用变量模拟(写法复杂且易错)
  2. 复制延迟靠两招:避免大事务 + 并行复制
  3. 分区表 WHERE 必须带分区键,否则裁剪失效
  4. LOAD DATA 比 INSERT 快 20 倍,大数据导入标配
  5. 分库分表要先规划 JOIN 和事务,设计阶段解决分布式问题

六、运维 & 故障排查

6.1 日志与监控

Q53:慢查询日志如何开启和分析?完整优化流程? ⭐⭐⭐

1
2
3
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
SET GLOBAL log_queries_not_using_indexes = ON; -- 记录未用索引的查询

分析工具对比:

工具优势
mysqldumpslowMySQL 自带,基础统计
pt-query-digestPercona 出品,报表详细,可分析 tcpdump
performance_schema数据库内置,实时细粒度

标准优化闭环:

1
2
3
① 慢查询日志 → ② pt-query-digest 分析 → ③ 找 TOP N 慢 SQL
→ ④ EXPLAIN 分析每一条 → ⑤ 加索引/改写 SQL/改表结构
→ ⑥ 验证效果(EXPLAIN + 实际执行时间对比)

🎯 面试总结:

“慢查询优化闭环:日志 → pt-query-digest → TOP N → EXPLAIN → 优化 → 验证。pt-query-digest 比 mysqldumpslow 强太多——面试提这个工具名说明你有实战经验。”


Q54:MySQL 如何进行性能排查?常用工具清单? ⭐⭐

工具/命令用途关键信息
SHOW PROCESSLIST当前运行的 SQL找执行时间长的 SQL、堆积的连接
SHOW ENGINE INNODB STATUSInnoDB 引擎诊断死锁日志、锁等待、事务状态
EXPLAIN单条 SQL 执行计划type/rows/Extra
performance_schema细粒度性能指标锁等待、内存使用、文件 IO
information_schema库表元数据表大小、索引信息、进程列表
pt-query-digest慢日志分析TOP SQL、执行频率、耗时分布
1
2
3
4
5
6
-- 快速找长事务(超过 60 秒的事务)
SELECT * FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;

-- 快速找锁等待(MySQL 8.0)
SELECT * FROM performance_schema.data_lock_waits;

🎯 面试总结:

“排查问题先从 SHOW PROCESSLIST 看有哪些 SQL 在执行,再从 information_schema.innodb_trx 找长事务,然后拿慢 SQL 去 EXPLAIN。SHOW ENGINE INNODB STATUS 是锁和死锁问题的第一手情报。”


Q55:FLUSH 系列命令的作用?

命令作用
FLUSH TABLES关闭所有打开的表文件,刷新表缓存
FLUSH TABLES WITH READ LOCK全局读锁(全库只读),用于 mysqldump 一致性备份
FLUSH LOGS关闭当前日志文件并新建(日志轮转/切割)
FLUSH PRIVILEGES重新加载权限表(GRANT 后需要)
FLUSH HOSTS清空 host cache(用于 DNS 变更后)

🎯 面试总结:

FLUSH LOGS 用于 binlog 切割;FLUSH TABLES WITH READ LOCK 用于全局一致性备份;FLUSH PRIVILEGES 用于手动改权限表后刷新。”


6.2 故障排查

Q56:如何处理大量并发连接? ⭐⭐

手段做法
连接池应用层用 HikariCP/Druid,复用连接,减少建连开销
增大 max_connections默认 151,视内存调整(每个连接约占用 2~4MB)
缩短查询SQL 优化,减少每个连接占用时间
读写分离读流量分流到从库
排查异常连接SHOW PROCESSLIST 看哪些连接在执行什么
空闲超时wait_timeout 踢掉空闲连接

🎯 面试总结:

“连接数打满的第一反应不是盲目调大 max_connections,而是看 SHOW PROCESSLIST 找异常——是慢 SQL 占着连接不放,还是哪个应用没配置连接池。治本靠 SQL 优化 + 连接池。”


Q57:如何排查和处理长时间运行的查询? ⭐⭐

1
2
3
4
5
6
7
8
9
10
11
-- 找耗时超过 10 秒的 SQL
SELECT id, user, host, db, time, state, info
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 10
ORDER BY time DESC;

-- 分析执行计划
EXPLAIN SELECT ...;

-- 紧急 Kill(谨慎)
KILL <thread_id>;

预案机制:

  • max_execution_time(5.7.8+):单条 SQL 最大执行时间,超时自动 kill
  • pt-kill:定时扫描并 kill 符合条件的查询

🎯 面试总结:

“先查 information_schema.processlist 定位长查询,EXPLAIN 分析原因,紧急情况下 kill。事前预防比事后杀更重要——给 OLTP 查询设 max_execution_time 硬限制。”


Q58:逻辑备份与物理备份怎么选? ⭐⭐

对比逻辑备份(mysqldump物理备份(xtrabackup
内容SQL 语句(CREATE + INSERT)数据文件的二进制拷贝
速度(逐行导出 SQL)(直接拷贝文件)
跨版本恢复✅ 支持❌ 一般不支持
增量备份❌ 每次全量✅ 支持增量
适用小库、迁移、导出部分表大库日常备份

🎯 面试总结:

“生产环境用 xtrabackup 做物理全量+增量备份,mysqldump 只用来导出结构或小表。物理备份快、支持增量、对业务影响小——这是生产标配。”


Q59:如何保证数据一致性和完整性? ⭐⭐

层面措施
数据库层主键 + 唯一索引 + NOT NULL + 数据类型约束
事务层正确的事务边界 + 合适的隔离级别
应用层幂等设计、补偿事务(Saga)、最终一致性对账
运维层主从一致性校验(pt-table-checksum)、定期备份恢复演练

🎯 面试总结:

“数据库一致性从四层保证:数据库约束(主键/唯一索引)、事务隔离、应用层幂等补偿、运维层校验演练。大厂禁止外键——一致性在应用层 + 对账任务中保证。分布式场景用最终一致性而非强一致 ACID。”


Q60:大表性能优化策略? ⭐⭐

策略说明适用场景
索引优化确保所有查询走索引、覆盖索引所有场景
分区按时间/ID 分区,利用分区裁剪有时序特征的表
归档冷数据迁移到归档表/对象存储历史数据占比高
分表水平拆分为多个子表单表 > 千万级
读写分离大查询走从库报表/分析查询
换引擎分析型换 ClickHouse/DorisOLAP 场景

🎯 面试总结:

“大表优化按成本从低到高:加索引 → 分区 → 归档冷数据 → 分表。超过千万行要关注——但关键不是行数,而是查询能不能走索引、单次扫描行数有多大。”


📋 运维 & 故障排查避坑小结

  1. 慢查询日志 + pt-query-digest = 优化的第一手情报
  2. 生产备份标配 xtrabackup,mysqldump 只做小库
  3. 长事务三大危害:锁不释放 + undo 膨胀 + 主从延迟
  4. 大表三板斧:分区 → 归档 → 分表
  5. 监控必须上:Prometheus + Grafana + MySQL Exporter

附录:面试快速自查清单

必背核心概念

  • SQL 执行顺序及每一步为何在这个位置
  • ACID 四个特性 + MySQL 实现机制(undo log / redo log / MVCC)
  • 四种隔离级别及各自解决的问题
  • MVCC 原理(ReadView + undo log 版本链 + 可见性规则)
  • InnoDB 三种行锁算法(Record / Gap / Next-Key)+ 行锁通过索引实现
  • 聚簇索引 vs 非聚簇索引,回表 vs 覆盖索引
  • 联合索引最左前缀原则 + 为什么
  • 索引失效 8 大场景 + 核心原因
  • EXPLAIN 三大指标(type / rows / Extra)
  • 主从复制原理(三个线程)
  • 深分页优化(游标分页)
  • B+ 树为什么是 MySQL 的最优索引结构

面试高频追问链

开局问题预判追问准备方向
“为什么用 B+ 树?”B+ vs B vs Hash vs 红黑树磁盘 IO、范围查询、叶子链表
“RR 能解决幻读吗?”MVCC 快照读 vs Gap Lock 当前读两个机制分工
“怎么优化这 SQL?”EXPLAIN 分析 → 加索引 → 改写优化四层框架
“行锁怎么实现的?”通过索引 → 没索引变表锁 → 锁膨胀索引加锁机制
“分布式怎么搞?”ShardingSphere → 全局 ID → JOIN/事务分库分表全景

最后提醒: 每个知识点都能用”在我们项目中遇到过这样的情况……”开头来回答。面试官不想要背书机器,他们要的是理解原理 + 有实战经验的人。

评论区

欢迎留下你的想法

评论系统还没有接入配置,界面已经预留好了。

上一篇
下一篇

CHENYE ARCHIVE

进入信号层

正在接入这片属于我的信号层

早上好!