跳到主要内容

高频问答速查

面试前一天过一遍的清单。每条给最短的正确答案,想看推导过程点链接。

架构与执行​

一条 SELECT 怎么执行的? 连接器(认证)→ 解析器(词法语法分析)→ 预处理器(校验表列)→ 优化器(选索引、定 join 顺序)→ 执行器(调引擎接口取数据)→ 存储引擎。8.0 移除了查询缓存。详见

一条 UPDATE 怎么执行的? 先走查询流程找到数据页(不在内存就从磁盘读入 Buffer Pool)→ 修改内存中的页(变脏页)→ 写 redo log(prepare) → 写 binlog → 写 redo log(commit)。这就是两阶段提交。详见

为什么要两阶段提交? redo log 和 binlog 是两个独立文件,分别顺序写会导致崩溃点落在中间时主从不一致(从库少数据或多数据)。2PC 把两者绑成原子操作。详见

Buffer Pool 的 LRU 为什么要改? 朴素 LRU 有两个问题 —— 预读失效(预读进来的页没被访问却占着头部)和 Buffer Pool 污染(全表扫描把热点页全挤出去)。解法是分 young/old 区(5:3),新页先进 old 区头部,且要在 old 区停留超过 innodb_old_blocks_time(1秒)后再次被访问才晋升。详见

Change Buffer 为什么对唯一索引无效? 唯一索引插入前必须判重,判重就得把页读进内存,那么直接更新就好,缓存变更没意义。写多读少时收益最大,写完立刻读是负收益。详见

为什么需要 Doublewrite? InnoDB 页 16KB,OS 页 4KB,刷盘要多次底层写,中途断电会产生「写坏一半的页」。redo log 救不了 —— 它记的是「在某页某偏移做什么改动」,前提是页本身完整。详见

索引​

为什么用 B+ 树? 非叶子节点只存键和指针 → 单页放更多键 → 树更矮(三层约存 2000 万行,查任意行最多 3 次 IO,根节点常驻内存实际 1~2 次);叶子节点用双向链表串联 → 范围查询顺序扫链表。哈希表不支持范围,红黑树树高太大。详见

回表是什么? 二级索引叶子只存「索引列 + 主键值」,拿到主键后要再去聚簇索引查完整行。覆盖索引(查询的列全在索引里)可以消除回表。详见

最左前缀为什么成立? 联合索引 (a,b,c) 的 B+ 树先按 a 排、a 相同按 b 排。所以只有从最左开始连续的前缀才能用于索引查找。推论:范围查询之后的列失效,所以等值条件放前面、范围条件放最后。详见

索引失效的情形? 对索引列用函数或运算、隐式类型转换(varchar 列传数字)、LIKE '%x'、OR 一侧无索引。另外优化器主动放弃不算失效 —— 估算回表行数占比过高(约 20~30%)时全表扫更快。详见

索引下推(ICP)? 联合索引 (name, age) 执行 WHERE name LIKE '张%' AND age=20,在索引层就把 age 判断掉,只对真正匹配的行回表。Extra 显示 Using index condition。详见

自增主键 vs UUID? 自增顺序写、只在 B+ 树右侧追加、页分裂少、8 字节紧凑;UUID 随机插入导致频繁页分裂和碎片,16/36 字节大且所有二级索引都存主键值。分布式要有序用雪花算法或 UUID v7,别用 v4。详见

为什么每张表必须有主键? 三个独立的硬理由 —— ① 无主键时 InnoDB 用全局共享的 48 位 row_id,耗尽后归 0 静默覆盖数据;② WRITESET 并行复制要求主键,否则退化串行导致主从延迟;③ 聚簇索引需要一个组织键。详见

事务与 MVCC​

ACID 靠什么实现? 原子性 → undo log;持久性 → redo log;隔离性 → 锁 + MVCC;一致性 → 以上三者 + 业务约束。详见

四种隔离级别? 读未提交(脏读)、读已提交(不可重复读)、可重复读(InnoDB 默认)、串行化。区别:不可重复读针对单行的值,幻读针对范围内的行数。详见

MVCC 怎么判断可见性? 行上有 DB_TRX_ID(最后修改的事务 ID)和 DB_ROLL_PTR(指向 undo 版本链)。ReadView 含 m_ids(活跃事务列表)、min_trx_id、max_trx_id、creator_trx_id。判断:等于自己 → 可见;小于 min → 已提交可见;大于等于 max → 不可见;区间内看是否在 m_ids 里。不可见就顺版本链往旧找。详见

