Skip to content

M18. InnoDB 中意向锁(IS / IX)的作用是? ​

考点:意向锁

A. 用于行锁之间的互斥 B. 是一种表级锁,用于快速判断“表中是否存在行锁”,避免加表锁时逐行检查 C. 用于实现 MVCC 的可见性判断 D. 用于主从复制的一致性校验

答案:B

【考点】意向锁的定位:表级锁 + 兼容性矩阵 + 与行锁的关系。

【结论】 选 B —— 意向锁是 InnoDB 自动添加的表级锁,唯一作用是快速判断“表中是否已有行锁”,让加表锁时不必逐行检查。

【逐项辨析】

  • A 用于行锁之间的互斥:错。行锁之间的互斥靠索引记录上的锁本身判断,与意向锁无关;意向锁是表级的。
  • B 是一种表级锁,用于快速判断表中是否存在行锁,避免加表锁时逐行检查:正确。 定位(表级)、作用(快速判断)、价值(避免逐行扫描)三项全对。
  • C 用于实现 MVCC 的可见性判断:错。可见性判断靠 ReadView + 隐藏列 + undo log(见 M12),意向锁不参与。
  • D 用于主从复制的一致性校验:错。主从一致性靠 binlog 与两阶段提交(见 M16),与锁无关。

【知识点】 先明确意向锁是什么:意向锁(IS / IX)是表级锁,由 InnoDB 在给行加锁时自动添加,用户无法手动干预。

意向锁全称何时自动添加
IS意向共享锁(Intention Shared)事务准备给某行加 S 锁(共享锁)之前
IX意向排他锁(Intention Exclusive)事务准备给某行加 X 锁(排他锁)之前

它要解决的问题:假设事务 A 已给表中 3 行加了行锁,事务 B 想给整张表加表级 X 锁。若没有意向锁,B 必须逐行扫描全表才能确认“有没有行锁”,表越大越慢;有了意向锁,B 只要看一眼“这张表上有没有 IX / IS”就立即得到答案。意向锁就是行锁的“表级代言人”。

兼容性矩阵(✅ 兼容,❌ 冲突):

已持有 ↓ / 请求 →ISIX表级 S表级 X
IS✅✅✅❌
IX✅✅❌❌
表级 S✅❌✅❌
表级 X❌❌❌❌

矩阵读法(三条结论):

  1. 意向锁之间全部互相兼容 —— IS / IX 只表示“我打算在某行上加锁”,多个事务可以同时在不同行上加锁。
  2. IS 与表级 S 兼容 —— 因为表级 S 与行级 S 方向一致,都是读。
  3. IX 与表级 S / X 都冲突 —— IX 意味着“表中有行要写”,与整表加读锁或写锁都无法共存。

推导:矩阵的实质是“表级锁必须与表内所有行级锁兼容才不冲突”。因为意向锁忠实地代表了“表中行锁的存在与类型”,所以判断“表锁能否加”只需检查意向锁,逻辑上等价于检查全表行锁。

【记忆锚点】 “意向锁是行锁的代言人,表锁问它就行” —— 意向锁之间不打架,只跟表锁打架。

【易混对比】

  • 意向锁(IS / IX)vs 表级锁(S / X):都是表级,但意向锁是自动加的、仅作标记,不真正阻塞别人读写行;表级 S / X 才真正锁住整表。
  • 意向锁 vs 行锁:行锁加在索引记录上(见 M17),意向锁加在表上;一次加行锁会连带加意向锁。
  • IS vs IX:IS 对应“要加行 S 锁”,IX 对应“要加行 X 锁”;IX 更“霸道”,与表级 S 也冲突。
  • 换问法:若题目问“事务要给某行加 X 锁,会先在表上加什么锁”,答案是 IX 锁。

【自测】 事务 A 执行 SELECT * FROM t WHERE id = 1 FOR UPDATE(id 是主键)。此时表 t 上会存在哪两种锁?

答:表级 IX 锁 + 主键索引记录上的 X 锁。先自动加 IX 表示“表内有行要写”,再在 id = 1 的索引记录上加 X 锁。与 M11、M17 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-19(表锁与行锁的兼容检查)。
  • 面试/工程:意向锁让 LOCK TABLES 之类表级请求不必逐行检查;理解它能解释为什么“有行锁时整表读仍可能被挡住”。
  • 面试追问:① IS 与 IX 互相兼容吗? ② 意向锁会阻塞普通 SELECT 吗?

【拓展延伸】

  • 变式问法:问“意向锁存在的唯一目的是什么”(快速判断表级与行级冲突)。
  • 参数/命令:SELECT * FROM performance_schema.data_locks WHERE lock_type='TABLE' 可看到 IS/IX;LOCK TABLE t READ/WRITE 会触发表级与意向锁交互。

M19. EXPLAIN 结果的 type 字段中,访问效率最好的是? ​

考点:EXPLAIN 的 type 字段

A. ALL(全表扫描) B. index(全索引扫描) C. range(范围扫描) D. const(通过主键或唯一索引等值查询,最多一行)

答案:D

【考点】EXPLAIN 中 type 字段的访问类型与优劣顺序。

【结论】 选 D —— type 效率从高到低大致为 system > const > eq_ref > ref > range > index > ALL,其中 const 通过主键或唯一索引等值查询最多命中一行,效率最高。

【逐项辨析】

  • A ALL 全表扫描:错。这是最差的访问类型,是首要优化信号,不是最好。
  • B index 全索引扫描:错。它比 ALL 略好(扫的是更小的索引文件,且通常有序),但仍需扫完整棵索引树,属需要优化的类型。
  • C range 范围扫描:错。range 已能用索引定位区间,是可接受的类型,但仍不如 const 精准。
  • D const 通过主键或唯一索引等值查询,最多一行:正确。 优化阶段就能确定结果集最多一行,访问代价最低。

【知识点】 type 表示“MySQL 决定用什么方式访问这张表”,是 EXPLAIN 中最该先看的一列。

type含义典型场景评价
system表只有一行(系统表)极少见最好
const通过主键 / 唯一索引等值查询,最多一行WHERE id = 5最好(实用场景)
eq_ref被驱动表通过唯一索引等值匹配,一行多表 JOIN 中被驱动表走主键很好
ref通过普通索引等值匹配,可能多行WHERE name = 'a'(name 为普通索引)好
range范围扫描索引BETWEEN、>、<、IN可接受
index全索引扫描扫完整个索引树需优化
ALL全表扫描无可用索引最需优化

推导:效率的本质是“要读多少数据、能不能精确定位”。const 在优化阶段就能借助唯一性约束推断出“最多一行”,连多读一次都不必;ref 只能确定“一批行”,要到执行阶段才知道有多少;range 要遍历区间内的所有索引项;index 要遍历整棵索引树;ALL 要遍历整张表的所有数据页 —— 从“精确一行”到“全表所有行”,扫描量单调递增,效率单调递减。

其他常见取值:ref_or_null(ref + 额外查 NULL)、index_merge(索引合并)、unique_subquery / index_subquery(子查询优化)、fulltext(全文索引)。

【记忆锚点】 "system > const > eq_ref > ref > range > index > ALL" —— 背下这条链,看 EXPLAIN 先对号入座。

【易混对比】

  • const vs eq_ref:const 是单表查询最多一行;eq_ref 是多表 JOIN 中被驱动表每匹配一行。二者都基于唯一索引等值,区别在单表 / 关联。
  • ref vs range:ref 是等值匹配(可能多行),range 是范围匹配(区间)。
  • index vs ALL:都“扫全”,区别是 index 只扫索引树(更小、通常有序),ALL 扫数据页。
  • 换问法:若题目问“哪个 type 说明需要重点优化”,答案是 ALL(其次 index)。

【自测】 表 t 有主键 id 与普通索引 name。EXPLAIN SELECT * FROM t WHERE name = 'a' 的 type 是什么?改成 WHERE id = 5 呢?

答:前者是 ref(普通索引等值,可能多行);后者是 const(主键等值,最多一行)。与 M20 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-25(ICP 时 type 可能是 range/ref,Extra 才是关键)。
  • 面试/工程:SQL 优化第一眼就是 type 与 key;ALL/index 大范围出现意味着索引设计或写法失败。
  • 面试追问:① type=range 一定比 ref 慢吗? ② 为什么 index(全索引扫描)有时仍可接受?

【拓展延伸】

  • 变式问法:给一串 type 要求排序:system > const > eq_ref > ref > range > index > ALL。
  • 参数/命令:EXPLAIN FORMAT=TREE(8.0.16+)/EXPLAIN ANALYZE 更直观;optimizer_trace 可看为何没选更好 type。

M20. EXPLAIN 的 Extra 中出现 Using filesort 说明? ​

考点:EXPLAIN 的 Extra 字段

A. 使用了磁盘临时文件排序,是需要优化的信号 B. 属于正常现象,无需关注 C. 说明已经利用了索引的有序性完成排序 D. 说明使用了覆盖索引

答案:A

【考点】Extra 中常见提示(Using filesort / Using temporary / Using index / Using where)的含义与优化方向。

【结论】 选 A —— Using filesort 说明 MySQL 无法利用索引的有序性、需要额外做一次排序,是需要优化的信号。

