8.0 KiB
id, title, type, domain, stack, tags, level, related, week
| id | title | type | domain | stack | tags | level | related | week | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.mysql.interview | MySQL 面试题 | interview | backend |
|
|
intermediate |
|
|
MySQL 面试题
招聘要求里 MySQL 出现频率很高,这部分建议全部能口述。PostgreSQL 的题见 PostgreSQL 面试题。
基础
【基础】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);需要跳页时用延迟关联并限制最大页数;导出走异步任务;历史数据归档。详见深分页场景。
追问:用户要求可以跳转到任意页,怎么办?→ 深分页限制最大页数;或者用延迟关联(先在覆盖索引上查主键,再回表);超深分页的需求通常可以引导为搜索 / 筛选。
【场景题】线上一个接口突然变慢,数据库 CPU 很高,你怎么排查?
参考答案:先 SHOW PROCESSLIST 看正在执行的慢语句和状态(是否大量 Waiting for lock);查慢查询日志按总耗时排序;对可疑 SQL 做 EXPLAIN,看是不是最近上线的新语句、索引被删、统计信息不准导致走错索引;有锁等待时查 information_schema / performance_schema 中的锁信息找到阻塞源。处理上可以先限流或下线问题功能止血,再加索引或改 SQL,最后复盘加监控。
追问:怎么防止慢查询再次发生?→ 上线前审查 SQL 和 EXPLAIN;开启慢查询日志并告警;对大表的变更进行评审。