RC 和 RR 的区别在实现上是什么? 同一套 MVCC,只差 ReadView 的生成时机 —— RC 每次 SELECT 都生成新的,RR 只在事务内第一次 SELECT 生成后复用。详见

RR 解决幻读了吗? 快照读靠 MVCC 完全避免;当前读靠间隙锁避免。但「先快照读再当前读」的组合仍能看到其他事务新插入的数据,所以是「基本解决」。详见

为什么要避免长事务? undo 无法回收(ibdata 膨胀)、版本链变长(查询变慢)、锁长期不释放、主从延迟。排查 information_schema.innodb_trx 按 trx_started 排序。详见

锁​

🔥 加锁规则? 两原则两优化一 bug —— 原则1:基本单位是 next-key lock(前开后闭);原则2:访问到的对象才加锁;优化1:唯一索引等值查询命中时退化为行锁;优化2:等值查询向右遍历且最后一个值不满足条件时退化为间隙锁;bug:唯一索引范围查询会多访问一个值。详见

间隙锁什么时候存在? 只在 RR 下,RC 退化为纯记录锁。间隙锁之间互相兼容,这是死锁的常见来源(两个事务都拿到同一间隙锁,然后各自要插入)。详见

意向锁解决什么? 加表级写锁时要确认表内无行锁,没有意向锁就得逐行检查 O(n),有了只需看表上标记 O(1)。详见

两阶段锁协议的实践推论? 行锁在需要时才加、但统一在提交时才释放。所以把最可能冲突的更新放在事务最后,缩短热点行的锁持有时间。详见

没有索引的更新条件会锁什么? 全表每一行和每个间隙 —— 等价于锁表。这是「更新条件必须走索引」的硬理由,不只是性能问题。详见

怎么减少死锁? 所有事务按同一顺序访问多行、事务短小(把 RPC/IO 移出事务)、批量更新按主键排序、用 INSERT ... ON DUPLICATE KEY UPDATE 把「先查再插」合一。热点行可以拆成多行提升并发度。详见

MDL 怎么导致全表不可用? 慢查询持有 MDL 读锁 → ALTER TABLE 等 MDL 写锁排队 → 后续所有新查询都排在写锁之后 → 整表瞬间不可用。改表前必须确认无长事务。详见

日志​

redo log 和 binlog 的区别? 归属(InnoDB 独有 vs Server 层所有引擎)、内容(物理日志「某页某偏移改成X」vs 逻辑日志)、写法(循环写会覆盖 vs 追加写永久保留)、用途(崩溃恢复 vs 复制和时间点恢复)。详见

崩溃恢复怎么判断提交还是回滚? 扫 redo log:commit 状态 → 提交;prepare 状态 → 去 binlog 找对应 XID,binlog 完整就提交(因为可能已传给从库,回滚会导致主少从多),不完整就回滚。详见

innodb_flush_log_at_trx_commit 三个值? 0 = 每秒刷(崩溃丢 1 秒);1 = 每次提交 fsync(默认,不丢);2 = 写 OS 缓存每秒 fsync(进程崩溃不丢、掉电丢 1 秒)。详见

binlog 为什么生产要用 ROW? ① STATEMENT 格式下 NOW()、UUID()、LIMIT 等不确定性语句会导致主从不一致;② 可用性优先切换时,ROW 格式会报错停止复制而 STATEMENT 会静默产生脏数据;③ Flashback 恢复依赖 ROW 的前后镜像。详见

复制与高可用​

GTID 解决什么? 传统复制靠「binlog 文件名 + 位点」,主备切换时位点很难找(原主库的位点在新主库上无意义)。GTID 模式下备库告诉新主库「我执行了哪些 GTID」,新主库算差集,不需要位点。详见

并行复制的演进? 5.6 按库并行(单库无效)→ 5.7 LOGICAL_CLOCK(同一 prepare 阶段的事务一定不冲突,可并行)→ 5.7.22/8.0 WRITESET(按修改的行算 hash 做冲突检测,行级并行度最高,要求有主键)。详见

主备切换选可靠性还是可用性? 绝大多数选可靠性优先(等从库追平再切,代价是几十秒不可写)。可用性优先会导致数据不一致,且 mixed/statement 格式下静默发生。详见

