个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <
文章目录
- [MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)](#MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版))
-
- [一、索引篇(Q1 - Q8)](#一、索引篇(Q1 - Q8))
-
- [Q1:为什么索引用 B+ 树,而不是二叉查找树或 B 树?](#Q1:为什么索引用 B+ 树,而不是二叉查找树或 B 树?)
- Q2:聚簇索引和二级索引有什么区别?什么是回表?
- Q3:联合索引的最左前缀原则是怎么回事?
- Q4:哪些情况会导致索引失效?
- Q5:什么是索引下推(ICP)?
- [Q6:EXPLAIN 里你主要看哪几列?](#Q6:EXPLAIN 里你主要看哪几列?)
- [Q7:`COUNT(*)`、`COUNT(1)`、`COUNT(列)` 有什么区别?](#Q7:
COUNT(*)、COUNT(1)、COUNT(列)有什么区别?) - [Q8:为什么推荐用自增 ID 做主键,UUID 有什么问题?](#Q8:为什么推荐用自增 ID 做主键,UUID 有什么问题?)
- [二、事务与锁篇(Q9 - Q14)](#二、事务与锁篇(Q9 - Q14))
-
- [Q9:ACID 分别靠什么实现?](#Q9:ACID 分别靠什么实现?)
- [Q10:四种隔离级别,MySQL 默认哪个?](#Q10:四种隔离级别,MySQL 默认哪个?)
- [Q11:MVCC 是怎么实现的?RC 和 RR 的差别在哪?](#Q11:MVCC 是怎么实现的?RC 和 RR 的差别在哪?)
- Q12:快照读和当前读有什么区别?
- [Q13:InnoDB 有哪几种锁?什么是 Next-Key Lock?](#Q13:InnoDB 有哪几种锁?什么是 Next-Key Lock?)
- Q14:死锁怎么排查和避免?
- [三、日志与存储篇(Q15 - Q19)](#三、日志与存储篇(Q15 - Q19))
-
- [Q15:redo log、undo log、binlog 有什么区别?](#Q15:redo log、undo log、binlog 有什么区别?)
- Q16:为什么要有"两阶段提交"?
- [Q17:一条 UPDATE 语句在 MySQL 里经历了什么?](#Q17:一条 UPDATE 语句在 MySQL 里经历了什么?)
- [Q18:InnoDB 和 MyISAM 的区别?](#Q18:InnoDB 和 MyISAM 的区别?)
- [Q19:为什么 8.0 移除了查询缓存(Query Cache)?](#Q19:为什么 8.0 移除了查询缓存(Query Cache)?)
- [四、SQL 优化篇(Q20 - Q24)](#四、SQL 优化篇(Q20 - Q24))
-
- [Q20:深分页 `LIMIT 1000000, 20` 为什么慢?怎么优化?](#Q20:深分页
LIMIT 1000000, 20为什么慢?怎么优化?) - [Q21:JOIN 有哪些算法?](#Q21:JOIN 有哪些算法?)
- [Q22:GROUP BY 和 ORDER BY 什么时候会用到临时表 / filesort?](#Q22:GROUP BY 和 ORDER BY 什么时候会用到临时表 / filesort?)
- Q23:慢查询的排查流程是什么?
- [Q24:CHAR 和 VARCHAR 怎么选?`VARCHAR(10)` 能存 10 个汉字吗?](#Q24:CHAR 和 VARCHAR 怎么选?
VARCHAR(10)能存 10 个汉字吗?)
- [Q20:深分页 `LIMIT 1000000, 20` 为什么慢?怎么优化?](#Q20:深分页
- [五、架构与运维篇(Q25 - Q28)](#五、架构与运维篇(Q25 - Q28))
-
- Q25:主从复制的原理是什么?延迟怎么解决?
- Q26:读写分离会遇到什么问题?
- Q27:大表加一个字段,怎么做到不影响业务?
- [Q28:分库分表怎么选分片键?全局 ID 怎么生成?](#Q28:分库分表怎么选分片键?全局 ID 怎么生成?)
- [六、场景题(Q29 - Q30)](#六、场景题(Q29 - Q30))
-
- [Q29:一个表有 `(a, b, c)` 联合索引,下面几条 SQL 谁走索引?](#Q29:一个表有
(a, b, c)联合索引,下面几条 SQL 谁走索引?) - [Q30:线上发现 CPU 100%,怎么定位?](#Q30:线上发现 CPU 100%,怎么定位?)
- [Q29:一个表有 `(a, b, c)` 联合索引,下面几条 SQL 谁走索引?](#Q29:一个表有
- 小结:面试回答的三个层次
MySQL 高频面试题 30 问:索引、事务、锁、日志、主从与分库分表(MySQL 8.0 版)
面试 MySQL 有个特点:题目就那些,但每一问都能往下追三层。
只会背"ACID 是原子性一致性隔离性持久性"的人,第一层就结束了。
这篇按模块整理 30 个高频问题,每题给出能答到第二层的答案 和常见的追问点 。
默认语境是 MySQL 8.0 + InnoDB,遇版本差异会单独标注。
一、索引篇(Q1 - Q8)
Q1:为什么索引用 B+ 树,而不是二叉查找树或 B 树?
| 结构 | 问题 |
|---|---|
| 二叉查找树 | 可能退化成链表,树高 = n |
| 平衡二叉树(AVL/红黑树) | 每个节点只存一个 key,树高太高。100 万行数据树高约 20,就要 20 次磁盘 IO |
| B 树 | 多叉,树高降低;但每个节点都存数据,一个页能放的 key 变少,树高反而更高 |
| B+ 树 | ✅ 非叶子节点只存 key 不存数据 ,一个 16KB 的页能塞上千个 key → 树高只有 3~4 层;且叶子节点用链表串起来,范围查询不用回溯到根 |
一句话总结 :B+ 树把"树高"压到 3~4 层,意味着亿级数据点查只需 3~4 次磁盘 IO ;
叶子链表让它天然擅长范围查询。
追问:为什么不用 Hash 索引?------ Hash 只支持等值查询,不支持范围和排序 ,
且有哈希冲突。InnoDB 的自适应哈希索引(AHI)是内部优化,用户不能显式创建(MEMORY 引擎除外)。
Q2:聚簇索引和二级索引有什么区别?什么是回表?
- 聚簇索引 :叶子节点存整行数据。InnoDB 每张表有且仅有一个,通常是主键
- 二级索引 :叶子节点存索引列 + 主键值
通过二级索引找到主键值后,再去聚簇索引查整行,这个过程叫回表。
sql
-- 假设 idx_name 建在 name 上
SELECT * FROM user WHERE name = '张三';
-- ① 走 idx_name 找到主键 id=100
-- ② 用 id=100 回聚簇索引取整行 ← 回表
减少回表的办法就是覆盖索引:
sql
-- 建联合索引,让查询需要的列都在索引里
CREATE INDEX idx_name_age ON user(name, age);
SELECT id, name, age FROM user WHERE name = '张三'; -- Extra: Using index,不用回表
Q3:联合索引的最左前缀原则是怎么回事?
索引 (a, b, c) 在 B+ 树里是按 a → b → c 的顺序排序的,
所以只能从最左边开始"连续"匹配:
| 条件 | 能否用上索引 | 用到哪些列 |
|---|---|---|
a = 1 |
✅ | a |
a = 1 AND b = 2 |
✅ | a, b |
a = 1 AND b = 2 AND c = 3 |
✅ | a, b, c |
b = 2 |
❌ | 无(没有 a,无法定位) |
a = 1 AND c = 3 |
部分 | 只有 a(b 断掉了) |
a > 1 AND b = 2 |
部分 | 只有 a(范围条件之后的列用不上) |
⚠️ 8.0 的索引跳跃扫描(Index Skip Scan) 可以在没有最左列时"跳着"用索引,
但只在该列基数很低(比如性别只有 2 个值)时才划算,不能依赖它。
Q4:哪些情况会导致索引失效?
| 场景 | 例子 | 说明 |
|---|---|---|
| 索引列上用函数 | WHERE YEAR(create_time) = 2024 |
改成范围条件 |
| 隐式类型转换 | WHERE phone = 13800138000(phone 是 varchar) |
字符串列传了数字,全表扫 |
| 隐式字符集转换 | 两表 JOIN 字段字符集不同 | utf8mb4 JOIN utf8 会失效 |
LIKE '%xxx' |
WHERE name LIKE '%三' |
前缀通配符无法定位;'三%' 可以 |
OR 连接非索引列 |
WHERE a=1 OR b=2(b 无索引) |
改成 UNION 或给 b 加索引 |
| 违反最左前缀 | 见 Q3 | --- |
| 优化器判断不划算 | 结果集占比太高 | 回表代价大于全表扫描,优化器会选全表 |
sql
-- ❌ 失效
SELECT * FROM orders WHERE DATE(create_time) = '2024-06-01';
-- ✅ 改写
SELECT * FROM orders WHERE create_time >= '2024-06-01' AND create_time < '2024-06-02';
Q5:什么是索引下推(ICP)?
MySQL 5.6 引入。联合索引 (name, age),查询 WHERE name LIKE '张%' AND age = 20:
- 没有 ICP :存储引擎只按
name LIKE '张%'返回所有匹配行,Server 层再过滤 age - 有 ICP :存储引擎在索引内部就顺手判断 age=20,不符合的直接跳过,减少回表次数
执行计划里表现为 Extra: Using index condition。
8.0 默认开启,可用 optimizer_switch='index_condition_pushdown=off' 关闭验证。
Q6:EXPLAIN 里你主要看哪几列?
| 列 | 关注点 |
|---|---|
type |
从优到劣:system > const > eq_ref > ref > range > index > ALL。见到 ALL 和 index 就要警惕 |
key |
实际用到的索引;possible_keys 有但 key 为 NULL 说明索引没被选中 |
rows |
预估扫描行数,与实际返回行数差太多说明统计信息不准 |
Extra |
Using index(覆盖索引,好)、Using filesort(排序没走索引)、Using temporary(用了临时表,通常要优化) |
filtered |
8.0 有,表示经过条件过滤后剩余的比例 |
sql
EXPLAIN FORMAT=JSON SELECT ...; -- 更详细,含 cost 估算
EXPLAIN ANALYZE SELECT ...; -- 8.0.18+,给出真实执行耗时和行数
Q7:COUNT(*)、COUNT(1)、COUNT(列) 有什么区别?
| 写法 | 行为 | 性能 |
|---|---|---|
COUNT(*) |
统计所有行数 | 8.0 对 COUNT(*) 有特殊优化,通常最快 |
COUNT(1) |
与 COUNT(*) 几乎等价 |
基本一样 |
COUNT(列) |
统计该列非 NULL 的行数 | 可能慢,且语义不同 |
COUNT(主键) |
统计主键非 NULL 行数 | 主键非空,等于总行数 |
结论 :直接用 COUNT(*),别再纠结 COUNT(1) 的都市传说了。
追问:InnoDB 为什么不把总行数存起来?------ 因为 MVCC,不同事务看到的行数不一样,没法存一个全局值。
Q8:为什么推荐用自增 ID 做主键,UUID 有什么问题?
| 维度 | 自增 ID | UUID |
|---|---|---|
| 插入性能 | 顺序写,页分裂少 | 随机写,频繁页分裂,产生碎片 |
| 索引体积 | 8 字节(BIGINT) | 36 字节(字符串)或 16 字节(BINARY(16)) |
| 聚簇索引 | 紧凑 | 二级索引都要存主键,主键大则所有索引都变大 |
| 分布式 | 需要额外方案(雪花、号段) | 天然全局唯一 |
| 安全性 | 可推测,有信息泄露风险 | 安全 |
关键原因是"页分裂" :InnoDB 数据按主键顺序存,随机主键会让新行插到已有页中间,
页满了就分裂、移动数据,产生碎片并降低页填充率。
追问:8.0 能用
UUID_TO_BIN(uuid(), 1)存成 BINARY(16) 并且把时间部分提前 ,从而让 UUID 变得相对有序,缓解这个问题。
二、事务与锁篇(Q9 - Q14)
Q9:ACID 分别靠什么实现?
| 特性 | 实现机制 |
|---|---|
| 原子性 Atomicity | undo log(回滚日志) |
| 一致性 Consistency | 前三者共同保证 + 应用逻辑 |
| 隔离性 Isolation | MVCC + 锁 |
| 持久性 Durability | redo log |
Q10:四种隔离级别,MySQL 默认哪个?
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ❌可能 | ❌可能 | ❌可能 |
| READ COMMITTED | ✅ | ❌可能 | ❌可能 |
| REPEATABLE READ(MySQL 默认) | ✅ | ✅ | ✅ InnoDB 下靠间隙锁解决 |
| SERIALIZABLE | ✅ | ✅ | ✅ |
MySQL 默认 RR,Oracle / PostgreSQL 默认 RC,这是常被对比的点。
Q11:MVCC 是怎么实现的?RC 和 RR 的差别在哪?
MVCC = undo log 版本链 + ReadView。
每行有三个隐藏字段:DB_TRX_ID(最后修改的事务 ID)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID。
通过 DB_ROLL_PTR 把历史版本串成一条链,读的时候按 ReadView 规则挑一个可见版本。
RC 和 RR 唯一的实现差异是 ReadView 生成时机:
| 级别 | ReadView 生成时机 | 效果 |
|---|---|---|
| RC | 每条 SELECT 都重新生成 | 每次看到最新已提交数据 → 不可重复读 |
| RR | 事务内第一次 SELECT 时生成,之后复用 | 整个事务看同一个快照 → 可重复读 |
Q12:快照读和当前读有什么区别?
sql
SELECT * FROM t WHERE id = 1; -- 快照读,走 MVCC,不加锁
SELECT * FROM t WHERE id = 1 FOR UPDATE; -- 当前读,加排他锁,读最新版本
UPDATE t SET a = 1 WHERE id = 1; -- UPDATE 本质是当前读
⚠️ 经典坑 :在 RR 下 SELECT 看到 1000,别的事务改成 900 并提交,
你再 UPDATE ... SET balance = balance - 100,UPDATE 是当前读,会基于 900 算 。
所以"先读后写"必须用 FOR UPDATE 加锁,否则丢失更新。
Q13:InnoDB 有哪几种锁?什么是 Next-Key Lock?
| 锁类型 | 说明 |
|---|---|
| 行锁 Record Lock | 锁单条索引记录 |
| 间隙锁 Gap Lock | 锁两条记录之间的间隙,防止插入 |
| Next-Key Lock | 行锁 + 间隙锁,左开右闭区间,RR 下的默认加锁单位 |
| 意向锁 IS / IX | 表级锁,用于快速判断表里有没有行锁 |
| 自增锁 AUTO-INC | 插入自增列时的特殊表级锁 |
间隙锁是 RR 下解决幻读的关键 。RR 下 SELECT ... FOR UPDATE 会加 Next-Key Lock,
阻止别的事务在区间内插入,所以"当前读"也不会出现幻读。
⚠️ RC 下基本没有间隙锁 (只剩外键和唯一性检查场景),
这是很多团队把 MySQL 改成 RC 的原因:锁范围小、死锁少、并发高。
Q14:死锁怎么排查和避免?
sql
-- 查看最近一次死锁(只保留最后一条)
SHOW ENGINE INNODB STATUS\G
-- 建议打开配置,把所有死锁写进 error log
SET GLOBAL innodb_print_all_deadlocks = ON;
死锁产生的四个必要条件 :互斥、占有且等待、不可抢占、循环等待。
破坏任意一个即可,工程上最有效的是破坏循环等待:
- 所有业务按固定顺序访问资源(比如先扣 A 账户再扣 B 账户,永远按 id 排序)
- 缩小事务范围,把查询逻辑移到事务外
- 避免长事务,避免事务里有 RPC / 慢查询
- 合理设置
innodb_lock_wait_timeout(默认 50s,可降到 10~30s 快速失败)
三、日志与存储篇(Q15 - Q19)
Q15:redo log、undo log、binlog 有什么区别?
| redo log | undo log | binlog | |
|---|---|---|---|
| 层级 | InnoDB 引擎层 | InnoDB 引擎层 | MySQL Server 层 |
| 作用 | 崩溃恢复,保证持久性 | 回滚 + MVCC 版本链 | 主从复制、数据恢复 |
| 内容 | 物理日志(页的修改) | 逻辑日志(反向操作) | 逻辑日志(SQL 或行变更) |
| 写入方式 | 循环写,固定大小 | 随事务生成 | 追加写,可归档 |
| 所有引擎都有 | ❌ | ❌ | ✅ |
Q16:为什么要有"两阶段提交"?
redo log 和 binlog 是两套独立的日志,必须保证它们状态一致,否则主从会不一致。
1. prepare 阶段:redo log 写入并标记 prepare
2. 写 binlog
3. commit 阶段:redo log 标记 commit
崩溃恢复时的判断逻辑:
- redo 里有 commit 标记 → 提交
- redo 里只有 prepare,但能找到对应 binlog → 提交
- redo 里只有 prepare,找不到 binlog → 回滚
Q17:一条 UPDATE 语句在 MySQL 里经历了什么?
1. 连接器:权限校验
2. 分析器:词法、语法分析
3. 优化器:选索引、定执行计划(基于 cost)
4. 执行器:调用 InnoDB 接口
├─ 数据页在 Buffer Pool?不在就从磁盘加载
├─ 写 undo log
├─ 更新 Buffer Pool 中的数据页(变脏页)
├─ 写 redo log(prepare)
├─ 写 binlog
└─ redo log 标记 commit
5. 后台线程异步刷脏页
⚠️ 注意顺序:先改内存,再写日志,这就是 WAL(Write-Ahead Logging)。
Q18:InnoDB 和 MyISAM 的区别?
| 维度 | InnoDB | MyISAM |
|---|---|---|
| 事务 | ✅ | ❌ |
| 锁粒度 | 行锁 | 表锁 |
| 外键 | ✅ | ❌ |
| 崩溃恢复 | ✅(redo log) | ❌ |
| MVCC | ✅ | ❌ |
| 索引结构 | 聚簇索引,数据即索引 | 非聚簇,索引存物理地址 |
| 行数统计 | 不保存,COUNT(*) 要扫 |
保存,COUNT(*) 极快 |
| 全文索引 | 5.6+ 支持 | 支持 |
| 8.0 分区 | ✅ 唯一支持分区的主流引擎 | ❌ 8.0 已移除分区支持 |
结论:除非只读且不需要事务(极少见),一律用 InnoDB。 5.5 之后它就是默认引擎。
Q19:为什么 8.0 移除了查询缓存(Query Cache)?
查询缓存在 5.7 默认关闭,8.0 直接移除。原因:
- 缓存失效太激进:表里任何一行改动,该表所有缓存全部失效
- 写多读少的场景下,维护缓存的开销大于收益
- 有全局锁竞争,是并发瓶颈
现在这个活儿交给应用层(Redis)或者用 Buffer Pool 缓存数据页(粒度更细、更有效)。
四、SQL 优化篇(Q20 - Q24)
Q20:深分页 LIMIT 1000000, 20 为什么慢?怎么优化?
MySQL 必须先扫描并丢弃前 1000000 行,只返回后 20 行。
sql
-- ❌ 慢
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- ✅ 方案一:延迟关联(用覆盖索引先拿到 id)
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id;
-- ✅ 方案二:游标分页(推荐,尤其无限下拉场景)
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
方案二最优,但要求排序键唯一且连续,且不支持"直接跳到第 N 页"。
Q21:JOIN 有哪些算法?
| 算法 | 版本 | 说明 |
|---|---|---|
| Simple Nested Loop Join | 老版本 | 双层循环,最差 |
| Index Nested Loop Join | 一直有 | 被驱动表走索引,最常见的最优路径 |
| Block Nested Loop Join | 5.x | 被驱动表无索引时,用 join buffer 批量比对 |
| Hash Join | 8.0.18+ | 无索引的等值 JOIN 场景下取代 BNL,性能大幅提升 |
优化要点:小表驱动大表 、被驱动表的 JOIN 字段必须有索引。
Q22:GROUP BY 和 ORDER BY 什么时候会用到临时表 / filesort?
ORDER BY的字段顺序、升降序与索引不一致 → Using filesortGROUP BY的字段没有合适索引 → Using temporary + Using filesort- 8.0 之前
GROUP BY会隐式排序,8.0 起不再默认排序(除非显式写ORDER BY)
sql
-- 建 (status, create_time) 联合索引,下面这条就能直接走索引有序性
SELECT status, COUNT(*) FROM orders GROUP BY status;
Q23:慢查询的排查流程是什么?
sql
-- 1. 开启慢日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
-- 2. 找出最慢的 SQL
SELECT DIGEST_TEXT, COUNT_STAR, AVG_TIMER_WAIT/1e12 AS avg_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC LIMIT 10;
-- 3. EXPLAIN 分析
EXPLAIN SELECT ...;
-- 4. 看优化器到底怎么选的
SET optimizer_trace = 'enabled=on';
SELECT ...;
SELECT * FROM information_schema.OPTIMIZER_TRACE\G
排查顺序一般是:有没有走索引 → 扫描行数是否过大 → 有没有 filesort/临时表 → 能不能改写 SQL。
Q24:CHAR 和 VARCHAR 怎么选?VARCHAR(10) 能存 10 个汉字吗?
| CHAR(n) | VARCHAR(n) | |
|---|---|---|
| 长度 | 固定,右边补空格 | 可变 |
| 检索 | 尾部空格被去掉 | 保留 |
| 额外开销 | 无 | 1~2 字节长度前缀 |
| 适用 | 长度几乎固定的短串(如状态码、MD5、手机号) | 长度变化大的字符串 |
VARCHAR(10) 能存 10 个"字符" ,utf8mb4 下一个汉字 4 字节,所以实际占 40 字节 + 前缀。
括号里的数字是字符数不是字节数,这是 MySQL 5.0 之后的约定。
追问:
INT(11)的 11 是什么?------显示宽度 ,只影响ZEROFILL时的补零,不改变存储范围(INT 永远是 4 字节)。8.0.17 起该显示宽度属性已废弃。
五、架构与运维篇(Q25 - Q28)
Q25:主从复制的原理是什么?延迟怎么解决?
主库:事务提交时写 binlog(binlog dump 线程负责推送)
从库:IO 线程拉取 binlog 写入 relay log
从库:SQL 线程回放 relay log
延迟的常见原因:
| 原因 | 解法 |
|---|---|
| 从库单线程回放跟不上 | 开并行复制:replica_parallel_workers=8 + LOGICAL_CLOCK |
| 主库有大事务 | 拆分大事务,一次删 100 万行改成每次 1 万行 |
| 从库硬件比主库差 | 从库配置不低于主库 |
| 从库承担大量读查询 | 分流,降低从库负载 |
| 无主键表,ROW 模式下回放慢 | 所有表必须有主键 |
Q26:读写分离会遇到什么问题?
最典型的是主从延迟导致"刚写完读不到":
| 方案 | 说明 |
|---|---|
| 写后读主库 | 关键业务(下单后查订单)强制走主库 |
| 二次读取 | 从库读不到再读一次主库 |
| 缓存标记 | 写操作后打标记,短时间内读主库 |
| 等 GTID | 8.0 可用 WAIT_FOR_EXECUTED_GTID_SET() 等从库追上(牺牲延迟换一致性) |
Q27:大表加一个字段,怎么做到不影响业务?
优先级从高到低:
- 8.0.12+ 的 INSTANT ADD COLUMN:只改数据字典,秒级完成,首选
- gh-ost / pt-osc:5.7 或需要改类型时的标准方案
- 原生 Online DDL(INPLACE):小表可以,大表风险高(row log 溢出、MDL 排队)
无论哪种,执行前必查长事务:
sql
SELECT * FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60;
因为 DDL 要拿排他 MDL 锁,前面有个长事务就会连锁阻塞所有业务 SQL。
Q28:分库分表怎么选分片键?全局 ID 怎么生成?
分片键选择原则:
- 选查询频率最高的那个维度(订单一般按 user_id,让单用户查询落在一片)
- 避免热点(按时间分片会导致新片成为热点)
- 尽量避免跨片 JOIN 和跨片事务
全局 ID 方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 数据库号段(segment) | 简单、单调递增、可控 | 有单点,需做好高可用 |
| 雪花算法 Snowflake | 本地生成、高性能、趋势递增 | 依赖机器时钟,时钟回拨会出问题 |
| UUID | 最简单 | 无序、占空间、索引性能差 |
| Redis INCR | 简单 | 依赖 Redis 持久化 |
六、场景题(Q29 - Q30)
Q29:一个表有 (a, b, c) 联合索引,下面几条 SQL 谁走索引?
sql
-- 表 t,索引 idx_abc(a,b,c)
SELECT * FROM t WHERE a=1 AND b>2 AND c=3; -- 用到 a、b;c 用不上(范围后断)
SELECT * FROM t WHERE c=3 AND b=2 AND a=1; -- ✅ 三个都用上(优化器会重排条件顺序)
SELECT * FROM t WHERE a=1 ORDER BY b, c; -- ✅ 走索引,且排序免 filesort
SELECT * FROM t WHERE a=1 ORDER BY c; -- ❌ 用 a,但排序要 filesort(b 缺失)
SELECT * FROM t WHERE b=2 ORDER BY a; -- ❌ 无 a,走不了索引
关键点 :WHERE 里的条件顺序不影响索引使用(优化器会重排),
但列的"连续性"会影响------断在哪一列,后面的列就用不上了。
Q30:线上发现 CPU 100%,怎么定位?
sql
-- 1. 看当前在跑什么
SELECT id, user, host, db, command, time, state, LEFT(info, 120)
FROM information_schema.PROCESSLIST
WHERE command <> 'Sleep' AND time > 5
ORDER BY time DESC;
-- 2. 看哪类 SQL 消耗最多
SELECT DIGEST_TEXT, COUNT_STAR,
SUM_TIMER_WAIT/1e12 AS total_sec,
SUM_ROWS_EXAMINED AS rows_examined
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 5;
-- 3. 在 OS 层确认是哪个线程
top -H -p $(pidof mysqld)
-- 把 OS 线程号对应到 MySQL 内部
SELECT * FROM performance_schema.threads WHERE THREAD_OS_ID = <pid>;
常见原因排序:慢 SQL(全表扫描 / 排序)> 高并发 > 锁等待 > 后台刷脏 。
CPU 100% 第一反应永远是"找那条慢 SQL",而不是加机器。
小结:面试回答的三个层次
| 层次 | 表现 | 例子 |
|---|---|---|
| 第一层 | 背定义 | "索引是 B+ 树" |
| 第二层 | 讲清为什么 | "因为 B+ 树非叶子只存 key,16KB 页能塞上千 key,树高压到 3~4 层" |
| 第三层 | 讲清取舍和场景 | "所以它适合范围和点查;但写多、基数低的列建索引收益不大,反而拖慢写入" |
面试官真正在意的不是你记住了多少,而是:
- 能不能讲清楚"为什么"------每个设计背后都有取舍
- 有没有踩过坑------说出一个真实事故比背十个定义有用
- 知不知道边界------"这个方案在XX场景下不适用",这句话非常加分
把这一系列(索引、SQL 优化、事务、锁、日志、备份、参数调优)串起来看,
你会发现 MySQL 的所有设计都围绕一条主线:
用内存换磁盘 IO、用空间换时间、用一致性换可用性 ------而调优的本质,
就是根据你的业务在这三者之间找到那个平衡点。