简介:MySQL 面试题知识点总结资料包围绕数据库岗位面试高频考点整理,适合正在准备后端开发、数据库工程师等岗位面试的开发者查漏补缺。内容涵盖关系型与非关系型数据库差异、一条 MySQL 语句的完整执行流程、索引底层数据结构与常见类型、MyISAM 与 InnoDB 的 B 树索引实现区别、B+ 树设计原因、普通索引与唯一索引选择、覆盖索引与索引下推,以及导致索引失效的典型操作等核心问题,并说明了哈希表、有序数组与 N 叉树的适用场景,以及主键索引和非主键索引的区别。资源共 1 个 docx 文档,压缩包仅 40KB,便于快速下载与离线阅读;目前已有 251 人学习。文档按问答形式组织,对每个考点都给出结论和原因说明,可作为面试前速记清单,也可配合实际项目复盘索引优化与执行原理。
1. 这份 MySQL 面试题资源,值得花一个周末把它吃透
做后端这几年,我发现一个很尴尬的现象:项目里 CURD 写了无数遍,但一问到“一条 UPDATE 语句在 MySQL 里到底怎么走完的”“redo log 和 binlog 凭什么不能互相替代”,大多数人就开始含糊。这份 MySQL 面试题知识点总结,我把 37 个问题全部过了一遍,它几乎覆盖了你面试中被追问的所有高频死角:执行链路、索引失效、B+ 树选型、两阶段提交、主备同步、误删恢复。它不是零散背题,而是把 MySQL 的“骨架”串成了一条线——从客户端请求到引擎层数据页,从 change buffer 到 crash-safe,全都有来龙去脉。适合两类人:准备跳槽的 Java/后端开发,以及被线上慢查询和主从延迟折磨过、想系统补课的同学。下文我会挑最有价值的 20 多个点,按“原理→用法→踩坑”的节奏拆开讲,并给出可以直接抄的实验步骤和参数配置。
2. 执行链路与索引选型:从一条 SQL 说起
2.1 一条 UPDATE 语句的完整旅程
MySQL 的 Server 层和引擎层分工很多人搞混。这份面试题里对执行步骤的描述非常清晰:客户端请求到达后,先是连接器负责验证身份和权限,然后查缓存(MySQL 8.0 已经移除了查询缓存,但面试仍会问),接着分析器做词法分析和语法分析,优化器决定走哪个索引、用哪种 join 顺序,最后执行器调用引擎接口。有个关键点容易被忽略:执行器在真正调用引擎接口之前,还会再校验一次用户权限。
update T set a = 1 where id = 666;
这条语句的完整路径是:连接器验证用户 → 分析器解析表 T 和字段 a → 优化器确认 id 是主键索引,走主键查找 → 执行器调用 InnoDB 接口,InnoDB 从数据页里找到 id=666 这一行,在内存中把 a 改为 1,然后写 redo log(prepare 状态)→ 写 binlog → 提交事务,redo log 状态改为 commit。
注意这里有一个面试高频追问:为什么是“先写 redo log 的 prepare,再写 binlog,最后把 redo log 改成 commit”?这就是两阶段提交,下文第 4 章会专门展开。执行链路部分最重要的是让面试官知道,你对“Server 层 vs 引擎层”的边界是清楚的——MySQL 5.5 之后默认引擎是 InnoDB,但 Server 层的连接器、优化器、执行器不依赖具体引擎。
2.2 为什么非主键索引的叶子节点存的是主键值
索引类型这块,题目里讲得很透:主键索引的叶子节点存整行数据,叫聚簇索引;非主键索引的叶子节点存主键的值,叫二级索引。二级索引查询需要先找到主键值,再回聚簇索引查整行,这个过程叫回表。
create table user ( id bigint primary key auto_increment, name varchar(32), age int, key idx_name (name) ) engine=InnoDB;
select * from user where name = '张三';
这条查询会先走 idx_name 找到主键 id,再回表查整行。而下面这条只查 id 和 name 的语句,因为 idx_name 已经包含了这两个字段,就不需要回表:
select id, name from user where name = '张三';
这就是覆盖索引。你可以在执行计划里看到 Using index 字样,表示没有回表。关于索引的使用,MySQL 官方和大多数资深 DBA 的建议一致:优先考虑非唯一索引,因为唯一索引的更新用不上 change buffer 优化机制。对于写多读少的业务,比如账单、日志系统,这个差异会被明显放大。
2.3 索引失效的四种典型场景
索引失效是线上慢查询最常见的根源,也是面试必问。我把题目里提到的场景整理成一张行为对照表:
| 查询写法 | 是否走索引 | 原因 |
|---|---|---|
| like 'abc%' | 走索引 | 前缀匹配,可以从索引树起始位置扫描 |
| like '%abc' | 索引失效 | 不知道从哪个索引值开始比较 |
| like '%abc%' | 索引失效 | 同左,且可能命中多条,只能全表扫 |
| where date(create_time) = '2024-01-01' | 索引失效 | 对索引字段做了函数运算,索引存的是原始值 |
| where id + 1 = 100 | 索引失效 | 对索引做了表达式计算,等价于函数运算 |
| where phone = 13800001111 | 索引失效 | phone 是 varchar,数字会被隐式转换为字符串 |
| where a = 1 or b = 2(b 无索引) | 索引失效 | OR 语句中只要有一个条件列不是索引列,就全表扫描 |
这里最容易翻车的是隐式转换。比如 phone 字段是 varchar,你写 where phone = 13800001111,MySQL 会把 phone 转成数字再比较,相当于对字段用了函数,索引直接作废。解决方式是把查询参数写成字符串:where phone = '13800001111'。另一种是字符串本身前缀区分度不够的情况,比如存邮箱,直接建完整索引很占空间,常见做法是建前缀索引,或者倒序存储后再建前缀索引。但要注意,前缀索引不能用覆盖索引,因为它保存的只是前缀部分。
3. 数据结构选型:为什么 InnoDB 死磕 B+ 树
3.1 三种索引结构的对比与适用边界
哈希表、有序数组、搜索树是索引的三种常见底层结构。哈希表的优点是等值查询 O(1),memcached 和部分 NoSQL 引擎用它;缺点是不支持范围查询,所以 InnoDB 不会把它作为默认索引结构。有序数组的等值和范围查询性能都很好,但更新成本太高——中间插入一条数据需要移动后续所有记录,只适合静态存储引擎。
注意这里有一个容易说错的点:很多人以为 InnoDB 用的是 B 树,实际上是 B+ 树。两者的核心区别在于:B 树的非叶子节点也存数据,导致连续数据的查询可能产生大量随机 IO;B+ 树的非叶子节点只存索引键和指针,叶子节点通过链表相连,顺序遍历时只需要沿链表走。InnoDB 之所以选 B+ 树,就是看中了它的顺序遍历能力和磁盘访问模式适配性。
3.2 一棵 1200 叉树能存多少数据
题目里有个数字值得记住:以 InnoDB 的一个整数字段索引为例,N 大约是 1200。这个数字怎么来的?InnoDB 数据页默认 16KB,索引键加上指针大概占用 13 字节左右,16KB / 13B 约等于 1200。当树高为 4 时,可以存 1200 的 3 次方个值,也就是大约 17 亿行数据。树根的数据块通常在内存中,所以一个 10 亿行的表,按整数字段索引查找一个值,最多访问 3 次磁盘;如果第二层也在内存中,访问次数更少。
这就是为什么索引字段要尽量短。如果主键是 varchar(64) 的 UUID,每个索引节点能存放的键值数量会大幅下降,树高增加,磁盘 IO 次数上升。所以 InnoDB 强烈建议用自增整数做主键,而不是业务随机字符串。从这份资源里延伸出一个判断索引好坏的通用标准:让树的扇出尽量大,也就是每个节点能容纳尽可能多的键值对。扇出越大,树越矮,访问磁盘次数越少。
3.3 MyISAM 与 InnoDB 的索引文件差异
MyISAM 的 B+ 树叶子节点保存的是数据记录的物理地址,索引文件和数据文件分离;InnoDB 的 B+ 树叶子节点保存的是数据本身,数据文件就是索引文件。这个差异带来了几个连锁后果:InnoDB 必须有主键,如果没有显式定义,它会生成一个隐藏的 rowid 作为聚簇索引;MyISAM 可以没有主键,索引叶子指向物理地址即可。另一个后果是,InnoDB 的二级索引查询必然经历回表,而 MyISAM 的二级索引可以直接通过地址取数据。但 InnoDB 通过覆盖索引和索引下推来弥补这个缺陷。
索引下推(Index Condition Pushdown,ICP)是 MySQL 5.6 引入的优化:在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。举个例子:
select * from user where name like '张%' and age > 20;
如果 name 和 age 建了联合索引,没有 ICP 时,先通过 name 前缀找到所有姓张的记录,再逐条回表判断 age;有 ICP 时,age > 20 的判断在索引层就完成了,只有满足条件的记录才回表。这个机制面试时经常和覆盖索引放在一起问,答题思路是:覆盖索引减少“搜索次数”,索引下推减少“回表次数”,两者都是围绕二级索引的 IO 优化。
4. 日志机制与 crash-safe:redo log、binlog 与两阶段提交
4.1 change buffer 的使用边界
change buffer 是 InnoDB 在更新数据页时的一种缓冲机制:如果数据页不在内存中,在不影响数据一致性的前提下,先把更新操作缓存在 change buffer 里,等下次查询需要访问这个数据页时,再把数据页读入内存,合并执行相关操作。这个机制的直接效果是减少随机读磁盘。唯一索引不能用 change buffer,因为唯一索引需要立即判断是否违反唯一约束,必须把数据页读入内存;普通索引则不需要。这就是为什么建议优先选非唯一索引。
适用场景也很明确:写多读少的业务,比如账单、日志系统,页面写完后被马上访问的概率低,change buffer 效果最好。反过来,如果写入之后马上查询,更新操作会立即触发 merge,随机 IO 次数没有减少,反而增加了 change buffer 的维护代价。如果你在面试中遇到“普通索引还是唯一索引”的选择题,回答要点是:业务允许的情况下,优先普通索引,因为可以用 change buffer。
4.2 redo log 的循环写机制与三个刷盘参数
redo log 是 InnoDB 引擎层的日志,采用循环写方式。它由内存中的 redo log buffer 和磁盘上的 redo log file 组成,典型配置是以四个文件为一组循环使用。write pos 是当前写入位置,checkpoint 是当前要擦除的位置,两者之间是“粉板”上还空着的部分。如果 write pos 追上 checkpoint,说明日志满了,必须停下来刷脏页、推进 checkpoint。
这里有三个参数面试官常问,我直接给你一个实验清单:
SET GLOBAL innodb_flush_log_at_trx_commit = 1;
| 参数值 | 行为 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|---|
| 0 | 延迟写:事务提交时不写 OS buffer,每秒刷一次盘 | 最差(最多丢 1 秒日志) | 最好 | 可以容忍丢失少量数据的场景 |
| 1 | 实时写实时刷:每次事务提交都 fsync 到磁盘 | 最好(不丢已提交事务) | 最差 | 默认值,金融、交易类业务必须用 |
| 2 | 实时写延迟刷:每次提交写到 OS buffer,每秒刷盘 | 中等(MySQL 崩溃不丢,操作系统崩溃可能丢) | 较好 | 大部分业务,可接受的折中 |
注意,参数为 2 时,如果 MySQL 进程崩溃,数据还在 OS buffer 里;但如果整个操作系统宕机,OS buffer 中的数据会丢失。所以安全等级上 2 严格低于 1。生产环境我一般强制设成 1,配合 SSD 的 fsync 能力,性能损耗在可控范围。
4.3 两阶段提交:redo log 和 binlog 如何保持一致
两阶段提交是这份资源里含金量最高的部分。redo log 是引擎层日志,负责 crash-safe 恢复未刷盘的数据;binlog 是 Server 层日志,负责主从复制和时间点恢复。两者是独立的写入链路,如果不做协调,崩溃后会出现数据不一致。具体场景:
- 先写 redo log、后写 binlog:如果 redo log 写完但 binlog 没写完就 crash,备库用 binlog 恢复时会缺一次更新,与主库当前数据不一致。
- 先写 binlog、后写 redo log:如果 binlog 写完但 redo log 没写完就 crash,事务判定为无效,但 binlog 里已经有这条记录,备库恢复时多了一次更新。
两阶段提交的流程是:redo log 先写入 prepare 状态 → 写 binlog → redo log 改成 commit 状态。崩溃恢复时,看到 redo log 是 commit 状态,说明 binlog 也写成功了,直接恢复数据;如果 redo log 是 prepare 状态,需要去查对应的 binlog 事务是否完整,完整则提交,不完整则回滚。
判断 binlog 是否完整的方法也很简单:statement 格式的 binlog 最后有 COMMIT,row 格式的 binlog 最后有 XID event。这条规则在恢复数据时非常有用,后面第 6 章还会用到。
4.4 WAL 技术为什么能扛住高频更新
WAL(Write-Ahead Logging)的核心是日志先写内存,再异步刷盘。MySQL 执行更新操作后,先写 redo log 记录变化,再在合适时机把数据页刷到磁盘。这样做的直接收益是:不用每次操作都实时写数据文件,SQL 响应速度大幅提升;即使 crash,也能通过 redo log 把未落盘的数据恢复出来。
这里要区分“写日志”和“刷磁盘”的粒度。每一条 DML 语句执行时,只会写入 redo log buffer,后续某个时间点才一次性将多条操作记录写入 redo log file,中间还隔着一个 OS buffer。这个设计把磁盘随机写变成了顺序写,因为 redo log file 本质上是追加循环写。所以 WAL 的本质不是“先写日志再写数据”这个顺序,而是“把随机 IO 变成顺序 IO,用日志的顺序写性能换取数据文件的随机写性能”。
5. 避坑手册:索引失效、慢查询排查、误删数据与 kill 不掉的 SQL
5.1 慢查询排查四步法
原本执行很快的 SQL 突然变慢,从大到小有四种原因:MySQL 数据库本身被堵住了(系统或网络资源不够);SQL 语句被锁堵住了(表锁、行锁,存储引擎不执行);索引使用不当,没有走索引;走了索引但回表次数庞大。前两种情况看系统负载和锁等待,后两种情况看执行计划。排查时用 explain 是第一步:
explain select * from order_detail where order_no = '20250101001' and status = 1;
重点关注 type 字段:如果是 all,说明全表扫描;如果是 ref 或 range,说明走了二级索引;如果是 const,说明走的是主键或唯一索引等值查询。rows 字段估算扫描行数,如果 rows 很大但 type 是 ref,要警惕回表次数。
解决慢查询的常见做法:用 force index 强行选择一个索引;修改语句引导优化器使用期望的索引。比如把“order by b limit 1”改成“order by b, a limit 1”,逻辑语义相同但可能改变优化器的选择;或者新建一个更合适的联合索引,删掉误用的索引。这里踩过最深的坑是:完全依赖优化器的判断,在数据分布不均的表上,优化器可能因为统计信息过期选错索引。定期执行 analyze table 更新统计信息,是 DBA 的基本盘。
5.2 kill 命令为什么可能失败
kill query + 线程 id 是终止正在执行的语句,kill connection + 线程 id 是断开连接。kill 不掉一般有三种情况:kill 命令还没到位,命令本身被堵住了;kill 到位了但没被立刻触发,比如语句处于无法被中断的状态;kill 被触发了,但事务回滚需要时间。第三种最常见,大事务回滚可能要几分钟,这时候看 information_schema 里的 innodb_trx 就能知道回滚进度。实际操作中还有个血泪经验:kill 大事务之前,先确认这个连接是否在等待锁。如果在等待锁,kill 掉的是等待方,持有锁的会话还活着,问题可能没解决。
5.3 drop、truncate、delete 怎么选
| 操作 | 是否可恢复 | 是否记日志 | 速度 | 触发触发器 |
|---|---|---|---|---|
| delete | 可回滚 | 逐行记录日志 | 慢 | 是 |
| truncate | 不可恢复 | 不记录逐行日志 | 快 | 否 |
| drop | 释放表空间 | 记录 DDL | 最快 | 否 |
删除部分数据用 delete,注意带上 where 子句,回滚段要足够大;删除整张表用 drop;保留表但清空数据,如果和事务无关用 truncate,如果和事务有关或想触发 trigger,用 delete。整理表内部碎片时,可以用 truncate 配合 reuse storage,再重新导入数据。这里有个容易忽略的细节:truncate 不逐行记录日志,所以删除后不能通过 binlog 闪回,只能靠全量备份恢复。面试时答“truncate 不能恢复”会显得太绝对,更准确的说法是:不能用 binlog 闪回,只能通过备份恢复。
5.4 误删数据后的恢复路径
误删数据这件事,预防永远比恢复重要。常见的预防手段有:权限控制与分配、操作规范、定期给开发做培训、搭建延迟备库——延迟备库是后悔药,一般延迟 1 小时,误删后可以从延迟节点找回;SQL 审计,线上 DML 和 DDL 都要审核;定期备份,数据量大用物理备份 xtrabackup,数据量小用 mysqldump,同时定期备份 binlog。
真发生误删,分两种情况处理。DML 误操作可以通过 binlog 闪回恢复,原理是解析 binlog event 后反转:delete 反转成 insert,insert 反转成 delete,update 的前后镜像对调。前提是 binlog_format=row 且 binlog_row_image=full,否则没有完整的前后镜像。常用的工具有开源的 myflash,本质都一样。注意恢复时先恢复到临时实例,确认无误后再导回主库。DDL 误操作(truncate 和 drop)比较麻烦,因为不管 binlog_format 是 row 还是 statement,DDL 在 binlog 里只记录语句不记录镜像,只能靠全量备份加应用 binlog 来恢复,数据量大时恢复时间会很长。rm 删除文件就完全依赖备份了,所以备份必须跨机房或跨城市保存。
5.5 kill 不掉的大查询带来的内存幻觉
大表查询为什么不会打爆内存?核心是 MySQL 边读边发。服务端不需要保存完整结果集,取数据和发数据都通过 next_buffer 操作,所以客户端读取慢,服务端会因为结果发不出去而阻塞事务执行,但不会在内存中堆积完整结果集。InnoDB 内部的 Buffer Pool 用改进的 LRU 算法管理,按照 5:3 的比例把 LRU 链表分成 young 区域和 old 区域,全表扫描大量冷数据时,只能占用 old 区域,不会把 young 区域的热数据挤出去。这也是为什么全表扫描虽然慢,但不能算内存泄漏的原因。
6. 主从同步与临时表:从原理到复现的完整流程
6.1 主备同步的建立与切换流程
主备同步的建立,一开始是由备库指定的。比如基于位点的主备关系,备库说“我要从 binlog 文件 A 的位置 P 开始同步”,主库就从指定位置开始发。主备关系搭建完成后,主库决定发给备库的数据,有新的日志也会主动推送。主备切换流程是:客户端直接访问主库 A,备库 B 持续拉取 A 的 binlog 并应用。如果 A 宕机,把 B 提升为新主库,客户端切换到 B 上继续读写。
这里面试官常追问的一个点是:备库延迟大怎么办?常见原因是备库单线程回放跟不上主库的写入速度,老版本 MySQL 尤其明显。解决办法包括并行复制(MTS)、把大事务拆小、以及用 row 格式的 binlog 减少回放时的计算量。另一个高频问题是主备切换时数据一致性的保证,答案就是两阶段提交的延伸:binlog 中每个事务都有 commit 标记,备库按 binlog 顺序回放,天然保证了最终一致。
6.2 临时表的使用边界与常用场景
MySQL 临时表的特性:只对当前 session 可见,可以与普通表重名,增删改查用的是临时表,show tables 不显示临时表,线程退出时自动删除。实际应用中,临时表一般用于处理复杂的计算逻辑。因为每个线程各见各的临时表,不需要考虑多个线程执行同一个处理时临时表重名的问题,也不需要显式清理。
常见用法示例:
create temporary table tmp_user_stat as select user_id, count(*) as cnt, sum(amount) as total from order_record where create_time >= date_sub(now(), interval 7 day) group by user_id;
select t.cnt, count(*) as user_cnt from tmp_user_stat t where t.total > 1000 group by t.cnt;
这段逻辑是:先汇总近 7 天的用户订单统计到临时表,再做二次聚合,避免一条 SQL 里嵌套多层子查询导致优化器选错执行计划。注意临时表在 MySQL 8.0 里优先使用 TempTable 引擎存储在内存中,超过阈值后自动转换为磁盘临时表,参数 tmp_table_size 和 max_heap_table_size 决定内存阈值。如果一个复杂查询老是报“磁盘临时表空间满”,优先看这两个参数,而不是怀疑 SQL 写错。
6.3 如何验证你已经理解 crash-safe:一个完整的恢复实验
到这里,光是背题已经没意义了,真正能验证理解的是手动跑一遍崩溃恢复实验。我建议你在本地环境按下面流程实操一次,半小时就能跑完:
docker run --name mysql-lab -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:8.0
mysql -h127.0.0.1 -uroot -p123456
create database lab; use lab; create table t (id int primary key, a int) engine=InnoDB; insert into t values (1, 100), (2, 200);
docker stop mysql-lab
docker start mysql-lab
select * from t;
验证点有两个:commit 状态下的事务在重启后完整存在(由 redo log 保证);如果你把 innodb_flush_log_at_trx_commit 改成 0 再插入数据并 kill 进程,可能丢失最近 1 秒的已提交数据——这就是参数选择带来的真实差异。docker 方式排查容器问题比本机安装省心,但要注意容器内文件系统 ext4 的 fsync 行为和裸金属不同,压测性能时别拿 docker 数据当结论。
从这次实验里你能直观感受到,数据是否丢失取决于日志刷盘策略而不是 SQL 是否返回成功。这也是为什么生产环境我坚持用双 1 配置:sync_binlog=1 和 innodb_flush_log_at_trx_commit=1,牺牲一点吞吐换不丢数据。从那以后,每次接新项目我第一件事就是检查这两个参数——确保 MySQL 在误删数据后还有后悔药吃。这套思路同样适用于面试:你会告诉面试官,自己不是背了两阶段提交的结论,而是亲手验证过日志刷盘对数据完整性的影响,希望帮到你。
本文还有配套的精品资源,点击获取