【逐项辨析】

  • A 使用了磁盘临时文件排序,是需要优化的信号:正确。 严格口径是:Extra 里出现 Using filesort 表示本次查询发生了 filesort —— 索引的有序性用不上,MySQL 必须额外做一次排序,因此是需要优化的信号。至于“磁盘临时文件”:排序先在 sort_buffer_size 的内存缓冲区里进行,只有数据量放不下时才落盘做归并排序,所以落盘是最坏情况而非必然动作(详见下文澄清)。本题仍选 A 的理由:四个选项里只有 A 命中“额外排序 + 需要优化”这一核心语义,其余三项都与 Using filesort 直接矛盾(B 说无需关注、C 说已用上索引有序性、D 说覆盖索引),按“最优选项”原则取 A。
  • B 属于正常现象、无需关注:错。它意味着多了一次排序开销,是明确的优化信号,不能说“无需关注”。
  • C 说明已经利用了索引的有序性完成排序:错。恰恰相反 —— 如果索引有序性够用,Extra 里根本不会出现 Using filesort。
  • D 说明使用了覆盖索引:错。覆盖索引的标志是 Using index,与排序无关。

【知识点】 Extra 列是 EXPLAIN 的“诊断意见”,比 type 更能指出具体的性能病灶。四个高频值对照:

Extra含义好坏优化方向
Using filesort无法用索引有序性,额外做一次排序❌ 需优化为 WHERE + ORDER BY 建联合索引,让索引天然有序
Using temporary使用了临时表❌ 需优化常见于 GROUP BY / DISTINCT 未走索引,补索引
Using index用到了覆盖索引,无需回表✅ 好事保持,这是理想状态
Using where需要在取到行之后再过滤⚠️ 中性视情况把过滤条件下推到索引

关于 Using filesort 名称的关键澄清:它不一定真的用磁盘文件。MySQL 会先在内存中排序(缓冲区大小由 sort_buffer_size 决定),只有数据量超出排序缓冲区时才退化为磁盘临时文件归并排序。所以 filesort 应当理解为“额外的一次排序操作”,而非“一定落磁盘”。

Using filesort 的三条常见成因:

  1. ORDER BY 的列与索引的顺序不匹配(例如索引是 (a, b),却按 b 排序);
  2. 排序方向不一致(例如索引是 (a ASC, b ASC),却 ORDER BY a ASC, b DESC,MySQL 8.0 之前无法直接利用);
  3. 排序列未包含在本次使用的索引中。

推导:B+ 树索引本身天然有序(叶子节点按键值顺序用双向链表相连),所以只要 ORDER BY 的列顺序与索引顺序完全对齐,MySQL 只需顺着索引叶子链表读就能拿到有序结果,无需再排。一旦对不齐,索引的有序性就“用不上”,只能把结果集取出来重新排一遍 —— 这就是 Using filesort 的由来。

【记忆锚点】 “Using index 是奖状,Using filesort / Using temporary 是病危通知” —— 一个说“省了回表”,两个说“多了一道开销”。

【易混对比】

  • Using filesort vs Using temporary:前者多一次排序,后者多建一张临时表;GROUP BY 走不了索引时两者常同时出现。
  • Using index vs Using where:Using index 表示“索引里就有全部所需列”(覆盖索引,好事);Using where 表示“存储引擎返回后还要再过滤”(中性)。
  • Using index vs Using index condition:前者是覆盖索引(不回表),后者是索引条件下推(ICP),仍可能回表,切勿混为一谈。
  • 换问法:若题目问“Extra 中出现哪个值说明用到了覆盖索引”,答案是 Using index。

【自测】 表 t 有联合索引 idx(a, b)。SELECT a, b FROM t ORDER BY a, b 与 SELECT a, b FROM t ORDER BY b 的 Extra 分别是什么?

答:前者不出现 Using filesort(排序键与索引顺序 (a, b) 一致,且两列都在索引中,会出现 Using index);后者出现 Using filesort(排序键只有 b,与索引顺序不对齐,用不上有序性)。与 M19 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-25(Extra 字段家族辨析)。
  • 面试/工程:Using filesort 不一定是“磁盘排序”,内存排序也会这么显示;优先消除的是“无法用索引有序”的根因。
  • 面试追问:① Using temporary 与 Using filesort 常一起出现吗? ② 如何消掉 filesort?

【拓展延伸】

  • 变式问法:把 Extra 选项改成 Using index / Using where / Using index condition 互考。
  • 参数/命令:sort_buffer_size、max_length_for_sort_data;ORDER BY 借索引时 Extra 无 filesort;EXPLAIN ANALYZE 可见 actual sort time。

M21. 开启并配置 MySQL 慢查询日志,涉及的参数是? ​

考点:慢查询日志

A. slow_query_log 与 long_query_time B. binlog_format C. innodb_buffer_pool_size D. max_connections

答案:A

【考点】慢查询日志的开启方式与配套参数。

【结论】 选 A:slow_query_log 管开关、long_query_time 管阈值,一开一卡,超过阈值的语句才会被记进慢查询日志。

【逐项辨析】

  • A slow_query_log 与 long_query_time:正确。 前者是慢查询日志的总开关(ON / OFF),后者是“超过多少秒算慢”的阈值(单位秒,支持小数如 0.2),两者配套使用才生效。
  • B binlog_format:错。它管的是 binlog 的记录格式(STATEMENT / ROW / MIXED),决定的是“变更怎么记”,与“慢语句要不要记”无关。
  • C innodb_buffer_pool_size:错。这是 InnoDB 缓冲池大小,属于性能调优参数,改它不影响慢查询日志是否开启。
  • D max_connections:错。这是允许的最大并发连接数,属连接层参数,与慢查询日志没有任何关系。

【知识点】 慢查询日志是 MySQL 的“慢 SQL 侦查器”:一条语句的实际执行时间超过 long_query_time,MySQL 就把它的原文、耗时、扫描行数、返回行数等写进日志文件,供事后分析。

参数作用典型取值
slow_query_log慢查询日志总开关ON / OFF
slow_query_log_file日志文件路径/var/log/mysql/slow.log
long_query_time阈值(秒,支持小数)0.2 / 0.5 / 1
log_queries_not_using_indexes额外记录未走索引的语句线上慎开(量极大)
log_output输出到文件还是表FILE / TABLE

生产排查是一条固定流水线:慢查询日志 → mysqldumpslow / pt-query-digest 汇总排序 → 对 TOP 慢 SQL 执行 EXPLAIN 看执行计划 → 加索引或改写 SQL。两个容易踩的点:一是 long_query_time 默认值通常为 10 秒,默认阈值偏高,线上一般调到 0.2~1 秒才抓得住问题;二是 log_queries_not_using_indexes 虽然能捞到未走索引的语句,但在大表上会瞬间刷爆日志文件,只建议短时排查时临时打开。

【记忆锚点】 “开日志看 slow_query_log,定快慢看 long_query_time” —— 一个管开关,一个管标尺。

【易混对比】

  • 慢查询日志 vs binlog:慢查询日志面向“性能排查”(记执行慢的语句);binlog 面向“数据复制与恢复”(记所有变更)。两者目的完全不同,binlog_format 管不了慢查询。
  • long_query_time vs log_queries_not_using_indexes:前者按时间筛,后者按是否走索引筛,排查时通常两者一起看。
  • 换问法:若题目问“记录未使用索引的语句用哪个参数”,答案就变成 log_queries_not_using_indexes;若问“慢查询日志写到哪个文件”,答案是 slow_query_log_file。

【自测】 某线上库想抓出所有执行时间超过 200 毫秒的 SQL,除 slow_query_log = ON 外,还要把哪个参数设成什么值?

答:long_query_time = 0.2。该参数单位是秒且支持小数,200 毫秒即 0.2 秒。与 M19、M20 同属日志与排查链路;DBA 岗与运维面试高频。

【知识关联】

  • 补题关联:补-25(慢 SQL 分析常从 type/Extra/ICP 入手)。
  • 面试/工程:慢查询日志 + mysqldumpslow/pt-query-digest 是 DBA 日常;应用侧还应配合 APM 采样,避免只看库不看调用方。
  • 面试追问:① long_query_time=0 有什么风险? ② 慢日志里的 Rows_examined 远大于 Rows_sent 说明什么?

【拓展延伸】

  • 变式问法:问“只记录未走索引的 SQL”用什么参数;问“如何动态开启不用重启”。
  • 参数/命令:slow_query_log、slow_query_log_file、long_query_time、log_queries_not_using_indexes、min_examined_row_limit;注意开关参数没有 long_query_log 这个名字 —— 5.6 之前叫 log_slow_queries,5.6 起统一为 slow_query_log;8.0 支持 SET GLOBAL slow_query_log=ON 热更新。

M22. InnoDB 建议使用自增主键的原因,不包括? ​

考点:主键选型

A. 自增主键顺序写入,减少页分裂与数据移动,插入性能更好 B. 自增主键占用空间小,而二级索引叶子要存主键值,因此索引更省空间 C. 自增主键天然唯一,避免业务键冲突 D. 自增主键可以避免回表

答案:D

【考点】主键选型对插入性能与索引体积的影响。

【结论】 选 D。回表与否只取决于“查询需要的列是否都在所用索引里”,与主键是否自增毫无关系;A、B、C 都是自增主键的真实优势,因此“不包括”的正是 D。

