MySQL 面试题是个很神奇的东西:网上版本一大堆,九成都停留在“默写答案”层面;真正到了面试追问、现场调优、线上故障复盘的时候,很多背过题的人一下子就露馅了。我自己从开发转 DBA,后来也做过技术面试官,一个很深的体会是——面试官问 MySQL,不是考你会不会背命令,而是看你是“用过数据库”,还是“想明白数据库”的人。这次我把工作里踩过的坑、社区里被反复追问的高频题重新梳理了一遍,归纳成 7 大领域共 50 道经典题,覆盖 SQL 基础、索引、事务、存储引擎、锁、主从复制、运维排障和真实场景设计。
这篇文章不会给你一份“标准答案合集”,而是把每类题目的复习逻辑、答题主线和容易忽略的深挖点拆开讲。准备跳槽的同学,可以把它当成自测清单;已经在做开发、运维的同学,从第七部分开始会更有感觉,因为那些问题基本都来自我能碰到的真实事故。
1. 领域一:SQL基础与数据检索,最先翻车的往往是这一块
1.1 为什么把SQL基础单独拎出来?
很多人的误区是“SQL 不就是 select 吗,有什么好复习的”。但面试官恰恰喜欢先用基础题暖场,再用追问判断你的熟练度。MySQL 底层是一个优化器驱动的数据库,SQL 写得好不好,直接决定有没有可能命中索引、扫描多少行、会不会产生临时表和文件排序。
这里最容易被追到的一个基础点,是 SQL 的逻辑执行顺序。注意我说的是“逻辑顺序”,因为优化器真正执行时可能会调整计划,但分析一条 SQL 的问题,还是要靠这条逻辑链:
FROM -> ON -> JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT
这条顺序能解释不少实用结论:WHERE 里不能使用 SELECT 中定义的别名,因为 WHERE 执行在 SELECT 之前;ORDER BY 可以使用别名,因为它在 SELECT 之后执行;GROUP BY 之后才能用 HAVING 过滤组。
1.2 先自测:这一组的8道题
我把这个领域常考的题固定在下面 8 道,编号 1-8,后面会统一汇总成 50 题的完整清单。
- 一条 SELECT 在逻辑上按什么顺序执行?为什么 WHERE 里不能直接用 SELECT 别名?
- LEFT JOIN 与 INNER JOIN 有什么本质区别?驱动表是固定的一张表吗?
- UNION 和 UNION ALL 该怎么选?它们的执行代价差在哪?
- WHERE 和 HAVING 的过滤时机有什么不同?
- 在 GROUP BY 之后,SELECT 为什么不能随意写非分组字段?
- DISTINCT 和 GROUP BY 都能去重,实际执行差在哪?
- EXISTS / IN 在子查询中怎么选?子查询慢怎么办?
- 一张学生课程成绩表,如何查出每门课最高分对应的完整学生记录?
1.3 重点拆解:LEFT JOIN、GROUP BY 与去重
第 2 题是典型的“会答但不一定对”。LEFT JOIN 的语义是“左表记录全部保留,右表匹配不到就补 NULL”。但很多人把过滤条件写在 WHERE 里,结果把语义悄悄改掉了。
看这两条 SQL:
sql复制-- 写法A
SELECT *
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.status = 1;
-- 写法B
SELECT *
FROM orders o
LEFT JOIN users u ON o.user_id = u.id AND u.status = 1;
写法 A 会先做 LEFT JOIN,再对结果集过滤,右表为 NULL 的行因为 status 不等于 1 被丢掉了,最终效果约等于 INNER JOIN;写法 B 是在 JOIN 时限定右表匹配条件,左表仍然全部保留,这才是真正的 LEFT JOIN。实际业务里要“查所有订单,附带有效用户信息”,必须用写法 B。这个细节几乎每年都会被拿来考。
第 5 题的坑也很经典。MySQL 5.7 之后默认开启 ONLY_FULL_GROUP_BY,如果执行 SELECT student_id, course_id, MAX(score) FROM scores GROUP BY student_id,会直接报错:非分组字段 course_id 不在 GROUP BY 中。但在更早的版本或关闭严格模式时,这条 SQL 能跑,可返回的 course_id 是这一组里的随机值,没有任何业务意义。
如果要查“每门课最高分对应的学生”,正确思路是先用子查询算出每组最大分数,再关联回原表:
sql复制SELECT a.*
FROM student_scores a
JOIN (
SELECT course_id, MAX(score) AS max_score
FROM student_scores
GROUP BY course_id
) b
ON a.course_id = b.course_id AND a.score = b.max_score;
在 MySQL 8.0 中还可以用窗口函数,面试时能主动说出两种写法,说明你对传统分组和窗口函数都有掌握:
sql复制SELECT course_id, student_id, score
FROM (
SELECT course_id, student_id, score,
ROW_NUMBER() OVER (PARTITION BY course_id ORDER BY score DESC) AS rn
FROM student_scores
) t
WHERE rn = 1;
第 6 题关于去重,还有一个结合搜索热词的高频追问:“OR 能去重吗?”。OR 的逻辑是“满足任一条件即返回”,它本身不会产生重复行,也不负责去重;去重用 DISTINCT。真正的性能问题是 OR 在不同的索引列上时,MySQL 经常会退化,把两侧条件合并后做全表扫描。这种情况下用 WHERE a = 1 UNION ALL SELECT ... WHERE a = 2 反而更容易让每个分支走索引。DISTINCT 和 GROUP BY 在执行层面有不少重合:DISTINCT 会隐式地按目标列分组,GROUP BY 也可以当成去重手段。但 GROUP BY 能配合聚合函数,DISTINCT 不行;无索引时两者都靠临时表完成,谁也别觉得谁多高明。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 领域二:索引与执行计划,面试官最愿意往深处追问的版块
2.1 先回答“为什么用索引”:数据库不是字典,是B+树
数据库表的一行行数据,如果没有任何索引,想找到一行只能全表扫,数据量到千万级别时IO次数会指数级上升。InnoDB 默认用 B+ 树索引,它的核心优势是矮胖、有序、叶子节点形成双向链表。B+ 树只有 3 到 4 层,几十亿行数据也只需要几次磁盘IO就能定位到目标。
面试问到这里,自然要区分聚簇索引和二级索引。InnoDB 表的整行数据就存在主键索引的叶子节点上,这叫聚簇索引;二级索引的叶子节点存的是主键值。你通过二级索引找到主键,再回聚簇索引取整行,这个动作叫回表。为什么二级索引叶子不直接存完整行?因为如果每张二级索引都复制一份完整行数据,表占用的空间和维护成本都会爆炸。
2.2 这一组8道题,先看自己卡在第几题
这一领域的题号从第 9 题到第 16 题,覆盖面很广:
- 为什么 B+ 树适合做索引?为什么不用哈希或二叉树?
- 聚簇索引与二级索引、回表有什么区别?
- 什么情况下索引会失效?请至少说出五种场景。
- 什么是覆盖索引?为什么覆盖索引能减少回表?
- 联合索引的最左前缀原则具体怎么解释?(a,b,c) 索引哪些查询能用?
- 如何理解 EXPLAIN 的 type 字段?const、ref、range、index、all 分别代表什么?
- 为什么索引不是越多越好?
- 明明有索引,但优化器没选,你会怎么排查?
第 9 题很多答案只答“B+ 树矮,IO少”,少了更关键的一点。哈希索引能 O(1) 查找,但不支持范围查询;二叉树在极端情况下会退化成长链表。B+ 树的数据都集中在叶子节点,并且叶子之间用指针串联,天然适合范围扫描和排序。
第 11 题是索引面试题的重灾区,我把它做成了一张对照表,方便记忆:
| 失效场景 | 背后的原因 | 解决思路 |
|---|---|---|
对索引列使用函数,如 WHERE YEAR(create_time)=2024 |
索引列的有序性被破坏 | 改写为范围条件 create_time BETWEEN ... |
| 索引列做隐式类型转换,如 varchar 字段用 int 查询 | 索引列被套 CAST,无法比较 | 参数类型与字段类型保持一致 |
LIKE 模糊查询以 % 开头,如 LIKE '%abc' |
无法利用有序前缀 | 改用全文索引或搜索引擎 |
| 联合索引未满足最左前缀 | B+ 树内部先按最左列排序 | 检查 WHERE 拼接条件顺序 |
| 优化器判断全表扫更快 | 数据量小、区分度低,走索引还要回表 | 增加区分度高的列建联合索引 |
关于隐式转换,可以记一个经典例子:手机号字段是 varchar,但你写 WHERE phone = 13800001111,MySQL 会将字符串列转成数字再比较,索引列上的索引就失效了。正确写法是给值加引号,WHERE phone = '13800001111'。
2.3 用 EXPLAIN 把模糊的调优落到证据上
第 14 题如果只能答出 type 的名称,面试官会认为你不够扎实。你应该顺手用执行计划讲一下表扫描路径:
sql复制EXPLAIN SELECT student_id, score
FROM student_scores
WHERE course_id = 1
ORDER BY score DESC;
type 从左到右性能从好到差:
- system:表只有一行,是 const 的一种特例。
- const:按主键或唯一索引等值查询,最多返回一行。
- eq_ref:连接查询时,被驱动表按主键或唯一索引等值匹配。
- ref:非唯一索引等值匹配。
- range:索引范围扫描,比如 BETWEEN、>、<。
- index:遍历整棵二级索引树,比 all 全表扫描好一点,但也不理想。
- all:全表扫描。
Extra 里出现 Using index 是好事,说明这条查询覆盖索引了;出现 Using filesort 要警惕,它代表 MySQL 需要额外排序,文件排序不一定真的用文件,但会增加排序开销。如果看到 Using temporary,则说明使用临时表,常见于 GROUP BY、去重和部分子查询。
第 12 题的覆盖索引有一个非常实用的推论:想让它生效,就不能无脑 SELECT *。因为覆盖索引的前提是“查询需要的列都包含在同一个索引里”,一旦加一个不在索引里的列,就得回表。比如一个联合索引是 (course_id, score),SELECT score FROM student_scores WHERE course_id=1 就能完全走索引完成;但如果变成 SELECT student_id, score ...,student_id 不在索引中,MySQL 只能回表去主键索引里取。
3. 领域三:事务、隔离级别与MVCC,答案要有主线
3.1 ACID 不是四个空词,背后是四套机制
第 17 题“ACID 分别靠什么保证”,答得好的标准不是背诵四个定义,而是能把每个词落到 MySQL 的内部机制上。
原子性靠 undo log。事务里执行的每条修改,InnoDB 都会先记录一份“修改前”的数据到 undo log;如果中途出错,就沿着 undo 把变更回滚。
持久性靠 redo log。InnoDB 采用 WAL,也就是先写日志再写数据页。事务提交时,不要求数据页立刻刷盘,redo log 先落盘;即使数据库突然宕机,重启后也能根据 redo log 把已提交但没来得及刷盘的数据重放