seconds_behind_master 为什么不可靠? ① 主备时钟不同步直接计入;② 大事务的 binlog 时间戳是提交时写的,开始执行时该值瞬间跳大;③ 不计算积压在 relay log 里未执行的量。更准的办法是对比 Retrieved_Gtid_Set 和 Executed_Gtid_Set,或用 pt-heartbeat。详见

读写分离的过期读怎么解决? 最优解是 wait_for_executed_gtid_set() —— 事务提交后拿到 GTID,从库上阻塞等它执行完(超时就降级读主库)。其他:强制读主库(最简单可靠)、master_pos_wait()、判断延迟为 0(不可靠)。详见

双 M 结构为什么不循环复制? binlog 保留原始 server_id,节点收到 binlog 时发现 server_id 等于自己就丢弃。详见

优化与排查​

EXPLAIN 的 type 从好到坏? const > eq_ref > ref > range > index > ALL。注意 index 不是好事(扫整棵索引树),生产底线是 range。详见

Extra 要警惕哪些? Using filesort(额外排序)、Using temporary(用了临时表)、Using join buffer (Block Nested Loop)(关联字段无索引)。好的是 Using index(覆盖索引)、Using index condition(索引下推)。详见

深分页怎么优化? 最优是游标式(WHERE id > 上一页最后的id LIMIT 20,耗时恒定但不能跳页);要跳页用延迟关联(子查询走覆盖索引只取 id,再用这些 id 回表,把 100 万次回表降到 20 次)。详见

ORDER BY 怎么优化? 不是调大 sort_buffer_size,而是建对索引让排序消失 —— 索引本身有序时直接顺序读,Using filesort 消失。两种排序模式:全字段排序(默认,内存换 IO)和 rowid 排序(单行超过 max_length_for_sort_data 时,只存排序字段+主键,要回表两次)。详见

Join 的三种算法? NLJ(被驱动表有索引,N + N·2·log₂M)、BNL(无索引,驱动表进 join_buffer,磁盘扫 N+M 但内存比较 N×M,还会污染 Buffer Pool)、Hash Join(8.0.18+,比较次数降到 N+M,8.0.20 起彻底替代 BNL)。规则:被驱动表关联字段必须有索引 + 小表做驱动表。详见

为什么 InnoDB 的 COUNT(*) 慢? MVCC 导致「这张表有多少行」对不同事务有不同答案,不存在可缓存的真实行数,必须逐行扫(优化是选最小的索引树扫)。MyISAM 把行数存在磁盘上所以 O(1),但加 WHERE 就失效。详见

COUNT(*) / COUNT(1) / COUNT(字段) 哪个快? COUNT(*) ≈ COUNT(1) < COUNT(主键) < COUNT(字段)。COUNT(*) 专门优化过不取值,COUNT(字段) 要取值还要判 NULL(且语义不同,不统计 NULL)。用 COUNT(*)。详见

用 Redis 存计数行不行? 不行 —— Redis 和 MySQL 无法放进同一事务,必然有不一致窗口。正解是把计数存在 MySQL 的一行里,和数据插入放进同一事务。详见

SQL 偶尔很慢是什么原因? 和「一直慢」要分开 —— 偶发抖动通常是刷脏页(redo log 写满必须推进 checkpoint,此时所有更新阻塞;或 Buffer Pool 无空闲页要淘汰脏页)、锁等待、大查询抢资源。详见

为什么 kill 不掉? kill 不是立即终止,只是改线程状态 + 发信号,线程要自己执行到埋点才检查并退出。三种情形:陷在长 IO 等待、innodb_thread_concurrency 用满(等待循环每 10ms 才检查一次)、大事务正在回滚(只能等)。Ctrl+C 本质也是另开连接发 kill。详见

优化器为什么选错索引? 基数(cardinality)靠采样 20 个页估算,必然有误差,大量删改后更失真。解法优先级:ANALYZE TABLE(最轻量)→ FORCE INDEX → 改写 SQL → 删掉误导优化器的索引。8.0 的直方图能为无索引列提供分布信息。详见

全表扫描会把内存打满吗? 不会。server 层「边读边发」—— 取一行写进 net_buffer(16KB),满了就发给 socket,所以内存里最多这么多。打爆的是客户端(默认 mysql_store_result 一次性接收全部)。InnoDB 层的污染靠 young/old 区机制缓解。详见

DDL 与空间​

DELETE 为什么不释放磁盘空间? 只把记录标记为「可复用」,留给后续插入。空洞来源:删除、插入导致的页分裂、更新(变长字段实际是「标记删除+插入」)。回收要重建表(ALTER TABLE t ENGINE=InnoDB)。详见