【逐项辨析】

  • A 自增主键顺序写入、减少页分裂与数据移动:是优势,不选。新记录总落在 B+ 树最右侧页,几乎不触发结构调整,插入是顺序 IO。
  • B 自增主键占用空间小、二级索引更省空间:是优势,不选。二级索引叶子要存主键值,主键越短,每个索引页能装的条目越多,树越矮。
  • C 自增主键天然唯一、避免业务键冲突:是优势,不选。自增值由数据库生成、不含业务语义,业务规则变更也不会牵动主键。
  • D 自增主键可以避免回表:错误命题。 回表的成因是“二级索引叶子只有索引列 + 主键,缺其余列”,与主键是否自增无关 —— 用自增主键去查非索引列,一样要回表。

【知识点】 主键选型的三条判据:有序性、长度、业务无关性。

主键类型写入有序性长度二级索引体积典型问题
自增 BIGINT顺序追加,几乎不页分裂8 字节最小主键值可被外部枚举
无序 UUID(v4)随机插入,频繁页分裂36 字节字符串膨胀数倍写放大、碎片多、命中率低
有序 UUID(v7 / 雪花 ID)近似顺序16 字节或更长较大实现复杂、依赖时钟
业务键(手机号 / 身份证)随机较长较大业务一变就要改主键

关键推导有两条。其一(写性能):InnoDB 的聚簇索引按主键有序组织,新主键若总是当前最大值,新行就追加到最右页,写满直接申请新页;若主键随机,新行要插进已有页的中间,页满时触发页分裂(一页拆两页、搬移约一半数据),同时产生空间碎片与随机 IO。其二(读性能):二级索引叶子存的是“索引列 + 主键值”,主键每长 1 字节,二级索引的每个条目就长 1 字节;主键从 8 字节 BIGINT 换成 36 字节 UUID,二级索引体积膨胀约 4 倍以上(口径:按窄索引列估算 —— 实算主键 1 字节时 4.11 倍、10 字节 2.56 倍、20 字节 2.00 倍,列越宽倍数越小),同样的缓冲池只能缓存更少的索引页,命中率随之下降。结论:自增 BIGINT 是 InnoDB 主键的默认最优解;必须用 UUID 时优先选有序 UUID。

【记忆锚点】 “自增主键三好处:写得顺(有序)、占得少(短)、管得省(无业务含义)” —— 回表不在这三条里。

【易混对比】

  • 自增主键的优势 vs 回表:前者讲“写入与索引体积”,后者讲“查询能否只查一棵树”,两者维度不同。本题的陷阱正是把这两件事硬扯上因果关系。
  • 自增主键 vs 业务主键:自增主键的缺点是可被外部枚举、分库分表后需改造(改用雪花 ID);业务主键的缺点是业务一变更就要改主键,代价极大。
  • 换问法:若题干改成“下列哪项是自增主键的优势”,答案就是 A / B / C 中任意一项;若问“二级索引叶子存什么”,答案仍是“索引列 + 主键值”(与 M01 互为镜像)。

【自测】 某表原用自增 BIGINT 主键,后改用 UUIDv4 字符串做主键,结果二级索引文件明显变大、插入 TPS 下降。这两个现象各自的直接原因是什么?

答:索引变大是因为二级索引叶子要存主键值,主键由 8 字节变 36 字节;插入变慢是因为 UUIDv4 随机无序,新行插在已有页中间,触发频繁页分裂与随机 IO。与 M01、M23 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-23(死锁检测与主键冲突场景)、补-24(金额用 DECIMAL,类型选型与主键长度同属“类型代价”)。
  • 面试/工程:分库分表后自增失效,常见雪花/号段发号器;对外暴露自增 ID 可被竞品枚举订单量,需混淆(雪花、加密 ID)。主库高并发写入时,自增近似顺序追加,UUID 则把写放大打满磁盘 IOPS——压测里同一业务换主键 TPS 可差数倍。
  • 面试追问:① 雪花 ID 会不会页分裂?(会,但远少于 UUIDv4:时间戳高位大致递增,只有同毫秒内或时钟回拨才可能乱序) ② 为什么二级索引更怕长主键?(每个二级索引叶子都要冗余一份主键,主键从 8B→16B,所有二级索引体积与 Buffer Pool 占用同步放大)

【拓展延伸】

  • 变式问法:正向问“InnoDB 主键推荐”→ 自增 BIGINT;反向问“自增主键缺点”→ 可枚举、分库分表难扩展、无法做业务语义。把“手机号当主键”设为错误项(业务变更+长度+随机)。
  • 参数/命令:innodb_autoinc_lock_mode(0 传统 / 1 连续 / 2 交错,影响批量插入与 statement 复制安全);SHOW CREATE TABLE 看主键定义;UUIDv7/雪花可存 BINARY(16) 而非 CHAR(36) 省一半空间;压测对比时用 performance_schema 看 page splits。

M23. 使用无序的 UUID 作为 InnoDB 主键,最主要的缺点是? ​

考点:页分裂

A. 无序插入导致频繁页分裂与数据移动,插入性能差;且长度长,二级索引占用空间更大 B. UUID 不保证唯一性 C. UUID 列不能建索引 D. UUID 无法参与排序

答案:A

【考点】页分裂的产生原因及其对写性能的影响。

【结论】 选 A:无序 UUID 会让新记录插到任意页中间,页满即页分裂并搬移约一半数据;叠加 36 字节的长度,二级索引也被撑大。

【逐项辨析】

  • A 无序插入导致频繁页分裂与数据移动、插入性能差;且长度长、二级索引占用空间更大:正确。 一句话把“写放大”与“空间膨胀”两个真实代价都点到了。
  • B UUID 不保证唯一性:错。唯一性恰恰是 UUID 的强项,它的短板是随机无序与长度长,与唯一性无关。
  • C UUID 列不能建索引:错。UUID 列完全可以建索引,只是索引体积更大、写入更慢 —— 把“代价高”说成了“做不到”。
  • D UUID 无法参与排序:错。UUID 是字符串,可比较、可排序,ORDER BY 照常工作,只是排序结果按字典序而非时间序。

【知识点】 页分裂的根源是“B+ 树要求页内有序”,是否分裂取决于插入位置是否连续。

主键形态新记录落点是否触发页分裂IO 特征
自增 BIGINT最右页尾部追加仅在页满时申请新页顺序 IO
无序 UUID任意页中间随机插入频繁分裂(拆页 + 搬移约一半数据)随机 IO

推导链条:聚簇索引的叶子页内部按键值升序排列 → 插入一条“键值落在中间”的记录,必须塞进对应位置 → 该页写满时 InnoDB 只能把一页拆成两页并重新分配记录(通常各占约一半)→ 拆分后两页都只填了一半,空间利用率下降、碎片增多,同时被拆页及其父节点都要落盘,产生额外的随机写。因此 UUID 表的典型症状是:写入 TPS 下降、表空间虚高、缓冲池命中率下降。补两个口径:一是有序 UUID(UUIDv7 / 雪花 ID)能大幅缓解但无法完全消除;二是自增主键并非绝对免疫,若删掉中间一段再随机回插,同样会触发页分裂。

【记忆锚点】 “自增往后加,UUID 到处插;插到页中间,一页变两页”。

【易混对比】

  • 页分裂 vs 页合并:分裂由“页写满 + 中间插入”触发;合并由“大量删除后页利用率过低”触发。二者是方向相反的动作。
  • UUIDv4 vs UUIDv7 / 雪花 ID:前者完全随机、必然页分裂;后者时间有序、接近顺序追加,是“用 UUID 又不吃页分裂亏”的主流方案。
  • 换问法:若题目问“为什么推荐 BIGINT 而不是 UUID 做主键”,答案仍是“顺序写入减少页分裂 + 长度短使二级索引更小”(与 M22 是同一知识点的正反两面)。

【自测】 一张日志表主键是自增 BIGINT,日常只追加写入、性能良好。某天业务改为“按时间回补历史数据”,随机插入大量旧时间戳记录,写性能明显恶化。原因是什么?

答:回补的旧记录键值落在已有页中间,破坏了“顺序追加”的前提,触发频繁页分裂与随机 IO。缓解方式是按时间分区或批量顺序回填。与 M22 连考;DBA 岗高频。

【知识关联】

  • 补题关联:补-22(页被载入 Buffer Pool,分裂增加 IO)。
  • 面试/工程:导入历史数据时先关索引再开,或按主键有序落地;监控 Innodb_row_inserts 与磁盘写放大可侧面看到分裂代价。
  • 面试追问:① 页分裂与页合并分别何时触发? ② 有序 UUID 能否完全避免分裂?

【拓展延伸】

  • 变式问法:问“为什么日志表突然变慢”——常是随机回补导致分裂。
  • 参数/命令:INFORMATION_SCHEMA.INNODB_METRICS 的 index_page_splits(本机 8.0.46 实测:NAME LIKE '%split%' 只有这一条;状态变量里没有 Innodb_buffer_pool_pages_split——Innodb_buffer_pool_pages_* 只有 data/dirty/flushed/free/misc/total 六项);页填充因子相关实践(预留空间减少分裂);innodb_page_size 影响一页能装多少行。

M24. SELECT * FROM t ORDER BY id LIMIT 1000000, 10 执行很慢,合理的优化思路是? ​

考点:深分页优化

A. 再多建几个索引就能解决 B. 采用延迟关联(先用覆盖索引取出主键,再 JOIN 回原表)或基于游标翻页(WHERE id > 上次最大 id LIMIT 10) C. 加上 ORDER BY 就能加速 D. 深分页无法优化

答案:B

【考点】深分页的性能瓶颈与两种标准解法。

【结论】 选 B。LIMIT 1000000, 10 的瓶颈是必须先扫过并丢弃前 1000000 行,解法就是“让这 100 万行不必回表”——延迟关联或游标翻页。

