MySQL 面试宝典
适用对象: 初级 ~ 中级后端开发,适配大厂面试、求职复习、刷题背诵。
使用建议: 先理解”深度理解” → 再记”面试总结” → 口述时用自己的话串起来。
目录
一、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 | ┌──────────────────────────────────────────────────────────────┐ |
⚠️ 易错点:
SELECT的别名只能在ORDER BY中使用,WHERE/HAVING/GROUP BY中不能用,因为它们都在SELECT之前执行。
延伸追问:
| 追问 | 答案 |
|---|---|
“为什么 WHERE 不能用聚合函数?” | WHERE 在 GROUP BY 之前,还没分组,聚合函数没有计算对象 |
“为什么 ORDER BY 能用别名?” | 因为 ORDER BY 在 SELECT 之后执行,别名已经定义好了 |
“ON 和 WHERE 在 JOIN 中有什么区别?” | 内连接时效果相同;外连接时 ON 决定”哪些行能匹配上”,WHERE 在匹配完后做行级过滤,会去掉外连接补的 NULL 行 |
🎯 面试总结:
“SQL 执行顺序是 FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT。这个顺序的核心逻辑是先取数据、再逐层过滤、最后输出——每步都依赖上一步的结果。理解这个顺序,就能解释为什么 WHERE 不能用别名、为什么 WHERE 不能用聚合函数、为什么 ORDER BY 能用别名。”
Q2:WHERE 和 HAVING 的区别? ⭐⭐
| 对比维度 | WHERE | HAVING |
|---|---|---|
| 执行时机 | GROUP BY 之前 | GROUP BY 之后 |
| 作用对象 | 原始行数据 | 分组后的聚合结果 |
| 能否用聚合函数 | ❌ 不能 | ✅ 可以 |
| 能否用列别名 | ❌ 不能(SELECT 未执行) | ❌ 不能(标准 SQL;MySQL 扩展允许但别用) |
| 性能 | 优先过滤,效率高 | 在分组后过滤,数据量通常较小但也有开销 |
深度理解 —— 为什么需要两个过滤条件?
WHERE 和 HAVING 分工不同,本质是时间差: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 | -- 基础分页(小数据量) |
| 方式 | 适用场景 | 缺点 |
|---|---|---|
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 | -- 用户变量(会话级别,前缀 @) |
| 对比维度 | 存储过程 | 函数 |
|---|---|---|
| 返回值 | 可选(通过 OUT 参数) | 必须有返回值 |
| 调用方式 | CALL proc() | SELECT func() 或表达式中 |
| 事务控制 | 允许 COMMIT/ROLLBACK | 不允许 |
| 适用场景 | 批量操作、复杂业务逻辑 | 计算、转换、格式化 |
深度理解 —— 为什么函数不能有事务语句?
函数通常在 SELECT 语句中被调用,而 SELECT 本身是一个只读操作。如果函数内部偷偷 COMMIT 或 ROLLBACK,会破坏外层语句的事务语义。MySQL 对此做了硬性限制。
🎯 面试总结:
“存储过程和函数的核心区别:函数必须返回一个值且不能用事务语句,适合做计算和转换;存储过程可以没有返回值、可以控制事务,适合封装批量操作。现代开发中存储过程逐渐被应用层代码替代,面试时讲清楚区别即可。”
Q6:UNION 和 UNION ALL 的区别? ⭐⭐
| 对比 | UNION | UNION ALL |
|---|---|---|
| 去重 | ✅ 去重(等价于 DISTINCT) | ❌ 不去重 |
| 内部处理 | 排序 + 比较去重(临时表) | 直接拼接 |
| 性能 | 慢(O(n log n)) | 快(O(n)) |
深度理解 —— 为什么 UNION 需要临时表?
UNION 的去重语义要求 MySQL 判断”这行有没有出现过”,而两个结果集是先后返回的,不可能在全部行出来之前完成去重。所以必须先把所有行写入临时表,然后排序、比较相邻行去重、最后输出。临时表 + 排序 = 双重开销。UNION ALL 不需要去重,直接逐行输出拼接结果。
🎯 面试总结:
“百分之九十的场景不需要去重,直接用
UNION ALL。UNION和UNION ALL的性能差距是数量级的——多了一次临时表创建和排序。面试时一定要强调’不需要去重就用 UNION ALL’。”
Q7:MySQL 中 NULL 值如何处理?对性能有什么影响? ⭐⭐
1 | -- NULL 判断只能用 IS NULL / IS NOT 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 | -- 建库/建表指定 |
| 概念 | 说明 |
|---|---|
| 字符集(charset) | 编码规则:字符 → 二进制 |
| 排序规则(collation) | 比较规则:决定两个字符谁大谁小 |
_ci | case insensitive,大小写不敏感('a' = 'A') |
_bin | 二进制比较,区分大小写和重音 |
_general_ci vs _unicode_ci | general 快但不精确,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 | CREATE VIEW v_active_users AS |
| 类型 | 说明 |
|---|---|
| 普通视图 | 仅存 SQL 定义,查询时实时从基表计算,不存数据 |
| 物化视图 | 存储计算结果,查询快但需刷新,MySQL 不原生支持(需触发器模拟) |
深度理解 —— 视图的两种执行算法:
MySQL 执行视图查询时有两种策略:
- MERGE 算法:把视图的 SQL 和外部查询的 SQL 合并成一个 SQL,然后一起优化执行。这是最优方式——视图和基表在同一个执行计划中。
- TEMPTABLE 算法:先把视图的结果物化到一个临时表,外部查询再对这个临时表查询。无法做索引优化,性能差。
简单视图(无聚合、无 DISTINCT、无子查询)走 MERGE,复杂视图被迫走 TEMPTABLE。这就是为什么多层嵌套视图性能急剧恶化的原因——每一层都可能产生临时表。
🎯 面试总结:
“视图本质是保存的 SQL 语句,不存数据。作用是简化复杂查询、做安全隔离。但它有两个坑:性能不确定(复杂视图走临时表),和 MySQL 不支持物化视图。生产环境中用视图做权限隔离可以,复杂查询建议直接写 SQL。”
1.3 触发器
Q10:触发器是什么?有哪些分类?为什么慎用? ⭐⭐
1 | CREATE TRIGGER before_insert_user |
| 分类 | 说明 | 典型用途 |
|---|---|---|
BEFORE INSERT | 插入前触发 | 自动填充默认值、数据校验 |
AFTER INSERT | 插入后触发 | 写入日志、更新统计表 |
BEFORE UPDATE | 更新前触发 | 数据校验、自动修改时间 |
AFTER UPDATE | 更新后触发 | 记录变更历史、同步缓存 |
BEFORE DELETE | 删除前触发 | 防止误删、备份数据 |
AFTER DELETE | 删除后触发 | 级联清理关联数据 |
NEW:引用 INSERT/UPDATE 之后的行OLD:引用 UPDATE/DELETE 之前的行
深度理解 —— 为什么大厂普遍禁用触发器?
- 隐式开销:每插入一行就额外执行一段逻辑,批量插入 10 万行 = 10 万次触发器调用,性能雪崩
- 调试困难:触发器在数据库内部静默执行,应用层看不见,出了问题排查成本极高
- 死锁放大器:触发器可能修改其他表,引入未预期的锁竞争
- 可移植性差:换数据库(如迁移到 PostgreSQL)时触发器语法完全不同
🎯 面试总结:
“触发器是由 DML 事件自动触发的代码块。面试时坦白说知道它的用法,但在生产环境中倾向于把触发逻辑放到应用层——更可控、更好调试、更好维护。如果面试官追问’什么场景适合用’,答审计日志(与业务无关的旁路逻辑)。”
1.4 临时表
Q11:临时表是什么?与普通表有什么本质区别? ⭐
1 | -- 临时表只对当前会话可见 |
| 特性 | 临时表 | 普通表 |
|---|---|---|
| 可见性 | 仅当前会话 | 所有会话 |
| 生命周期 | 会话断开时自动 DROP | 永久 |
| 同名冲突 | 和普通表同名时,优先访问临时表 | — |
| 存储位置 | 磁盘(InnoDB 临时表空间)或内存 | 磁盘 |
深度理解 —— 临时表的两种存储引擎切换:
MySQL 优先在内存(TempTable 引擎)中创建临时表,超过 tmp_table_size / max_heap_table_size 后自动转磁盘(InnoDB 临时表空间)。这意味着——临时表不一定”快”,如果数据量超过内存阈值,转为磁盘临时表后会显著变慢。大结果集不要用临时表,用分批处理。
🎯 面试总结:
“临时表最大的好处是会话隔离,会话结束自动清理,适合存储复杂计算的中间结果。注意事项:大临时表会落盘,性能下降;和普通表同名时优先访问临时表可能引起隐蔽 bug。”
1.5 FULLTEXT 全文搜索
Q12:MySQL 的 FULLTEXT 全文搜索原理和用法? ⭐⭐
1 | ALTER TABLE articles ADD FULLTEXT INDEX ft_title_body (title, body); |
深度理解 —— 为什么 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 | -- LBS 附近搜索(查找 5km 内的门店) |
深度理解 —— 为什么空间索引用 R-Tree 而不是 B+ 树?
B+ 树是一维排序(按值的线性大小排列),而地理位置是二维的(经纬度)。一维排序无法表达二维空间的”附近”关系——经度接近的不一定纬度也接近。R-Tree(矩形树)将空间划分为嵌套的矩形区域,搜索时只检查相交的矩形,天然适合二维范围查询。
🎯 面试总结:
“地理位置查询建 SPATIAL INDEX(底层 R-Tree),不要用双字段
lat BETWEEN x AND y AND lng BETWEEN a AND b,因为 B+ 树无法同时利用两个独立索引实现二维范围过滤。”
📋 基础概念避坑小结
utf8mb4≠utf8,MySQL 的utf8是阉割版NULL = NULL返回 NULL(不是 TRUE),判断 NULL 必须用IS NULLCOUNT(列名)不统计 NULL,COUNT(*)统计所有行,两者结果可能不同- 视图不存数据,复杂视图触发临时表算法,性能差
- 触发器隐式执行、不好调试,大厂普遍禁用
二、数据库设计 & 索引
2.1 数据库设计
Q14:数据库三大范式是什么?为什么要反范式化? ⭐⭐⭐
| 范式 | 核心要求 | 反例 | 解决的问题 |
|---|---|---|---|
| 1NF | 字段原子性,列不可再分 | "张三,李四" 存一个字段 | 数据可操作 |
| 2NF | 非主键列完全依赖主键(消除部分依赖) | 联合主键下某列只依赖其中一个主键 | 数据冗余、更新异常 |
| 3NF | 非主键列不传递依赖主键(消除传递依赖) | 学生表存了班主任电话(应通过班级ID关联) | 数据冗余、删除异常 |
深度理解 —— 范式解决了什么问题?
1 | 反范式问题示例: |
那为什么实际开发中又允许”反范式化”?
典型的反范式化例子:订单表存冗余的 user_name。为什么这样做?——如果每次都 JOIN 用户表拿名字,高并发下 JOIN 是瓶颈。冗余字段避免了 JOIN,用存储换时间。范式保证数据一致性,反范式化追求查询性能,这是永恒的矛盾。
🎯 面试总结:
“三范式本质是消除数据冗余和操作异常——1NF 保证原子性、2NF 消除部分依赖、3NF 消除传递依赖。实际开发中会根据查询需求适当反范式化冗余字段,用空间换时间。核心原则:知道自己在违反哪个范式、为了什么性能收益、如何弥补(用应用层保证一致性)。”
Q15:MySQL 中 InnoDB 和 MyISAM 的区别?为什么 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 | ALTER TABLE orders ADD CONSTRAINT fk_user |
| 级联操作 | 效果 |
|---|---|
CASCADE | 主表行被删除时,子表关联行也删除 |
SET NULL | 主表行被删除时,子表外键列置 NULL |
RESTRICT | 有子表行时禁止删除主表行 |
深度理解 —— 为什么阿里开发手册禁止外键?
外键约束会导致隐式加锁。当你插入子表时,MySQL 需要去主表检查外键值是否存在——这就要求对主表相关行加共享锁。高并发下这种隐式锁会引发意想不到的锁等待和死锁,开发者难以从应用层 SQL 中直接看到这些锁。
另一个问题是级联操作的不确定性:ON DELETE CASCADE 一次删除可能会连锁触发多表删除,产生大量 undo log、拖慢事务,而这些连锁反应对开发者是”透明的”——难以预估影响范围。
🎯 面试总结:
“外键保证引用完整性,但带来隐式锁、级联风险。大厂普遍禁止外键,将关系约束放在应用层保证。面试时这样答:’我知道外键的作用,但在高并发场景下倾向于用应用层逻辑 + 对账任务来保证数据一致性’。”
Q17:主键和索引的设计原则?为什么推荐自增主键? ⭐⭐⭐
主键设计核心原则:短 + 有序 + 不变。
| 原则 | 为什么 |
|---|---|
| 自增整型 | 插入有序 → 页分裂最少;4/8 字节 → 短 |
| 避免 UUID | UUID 无序 → 插入位置随机 → 频繁页分裂 → 碎片严重 |
| 避免业务主键 | 可能变动(如手机号换绑),更新主键代价极大(所有二级索引叶子都要改) |
| 分布式用 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 | InnoDB(聚簇索引): |
| 对比 | 聚簇索引(InnoDB) | 非聚簇索引(MyISAM) |
|---|---|---|
| 叶子存什么 | 完整的行数据 | 数据的物理地址(行指针) |
| 每表能建几个 | 只能 1 个 | 多个 |
| 二级索引怎么找数据 | 先查二级索引拿到主键 → 回表查聚簇索引 | 直接走物理地址 |
| 主键大小的影响 | 主键越长,二级索引就越大(叶子存主键) | 无影响 |
深度理解 —— 为什么聚簇索引只能有一个?
因为数据本身只有一个物理存储。聚簇索引的 B+ 树叶子节点就是数据行——你不能把同一行数据存在两个不同的 B+ 树里。所以聚簇索引只有一个,必然是主键索引。
🎯 面试总结:
“聚簇索引的叶子节点存完整行数据,一张表只能有一个。InnoDB 的主键就是聚簇索引,二级索引查完后要回表。这就是为什么主键要短——主键越长,所有二级索引的叶子都越大,占更多磁盘空间。”
Q21:什么是覆盖索引和回表?为什么重要? ⭐⭐⭐
1 | 假设表有索引 idx_name_age (name, age) |
深度理解 —— 为什么回表代价大?
回表 = 多一次随机 IO。二级索引叶子存的主键 ID 是”有序但离散”的——不是连续的行号——所以回表访问聚簇索引时是随机读而非顺序读。随机读在机械硬盘上大约 5~10ms,SSD 上也要 0.1ms。如果一个查询回表扫描 10000 行,累积的随机读延迟是致命的。覆盖索引把这些随机读全部消掉。
🎯 面试总结:
“回表是查询性能的大敌——多一次随机 IO。覆盖索引让查询字段全部落在索引中,不回表。减少回表的方法:不写
SELECT *,按业务查询的字段建联合索引覆盖它们。面试时记住检查清单:EXPLAIN Extra 出现Using index= 用到了覆盖索引。”
Q22:联合索引的最左前缀原则是什么?为什么? ⭐⭐⭐
1 | -- 联合索引 (a, b, c) |
深度理解 —— 为什么跳过最左列整个索引就用不了?
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 IN | WHERE status != 0 | 大多数行不满足 → 优化器选全表 |
高 NULL 比例的 IS NOT NULL | WHERE 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 | CREATE TABLE orders ( |
每个分区是独立的物理文件,可以有自己的索引。查询时优化器根据 WHERE 条件中的分区键,跳过不相关的分区——这就是分区裁剪(Partition Pruning)。
🎯 面试总结:
“分区表把一个逻辑表拆成多个物理文件,核心价值是分区裁剪——只访问相关分区。最常用 RANGE 分区(按时间)。注意事项:WHERE 条件必须包含分区键才能触发裁剪,否则扫全部分区。”
📋 数据库设计 & 索引避坑小结
- 主键自增 > UUID:UUID 的随机性导致页分裂,索引碎片化
SELECT *是覆盖索引杀手:多一个字段就多一次回表- 联合索引顺序很重要:区分度高的列放最左
- 索引不是免费的:每个索引拖慢 INSERT/UPDATE/DELETE
- 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 | system > const > eq_ref > ref > range > index > ALL |
Extra 信号灯:
| Extra | 含义 | 🟢/🔴 |
|---|---|---|
Using index | 覆盖索引,不回表 | 🟢 最优 |
Using index condition | 索引下推(ICP),在引擎层用索引列过滤 | 🟢 好 |
Using where | 用 WHERE 条件在 Server 层过滤 | 🟡 正常 |
Using temporary | 用了临时表 | 🔴 排查:GROUP BY / DISTINCT 没走索引? |
Using filesort | 额外排序 | 🔴 排查:ORDER BY 没走索引? |
Using join buffer | JOIN 没走索引,用了 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 | 优化四个层次(越往下成本越高,先从上往下走): |
深度理解 —— 为什么按这个顺序?
索引是数据库内部的”免费加速器”——加一个索引,不改任何代码,查询瞬间变快。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 | -- ❌ 旧版 MySQL 中的慢写法(每行执行一次子查询) |
深度理解 —— 子查询慢的真正原因:
旧版 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 | -- 如果 orders.user_id 有 NULL 值,NOT IN 返回空! |
必须用 NOT EXISTS 或 LEFT JOIN ... IS NULL 替代 NOT IN。
🎯 面试总结:
“MySQL 5.6+ 子查询做了物化优化,
IN子查询性能大幅改善。但NOT IN仍然是坑——遇到 NULL 值直接返回空结果。永远用NOT EXISTS或LEFT JOIN ... IS NULL替代NOT IN。”
Q30:IN 和 EXISTS 怎么选?核心原则是什么? ⭐⭐
1 | -- EXISTS:外表驱动,对内表逐条用索引查 |
| 场景 | 推荐 | 原因 |
|---|---|---|
| 外表小,内表大 | EXISTS | 循环次数少 × 每次用索引快速查 = 快 |
| 外表大,内表小 | IN | 子查询结果集小,物化成本低 |
深层原则:小表驱动大表——让外层循环的次数最少。
🎯 面试总结:
“
EXISTS是循环外表逐条对内表做索引查找,IN是先算出内表结果集再查外层列。选谁看谁小——小表驱动大表。记住:EXISTS的外层是驱动表。”
Q31:如何优化 ORDER BY 查询? ⭐⭐
1 | -- 让 WHERE 列和 ORDER BY 列组成联合索引 |
能用索引排序的前提(缺一不可):
ORDER BY的列必须在联合索引中且顺序匹配ORDER BY不能有ASC/DESC混合(8.0 之前)WHERE条件不能有范围查询夹在索引中间
深度理解 —— filesort 是什么?为什么它慢?
filesort 不等于”读文件排序”,分两种情况:
- 排序数据量 ≤
sort_buffer_size:内存排序(快速排序),较快 - 排序数据量 >
sort_buffer_size:磁盘排序(外排序,写临时文件),极慢
filesort 的代价在于:即使走内存排序,它也需要把数据行从数据页复制到 sort_buffer 再排序——额外内存和 CPU 开销。如果能利用索引已有的有序性,连排序都省了。
🎯 面试总结:
“
ORDER BY优化的核心是让排序走索引——建联合索引(WHERE 列在前,ORDER BY 列在后),利用索引天然有序性免排序。EXPLAIN 看到Using filesort就是没利用索引排序的信号。”
Q32:如何优化 DISTINCT 查询? ⭐
1 | -- DISTINCT 去重 |
深度理解 —— DISTINCT 和 GROUP BY 在内部实现的差异:
DISTINCT 本质是隐藏的 GROUP BY(对所有 SELECT 列分组)。在 MySQL 优化器中,DISTINCT 和 GROUP BY 经常走到相同的执行路径。但 GROUP BY 可以利用松索引扫描(Loose Index Scan)——当只需要分组列的值而不需要聚合时,直接跳读索引中每个分组的第一行,跳过组内行。DISTINCT 有时不会触发这个优化。
🎯 面试总结:
“
DISTINCT的优化:优先建索引覆盖去重列,让它利用索引的有序性去重。如果真的只是要SELECT DISTINCT col FROM t,给 col 建索引,GROUP BY col有时比 DISTINCT 更好地利用松索引扫描。”
Q33:大批量 UPDATE / DELETE / SELECT 如何安全处理? ⭐⭐
1 | -- ❌ 致命做法:一条 SQL 锁大量行,阻塞所有并发操作 |
深度理解 —— 大批量操作的三重危害:
- 锁膨胀:InnoDB 对扫描到的所有行加锁(没索引的表直接锁全表)。分批操作每次只锁一小批,不影响其他事务
- undo log 膨胀:一个事务修改 100 万行 = 100 万行旧版本存在 undo log 中供 MVCC 使用 → 占用巨大磁盘空间 → 其他事务读这些行时要沿 undo 链回溯
- 主从延迟:大事务提交后,从库回放同等时间,复制延迟飙升
🎯 面试总结:
“大批量 DML 的核心原则:分批操作 + 避免长事务。每条 SQL 只操作 1000~10000 行,循环执行,每批之间短暂 SLEEP。一箭三雕——减少锁持有时间、控制 undo log 大小、避免主从延迟。”
Q34:如何预防和清理重复数据? ⭐
1 | -- 查找重复行 |
🎯 面试总结:
“处理重复数据分两步:先用 GROUP BY + HAVING 找到重复行,再用自 JOIN 删除保留一条。根本解决方法是建唯一索引——防患于未然。”
Q35:优化器提示(Optimizer Hints)什么时候用? ⭐⭐
1 | -- 语法 |
深度理解 —— 为什么优化器会选错索引?
优化器基于统计信息(索引基数、数据分布)做代价估算,但统计信息可能是过时的或不准确的(ANALYZE TABLE 不及时)。数据倾斜严重时(某个值占了 90% 的行),优化器可能低估/高估某条索引的代价。
🎯 面试总结:
“Hints 是最后手段,不是常规优化。优化器选错索引时先
ANALYZE TABLE更新统计信息,无效才用 Hints。长期看应优化索引设计,而非靠 Hints 硬纠正。”
📋 SQL 优化避坑小结
EXPLAIN三看:type → rows → ExtraNOT IN永远不要用,NULL 值会让结果为空- 深分页用游标分页,
LIMIT 1000000, 10= 扫描 1000010 行 - 大批量 DML 必须分批,每批 ≤ 10000 行
- 覆盖索引 + 避免 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 级别用了两个武器:
- 快照读(Snapshot Read):
SELECT ...不加锁的普通查询,用 MVCC 的 ReadView 保证读取事务开始时的快照——即使其他事务插入了新行,也看不到(因为新行的 DB_TRX_ID 大于当前 ReadView)→ 避免了快照读的幻读 - 当前读(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 | 每行数据有三个隐藏列: |
版本可见性判断逻辑(一句话版):
1 | 1. trx_id == 我 → 我自己改的,可见 |
深度理解 —— 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/WRITE | MyISAM,或 InnoDB 手动加 |
| 行级锁 | InnoDB 自动(通过索引) | 高并发 OLTP |
| 元数据锁(MDL) | DDL 时自动 | 防止 DDL 和 DML 并发 |
InnoDB 行锁按算法分(核心!):
1 | 假设索引 id 有值:1, 5, 10, 15 |
深度理解 —— 为什么行锁必须通过索引?
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 | -- 悲观锁:先锁后改 |
深度理解 —— 乐观锁的 ABA 问题:
乐观锁只用版本号判断”有没有被人改过”,不关心改了几次或改了什么。理论上存在 ABA 问题(版本从 5→6→5,你看不出来),实际中不太可能连续改两次恰好回到同一个版本号。如果在意,用时间戳做版本号。
🎯 面试总结:
“乐观锁是应用层思想,悲观锁是数据库层实现。冲突少用乐观(性能好、无锁等待),冲突多用悲观(减少无效重试)。库存扣减场景:乐观锁的首选是原子 UPDATE
SET stock = stock - 1 WHERE stock > 0——利用 InnoDB 行锁的天然串行性,一条 SQL 搞定。”
Q42:死锁如何产生?如何预防?发生后怎么处理? ⭐⭐⭐
产生条件(四个缺一不可):
1 | ① 互斥 — 资源不能共享 |
经典死锁场景:
1 | -- 事务A -- 事务B |
预防四板斧:
| 策略 | 做法 | 原理 |
|---|---|---|
| 统一加锁顺序 | 所有事务按相同顺序访问资源 | 打破循环等待 |
| 缩短事务 | 减少锁持有时间,禁用事务中的 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 | 冲突率低 → 乐观锁(版本号 CAS) |
1 | -- 方案一:原子 UPDATE(一行 SQL,行锁保护,推荐首选) |
深度理解 —— 为什么原子 UPDATE 比 SELECT FOR UPDATE 更好?
原子 UPDATE 是一条 SQL,InnoDB 对 UPDATE 自动加行锁,锁从获取到释放只在一条语句的执行期间,极短。而 SELECT ... FOR UPDATE 的锁从 SELECT 开始持有直到事务 COMMIT——如果事务中还有业务逻辑、RPC 调用,锁持有时间会被大幅拉长,降低并发吞吐。
🎯 面试总结:
“高并发扣减按冲突程度分级处理:轻冲突用乐观锁,中等冲突用原子 UPDATE(利用行锁自动串行),重冲突用悲观锁,秒杀用 Redis 前置扣减。核心原则:锁持有时间越短越好——原子 UPDATE 的锁只持续一条语句,比 SELECT FOR UPDATE 的跨语句锁优越。”
📋 事务 & 锁机制避坑小结
- 行锁必须走索引,否则膨胀为表锁——面试高频必考
- 长事务是万恶之源:锁不释放、undo 膨胀、主从延迟
- 死锁靠两招:降低概率(统一顺序+短事务)+ 重试兜底
FOR UPDATE必须在事务中,否则不持有锁- 乐观锁必须处理
affected_rows = 0的重试逻辑
五、高阶特性
5.1 窗口函数
Q45:MySQL 的窗口函数有哪些?窗口函数和 GROUP BY 的本质区别? ⭐⭐⭐
1 | SELECT name, dept, salary, |
| 函数 | 结果示例 | 说明 |
|---|---|---|
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 | Master Slave |
三步流程:
- Master 的 binlog dump 线程把 binlog 事件推给 Slave
- Slave 的 I/O 线程接收 binlog 并写入 relay log(中继日志)
- 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 | CREATE TABLE logs ( |
核心价值: 分区裁剪(查询只访问匹配的分区)+ 快速清理历史数据。
注意事项: 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 | SET autocommit = 0; -- 关闭自动提交,手动批量 COMMIT |
🎯 面试总结:
“大数据导入用
LOAD DATA INFILE,比逐行 INSERT 快 20 倍。导入前关闭 autocommit、unique_checks、foreign_key_checks,导入后恢复。”
Q51:MySQL 如何实现数据压缩? ⭐
1 | -- InnoDB 页级压缩 |
| 压缩方式 | 级别 | 说明 |
|---|---|---|
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)) | 生产环境查看 |
| 静态脱敏 | 导出数据时批量改写 | 脱敏后给测试库 |
| 视图脱敏 | 建脱敏视图,应用查视图 | 权限控制 |
🎯 面试总结:
“脱敏不是加密——脱敏后的数据无法还原,加密可以解密还原。生产环境做动态脱敏,测试环境做静态脱敏。最低成本方案:建脱敏视图。”
📋 高阶特性避坑小结
- 窗口函数 8.0+ 才有,低版本用变量模拟(写法复杂且易错)
- 复制延迟靠两招:避免大事务 + 并行复制
- 分区表 WHERE 必须带分区键,否则裁剪失效
LOAD DATA比 INSERT 快 20 倍,大数据导入标配- 分库分表要先规划 JOIN 和事务,设计阶段解决分布式问题
六、运维 & 故障排查
6.1 日志与监控
Q53:慢查询日志如何开启和分析?完整优化流程? ⭐⭐⭐
1 | SET GLOBAL slow_query_log = ON; |
分析工具对比:
| 工具 | 优势 |
|---|---|
mysqldumpslow | MySQL 自带,基础统计 |
pt-query-digest | Percona 出品,报表详细,可分析 tcpdump |
performance_schema | 数据库内置,实时细粒度 |
标准优化闭环:
1 | ① 慢查询日志 → ② pt-query-digest 分析 → ③ 找 TOP N 慢 SQL |
🎯 面试总结:
“慢查询优化闭环:日志 → pt-query-digest → TOP N → EXPLAIN → 优化 → 验证。pt-query-digest 比 mysqldumpslow 强太多——面试提这个工具名说明你有实战经验。”
Q54:MySQL 如何进行性能排查?常用工具清单? ⭐⭐
| 工具/命令 | 用途 | 关键信息 |
|---|---|---|
SHOW PROCESSLIST | 当前运行的 SQL | 找执行时间长的 SQL、堆积的连接 |
SHOW ENGINE INNODB STATUS | InnoDB 引擎诊断 | 死锁日志、锁等待、事务状态 |
EXPLAIN | 单条 SQL 执行计划 | type/rows/Extra |
performance_schema | 细粒度性能指标 | 锁等待、内存使用、文件 IO |
information_schema | 库表元数据 | 表大小、索引信息、进程列表 |
pt-query-digest | 慢日志分析 | TOP SQL、执行频率、耗时分布 |
1 | -- 快速找长事务(超过 60 秒的事务) |
🎯 面试总结:
“排查问题先从
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 | -- 找耗时超过 10 秒的 SQL |
预案机制:
max_execution_time(5.7.8+):单条 SQL 最大执行时间,超时自动 killpt-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/Doris | OLAP 场景 |
🎯 面试总结:
“大表优化按成本从低到高:加索引 → 分区 → 归档冷数据 → 分表。超过千万行要关注——但关键不是行数,而是查询能不能走索引、单次扫描行数有多大。”
📋 运维 & 故障排查避坑小结
- 慢查询日志 + pt-query-digest = 优化的第一手情报
- 生产备份标配 xtrabackup,mysqldump 只做小库
- 长事务三大危害:锁不释放 + undo 膨胀 + 主从延迟
- 大表三板斧:分区 → 归档 → 分表
- 监控必须上: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/事务 | 分库分表全景 |
最后提醒: 每个知识点都能用”在我们项目中遇到过这样的情况……”开头来回答。面试官不想要背书机器,他们要的是理解原理 + 有实战经验的人。


评论区
欢迎留下你的想法评论系统还没有接入配置,界面已经预留好了。