Online DDL 怎么做到不阻塞? 引入 row log —— MDL 写锁只在开头和结尾短暂持有,中间降级为读锁允许 DML,期间的写操作记入 row log,临时文件生成后再应用上去。详见

inplace 和 online 的区别? inplace 指操作在 InnoDB 内部完成(不经 server 层临时表),online 指不阻塞 DML。加全文索引是 inplace 但不是 online —— 这是两者不等价的标准例子。详见

自增 ID 用完会怎样? 显式自增主键 → 达到上限后报 Duplicate entry 错误(能发现);无主键依赖 row_id → 48 位用完后归 0 静默覆盖数据(最危险)。详见

自增值为什么不连续? ① 唯一键冲突(先申请自增值再判重,失败不回退);② 事务回滚;③ 批量插入的双倍申请策略(1,2,4,8...,多申请的被浪费)。不设计成回退是因为那样要加锁串行化,牺牲并发。详见

分库分表​

什么时候该分? 顺序是 索引优化 → 加缓存 → 读写分离 → 垂直拆分 → 水平分表 → 水平分库,分库分表是最后手段。阈值经验值:单表 1000~2000 万行或 20GB。本质原因是B+ 树层数增加(三层约 2000 万行,再多变四层)+ DDL/备份时间不可接受。详见

分片键怎么选? 高频查询条件必须包含它(否则广播查询)、分布均匀、不可变、基数足够大。订单表的「用户维度 vs 商家维度」冲突用异构索引表、双写冗余或基因法(把用户 ID 的哈希位嵌进订单 ID)解决。详见

跨库分页怎么做? 全局查询法(各分片取 LIMIT 0, N+offset 再归并,页深时爆炸)、禁止跳页用游标(推荐)、二次查询法(精确且高效但实现复杂)、限制最大页数、走 ES。注意 AVG 不能直接平均,要分别取 SUM 和 COUNT。详见

分布式事务怎么选? 优先设计成不需要分布式事务(需要一起成功的数据放进同一分片)。实在要用:本地消息表 / 事务消息(最常用,把消息和业务数据写进同一个本地事务)> TCC > Saga > XA/2PC(性能差、协调者单点)。所有方案都要配幂等 + 对账兜底。详见

备份恢复​

误删行怎么恢复? Flashback 反向生成 SQL(要求 binlog_format=ROW 且 binlog_row_image=FULL)—— Delete_rows 改成 Write_rows、Update_rows 前后镜像对调,多事务倒序执行。工具 binlog2sql / MyFlash。先在临时库验证再同步回生产。 详见

误删表/库怎么恢复? Flashback 救不了 —— DROP/TRUNCATE 在 binlog 里是一条 DDL 语句,没有行数据可反推。只能全量备份 + binlog 重放到误操作前一刻。提速办法是把临时实例设为备份库的从库用并行复制重放。详见

mysqldump --single-transaction 的作用? 开启 RR 事务靠 MVCC 拿一致性快照,不锁表。不加它就用 FTWRL 全库只读。注意它只对 InnoDB 有效,混合引擎的库不行。详见

怎么预防误删? 权限收紧(业务账号不给 DROP/TRUNCATE)、删除改为先 RENAME 观察一周、sql_safe_updates=ON、SQL 审核平台、延迟从库(专门的时间机器,比全备+重放快得多)、DELETE ... LIMIT 分批执行。没演练过的备份等于没有备份。 详见

版本差异(答老结论会被纠正的)​

结论现状
「查询缓存能提升性能」8.0 已移除(命中率低,任何写入都让整表缓存失效)
「join 无索引性能极差」8.0.18+ 有 Hash Join,8.0.20 起彻底替代 BNL,已大幅缓和
「GROUP BY 会自动排序」8.0 移除了隐式排序,且不报错,升级后结果顺序会变
「索引不支持降序」8.0 支持真正的降序索引(5.7 里 DESC 被忽略)
「加字段必须重建表」8.0.12+ 的 Instant DDL 秒级完成
「innodb_locks 看锁」8.0 已移除,用 performance_schema.data_locks
「自增值重启后会回退」8.0 把自增值写入 redo log,已修复
「DDL 不能回滚」8.0 支持原子 DDL
「SET GLOBAL 重启就丢」8.0 有 SET PERSIST
「InnoDB 不支持全文索引」5.6+ 已支持