【逐项辨析】

  • A 再多建几个索引就能解决:错。多建索引改变不了“要跳过 N 行”这个动作本身,甚至因索引更多、优化器选择变差而更慢;问题出在偏移量,不在索引数量。
  • B 延迟关联或基于游标翻页:正确。 延迟关联把“排序 + 偏移”压进覆盖索引内完成,只对最终 10 行回表;游标翻页把偏移量换成范围条件,彻底绕开“扫描丢弃”。
  • C 加上 ORDER BY 就能加速:错。原语句本来就有 ORDER BY id,再加没有意义;ORDER BY 本身也不是加速手段,能否加速取决于排序能否用上索引的有序性。
  • D 深分页无法优化:错。属于“放弃治疗”式表述,B 的两种方案都是成熟工程实践。

【知识点】 深分页慢的本质是偏移量被当成了“要扫过的行数”。

LIMIT m, n 的执行语义不是“直接跳到第 m 行”,而是“先取前 m + n 行,再丢掉前 m 行,返回剩下 n 行”。若查询走的是二级索引,被丢掉的那 100 万行每一行都要回表取 * 所需的列,于是 100 万次随机 IO 白做。两种解法正是针对这两点分别下刀:

方案写法要点优化掉的是什么代价
延迟关联子查询只在覆盖索引里 ORDER BY ... LIMIT m, n 取主键,外层 JOIN 回表取整行把“100 万次回表”降为只有最终 n 行回表仍需扫描并丢弃前 m 行(但只扫索引,代价小得多)
游标 / 书签翻页记住上一页最大 id,改成 WHERE id > 上次最大 id ORDER BY id LIMIT n把“偏移量”换成范围查询,不再扫描丢弃不支持随机跳页,只能上一页 / 下一页

延迟关联的完整形态:

sql
SELECT t.* FROM t
JOIN (SELECT id FROM t ORDER BY id LIMIT 1000000, 10) AS x USING (id);

子查询只需要 id 一列,而 id 恰好在主键索引(聚簇索引)上,因此这一步是覆盖索引扫描、不回表;外层 JOIN 只对 10 个 id 回表。游标翻页则把复杂度从 O(m + n) 降到 O(log N + n)(借助 B+ 树定位起点)。选型建议:C 端列表页(只能上一页 / 下一页)用游标翻页;后台管理页(需要跳页)用延迟关联。

【记忆锚点】 “深分页慢在”白扫一大片“——延迟关联让扫描不回表,游标翻页让扫描不存在”。

【易混对比】

  • 延迟关联 vs 游标翻页:前者保留跳页能力、性能改善有限;后者性能最好但只能顺序翻页。二者是“能力与性能”的取舍。
  • 深分页 vs ORDER BY 无索引:两者都可能慢,但成因不同 —— 深分页慢在偏移量大(即使有索引也慢),无索引排序慢在排序本身(加合适索引即可解决)。本题 ORDER BY id 已走主键,属前者。
  • 换问法:若题目问“必须支持跳页时怎么优化”,答案就是延迟关联;若问“只支持下一页时最优解”,答案就是游标翻页。

【自测】 表 t 有 500 万行,业务必须支持跳页。SELECT * FROM t ORDER BY id LIMIT 4000000, 20 很慢,改写成延迟关联后明显变快。请说明“变快”的直接原因。

答:排序与偏移 400 万行的动作被压进了覆盖索引(只需 id 一列、不回表),只有最终 20 行回表取整行,省掉了约 400 万次随机回表。与 M19、M20 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-25(ICP 在索引层过滤,与延迟关联同属“少回表”)、补-22(Buffer Pool 命中率受索引/扫描方式影响)。
  • 面试/工程:后台导出、报表深分页是慢查询 TOP 来源;C 端“加载更多”几乎都应改成游标。电商订单列表若仍 LIMIT 1000000,10,DBA 会直接打回。
  • 面试追问:① 延迟关联为什么仍可能扫很多索引页?(前 m 行仍要在覆盖索引里顺序扫过并丢弃,只是不回表) ② 游标翻页如何保证不丢不重?(依赖稳定排序键,通常用主键或 (create_time,id) 复合游标,避免同时间戳抖动)

【拓展延伸】

  • 变式问法:题干给出 LIMIT 1000000,10 问优化方案;或问“为什么 WHERE id > 1000000 LIMIT 10 比 LIMIT 1000000,10 快”——前者走主键范围定位,复杂度 O(log N + n)。
  • 参数/命令:EXPLAIN 看 rows 与 Extra(Using index vs Using filesort);延迟关联模板见知识点;游标字段建议唯一且递增;若必须跳页,可粗分桶(按日期分区)再页内精定位。

M25. 关于 COUNT 函数,下列说法正确的是? ​

考点:MySQL 的 count

A. COUNT(*) 会跳过 NULL 行,COUNT(列) 统计所有行 B. COUNT(*)、COUNT(1)、COUNT(主键) 三者在语义上完全等价 C. COUNT(列) 会跳过该列为 NULL 的行,COUNT(*) 统计所有行 D. COUNT(1) 会跳过 NULL

答案:C

【考点】COUNT 的语义差异与 InnoDB 的计数优化。

【结论】 选 C。COUNT(列) 数的是“该列非 NULL 的行数”,所以跳过 NULL;COUNT(*) 数的是行数,与列值无关。

【逐项辨析】

  • A COUNT(*) 会跳过 NULL 行、COUNT(列) 统计所有行:错。方向恰好说反 —— 数所有行(因而不受 NULL 影响)的是 COUNT(*),跳过 NULL 的是 COUNT(列)。
  • B COUNT(*)、COUNT(1)、COUNT(主键) 三者语义完全等价:错。表述过于绝对 —— COUNT(*) 与 COUNT(1) 确实等价(都数行),而 COUNT(主键) 严格说是“数主键非 NULL 的行数”,只因主键不允许为 NULL,结果才恰好一致。
  • C COUNT(列) 跳过该列为 NULL 的行,COUNT(*) 统计所有行:正确。 这正是两者的分水岭。
  • D COUNT(1) 会跳过 NULL:错。COUNT(1) 里的 1 是常量,根本不涉及列值判断,不存在“跳过 NULL”一说。

【知识点】 聚合函数对 NULL 的处理规律 + InnoDB 的计数实现。

写法数的是什么是否受 NULL 影响说明
COUNT(*)行数否优化器专门处理,选最小的可用索引遍历
COUNT(1)行数否与 COUNT(*) 结果相同,语义等价
COUNT(列)该列非 NULL 的个数是,跳过 NULL列可空时结果小于总行数
COUNT(主键)主键非 NULL 的个数否(主键非空)结果等于行数,但语义上仍走“判空”

推导要抓两点。其一(语义):COUNT(expr) 的规则是“对每一行求 expr,结果不为 NULL 则计数加一”;COUNT(*) 是这条规则的特例 —— 它不针对任何列,直接数行。其二(实现):InnoDB 不像 MyISAM 那样保存行数(MyISAM 是表级锁、无 MVCC,可以存一个计数器;InnoDB 有 MVCC,同一时刻不同事务看到的行数可能不同,无法缓存统一值),因此 COUNT(*) 必须真的去扫。优化器的做法是选一棵最小的二级索引来遍历(二级索引叶子比聚簇索引小,扫的页更少),这也解释了“COUNT(*) 通常比 COUNT(某个可空列) 更快” —— 后者必须读列值判空,用不上这个优化。

补两个工程口径:一是超大表求总数,COUNT(*) 依然要扫全索引,建议改用近似值(EXPLAIN 的 rows 估算)、Redis 计数器或单独维护计数表;二是 COUNT(*) 与 COUNT(1) 在 MySQL 8.0 中已被优化到基本一致,不必纠结用哪个。

【记忆锚点】 “COUNT(*) 和 COUNT(1) 数行不挑食;COUNT(列) 数值挑食,NULL 一律不算”。

【易混对比】

  • COUNT(*) vs COUNT(列):前者数行、含 NULL 行;后者数值、跳 NULL 行。
  • COUNT(1) vs COUNT(列):前者不读列、可走最小索引;后者必须读列判空,通常更慢。
  • NULL vs 空字符串:'' 是“已知的空”,会被 COUNT(列) 计入;NULL 是“未知”,不计入。二者在 SQL 里语义完全不同。
  • 换问法:若题干改成“某列有 3 行为 NULL、表共 10 行,求 COUNT(该列)”,答案就是 7(与补-09 是同型计算题)。

【自测】 表 t 共 100 万行,列 email 有 5 万行为 NULL。分别求 COUNT(*) 与 COUNT(email),并说明 COUNT(email) 为什么通常更慢。

答:COUNT(*) = 1000000;COUNT(email) = 950000。COUNT(email) 更慢是因为它必须读取 email 列并逐行判空,无法使用“选最小索引直接数行”的优化。与补-09、M19 连考;互联网笔试高频。

【知识关联】

  • 补题关联:补-09(COUNT(*) 与 COUNT(col)/NULL 语义)。
  • 面试/工程:大表 COUNT(*) 会拖垮主库,应近似值(表统计信息)或异步计数表;分页总数可缓存。
  • 面试追问:① COUNT(1) 与 COUNT(*) 等价吗? ② COUNT(主键) 与 COUNT(*) 谁可能更慢?

