这篇文章按图片里的分类整理 MySQL 面试高频问题。写法尽量通俗,重点放在面试常问、开发常用、容易踩坑的地方,不做过度源码展开。
MySQL 面试里最常问的三类日志是 redo log、undo log、binlog。
redo log 是 InnoDB 的重做日志,主要保证事务的持久性。事务提交后,即使数据库突然宕机,也可以通过 redo log 把已经提交的数据恢复回来。
undo log 是回滚日志,主要保证事务原子性,也用于 MVCC。事务执行过程中会记录修改前的数据,如果事务回滚,就可以根据 undo log 恢复旧值。
binlog 是 MySQL Server 层的二进制日志,主要用于主从复制、数据恢复和审计。它记录的是数据库发生了哪些逻辑变更。
一句话记忆:
redo log 能保证崩溃恢复,核心靠 WAL,也就是 Write Ahead Logging,先写日志,再写数据页。
InnoDB 修改数据时,不会每次都立刻把磁盘上的数据页改掉,而是先修改内存中的 Buffer Pool,同时记录 redo log。事务提交时,只要 redo log 持久化成功,就认为事务提交成功。
如果数据库宕机,内存里的脏页可能还没刷到磁盘,但 redo log 已经在磁盘上。重启后 InnoDB 会根据 redo log 重放修改,把数据恢复到提交后的状态。
通俗说:redo log 像快递签收记录,货物还没完全入库,但签收记录在,系统恢复后可以按记录补齐。
binlog 是 MySQL Server 层的二进制日志,记录数据库变更,常用于主从复制和数据恢复。
它有三种常见格式:
statement:记录 SQL 语句。优点是日志量小,缺点是某些 SQL 在主从执行结果可能不一致,比如 now()、uuid()、不确定顺序的更新。
row:记录每一行数据的变化。优点是最准确,主从一致性最好;缺点是日志量可能很大。
mixed:混合模式。MySQL 会根据 SQL 是否安全,自动选择 statement 或 row。
生产环境更常用 row,因为复制更可靠,也方便做数据订正和恢复。
redo log 和 binlog 经常一起问,因为它们都记录修改,但定位完全不同。
redo log 是 InnoDB 引擎层日志,binlog 是 MySQL Server 层日志。
redo log 主要用于崩溃恢复,binlog 主要用于主从复制和数据恢复。
redo log 是循环写,空间固定,会覆盖旧日志;binlog 是追加写,一个文件写满后切换到下一个。
redo log 记录偏物理变化,比如某个页做了什么修改;binlog 记录偏逻辑变化,比如执行了什么 SQL 或哪行变成什么样。
一句话:redo log 管“宕机后自己怎么恢复”,binlog 管“别人怎么同步和回放”。
两阶段提交是为了保证 redo log 和 binlog 一致。
大致流程:
为什么要这样?因为事务提交涉及两个日志,如果只写成功一个就宕机,会出现主库和从库数据不一致。
恢复时会判断:
这样可以保证主库崩溃恢复结果和 binlog 复制结果一致。
一条 SQL 大致会经过这些步骤:
如果是更新语句,还会涉及 undo log、redo log、binlog 等日志。
explain 用来查看 SQL 执行计划,面试常问这些字段:
id:查询执行顺序标识。id 越大通常越先执行;相同 id 从上往下执行。
select_type:查询类型,比如 SIMPLE、PRIMARY、SUBQUERY、DERIVED。
type:访问类型,表示查表效率,优化重点字段。
key:实际使用的索引。
rows:优化器预估要扫描的行数。
Extra:额外信息,比如 Using index、Using where、Using filesort、Using temporary。
看 explain 时,重点关注 type、key、rows、Extra。
type 表示 MySQL 怎么访问表,常见效率从差到好大概是:
ALL < index < range < ref < eq_ref < const < system
ALL:全表扫描,通常最差。index:扫描整个索引树,比全表扫描稍好,但仍然扫很多。range:范围扫描,比如 between、>、in。ref:普通索引等值匹配,可能匹配多行。eq_ref:唯一索引或主键关联查询,每次最多匹配一行。const:主键或唯一索引等值查询,结果最多一行。system:表只有一行,是 const 的特殊情况。实际优化目标一般是避免 ALL,尽量达到 range、ref 或更好。
慢查询日志用于记录执行时间超过阈值的 SQL。
常用参数:
slow_query_log:是否开启慢查询日志。long_query_time:超过多少秒算慢 SQL。slow_query_log_file:慢查询日志文件路径。排查时重点看:
常用分析工具有 mysqldumpslow 和 pt-query-digest。
慢 SQL 优化可以按这个顺序来:
explain 看执行计划。核心原则:少扫行、少回表、少排序、少临时表。
limit 1000000, 10 慢,是因为 MySQL 需要先扫描并丢弃前 1000000 行,再返回 10 行。
常见优化方式:
第一种,基于上一页最大 id 做游标分页:
select * from userwhere id > 1000000order by idlimit 10;
第二种,先用覆盖索引查出 id,再回表:
select u.*from user ujoin ( select id from user order by id limit 1000000, 10) t on u.id = t.id;
第三种,产品层面避免跳到特别深的页,比如搜索引擎通常只展示前几十页。
order by 如果能直接利用索引顺序,就不需要额外排序。
如果不能利用索引排序,MySQL 会使用 filesort。这里的 filesort 不一定真的落磁盘,它表示额外排序算法,数据大时可能用临时文件。
常见触发原因:
优化方式:
where + order by 建合适联合索引。group by 用于分组聚合。MySQL 执行时通常需要按分组字段聚集数据,可能用索引,也可能用临时表和排序。
优化思路:
如果 explain 里出现 Using temporary、Using filesort,说明可能存在额外临时表和排序成本。
join 本质是把多张表按条件关联起来。
内连接 inner join:只返回两边都匹配的数据。
左连接 left join:返回左表全部数据,右表匹配不到时右表字段为 null。
右连接 right join:返回右表全部数据,左表匹配不到时左表字段为 null。
开发中更常用 inner join 和 left join。右连接通常可以改写成左连接,保持阅读习惯统一。
大表 join 优化重点是减少驱动表数据量,并让被驱动表能走索引。
常见做法:
select *。一句话:让参与 join 的数据尽量少,让匹配过程尽量走索引。
MySQL 常见存储引擎有 InnoDB、MyISAM、Memory、Archive 等。现在生产最常用的是 InnoDB。
InnoDB 支持事务、行级锁、外键、崩溃恢复,适合高并发和事务场景。
MyISAM 不支持事务,不支持行锁,主要是表级锁,崩溃恢复能力弱,但结构简单,早期读多写少场景用得较多。
现在默认优先选择 InnoDB。
InnoDB 的优势主要有:
面试里可以直接说:InnoDB 更适合现代业务系统,尤其是高并发、强一致、需要事务的场景。
事务是一组操作的集合,要么全部成功,要么全部失败。
ACID 分别是:
A Atomicity 原子性:事务内操作要么全成功,要么全失败。主要靠 undo log。
C Consistency 一致性:事务执行前后,数据从一个一致状态变成另一个一致状态。
I Isolation 隔离性:多个事务并发执行时,彼此影响受隔离级别控制。
D Durability 持久性:事务提交后数据不会丢。主要靠 redo log。
第一范式:字段不可再分,保证原子性。
第二范式:非主键字段必须完全依赖主键,避免部分依赖。
第三范式:非主键字段不能依赖其他非主键字段,避免传递依赖。
日常开发不一定死守三范式。范式能减少冗余、提高一致性,但有时为了查询性能,会适当反范式,比如冗余用户名、订单快照、统计字段。
原则是:核心数据保证一致,读多性能瓶颈场景可以有控制地冗余。
以更新语句为例:
查询语句则主要涉及解析、优化、执行、走索引、回表、返回结果。
InnoDB 常见锁包括:
InnoDB 的行锁是加在索引上的,如果查询条件没有走索引,可能导致锁范围变大。
严格说,InnoDB 行锁不会真的“升级”为表锁;但如果 SQL 没有走索引,InnoDB 可能扫描很多行并对大量记录加锁,看起来像锁表。
常见原因:
所以更新和删除时一定要确认条件走索引,尤其是大表。
记录锁 Record Lock:锁住某一条索引记录。
间隙锁 Gap Lock:锁住索引记录之间的间隙,不锁具体记录,主要防止幻读。
临键锁 Next-Key Lock:记录锁 + 间隙锁,既锁记录,也锁记录前面的间隙。
例如索引里有 10 和 20,间隙锁可能锁住 (10, 20) 这个范围,防止其他事务插入 15。
幻读是同一个事务内,两次范围查询结果条数不一致,比如第一次查没有某条记录,第二次查突然出现了。
在可重复读隔离级别下,InnoDB 对范围查询加临键锁,锁住已有记录和记录之间的间隙。这样其他事务就不能在这个范围里插入新记录。
所以临键锁通过“锁记录 + 锁间隙”防止范围内新增数据,从而解决当前读下的幻读问题。
死锁是多个事务互相等待对方持有的锁,导致都无法继续。
产生条件:
排查方式:
show engine innodb status 查看最近一次死锁信息。避免方式:
MySQL 标准隔离级别有四种:
隔离级别越高,并发能力通常越低,一致性约束越强。
脏读:读到了其他事务还没提交的数据。如果对方回滚,你读到的就是脏数据。
不可重复读:同一个事务内,两次读取同一行数据,结果不一样。通常是其他事务提交了修改。
幻读:同一个事务内,两次范围查询,结果集条数不一样。通常是其他事务插入或删除了符合条件的数据。
简单记:脏读读到未提交,不可重复读是同一行变了,幻读是结果集行数变了。
读未提交:什么都解决不了,可能脏读、不可重复读、幻读。
读已提交:解决脏读,但可能不可重复读和幻读。
可重复读:解决脏读、不可重复读。InnoDB 通过 MVCC 和临键锁在很多场景下也解决幻读。
串行化:基本都能解决,但并发性能最低。
生产中 MySQL InnoDB 默认是可重复读。
InnoDB 默认隔离级别是 Repeatable Read,也就是可重复读。
它通过 MVCC 保证普通快照读的可重复读,通过 next-key lock 处理当前读下的幻读问题。
幻读指一个事务内两次范围查询,第二次出现了第一次没有的记录,像“幻影”一样。
解决方式:
注意:MVCC 主要解决快照读的一致性,临键锁主要解决当前读的幻读。
事务能力不是一个单独机制完成的,而是多个机制配合:
MVCC 是多版本并发控制。它让读操作可以读到某个时间点的数据版本,而不是总被写操作阻塞。
通俗说:数据被修改后,旧版本不会立刻消失,读事务可以根据规则找到自己应该看到的版本。
要分情况说。
对于普通 select 快照读,可重复读通过 MVCC 的 Read View 能保证同一事务内查询结果一致,基本避免幻读。
对于 select ... for update、update、delete 这类当前读,InnoDB 通过 next-key lock 锁范围,防止其他事务插入,从而避免幻读。
但如果混用快照读和当前读,可能看到不一样的结果,这是面试里的加分点。
MVCC 全称 Multi-Version Concurrency Control,多版本并发控制。
InnoDB 每行记录都有隐藏字段,配合 undo log 保存历史版本,再通过 Read View 判断当前事务能看到哪个版本。
它的核心目标是提高读写并发:读不阻塞写,写不阻塞普通读。
快照读读取的是某个时间点的数据版本,不加锁。
普通 select 通常是快照读:
select * from user where id = 1;
当前读读取的是最新数据,并且通常要加锁。
常见当前读:
select * from user where id = 1 for update;update user set name = 'Tom' where id = 1;delete from user where id = 1;
快照读看历史版本,当前读看最新版本。
InnoDB 行记录里有几个隐藏字段:
DB_TRX_ID:最近一次修改这行记录的事务 id。
DB_ROLL_PTR:回滚指针,指向 undo log 中的旧版本。
DB_ROW_ID:如果表没有主键和唯一非空索引,InnoDB 会生成隐藏行 id。
MVCC 主要依赖 DB_TRX_ID 和 DB_ROLL_PTR 找到可见版本。
undo log 记录数据修改前的旧值,用于事务回滚和 MVCC。
当一行数据被多次修改时,每次修改都会产生 undo log,记录之间通过回滚指针串起来,就形成版本链。
查询时,如果当前版本对事务不可见,InnoDB 会沿着版本链往前找,直到找到一个可见版本,或者找不到。
Read View 是 MVCC 判断版本是否可见的核心。
常见字段:
m_ids:创建 Read View 时,系统中活跃事务 id 列表。min_trx_id:活跃事务中最小 id。max_trx_id:下一个将要分配的事务 id。creator_trx_id:创建这个 Read View 的事务 id。可见性规则简化理解:
min_trx_id,说明早就提交了,可见。max_trx_id,说明是之后才出现的,不可见。m_ids 里,说明当时还没提交,不可见。m_ids 里,说明已经提交,可见。读已提交 RC:每次执行普通 select 都会生成新的 Read View。所以同一个事务里,第二次查询能看到其他事务已经提交的数据。
可重复读 RR:事务中第一次普通 select 时生成 Read View,后续普通 select 复用这个 Read View。所以同一个事务里多次读取结果一致。
区别就在于 Read View 的创建时机。
数据库索引存储在磁盘上,核心目标是减少磁盘 IO。
二叉树和红黑树高度相对较高,数据量大时查找层数多,磁盘 IO 多。
B 树每个节点既存 key 也存数据,单页能放的 key 数量相对少,树可能更高。
B+ 树非叶子节点只存 key 和指针,叶子节点存完整数据或主键,单页能放更多 key,树更矮,IO 更少。叶子节点还用链表连接,范围查询更方便。
所以 InnoDB 选择 B+ 树是为了降低 IO、提高范围查询效率。
B 树的每个节点都可能存数据。
B+ 树只有叶子节点存数据,非叶子节点只做索引导航。
B+ 树叶子节点之间有链表,范围查询更快。
B+ 树单个非叶子节点能存更多 key,树更矮,磁盘 IO 更少。
聚簇索引的叶子节点存整行数据。在 InnoDB 中,主键索引就是聚簇索引。
非聚簇索引,也叫二级索引,叶子节点存的是索引字段和主键值。
通过二级索引查到主键后,如果还需要其他字段,就要再根据主键回到聚簇索引查整行,这就是回表。
主键索引:唯一且不能为空,一张表只能有一个主键。
唯一索引:值不能重复,但通常允许 null,具体行为和数据库规则有关。
普通索引:没有唯一性限制,只提升查询效率。
联合索引:多个字段组合成一个索引,比如 (a, b, c),使用时要遵守最左前缀原则。
回表是指通过二级索引查到主键后,再根据主键去聚簇索引查整行数据。
例如有索引 (name):
select age from user where name = 'Tom';
如果 age 不在索引里,就需要回表。
避免回表的方式是使用覆盖索引,把查询需要的字段都放进索引:
create index idx_name_age on user(name, age);
覆盖索引是指查询需要的字段都能从索引里拿到,不需要回表。
例如联合索引 (name, age):
select name, age from user where name = 'Tom';
这种情况下只查索引就够了。
覆盖索引适合高频查询、列表页、分页查询等场景,可以减少回表 IO。
联合索引按字段顺序排序,比如 (a, b, c),索引先按 a 排,再按 b 排,最后按 c 排。
所以查询必须从最左边字段开始连续使用,才能充分利用索引。
可以走索引的例子:
where a = 1where a = 1 and b = 2where a = 1 and b = 2 and c = 3
不符合的例子:
where b = 2where c = 3
因为缺少最左字段 a,索引整体顺序用不上。
索引下推 Index Condition Pushdown,简称 ICP,是 MySQL 的一种优化。
没有索引下推时,存储引擎根据索引找到记录后,可能先回表,再由 Server 层判断其他条件。
有索引下推时,能在存储引擎层先用索引里的字段过滤一部分数据,减少回表次数。
典型场景是联合索引中部分字段可以用于过滤,但不能完全用于定位。
索引失效是指 SQL 虽然有索引,但优化器没有使用,或者只能使用一部分。
常见原因:
like '%xxx' 前缀模糊。or 且部分条件没有索引。优化时不要只看“建没建索引”,要看 explain 里实际有没有用。
like 'xxx%' 可以走索引,因为前缀确定,B+ 树可以按范围查。
like '%xxx' 一般不能走普通 B+ 树索引,因为开头不确定,无法从索引树定位范围。
如果必须支持任意位置模糊搜索,可以考虑全文索引、搜索引擎,或者业务侧倒排索引。
如果字段类型和查询条件类型不一致,MySQL 可能对字段做隐式转换。
例如手机号字段是 varchar,却这样查:
where phone = 13800138000
MySQL 可能把 phone 转成数字比较,相当于对索引列做函数处理,索引就可能失效。
正确写法:
where phone = '13800138000'
不建议 select * 的原因:
生产建议明确写出需要的字段。
联合索引字段顺序一般考虑:
没有绝对公式,要结合真实 SQL 和 explain 判断。
这些场景不太适合建索引:
索引不是越多越好。它能加快查询,但会拖慢写入,并占用磁盘空间。
like 'xxx%' 通常可走索引,like '%xxx' 通常不走普通索引。MySQL 面试题看起来分散,其实主线很清楚:
面试回答时建议先说结论,再讲原理,最后补一句实际开发中的坑点,这样比单纯背概念更容易拿分。