Files
zhuangang/topics/backend/mysql/interview.md
T

94 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: backend.mysql.interview
title: MySQL 面试题
type: interview
domain: backend
stack: [mysql]
tags: [MySQL, InnoDB, 索引, 事务, 面试]
level: intermediate
related: [backend.mysql.index-explain, backend.mysql.tx-locks-mvcc, backend.mysql.ops-scale, backend.postgresql.interview]
week: [2, 14, 16]
---
# MySQL 面试题
> 招聘要求里 MySQL 出现频率很高,这部分建议全部能口述。PostgreSQL 的题见 [PostgreSQL 面试题](../postgresql/interview.md)。
## 基础
### 【基础】InnoDB 为什么用 B+ 树而不是 B 树、红黑树或哈希?
**参考答案**:B+ 树的非叶子节点只存键,一个页能放更多键,树更矮,磁盘 IO 次数少;所有数据在叶子节点且叶子之间用链表相连,范围查询和排序高效。红黑树是二叉树,数据多时太高;哈希只支持等值查询,不支持范围和排序。
**追问**:一棵 3 层的 B+ 树大约能存多少行?→ 粗略估算:16KB 页,非叶子节点每条约十几字节,可放一千多个指针;叶子页按每行 1KB 放 16 行,约 1000 × 1000 × 16 ≈ 两千万行。这是数量级估算,实际取决于行大小。
### 【基础】什么是聚簇索引、回表和覆盖索引?
**参考答案**:InnoDB 的主键索引叶子节点存整行数据,叫聚簇索引;二级索引叶子节点存索引列和主键值。通过二级索引找到主键再去主键索引取整行叫回表。如果查询需要的列都在二级索引里,就不用回表,叫覆盖索引(EXPLAIN 的 Extra 显示 Using index)。
**追问**:为什么建议主键用自增整数?→ 顺序插入页分裂少;主键越短,二级索引越小。
### 【基础】联合索引的最左前缀原则是什么?
**参考答案**:联合索引 `(a, b, c)` 先按 a 排序,a 相同再按 b,依此类推。查询条件要从 a 开始连续使用才能利用索引定位;只用 b 或 c 用不上;中间遇到范围条件(如 `a > 1`)后,后面的列一般不能再用于定位(可以靠索引下推过滤)。
**追问**:`WHERE b = 1 AND a = 2` 能用上 (a, b) 索引吗?→ 能,优化器会调整条件顺序;最左前缀看的是条件中是否包含最左列,不看书写顺序。
### 【基础】有哪些常见的索引失效情况?
**参考答案**:索引列上用函数或运算;隐式类型转换(字符串列用数字比较);`LIKE` 以 `%` 开头;不满足最左前缀;`OR` 连接的列有的没索引;优化器判断走索引不如全表扫描(区分度低、要扫的行太多)。
**追问**:怎么确认索引有没有用上?→ 用 `EXPLAIN` 看 `type`、`key`、`rows`、`Extra`;MySQL 8.0.18+ 可以用 `EXPLAIN ANALYZE` 看实际执行情况。
### 【基础】redo log、undo log、binlog 分别是什么?
**参考答案**:redo log 是 InnoDB 的物理日志,记录“某页做了什么修改”,保证崩溃后已提交的事务不丢(持久性);undo log 记录修改前的值,用于回滚和 MVCC 读旧版本(原子性、隔离性);binlog 是 Server 层的逻辑日志,用于主从复制和按时间点恢复。redo log 和 binlog 用两阶段提交保证一致。
**追问**:为什么需要两阶段提交?→ redo log 和 binlog 是两份日志,必须保证一致:否则崩溃恢复后主库数据和从库(通过 binlog 复制)不一致。
## 进阶
### 【进阶】InnoDB 的可重复读是怎么实现的?能解决幻读吗?
**参考答案**:普通 SELECT 是快照读,事务第一次读时生成 Read View,之后一直用它判断数据版本是否可见,所以多次读结果一致,也看不到别人新插入的行。当前读(`FOR UPDATE`、`UPDATE`)读最新数据,通过临键锁(记录锁 + 间隙锁)阻止其他事务在范围内插入,从而避免幻读。但如果事务里先快照读、再当前读,仍可能看到不一致,所以不能说“完全解决”。
**追问**:间隙锁会带来什么问题?→ 锁住的范围比实际修改的行大,并发插入会被阻塞,也更容易死锁;如果业务能接受,可以使用读已提交隔离级别减少间隙锁。
### 【进阶】什么情况下会死锁?怎么排查和避免?
**参考答案**:两个事务以不同顺序锁定相同的资源,互相等待。InnoDB 会检测并回滚代价小的那个事务(错误 1213)。排查:`SHOW ENGINE INNODB STATUS` 的 LATEST DETECTED DEADLOCK 段,看两个事务各自持有和等待的锁。避免:固定加锁顺序(如按 id 排序)、缩小事务、更新条件走索引减少锁范围、必要时降低到读已提交减少间隙锁;应用层捕获死锁并重试。
**追问**:死锁发生后 MySQL 怎么处理?→ InnoDB 检测到死锁后回滚代价较小的事务,另一个继续执行;应用层要捕获死锁错误并重试整个事务。
### 【进阶】主从延迟的原因和处理方式?
**参考答案**:原因:大事务、从库重放并行度低、从库负载高或配置差、网络。处理:拆分大事务、开启并行复制、关键读走主库(写后一段时间内读主库)、监控延迟超过阈值时把读流量切回主库。
**追问**:刚写入就读不到,业务上怎么处理?→ 写后立即读的请求走主库(或一段时间内该用户的读走主库);或者前端直接使用写接口的返回结果。
### 【进阶】什么时候需要分库分表?分片键怎么选?
**参考答案**:单表数据量大到索引优化后仍然慢、或单库写入成为瓶颈,并且归档、缓存、读写分离都用过之后再考虑。分片键选最常用的查询维度(如用户 ID),让大部分请求只访问一个分片,并且数据分布均匀。代价是跨分片查询、分页、事务、全局 ID 和扩容迁移都变复杂。
**追问**:分库分表后跨分片查询怎么办?→ 尽量通过分片键路由;后台统计类查询同步到 ES 或数仓;避免跨分片 JOIN 和分布式事务。
## 场景题
### 【场景题】“先查库存再扣减”在高并发下超卖了,怎么改?
**参考答案**:查和扣两步之间有并发窗口。改成一条条件更新:`UPDATE stock SET num = num - 1 WHERE sku_id = ? AND num >= 1`,判断影响行数;或者 `SELECT ... FOR UPDATE` 加锁后再扣。并发非常高时(秒杀),数据库行锁会成为瓶颈,在 Redis 里预扣库存、消息队列异步落库(见 Redis 专题的库存场景和跨栈场景“秒杀”)。
**追问**:用乐观锁还是 `UPDATE ... WHERE stock >= 1`?→ 扣库存这种只需要一个条件的场景,直接条件更新最简单;需要读出数据做复杂计算后再写入的,用版本号乐观锁。
### 【场景题】一张几千万行的表,分页越往后越慢,怎么优化?
**参考答案**:`LIMIT offset` 需要扫描并丢弃前面的行。改为游标分页(记住上一页最后一条的排序键和 id);需要跳页时用延迟关联并限制最大页数;导出走异步任务;历史数据归档。详见[深分页场景](scenarios/deep-pagination.md)。
**追问**:用户要求可以跳转到任意页,怎么办?→ 深分页限制最大页数;或者用延迟关联(先在覆盖索引上查主键,再回表);超深分页的需求通常可以引导为搜索 / 筛选。
### 【场景题】线上一个接口突然变慢,数据库 CPU 很高,你怎么排查?
**参考答案**:先 `SHOW PROCESSLIST` 看正在执行的慢语句和状态(是否大量 Waiting for lock);查慢查询日志按总耗时排序;对可疑 SQL 做 EXPLAIN,看是不是最近上线的新语句、索引被删、统计信息不准导致走错索引;有锁等待时查 `information_schema` / `performance_schema` 中的锁信息找到阻塞源。处理上可以先限流或下线问题功能止血,再加索引或改 SQL,最后复盘加监控。
**追问**:怎么防止慢查询再次发生?→ 上线前审查 SQL 和 `EXPLAIN`;开启慢查询日志并告警;对大表的变更进行评审。