【拓展延伸】

  • 变式问法:给含 NULL 的表算 COUNT(*)/COUNT(col)/COUNT(DISTINCT col)。
  • 参数/命令:SHOW TABLE STATUS 的 Rows 是估算;EXPLAIN 对 COUNT(*) 有时给 rows 估算;可用触发器/应用维护精确计数。

M26. char(10) 与 varchar(10) 的区别,正确的是? ​

考点:char 与 varchar

A. char 定长,不足部分用空格补齐;varchar 变长,需额外 1~2 字节记录实际长度 B. 两者完全相同,只是写法不同 C. varchar 是定长,char 是变长 D. char 的最大长度只能是 255 个字符

答案:A

【考点】定长与变长字符类型的存储差异及适用场景。

【结论】 选 A:char 定长、不足部分用空格补齐;varchar 变长、额外用 1~2 字节记录实际长度。

【逐项辨析】

  • A char 定长不足补空格,varchar 变长需额外 1~2 字节记长度:正确。 这一句把两者的存储差异说全了。
  • B 两者完全相同、只是写法不同:错。存储方式根本不同(定长 vs 变长),一个补空格、一个记长度,二者不可互换。
  • C varchar 是定长、char 是变长:错。把两者的定义完全说反。
  • D char 的最大长度只能是 255 个字符:错在答非所问 —— 本题问的是 char(10) 与 varchar(10) 的存储差异,D 没有回答差异。(补充口径:CHAR 的声明上限确实是 255 个字符,与字符集无关;utf8mb4 下 CHAR(255) 占 1020 字节,仍可正常建表,只是它会吃掉整行 65535 字节上限里的一大块。别把「字节数放大」误记成「字符数上限变小」。这一点在 5.7/8.0 一致。)

【知识点】 定长与变长的存储模型差异。

类型存储方式长度开销尾部空格适用场景
char(n)定长,永远占 n 个字符的位置无额外字节不足补空格(读取时去掉尾部空格)长度固定或变化极小:性别、手机号、邮编、MD5、定长 UUID
varchar(n)变长,按实际长度存额外 1~2 字节记长度不补空格,原样保存长度差异大:姓名、地址、备注、URL

varchar 的长度前缀规则是:按该列定义的最大可能字节数判定 —— ≤ 255 用 1 字节,> 255 用 2 字节(最大可能字节数 = 声明字符数 × 字符集单字符最大字节数)。两点要抓住:① 判据是字节数而非字符数;② 是声明推出的上限而非某行实际长度,因为同一列每行前缀宽度必须一致。utf8mb4 下 varchar(100) 上限 400 字节 > 255,故恒为 2 字节前缀;latin1 下才是 1 字节。

选型逻辑:char 的优势是定长 —— 行内偏移可直接算出,读取不必先解长度、更新也不会因变长而挪动后续记录、不易产生碎片;代价是浪费空间(存一个“男”也要占满 10 个字符)。varchar 的优势是省空间;代价是更新时若长度变长,可能触发页内记录搬移。还有一个易忽略的语义差异:char 会去掉尾部空格,varchar 会保留,做等值比较时这一点可能造成结果不同。

【记忆锚点】 “char 定长补空格,varchar 变长带长度” —— 判据是“长度是否固定”,不是“哪个更省空间”。

【易混对比】

  • char vs varchar:定长 vs 变长;补空格 vs 记长度;长度固定列 vs 长度不定列。
  • 字符数 vs 字节数:varchar(100) 的 100 是字符数,但长度前缀按字节数判断,两者不能混。
  • char 去尾空格 vs varchar 保留空格:写 3 个空格进 varchar 就存 3 个;写进 char 会被并入补齐逻辑并在读取时去除。
  • 换问法:若题目问“varchar(100) 的长度前缀占几个字节”,判据是该列的最大可能字节数(声明字符数 × 字符集单字符最大字节数)是否超过 255,而不是某一行实际存了多少字节——同一列每行的前缀宽度必须一致,否则记录无法解析。utf8mb4 下 100×4=400>255 → 恒为 2 字节;latin1 下 100×1=100≤255 → 1 字节。

【自测】 列 code 固定存 32 位十六进制 MD5 值,列 remark 存用户备注(几字到几百字不等)。分别应选 char 还是 varchar?

答:code 用 char(32)(长度固定、读取快、无碎片);remark 用 varchar(长度差异大、省空间)。与 M22 主键长度、M24 索引体积连考;建表评审高频。

【知识关联】

  • 补题关联:补-24(金额用 DECIMAL,同属类型选型)、补-28(编码思想可类比:省内存 vs 灵活性)。
  • 面试/工程:定长且几乎不变用 CHAR(如状态码、MD5 定长哈希),更新少、不易碎片;可变长度用 VARCHAR,更省空间但更新可能导致行迁移。UTF-8 下 VARCHAR(n) 的 n 是字符数,实际字节上限受 row_format 与整行 65535 限制约束。大文本不要塞 VARCHAR,用 TEXT 并注意是否溢出页存储。
  • 面试追问:① VARCHAR(255) 与 VARCHAR(256) 存储差在哪?(单字节字符集下恰是长度前缀 1↔2 字节的临界点,5.0.3+ 语义;换 utf8mb4 则临界点提前到 varchar(63)/varchar(64),因为 64×4=256 已超 255) ② 为什么金额不能用 FLOAT?(二进制浮点无法精确表示十进制小数,会累计误差)

【拓展延伸】

  • 变式问法:给出字段场景选类型(手机号 CHAR(11)、用户名 VARCHAR、价格 DECIMAL、简介 TEXT)。
  • 参数/命令:CHAR/VARCHAR/TEXT/DECIMAL(M,D);row_format=DYNAMIC;information_schema.TABLES 看数据长度;SHOW CREATE TABLE 审字符集与排序规则。

M27. 在 MySQL 5.6 及以上版本中,对已有大表执行 ALTER TABLE ... ADD INDEX ... 的实际情况是? ​

考点:Online DDL 加索引

A. 一定全程锁表,业务无法读写 B. 支持 Online DDL,多数场景不阻塞读写(仅开始与结束阶段需要短暂获取元数据锁) C. 大表根本无法加索引 D. 只能通过“建新表 + 导数 + 改名”的方式实现

答案:B

【考点】Online DDL 机制与 DDL 变更的可用性权衡。

【结论】 选 B。MySQL 5.6 起 ADD INDEX 走 Online DDL,绝大多数时间不阻塞读写,只有开始与结束两个瞬间需要短暂获取元数据锁。

【逐项辨析】

  • A 一定全程锁表、业务无法读写:错。这是 MySQL 5.5 及更早的 COPY 方式(拷全表期间禁写),5.6 之后已被 Online DDL 取代,“一定全程”是过时口径。
  • B 支持 Online DDL、多数场景不阻塞读写,仅开始与结束阶段需短暂获取元数据锁:正确。 关键在于“短暂 MDL”这个限定,它才是线上真正的风险点。
  • C 大表根本无法加索引:错。大表可以加索引,只是耗时长、资源占用高,需要配合低峰执行或外部工具。
  • D 只能通过“建新表 + 导数 + 改名”实现:错。这是 pt-online-schema-change 等外部工具的思路,属“可选方案”而非“唯一途径”,说“只能”过于绝对。

【知识点】 DDL 的三种执行算法与“期间能否并发 DML”的对应关系。

算法执行方式期间能否 DML典型场景
COPY建临时表、逐行拷贝数据不能(阻塞写)5.5 时代;改列类型等重操作
INPLACE在原表上直接改,不拷全表多数可以5.6+ 的 ADD INDEX、部分 ADD COLUMN
INSTANT只改元数据,秒级完成可以8.0+ 的 ADD COLUMN(加在末尾)、改默认值

Online DDL 的实现思路:在原表上直接构建新索引,同时把构建期间发生的 DML 变更记录到一块在线日志(row log)中,待索引构建完成后把这批增量补到新索引上,最后短暂加锁切换。整个过程真正的“锁”出现在两端 —— 开始阶段要拿 MDL 共享锁确认表结构、结束阶段要升级为排他锁完成切换;若此刻有长事务未提交,它会一直持有 MDL,把 DDL 卡在中间,进而阻塞该表上后续所有查询,这就是线上“加个索引把库搞挂”的经典事故链。

生产建议三条:一是低峰执行;二是先确认无长事务(查 information_schema.innodb_trx);三是超大表或需要变更列类型等 COPY 类操作,改用 pt-online-schema-change(建影子表 + 触发器同步增量)或 gh-ost(读 binlog 同步增量、不用触发器)。

【记忆锚点】 “Online DDL 两头锁、中间放开” —— 怕的不是建索引本身,而是“长事务把 MDL 卡住”。

【易混对比】

  • Online DDL vs 外部工具:前者是原生能力(ALGORITHM=INPLACE),适合常规加索引;后者适合超大表与 COPY 类变更,能限速、可暂停,但多一层复杂度。
  • MDL 元数据锁 vs InnoDB 行锁:MDL 保护表结构,DDL 与 DML 靠它互斥;行锁保护数据行。DDL 被长事务卡住,卡的是 MDL。
  • INPLACE vs INSTANT:INPLACE 仍要重建索引 / 修改数据,耗时可观;INSTANT 只改元数据,秒级完成。
  • 换问法:若题目问“Online DDL 期间最大的风险是什么”,答案就是“开始 / 结束时需要 MDL,被长事务阻塞后会连锁阻塞查询”。

