MySQL 面试题
MySQL 面试题
两部分,各分入门 / 中等 / 高难三档,每档 20 题以上,★ 是高频题。 SQL 部分的例子按 PostgreSQL 写,和 MySQL 写法不同的地方会标出来;高难手写题都在本地 PG 里跑通过(文末附测试数据)。 MySQL 是你的老本行,重点是讲出原理 + 准备你自己的真实案例;每题尽量顺带说一句 PG 的不同(见 07 文档)。
MySQL 入门(22 题)
1. ★ MySQL 的整体架构?一条 SELECT 是怎么执行的?
Server 层:连接器(认证、权限、管理连接)→(8.0 已删除查询缓存)→ 分析器(词法、语法分析)→ 优化器(选索引、定连接顺序)→ 执行器(调用存储引擎接口逐行取数据)。存储引擎层:InnoDB(默认)、MyISAM 等,插件式。
2. ★ InnoDB 和 MyISAM 的区别?
| InnoDB | MyISAM | |
|---|---|---|
| 事务 | 支持 | 不支持 |
| 锁 | 行锁 | 表锁 |
| 外键 | 支持 | 不支持 |
| 崩溃恢复 | 有(redo log) | 容易损坏 |
| 索引 | 聚簇索引(数据和主键索引在一起) | 非聚簇(索引和数据分开存) |
COUNT(*) | 要扫描 | 直接存了行数 |
| 现在一律用 InnoDB。 |
3. ★ 常用数据类型怎么选?
整数 tinyint(1 字节)/ int(4)/ bigint(8);金额 decimal(M,D);字符串 varchar(n)(n 是字符数);大文本 text;时间 datetime(8 字节,与时区无关,范围到 9999 年)/ timestamp(4 字节,存 UTC、按会话时区显示,2038 年问题);JSON 类型(5.7+)。
4. ★ utf8 和 utf8mb4 的区别?
MySQL 的 utf8 其实是 utf8mb3,每个字符最多 3 字节,存不了 emoji 和部分生僻字;utf8mb4 才是完整的 UTF-8(最多 4 字节)。8.0 默认 utf8mb4。排序规则(collation):utf8mb4_general_ci(快、不太准)、utf8mb4_unicode_ci、utf8mb4_0900_ai_ci(8.0 默认);_ci 表示不区分大小写,_bin 按二进制比较。
5. ★ 主键用自增还是 UUID?
InnoDB 数据按主键顺序存在 B+ 树里:自增主键顺序追加,页写满再开新页,紧凑高效;随机 UUID 每次插到树的随机位置,频繁页分裂、空间碎片多、缓存命中差,而且 36 字节的字符串主键会让所有二级索引都变大(二级索引存主键值)。分布式需要全局 ID 时用雪花算法或有序的 UUID(v7)。
6. ★ 索引有哪些类型?
按数据结构:B+ 树索引(默认)、哈希索引(Memory 引擎;InnoDB 有自适应哈希索引,自动建)、全文索引。按功能:主键索引、唯一索引、普通索引、联合索引、前缀索引。
7. ★ 聚簇索引和二级索引?
- 聚簇索引:叶子节点存整行数据,就是主键索引,一张表只有一个(没有主键时用第一个非空唯一索引,再没有就用隐藏的 row_id);
- 二级索引(辅助索引):叶子节点存索引列 + 主键值。
8. ★ 什么是回表?
通过二级索引找到主键值后,再去聚簇索引里按主键查整行。查询的列不全在二级索引里时就要回表,回表次数多(返回行数多)时,优化器可能干脆选择全表扫描。
9. ★ 什么是覆盖索引?
查询需要的列全部包含在某个索引里,不用回表。EXPLAIN 的 Extra 显示 Using index。例如索引 (user_id, status),查询 SELECT status FROM orders WHERE user_id = 1 就是覆盖索引。
10. ★ 什么是最左前缀原则?
联合索引 (a, b, c) 按 a 排序、a 相同按 b、b 相同按 c。所以能用上索引的条件是从最左边的列开始连续匹配:a、a, b、a, b, c 可以;b、c、b, c 不行;a, c 只能用到 a。遇到范围查询(a > 1)后,后面的列就不能再用于定位(8.0 的索引跳跃扫描是例外情况)。
11. ★ MySQL 默认的隔离级别?
可重复读(RR)。在 RR 下通过 MVCC 和临键锁很大程度上避免了幻读。(PG 默认是读已提交)
12. ★ MVCC 是什么?(简述)
多版本并发控制:每行有隐藏的事务 ID 和回滚指针,旧版本通过 undo log 串成版本链;读的时候根据 ReadView 判断该读哪个版本。效果:读不加锁、读写不互相阻塞。
13. ★ MySQL 有哪几种重要的日志?
- redo log(InnoDB):物理日志,记录「某页做了什么修改」,保证崩溃恢复(持久性);
- undo log(InnoDB):记录修改前的值,用于回滚和 MVCC;
- binlog(Server 层):逻辑日志,记录所有修改,用于主从复制和数据恢复;
- 其他:慢查询日志、错误日志、通用查询日志、relay log(从库)。
14. ★ 主从复制的基本流程?
主库把变更写入 binlog → 从库的 IO 线程拉取 binlog 写到本地 relay log → 从库的 SQL 线程重放 relay log。默认是异步的,所以有主从延迟。
15. ★ 怎么开启和分析慢查询日志?
slow_query_log = ON、long_query_time = 1(秒)、log_queries_not_using_indexes。用 mysqldumpslow 或 pt-query-digest 按耗时、次数汇总分析。
16. ★ EXPLAIN 要看哪些字段?
type(访问类型,从好到差:system > const > eq_ref > ref > range > index > ALL,至少要到 range)、key(实际用的索引)、rows(预估扫描行数)、Extra(Using index 覆盖索引好、Using where、Using filesort 额外排序、Using temporary 临时表,后两个要注意)。
17. COUNT(*) 在 InnoDB 里是怎么执行的?
因为 MVCC,每个事务看到的行数可能不同,InnoDB 不存总行数,要遍历。优化器会选最小的二级索引去扫(比扫聚簇索引读的页少)。需要频繁计数的场景用计数表或 Redis。
18. 自增主键为什么会不连续?
插入失败(唯一键冲突)或事务回滚时,已分配的自增值不会归还;批量插入时按批预申请;8.0 以前自增值存在内存里,重启后按 max(id) + 1 重新计算(8.0 起持久化)。
19. ★ datetime 和 timestamp 怎么选?
timestamp 存 UTC、会按连接的时区转换,范围只到 2038 年;datetime 存什么就是什么,范围大。跨时区业务用 timestamp 或者统一存 UTC 的 datetime;只服务一个时区的业务用 datetime 最简单(我们现在的项目也是统一存东八区的不带时区时间,思路一样:统一一种口径)。
20. 前缀索引是什么?
只对字符串的前 n 个字符建索引:INDEX(email(10)),节省空间。缺点:不能用于 ORDER BY 和覆盖索引。选 n 的方法:让 COUNT(DISTINCT LEFT(col, n)) / COUNT(*) 接近整列的区分度。
21. 一张表最多建多少个索引合适?
没有固定数字,一般建议单表索引不超过 5–6 个、联合索引的列不超过 5 个。索引越多写入越慢、占空间越多、优化器选择越复杂。定期用 sys.schema_unused_indexes 找出从没用过的索引删掉。
22. 为什么不建议用 SELECT *?
多读不需要的列(网络和内存开销);用不上覆盖索引,必须回表;表结构变化时可能影响程序;大字段(text / blob)尤其明显。
MySQL 中等(22 题)
1. ★ 为什么 InnoDB 用 B+ 树?一个页多大?
页 16KB;B+ 树非叶子节点只存键和指针,一页能放上千个,3 层能存约两千万行(计算见 13 文档中等第 5 题),查询只要 1–3 次磁盘 IO;叶子节点的双向链表让范围查询顺序读。对比哈希(不支持范围)、红黑树(太高)、B 树(非叶子存数据、更高、范围查询不便)。
2. ★ 索引下推(ICP)是什么?
5.6 引入。联合索引 (name, age),查询 WHERE name LIKE '张%' AND age = 20:没有 ICP 时,存储引擎按 name 找到所有姓张的记录,每条都回表交给 Server 层再判断 age;有 ICP 时,在索引里就先把 age 判断掉,只对满足的记录回表,大幅减少回表次数。Extra 显示 Using index condition。
3. ★ 索引失效的常见场景?
对索引列使用函数或运算;隐式类型转换(varchar 列用数字比较,字符串和数字比较时 MySQL 把字符串转成数字,所以是索引列被转换了);LIKE '%xx';不满足最左前缀;OR 的一侧没有索引;两个表 JOIN 时字符集不同(utf8 和 utf8mb4 的列比较会发生转换);优化器判断走全表更快(返回行数占比大)。
4. ★ 联合索引怎么设计?
- 把等值查询的列放前面,范围查询和排序的列放后面;
- 区分度高的列靠前(但要以查询模式为主);
- 尽量让常用查询能用上覆盖索引;
- 利用最左前缀:
(a, b)能顶替单独的(a)索引,不要重复建; - 考虑 ORDER BY:
WHERE a = ? ORDER BY b用(a, b)可以避免 filesort。
5. ★ MVCC 的实现原理?
- 每行有隐藏列
DB_TRX_ID(最后修改它的事务 ID)和DB_ROLL_PTR(指向 undo log 里的上一个版本),形成版本链; - ReadView 包含:
m_ids(创建时所有活跃的读写事务 ID)、min_trx_id(其中最小的)、max_trx_id(下一个要分配的事务 ID)、creator_trx_id(自己); - 判断某个版本是否可见:
trx_id == creator_trx_id:自己改的,可见;trx_id < min_trx_id:创建 ReadView 前就提交了,可见;trx_id >= max_trx_id:在 ReadView 之后才开启,不可见;- 在两者之间:如果在
m_ids里(还没提交)不可见,否则可见; - 不可见就顺着版本链找上一个版本继续判断。
6. ★ RC 和 RR 在 MVCC 上的区别?
ReadView 的生成时机:RC 每条 SELECT 都生成新的 ReadView(所以能看到别人刚提交的数据);RR 只在事务中第一条 SELECT 时生成,之后一直复用(所以可重复读)。
7. ★ 快照读和当前读?
- 快照读:普通的
SELECT,读 MVCC 的历史版本,不加锁; - 当前读:
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE(8.0 写成FOR SHARE)、INSERT、UPDATE、DELETE,读最新版本并加锁。 - 一个经典现象:RR 下事务 A 先快照读到 10 行,B 插入一行并提交,A 再快照读还是 10 行;但 A 执行
UPDATE时(当前读)会更新到 B 插入的那行,之后再快照读就能看到它了 —— 这就是 MySQL RR 下仍可能出现的「幻读」。
8. ★ MySQL 的 RR 是怎么解决幻读的?
快照读靠 MVCC(同一个 ReadView,看不到新插入的行);当前读靠临键锁(next-key lock = 记录锁 + 间隙锁),锁住记录和记录之间的间隙,别的事务插不进来。但如第 7 题所说,快照读和当前读混用时仍可能出现幻读现象。
9. ★ 行锁有哪几种?
- 记录锁(Record Lock):锁住索引上的一条记录;
- 间隙锁(Gap Lock):锁住两条记录之间的间隙(不包含记录本身),只在 RR 下存在,间隙锁之间不冲突,只阻止插入;
- 临键锁(Next-Key Lock):记录锁 + 前面的间隙,左开右闭区间,是 RR 下加锁的基本单位;
- 插入意向锁:插入前加的一种特殊间隙锁,多个插入同一间隙不同位置的事务不互斥。
- 注意:InnoDB 的行锁是加在索引上的,如果 UPDATE 的条件没有走索引,会扫描全表并锁住所有记录(相当于锁表)。
10. 意向锁是干什么的?
表级锁,表示「这个表里有行被加了共享锁 / 排他锁」。作用:别的事务要加表锁时,不用逐行检查有没有行锁,看一眼意向锁就知道。意向锁之间不冲突,只和表级的 S / X 锁冲突。
11. ★ 怎么排查死锁?
SHOW ENGINE INNODB STATUS 里的 LATEST DETECTED DEADLOCK 会显示两个事务各自持有和等待的锁、执行的 SQL;开启 innodb_print_all_deadlocks 把所有死锁记到错误日志。InnoDB 会自动检测死锁并回滚代价小的事务(innodb_deadlock_detect)。预防:按固定顺序访问资源、事务尽量小、走索引减少锁的范围、降低隔离级别到 RC(没有间隙锁,死锁大幅减少)。
12. ★ redo log 是怎么保证持久性的?刷盘策略?
WAL:修改先写 redo log(顺序写,快),数据页之后再慢慢刷。崩溃后用 redo log 重做已提交事务的修改。redo log 是固定大小、循环写的(write pos 和 checkpoint 两个指针)。 innodb_flush_log_at_trx_commit:
1(默认,最安全):每次提交都写入并 fsync;0:每秒写入并 fsync 一次,MySQL 崩溃可能丢 1 秒;2:每次提交写到操作系统缓存,每秒 fsync,MySQL 崩溃不丢、操作系统崩溃可能丢 1 秒。
13. ★ binlog 有哪几种格式?
STATEMENT:记录 SQL 原文,日志小,但now()、uuid()、LIMIT无 ORDER BY 等不确定的语句在从库可能产生不同结果;ROW(推荐,8.0 默认):记录每一行修改前后的值,准确,但日志大(一条 UPDATE 改 100 万行就记 100 万行);Canal、Debezium 这类 CDC 工具需要 ROW 格式;MIXED:一般用 statement,不安全时自动切 row。sync_binlog = 1:每次提交都 fsync binlog(「双 1」配置:它和innodb_flush_log_at_trx_commit = 1一起保证不丢数据)。
14. ★ 什么是两阶段提交?为什么需要?
redo log(InnoDB)和 binlog(Server 层)是两份日志,必须保证一致:否则崩溃恢复后主库(按 redo log)和从库(按 binlog)的数据会不一样。流程:
- 写 redo log,状态为 prepare;
- 写 binlog;
- redo log 改为 commit。 崩溃恢复时:redo log 是 commit 的直接提交;是 prepare 的就看 binlog 里有没有这个事务(通过 XID 关联),有就提交、没有就回滚。
15. ★ undo log 有什么用?
记录修改前的数据:① 事务回滚时用它恢复;② MVCC 的版本链由它构成。undo log 在没有任何事务需要读那个版本后,由 purge 线程清理。长事务会导致 undo log 无法清理、不断膨胀(和 PG 的长事务导致表膨胀是同一类问题)。
16. ★ Buffer Pool 是什么?它的 LRU 做了什么改进?
InnoDB 在内存里缓存数据页和索引页的区域(一般设置为机器内存的 60%–80%),读写都先操作 Buffer Pool,脏页由后台线程刷盘。 改进的 LRU:链表分成 young 区(约 5/8)和 old 区(约 3/8),新读入的页先放到 old 区的头部,在 old 区停留超过 innodb_old_blocks_time(默认 1 秒)后再被访问才移到 young 区。目的:防止全表扫描一次性读入大量只用一次的页,把真正的热点数据挤出去;也防止预读的页污染缓存。
17. change buffer 是什么?
更新二级索引页时,如果这个页不在 Buffer Pool 里,不立刻从磁盘读出来,而是把修改记录在 change buffer 里,等以后这个页被读入时再合并(merge)。减少随机读 IO,适合写多读少的场景。唯一索引用不了 change buffer(插入前必须读出页来检查唯一性)。
18. ★ 主从延迟有哪些原因?
从库机器性能差;从库上跑了很多查询;大事务(主库执行 10 分钟,从库也要重放 10 分钟);从库单线程重放跟不上主库的并发写入;网络延迟。看延迟:SHOW REPLICA STATUS 的 Seconds_Behind_Source(老版本是 Seconds_Behind_Master)。
19. 半同步复制是什么?
主库提交事务后,至少等一个从库确认收到 binlog(写入 relay log)才返回给客户端,降低主库宕机丢数据的风险。超时会退化成异步。5.7 的「无损半同步」(AFTER_SYNC)在写 binlog 后、引擎提交前等待从库,主库宕机时从库一定有这个事务。
20. ★ ORDER BY 是怎么执行的?什么时候会 filesort?
如果排序字段和索引顺序一致(并且是同一个索引能覆盖的条件),直接按索引顺序读,不需要排序;否则要 filesort:在 sort_buffer(sort_buffer_size)里排序,数据太多就用临时文件做归并排序。两种方式:全字段排序(把要返回的列都放进 buffer)、rowid 排序(只放排序列和主键,排完再回表)。优化:建合适的联合索引让排序走索引。
21. JOIN 是怎么执行的?
- Index Nested-Loop Join:被驱动表的连接列上有索引,驱动表每一行去索引里查,效率好;
- Block Nested-Loop Join:被驱动表没索引,把驱动表的数据分块读进
join_buffer,再扫描被驱动表逐块比较,很慢; - 8.0.18 起用 Hash Join 替代 BNL;
- 原则:小表驱动大表(「小」指过滤后参与连接的数据量),被驱动表的连接列要有索引。
22. 唯一索引和普通索引在性能上有什么区别?
查询:几乎一样(唯一索引找到一条就停,普通索引要再往后看一条,差别可以忽略)。更新:普通索引可以用 change buffer,唯一索引必须读出数据页检查唯一性。所以在业务能保证唯一(或者允许由应用层保证)且写多读少时,普通索引的写入性能更好;但需要数据库兜底唯一性时还是用唯一索引。
MySQL 高难(22 题)
1. ★ 深分页(LIMIT 1000000, 20)怎么优化?
MySQL 要先读出 100 万零 20 行(还要回表)再丢掉前 100 万。
- 游标分页:
WHERE id > 上一页最大id ORDER BY id LIMIT 20; - 延迟关联:先在覆盖索引上分页拿主键,再 JOIN 回表:
SELECT o.* FROM orders o JOIN (SELECT id FROM orders WHERE user_id = 1 ORDER BY created_at LIMIT 1000000, 20) t ON o.id = t.id; - 业务上限制最大页数(搜索引擎也只给前几十页)。
2. ★ 千万级大表要加字段 / 加索引,怎么不影响线上?
- 5.6 起的 Online DDL:
ALTER TABLE ... ALGORITHM=INPLACE, LOCK=NONE,大部分操作期间可以读写;8.0 的ALGORITHM=INSTANT可以秒级加列(只改元数据); - 注意:DDL 开始和结束时都要拿一下 MDL 写锁,如果前面有长事务或长查询,DDL 会等待,后面所有的查询都被它阻塞 → 先检查并处理长事务,设置
lock_wait_timeout; - 工具:gh-ost(读 binlog 同步增量到影子表,不用触发器,可暂停、可限速)、pt-online-schema-change(用触发器同步增量);原理都是「建新表 → 拷贝数据 → 同步增量 → 原子改名」;
- 主从环境:大 DDL 在从库重放也会造成延迟。
3. ★ 一张表 5000 万行,查询越来越慢,你的优化思路?
按代价由小到大:
- SQL 和索引:慢查询日志找出问题 SQL,EXPLAIN 分析,加 / 改索引、改写 SQL;
- 表结构:拆出大字段(垂直拆分)、冷热字段分离、字段类型精简;
- 缓存:热点数据放 Redis;
- 读写分离:读压力大时加从库;
- 归档:历史数据迁到归档表或分析库(ClickHouse),在线表只留热数据;
- 分区表:按时间分区(但 MySQL 分区表限制多,唯一索引必须包含分区键);
- 分库分表:最后的手段(见下一题)。 准备你在旧项目里真实做过的那次慢查询优化。
4. ★ 分库分表怎么做?会带来哪些问题?
- 垂直拆分:按业务把不同的表拆到不同的库(用户库、订单库);把一张宽表的列拆成多张表;
- 水平拆分:同一张表的数据按分片键拆到多个库 / 表(
user_id % 16、按时间范围); - 分片键选择:选最常用的查询条件(订单按 user_id,保证「查我的订单」只落一个分片);
- 带来的问题:
- 分布式 ID:自增不能用了 → 雪花算法 / 号段模式;
- 跨分片查询:不带分片键的查询要查所有分片(按商家查订单 → 再建一份按商家分片的冗余数据,或同步到 ES);
- 跨分片排序分页、聚合:在中间层合并;
- 分布式事务:尽量避免,用最终一致(本地消息表 / Saga);
- 扩容迁移:取模分片扩容要迁移大量数据 → 一开始就多分(比如 1024 个逻辑分片映射到少量物理库),或用一致性哈希;
- 中间件:ShardingSphere(客户端 / 代理)、Vitess、MyCat。也可以直接上分布式数据库(TiDB、OceanBase)。
5. ★ 读写分离下,刚写完就读不到怎么办?
- 写后立刻读的请求强制走主库(同一个请求 / 会话内写过就读主库,或者按业务标记关键读走主库);
- 判断从库是否追上:记录写入后主库的 GTID,读之前在从库上等这个 GTID 执行完(
WAIT_FOR_EXECUTED_GTID_SET); - 业务上接受短暂延迟(比如提示「数据正在同步」);
- 降低延迟:并行复制、避免大事务。
6. ★ 主从延迟很大,怎么排查和解决?
- 看是 IO 线程慢(网络、主库 binlog 量大)还是 SQL 线程慢(重放跟不上);
- 常见原因:大事务(一次删几百万行 → 改成分批)、DDL、从库查询压力大、从库配置差、无主键的表(ROW 格式下每行都要全表扫描找记录);
- 解决:并行复制(5.7 基于组提交、8.0 基于 WRITESET 的并行复制,
replica_parallel_workers);拆分大事务;给所有表加主键;从库适当放宽持久化配置(非双 1)。
7. ★ 讲一个典型的死锁案例
两个事务在 RR 下按不同顺序插入:
- 表上有唯一索引,现有记录 id = 10、20;
- 事务 A:
SELECT ... WHERE id = 15 FOR UPDATE(不存在,加了 (10, 20) 的间隙锁); - 事务 B:同样
SELECT ... WHERE id = 16 FOR UPDATE(也加了 (10, 20) 的间隙锁 —— 间隙锁之间不冲突,所以都成功了); - A:
INSERT id = 15→ 要插入意向锁,被 B 的间隙锁阻塞; - B:
INSERT id = 16→ 被 A 的间隙锁阻塞 → 死锁。 这是「先查一下不存在再插入」这种写法在 RR 下的经典死锁。解决:直接INSERT ... ON DUPLICATE KEY UPDATE;或者用 RC 隔离级别(没有间隙锁)。
8. ★ 热点行(比如商品库存、账户余额)高并发更新怎么优化?
所有请求更新同一行,行锁导致串行,还有死锁检测的开销(每个等待的线程都要检测,并发越高越慢)。
- 应用层排队:同一行的更新在应用层合并或排队,减少同时到达数据库的并发;
- 库存拆分:一个商品拆成多行子库存,随机扣其中一行;
- Redis 预扣 + 异步落库;
- 合并更新:多个请求的扣减攒成一次
UPDATE; - 阿里云 / 腾讯云的 MySQL 分支有专门的热点行优化。
9. ★ 优化器选错了索引怎么办?
- 原因:统计信息不准(InnoDB 采样估算区分度);优化器认为回表代价太高;排序因素(优化器为了避免 filesort 选了排序列的索引,结果扫了很多行);
- 处理:
ANALYZE TABLE重新统计;FORCE INDEX临时强制(不推荐长期用,表结构变了容易出问题);改写 SQL 引导优化器;调整或删除干扰的索引;8.0 可以用直方图(ANALYZE TABLE ... UPDATE HISTOGRAM)改善数据倾斜列的估算。
10. ★ 大事务有什么危害?
锁持有时间长,阻塞其他事务;undo log 膨胀;主从延迟(从库要重放整个大事务);回滚很慢(回滚可能比执行还久);binlog 一次写入很大。规范:批量操作分批提交(每批几千行)、事务里不要调用外部接口、不要在事务里等待用户输入。
11. 自增锁(AUTO-INC 锁)是什么?
插入时分配自增值要加的锁。innodb_autoinc_lock_mode:
- 0:传统模式,表级锁,语句结束才释放;
- 1:连续模式(5.x 默认),简单插入申请完值就释放,批量插入(
INSERT ... SELECT)保持到语句结束; - 2:交错模式(8.0 默认),所有插入申请完就释放,并发最好,但批量插入的 id 可能不连续 —— 配合 ROW 格式的 binlog 才是安全的(STATEMENT 格式下主从可能分配出不同的 id)。
12. doublewrite buffer 是干什么的?
解决页的部分写失效:InnoDB 的页是 16KB,而操作系统 / 磁盘的原子写单位通常是 4KB,写到一半断电就会出现「半新半旧」的损坏页,redo log 也救不了(redo 记录的是对页的修改,需要基于一个完整的页)。所以刷脏页时先把页顺序写到 doublewrite 区域,再写到真实位置;崩溃后发现页损坏,就从 doublewrite 里恢复。对应 PG 的 full_page_writes。
13. ★ MySQL 是怎么做到 crash-safe 的?
redo log(WAL)保证已提交事务的修改不丢;undo log 回滚未提交的事务;两阶段提交保证 redo log 和 binlog 一致;doublewrite 保证数据页本身不损坏;配合「双 1」(innodb_flush_log_at_trx_commit = 1、sync_binlog = 1)。
14. ★ JOIN 时两个表的字段字符集不同,会有什么问题?
比如 a 表的 user_id 是 utf8mb4、b 表的是 utf8,a.user_id = b.user_id 时 MySQL 会把 utf8 转成 utf8mb4 来比较,如果被转换的那一侧是被驱动表的索引列,索引就失效了,每次都是全表扫描。表结构规范里要统一字符集和排序规则。类似的还有字段类型不一致(int 和 varchar 关联)。
15. COUNT 很慢,怎么优化?
- 不需要精确值:
SHOW TABLE STATUS的 rows(估算)、或者EXPLAIN的 rows; - 需要精确值且频繁:单独维护计数表(在同一个事务里更新,保证一致)或 Redis 计数(和数据库之间可能不一致);
- 带条件的计数:确保条件走索引,尽量覆盖索引;
- 分析类的计数放到分析库。
16. 什么时候会用到临时表?Using temporary 怎么优化?
GROUP BY 的列没有索引、DISTINCT 和 ORDER BY 列不同、UNION(不是 UNION ALL)、部分子查询。优化:给 GROUP BY 的列建索引(让分组直接按索引顺序进行);能用 UNION ALL 就不用 UNION;调整 tmp_table_size 让临时表留在内存里(超过会转成磁盘临时表)。
17. 主键用 UUID,表变得很大、插入很慢,怎么解释和解决?
随机 UUID 插入到 B+ 树的随机位置,频繁页分裂(页利用率可能降到一半左右)、Buffer Pool 缓存命中率低(每次插入的页都不一样);而且 36 字节的字符串主键会被复制到每一个二级索引的叶子节点里,所有索引都变大。解决:主键改用自增或有序 ID(雪花、UUID v7、MySQL 8 的 UUID_TO_BIN(uuid, 1) 交换时间位使之有序)并用 BINARY(16) 存储;UUID 作为业务字段单独存放。
18. 线上误删了数据,怎么恢复?
- 有全量备份 + binlog:用备份恢复到一个临时实例,再用
mysqlbinlog重放 binlog 到误操作前的位置(按时间或 position),导出被删的数据,再导回生产; - 只是误删 / 误改了少量行、binlog 是 ROW 格式:用 binlog2sql、MyFlash 这类工具从 binlog 里生成反向 SQL(DELETE 变 INSERT、UPDATE 前后对调);
- 有延迟从库(专门设置延迟 1 小时复制的从库):直接从延迟从库上取;
- 预防:权限控制、生产操作走审核、危险操作先备份、开启
sql_safe_updates(没有 WHERE 或 LIMIT 的 UPDATE / DELETE 拒绝执行)。
19. 数据库连接数满了(Too many connections)怎么办?
- 应急:用预留的管理连接登录(
max_connections + 1留给 SUPER 用户;8.0 有单独的管理端口admin_port),SHOW PROCESSLIST找出大量空闲或卡住的连接,KILL掉; - 排查:是连接池配置太大(实例数 × 池大小超过上限)、连接泄漏(借了不还)、还是慢查询让连接都占着不释放;
- 解决:调整连接池大小、修复泄漏、修复慢查询、调大
max_connections(要考虑内存)、加中间件做连接复用(ProxySQL)。
20. MySQL 和 PostgreSQL 怎么选?
| MySQL | PostgreSQL | |
|---|---|---|
| 优势 | 简单易用、国内生态和运维经验丰富、读多写少的互联网业务久经考验、分库分表中间件成熟 | 功能强(丰富的类型、jsonb、GIN / GiST 索引、窗口函数和 CTE 支持早、扩展生态:pgvector、PostGIS)、SQL 标准兼容好、复杂查询优化器更强 |
| 存储 | 聚簇索引、undo log | 堆表、多版本存在表里(需要 VACUUM) |
| 典型场景 | 高并发 OLTP、简单的增删改查 | 复杂业务、数据分析、地理信息、AI 应用(向量) |
| 我们现在的项目选 PG:jsonb 存半结构化的配置和运行日志、扩展能力强,将来要做向量检索也可以直接用 pgvector。 |
21. 怎么做数据库的水平扩容(从 4 个分片扩到 8 个)?
- 如果一开始就是「逻辑分片(比如 1024 个)→ 物理库」的两级映射:扩容只需要把一部分逻辑分片整体迁到新库,改一下映射表,不用重新计算每行数据的归属;
- 迁移步骤:全量拷贝 → 增量同步(binlog / CDC)→ 数据校验 → 短暂停写切换路由 → 观察 → 清理旧数据;
- 取模直接从 % 4 改成 % 8:一半的数据要迁移,而且所有数据都要重新计算,代价大 —— 这就是要在设计时预留分片数的原因。
22. 讲一个你真实做过的 MySQL 优化案例(必须准备)
用 STAR 结构准备:
- S:哪个系统、哪张表(数据量多大)、什么现象(某个接口 P99 多少秒、慢查询日志里出现了什么);
- T:目标(降到多少毫秒);
- A:怎么定位(慢查询日志 / EXPLAIN 看到了什么:全表扫描、filesort、索引选错)→ 怎么改(加了什么联合索引、改写了什么 SQL、有没有改业务逻辑)→ 怎么验证(压测、灰度);
- R:结果(从几秒降到几十毫秒、慢查询数量下降多少)。 简历上写了「负责过公司的数据库慢查询优化」,这题一定会被问到。