【自测】 业务低峰执行 ALTER TABLE big_t ADD INDEX idx_c(c),命令长时间不返回,同时监控显示该表上的普通 SELECT 也全部卡住。最可能的原因是什么?

答:存在未提交的长事务一直持有 MDL,DDL 在等待元数据锁,并把后续查询一起挡住。应先查 information_schema.innodb_trx 找出长事务并结束它。与 M18 意向锁、M28 复制连考;DBA 岗高频。

【知识关联】

  • 补题关联:补-22(DDL 期间 Buffer Pool/IO 压力)。
  • 面试/工程:大表加索引用 Online DDL + 低峰窗口,或 gh-ost/pt-osc;必须观察磁盘与复制延迟。
  • 面试追问:① Online DDL 的“online”指什么? ② 什么 DDL 仍会锁表?

【拓展延伸】

  • 变式问法:5.5 及以前是复制表+重建(表锁),5.6+ 默认 Online ADD INDEX。
  • 参数/命令:ALTER TABLE t ADD INDEX idx(c), ALGORITHM=INPLACE, LOCK=NONE;innodb_online_alter_log_max_size;lock_wait_timeout。

M28. MySQL 主从复制中,从库的 SQL 线程的作用是? ​

考点:主从复制的线程模型

A. 从主库拉取 binlog B. 在主库上生成 binlog C. 读取 relay log 并在从库重放,把变更应用到从库数据 D. 执行 redo log 的刷盘

答案:C

【考点】主从复制的三个关键线程及其分工。

【结论】 选 C。从库 SQL 线程的职责是读 relay log 并在从库重放变更,把数据真正“落”到从库。

【逐项辨析】

  • A 从主库拉取 binlog:错。这是从库 IO 线程的职责(连主库、收 binlog、写 relay log),不是 SQL 线程。
  • B 在主库上生成 binlog:错。binlog 是主库事务提交时由 Server 层写入的,位置在主库不在从库,也不是由某个复制线程专门生成。
  • C 读取 relay log 并在从库重放:正确。 SQL 线程是“执行者”,消费 IO 线程落地的中继日志。
  • D 执行 redo log 的刷盘:错。redo log 刷盘是 InnoDB 引擎的行为(受 innodb_flush_log_at_trx_commit 控制),与复制线程无关。

【知识点】 一条 binlog 从主库到从库要经过三个线程、两份日志。

线程所在位置职责产出
dump 线程主库监听 binlog,把变更推送给从库网络传输
IO 线程从库连接主库、接收 binlog、写入本地 relay logrelay log
SQL 线程从库读取 relay log,重放变更到从库数据从库数据更新

流水线是:主库写 binlog → dump 线程推送 → 从库 IO 线程落 relay log → 从库 SQL 线程重放。注意“两份日志”:binlog 在主库、是复制源;relay log 在从库、只是 binlog 的本地暂存副本,重放完即可清理(relay_log_purge 默认开启)。配套参数还有 read_only / super_read_only(防止从库被误写)。

延迟的根也在这里:IO 线程始终是单线程,而 SQL 线程在 5.6 之前只能单线程重放;5.6 起可按库并行、5.7 起按组提交并行、8.0.27 起 replica_parallel_workers 默认 4(本机 8.0.46 实测 @@replica_parallel_workers=4,从库回放默认已多线程),「单线程重放」只对老版本或显式设为 0/1 的实例成立(slave_parallel_workers 只作用于 SQL 线程,不影响 IO 线程)—— 主库多并发写入、从库串行重放,队列越积越长,这就是主从延迟的主要成因。并行复制的演进分三代:5.6 引入基于库(schema)粒度的并行复制(slave_parallel_workers > 0,只有分属不同库的事务能并发重放,单库写入仍然串行);5.7 才引入基于组提交的逻辑时钟 LOGICAL_CLOCK(同一组提交里的事务可并发重放,不再依赖多库);8.0 进一步用 WRITESET(依赖追踪参数 binlog_transaction_dependency_tracking 自 5.7.20 提供;8.0 的默认值不是 WRITESET——本机全新 8.0.46 容器实测 @@binlog_transaction_dependency_tracking=COMMIT_ORDER(@@transaction_write_set_extraction=XXHASH64),要 WRITESET 得显式设;8.0.35/8.2.0 起废弃,8.4 起参数移除、多线程回放时源端固定按 write set 生成依赖信息(等效 WRITESET)),并行度最高。

【记忆锚点】 “主库 dump 往外推,从库 IO 往回收,从库 SQL 往里放” —— 一推、一收、一放,对应 binlog → relay log → 数据。

【易混对比】

  • IO 线程 vs SQL 线程:IO 线程负责“把日志搬回来”(网络 + 落盘),SQL 线程负责“把日志执行掉”(重放)。延迟高时要先判断是 IO 落后还是 SQL 落后,优化方向完全不同。
  • binlog vs relay log:前者在主库、是复制源;后者在从库、是复制中转,内容同源。
  • 主库 dump 线程 vs 从库 SQL 线程:前者只发送、不执行;后者只执行、不传输。
  • 换问法:若题目问“从库 IO 线程的作用”,答案就换成“连接主库拉取 binlog 并写入 relay log”(与本题互为镜像,务必成对记)。

【自测】 从库 SHOW SLAVE STATUS 显示 Slave_IO_Running: Yes、Slave_SQL_Running: Yes,但 Seconds_Behind_Master 持续增大。是哪个线程跟不上?

答:SQL 线程。IO 线程正常说明 binlog 能拉回来,延迟来自重放能力不足,优化方向是开并行复制(正名 replica_parallel_workers,8.0.26 起改名、slave_parallel_workers 为废弃别名;8.0.27 起默认 4)。与 M29 连考;运维面试高频。

【知识关联】

  • 补题关联:补-20(redo/undo 与检查点的崩溃恢复判据;复制链路里的日志角色见本题正文)。
  • 面试/工程:单线程 SQL 线程是延迟主因之一;并行复制的标准演进是 5.6 基于库(schema)粒度 → 5.7 基于组提交的 LOGICAL_CLOCK → 8.0 基于 WRITESET,版本归属别记混。
  • 面试追问:① IO 线程与 SQL 线程分别做什么? ② 并行复制如何保证事务顺序?

【拓展延伸】

  • 变式问法:问“relay log 是谁写的”(IO 线程);问“谁负责重放”(SQL 线程)。
  • 参数/命令:SHOW REPLICA STATUS(8.0.22+)/SHOW SLAVE STATUS;replica_parallel_workers、binlog_transaction_dependency_tracking(取值 COMMIT_ORDER / WRITESET)。

M29. 下列不属于主从延迟常见原因的是? ​

考点:主从延迟的原因

A. 从库 SQL 线程单线程重放,跟不上主库并发写入 B. 主库执行了长时间的大事务(如一次删除百万行) C. 从库硬件配置远低于主库 D. 主库使用了 InnoDB 存储引擎

答案:D

【考点】主从延迟的成因与缓解手段。

【结论】 选 D。InnoDB 是主从双方通用的存储引擎,它既不制造延迟、也不区分主从,与延迟无因果关系。

【逐项辨析】

  • A 从库 SQL 线程重放能力跟不上主库并发写入:是真实成因。5.6 之前 SQL 线程只能单线程串行重放,即便 5.6 起有了并行复制(8.0.27 起默认 4 个 worker),并行粒度、并行度配置与主库的并发写入之间仍可能失衡,重放排队积压,历来是延迟的第一大来源。
  • B 主库执行长时间大事务(如一次删除百万行):是真实成因。大事务产生的 binlog 必须整体重放完才在从库可见,期间延迟持续累积。
  • C 从库硬件配置远低于主库:是真实成因。磁盘 IOPS、CPU、内存不足会直接拖慢重放速度。
  • D 主库使用了 InnoDB 存储引擎:不是成因。 InnoDB 是 MySQL 默认引擎,主从两侧通常都用它,既不制造也不放大延迟,属于“与结论无关的选项”。

【知识点】 主从延迟的本质是“从库消费 binlog 的速度跟不上主库生产的速度”,一切成因都能归到这条流水线的某个环节。

环节成因表现缓解手段
主库生产大事务(一次删百万行、大批量 INSERT)单事务 binlog 达几百 MB,必须整体重放拆小事务、分批提交
网络传输带宽不足、跨机房链路抖动Slave_IO_Running 落后提升带宽、就近部署
从库重放SQL 线程单线程 / 并行粒度不够Seconds_Behind_Master 持续增大开并行复制(slave_parallel_workers)
从库资源硬件弱、还承担大量只读查询重放被只读流量挤占升配、读流量分流到专用从库
锁冲突从库上长查询 / 大事务占锁重放线程被阻塞控制从库大查询

诊断思路:先看 Seconds_Behind_Master,再分别看 Slave_IO_Running 与 Slave_SQL_Running —— IO 线程落后说明“日志搬不回来”(网络 / 主库压力),SQL 线程落后说明“日志执行不完”(重放能力 / 锁冲突)。补两个口径:一是 Seconds_Behind_Master 只是估算值(基于主库 binlog 时间戳与从库当前时间之差),主库长时间无写入时它可能显示 0 却掩盖真实差距,更可靠的是 pt-heartbeat 这类心跳工具;二是主从延迟无法彻底消除,只能压到业务可接受的范围,对一致性要求高的读(如刚下单后立刻查订单)应强制走主库。

【记忆锚点】 “生产快、传输慢、重放更慢” —— 延迟由三段流水线里“最慢那一环”决定,InnoDB 不在任何一环里。

【易混对比】

  • 主从延迟 vs 主从数据不一致:延迟是“暂时落后”(最终会追上),不一致是“永久错误”(误操作、复制中断、sync_binlog 配置不当导致丢事务),严重程度完全不同。
  • 异步复制 vs 半同步复制(semi-sync):异步下主库不等从库确认就返回,延迟不影响主库;半同步要等至少一个从库收到 binlog 才返回,能降低丢数据风险,代价是增加主库提交延迟。
  • 延迟 vs 复制中断:Slave_SQL_Running: No 是复制停了(如主键冲突),不是“慢”,需人工介入跳过或修复。
  • 换问法:若题目改成“下列属于主从延迟原因的是”,A / B / C 任选其一都对,D 仍不对 —— 否定型题目要把每个选项独立判真伪,不要被“哪个更像”带偏。

【自测】 主库一次 DELETE FROM log WHERE dt < '2025-01-01' 删掉 800 万行,此后从库 Seconds_Behind_Master 持续增长数小时。最有效的直接措施是什么?

答:把大事务拆成多批小事务(如按天分批 DELETE ... LIMIT 10000 循环),让 binlog 分片、从库可持续追赶。与 M28 连考;运维与后端面试高频。

【知识关联】

  • 补题关联:补-20(redo/undo 与检查点的崩溃恢复判据;复制链路里的日志角色见本题正文)、补-18(主从并发重放与 2PL/锁的关系可对照理解)。
  • 面试/工程:读写分离架构必须容忍秒级延迟;关键读(支付结果、库存扣减后的确认)应强制走主库。延迟五大主因:大事务、无主键/无索引更新导致从库回放变慢、DDL、从库单线程或并行度不足、机器资源(CPU/IO/内存)不够。大促前要压测从库回放能力而不只是主库写入。
  • 面试追问:① 从库延迟如何定量观测?(Seconds_Behind_Source 有局限;更准看 performance_schema.replication_connection_status 的 heartbeat 与位点差、或 GTID executed 集合差) ② 为什么“无主键表的更新”特别伤复制?(从库回放行变更时要全表扫定位行,把顺序回放打成随机 IO)

【拓展延伸】

  • 变式问法:把“网络抖动 / 大事务 / 从库规格低 / 主库大 DDL”混在选项里辨认;或问“并行复制为何能降延迟”——把无冲突事务在从库并发重放。
  • 参数/命令:SHOW REPLICA STATUS\G(8.0.22+)看 Seconds_Behind_Source、Retrieved_Gtid_Set/Executed_Gtid_Set;replica_parallel_workers、replica_parallel_type;binlog_transaction_dependency_tracking=WRITESET(该参数 5.7.20 引入;8.0 默认仍是 COMMIT_ORDER(本机 8.0.46 实测),WRITESET 需显式开;8.0.35/8.2.0 废弃,8.4 起参数移除并固定按 write set 生成依赖信息)提高主库组提交后的可并行度;业务侧关键读 /*+ MASTER */ 或路由到主库。

M30. 什么时候应该考虑分库分表? ​

考点:分库分表的时机

A. 表刚建好时就分,一劳永逸 B. 单表数据量过大(通常千万级以上)导致查询/写入性能显著下降,且已通过索引优化、缓存、读写分离仍无法满足时 C. 只要建了索引就不需要分库分表 D. 任何表都应该尽早分库分表

答案:B

【考点】分库分表的适用条件与代价。

【结论】 选 B。分库分表是最后手段 —— 只有当单表数据量过大、且索引优化 / 缓存 / 读写分离都已用尽仍不达标时,才值得付出分布式复杂度。

【逐项辨析】

  • A 表刚建好时就分、一劳永逸:错。过早分片会凭空引入分布式事务、跨分片查询、全局 ID 等复杂度,而此时数据量根本不需要,属于“用大炮打蚊子”。
  • B 单表过大导致性能显著下降,且已通过索引优化、缓存、读写分离仍无法满足:正确。 它同时给出了“数据量判据”与“前置手段已穷尽”两个条件,是唯一完整的表述。
  • C 只要建了索引就不需要分库分表:错。过于绝对 —— 索引解决的是“查询定位”,解决不了单机写入热点、单表存储容量上限与 DDL 的维护代价。
  • D 任何表都应该尽早分库分表:错。与 A 同病,“任何”“尽早”两个绝对化词直接排除;分片是按需引入的重架构变更。

【知识点】 分库分表的定位是“优化阶梯的最后一级”,顺序不可跳。

阶段手段解决什么问题代价
①SQL 与索引优化慢查询、全表扫描低(改代码 / 加索引)
②加缓存(Redis)热点读、减轻库压力中(一致性维护)
③读写分离读扩展中(主从延迟,见 M29)
④归档冷数据单表行数下降低(需业务配合)
⑤垂直拆分按业务拆库、大字段独立成表中(跨库 JOIN 变多)
⑥水平分库分表突破单机容量与写入上限高(分布式事务、跨片查询、全局 ID、扩容重分片)

判断标准要落到指标而不是拍脑袋:单表行数、慢查询占比、写入延迟、磁盘占用、DDL 耗时。经验口径是“单表千万级开始评估”,但具体阈值取决于列宽、索引数量与访问模式 —— 一行 200 字节的表到 5000 万行可能依然健康,而一行 5 KB 的表 1000 万行就已吃力。真正的触发条件永远是“性能指标确实恶化了”,而不是某个固定行数。

水平分片引入的额外问题必须提前想清楚:一是分片键选择(决定能否避免跨片查询,选错会导致全片扫描);二是全局唯一 ID(自增主键失效,需雪花 ID / 号段模式,与 M22 主键选型呼应);三是跨片查询与分页(ORDER BY ... LIMIT 需各片各取再归并);四是分布式事务(跨片写要用 TCC / 本地消息表等);五是扩容重分片(从 4 库扩到 8 库要迁移数据,需预留平滑方案)。这些正是它必须“最后才用”的原因。

【记忆锚点】 “先优化、再缓存、后读写分离,实在不行才分库分表” —— 分片是“最后手段”,不是“最优手段”。

【易混对比】

  • 垂直拆分 vs 水平拆分:垂直是“按业务 / 按列切”(用户库、订单库;大字段挪到附表),水平是“按行切”(同结构表按分片键分散到多个库表)。前者复杂度低、更常见,后者才是“分库分表”通常所指。
  • 分库分表 vs 分区表:分区表是单机内由数据库管理的物理划分,应用无感知、无需改代码,但仍在同一实例,突破不了单机上限;分库分表是应用层方案,能真正扩展。
  • 分表 vs 分库:分表解决单表过大;分库解决单实例连接数 / IO 上限。通常先分表,压力仍大再分库。
  • 换问法:若题目问“分库分表后自增主键为什么不能用了”,答案就回到 M22 的“需改用雪花 ID / 号段模式生成全局唯一 ID”。

【自测】 某订单表 2000 万行、单行约 300 字节,按 create_time 排序的查询变慢,已加联合索引仍有慢查询,且缓存命中率低(订单查询按用户分散)。按优化阶梯,下一步最该做什么?

答:先做冷数据归档(把历史订单迁到归档表,把单表行数压下来),再评估是否值得水平分片 —— 分片是第 ⑥ 级,不该在 ④ 之前上。与 M24 深分页、M22 主键选型连考;架构岗与后端社招高频。

【知识关联】

  • 补题关联:无直接补题;可与补-24(表设计)连读。分库分表是“设计无法再水平扩展时的最后手段”。
  • 面试/工程:单表行数经验阈值(如千万级)不是铁律,更该看:索引树高、Buffer Pool 命中率、慢查询、主从延迟、备份窗口。优先顺序通常是:优化 SQL/索引 → 读写分离 → 缓存 → 垂直拆分 → 水平分库分表。过早分片会把简单问题复杂化(跨片 JOIN、分布式事务、扩容迁移)。
  • 面试追问:① 为什么不能只看行数决定分表?(行宽、访问模式、索引大小、硬件都影响;500 万窄表可能仍轻松) ② 分片键怎么选?(按最高频查询维度,尽量让常用查询落在单分片)

【拓展延伸】

  • 变式问法:问“什么时候该分库分表”选“容量/性能/可用性瓶颈且其他手段无效”;把“刚过一百万行就分”设为错误项。
  • 参数/命令:EXPLAIN、慢查询日志、information_schema.INNODB_BUFFER_PAGE 命中;ShardingSphere/Sharding-JDBC;一致性哈希或范围分片;扩容用双写迁移。

M31. 防止 SQL 注入最有效的方式是? ​

考点:SQL 注入防护

A. 使用 PreparedStatement / 参数化查询(占位符绑定参数) B. 用字符串拼接构造 SQL C. 手动过滤掉单引号 D. 给相关列加上索引

答案:A

【考点】SQL 注入的原理与参数化查询的防护机制。

【结论】 防止 SQL 注入最有效的手段是参数化查询(PreparedStatement / 占位符绑定) —— 它把 SQL 的“结构”与“数据”彻底分离,选 A。

【逐项辨析】

  • A 使用 PreparedStatement / 参数化查询(占位符绑定参数):正确。 SQL 模板先被数据库预编译,语法结构在参数到达之前就已固定;参数只以“数据”身份填入占位符,无法改变语句结构。
  • B 用字符串拼接构造 SQL:错。这正是漏洞的根源 —— 用户输入与 SQL 语法拼在同一条字符串里,输入中的 ' 会闭合原引号,其后内容被当作语法解析。
  • C 手动过滤掉单引号:错。错在“过滤”二字 —— 字符替换属“打补丁”式防护,可用十六进制编码、注释符、宽字节注入(%bf%27)等方式绕过,是不可靠方案。
  • D 给相关列加上索引:错。索引只影响查询性能,与输入是否被当作语法解析完全无关,属答非所问。

【知识点】 SQL 注入的定义:应用程序把用户可控的输入未经分离处理就拼接进 SQL 语句,使输入被数据库当作 SQL 语法解析执行。其成立需两个条件同时满足:

成立条件含义参数化查询如何破坏它
输入进入 SQL 字符串输入与语法处在同一段文本里输入不进入 SQL 文本,只走参数通道
数据库再次解析该文本输入被当成语法模板先预编译,结构已固化

推导:SQL 注入的本质是“数据越权升级为语法”。若语句写成 SELECT * FROM user WHERE name = ' + 输入 + ',当输入为 ' OR '1'='1 时,整句变成 WHERE name = '' OR '1'='1',条件恒真 → 全表泄露。参数化查询走的是两段式协议:先 PREPARE 把模板编译成执行计划(此时结构已定),再 EXECUTE 把参数按值传入,数据库不会再对参数做语法解析,于是 ' OR '1'='1 只是一个“名字很怪的普通字符串”。

补充两点工程口径:① MyBatis 中 #{} 是预编译占位符(安全),${} 是字符串拼接(有注入风险);② 参数化查询覆盖不了表名、列名、ORDER BY 方向等结构位置,这些位置必须用白名单校验。

【记忆锚点】 「结构先定死,数据只当值」 —— 参数化查询的防护力来自“SQL 结构在参数到来之前就已经编译完成”。

【易混对比】

  • 参数化查询 vs 转义 / 过滤:前者是结构性防护(让输入永远无法成为语法),后者是字符级修补(永远可能漏掉某种编码)。工程上应以前者为主、后者为辅。
  • #{} vs ${}(MyBatis):#{} → ? 占位符 → 预编译,安全;${} → 直接文本替换 → 拼接,危险。
  • SQL 注入 vs XSS:前者攻击数据库(伪造 SQL 语法),后者攻击浏览器(注入脚本)。二者同源(数据与代码未分离),但落点不同。
  • 换问法:若题干改成“下列哪种做法不能防止 SQL 注入”,答案就落在“字符串拼接”“手动过滤单引号”上。

【自测】 MyBatis 中 ${} 与 #{} 哪个存在 SQL 注入风险?若确实需要动态指定排序字段,应如何防护?

答:${} 有风险(直接文本替换,可注入);#{} 走预编译占位符,安全。动态排序字段无法参数化,必须对字段名做白名单校验。与 R06 的“输入不可信”思路同源;互联网 / 国企笔试高频。

【知识关联】

  • 补题关联:补-10(视图可收窄可见列,作为纵深防御)。注:补-07 考 WHERE 与 HAVING 的作用阶段、补-06 考 DDL 语句分类,都与注入无关;本库补题暂无 SQL 注入专项,注入的工程口径见本题【知识点】与【自测】。
  • 面试/工程:ORM 默认参数化即可挡住值注入;仍危险的是标识符注入(动态表名、列名、ORDER BY 方向)——必须白名单映射,绝不能拼用户输入。二次注入:恶意数据先“安全地”入库,后续查询拼进语句时才爆发,预编译救不了已入库的脏数据,必须入库前校验/转义。
  • 面试追问:① 参数化能否防住所有注入?(不能防表名/列名/关键字位置;只能防值位) ② 二次注入是什么?(恶意数据被安全写入,之后在另一条动态 SQL 中被当成可信片段拼接)

【拓展延伸】

  • 变式问法:对比“字符串拼接 vs PreparedStatement 参数绑定 vs 存储过程(不一定更安全)”;给出一段拼接 SQL 选注入点。
  • 参数/命令:JDBC useServerPrepStmts=true 走服务端预编译;账号 GRANT 最小权限,禁止应用账号 DDL;sql_mode 加固;WAF/网关做二次拦截;代码评审禁用字符串拼 SQL。

M32. READ COMMITTED(读已提交)能避免下列哪种并发问题? ​

考点:隔离级别能解决的问题

A. 脏读 B. 不可重复读 C. 幻读 D. 以上全部

答案:A

【考点】四种隔离级别与三类并发问题的对应关系。

【结论】 READ COMMITTED 只能避免脏读,不可重复读与幻读仍会发生 —— 选 A。

【逐项辨析】

  • A 脏读:正确。 RC 只读取“已提交”的数据,读不到别的事务未提交的中间状态,因此脏读被杜绝。
  • B 不可重复读:错。RC 下每次 SELECT 都生成新的 ReadView,同一事务两次读之间其他事务提交的修改会被读到,值发生变化。
  • C 幻读:错。RC 既不使用间隙锁、也不复用快照,两次范围查询之间的新增 / 删除行会被看到,幻读同样可能发生。
  • D 以上全部:错。错在“全部”二字 —— RC 解决的是三者中最轻的脏读,把不可重复读与幻读也算进去,是夸大了它的能力。

【知识点】 三类并发问题的定义(均以“事务 A 读、事务 B 写”为背景):

并发问题现象反例
脏读读到别的事务未提交的数据B 把余额 100 改成 200(未提交),A 读到 200;B 回滚,A 读到的是从未存在过的值
不可重复读同一事务内两次读同一行,值不同A 先读余额 100,B 提交改成 200,A 再读变成 200
幻读同一事务内两次范围查询,行数不同A 查“年龄 > 20 共 5 行”,B 插入 1 行并提交,A 再查变成 6 行

四级别与三类问题的完整对照:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不可能可能可能
REPEATABLE READ不可能不可能标准下可能(MySQL InnoDB 通过间隙锁基本避免)
SERIALIZABLE不可能不可能不可能

推导:脏读的根源是“读到了未提交的数据”,只要规定“只能读已提交版本”即可消除 —— 这正是 RC 做的事,所以 RC 是“解决脏读的最低成本级别”。而不可重复读与幻读的根源是“快照被刷新”:RC 每次查询都重建 ReadView,两次读之间别人一提交,快照就变了。要消除它们,必须让同一事务在多次读之间复用同一个快照(RR),或者加锁串行化(SERIALIZABLE)。

工程口径:RC 是 Oracle / SQL Server 的默认级别,也是不少互联网公司的实际选择 —— 因为它不加间隙锁、锁范围小、并发度更高,幻读交由业务幂等或乐观锁处理;而 MySQL InnoDB 默认是 RR。

【记忆锚点】 「RC 只挡脏读,RR 再挡不可重复读,SERIALIZABLE 全挡」 —— 级别越高,问题越少,并发越差。

【易混对比】

  • 不可重复读 vs 幻读:前者是同一行的值变了(UPDATE),后者是同一个范围的行数变了(INSERT / DELETE)。一个针对“值”,一个针对“集合”。
  • RC vs RR:RC 每次读新建 ReadView(读最新已提交),RR 事务内复用第一个 ReadView(读快照)。这是两者一切差异的总根源。
  • 隔离级别 vs 锁:隔离级别决定“要不要加锁、加什么锁”;MVCC 提供无锁的快照读。RR 下普通 SELECT 走快照读(不加锁),SELECT ... FOR UPDATE 走当前读(加间隙锁)。
  • 换问法:若题干改成“能避免不可重复读的级别”,答案就变成“REPEATABLE READ 及以上”;若问“哪个级别不加间隙锁”,答案是“READ COMMITTED 及以下”。

【自测】 MySQL InnoDB 的默认隔离级别是什么?在该级别下,普通 SELECT(快照读)还会出现幻读吗?

答:默认 REPEATABLE READ;快照读下不会 —— 事务内复用同一个 ReadView,B 新插入的行不在快照里,A 看不到。但当前读(SELECT ... FOR UPDATE / UPDATE)下,InnoDB 靠间隙锁(Gap Lock)+ Next-Key Lock 阻止范围内插入,从而基本避免幻读。与 M31 连考;大厂面试高频。

【知识关联】

  • 补题关联:补-17(ACID 与隔离性)、补-18(2PL)、补-19(S/X 锁相容矩阵)——原理层的隔离能力矩阵。
  • 面试/工程:不少公司把默认级别改成 RC:锁竞争更少、复制在 ROW 模式下更安全、死锁形态更可预期;幻读交给业务唯一约束/防重表兜底。银行/国企笔试常考“哪个级别解决哪个问题”的矩阵,必须背死。
  • 面试追问:① RC 完全不能防幻读吗?(RC 无普通间隙锁,范围读两次结果可能变;但唯一索引等值插入冲突仍能挡住“重复键”类插入) ② 为什么互联网偏 RC?(并发更高、锁更少,且多数业务用唯一键防重即可)

【拓展延伸】

  • 变式问法:矩阵填空:RR 能否避免幻读 → InnoDB RR + Next-Key 可“很大程度避免”,严格串行需 SERIALIZABLE;或给出四个级别选项问“哪个能避免不可重复读”。
  • 参数/命令:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;;tx_isolation/transaction_isolation;注意 RC 下无普通 Gap Lock(外键/唯一检查除外),死锁与阻塞形态会变;压测对比 RC vs RR 的 TPS 与锁等待。

持续学习,持续积累。