← 全部材料 MySQL
MYSQL · 17 个高频考点 · InnoDB 8.0

慢的 SQL都慢在同一个地方

要么索引没走对,要么锁等太久。EXPLAIN 里的一行 type 和 rows 就能解释九成的慢查询,剩下那一成藏在 MVCC 和间隙锁里。这份里的每个数字都是在 MySQL 8.0.41 上真跑出来的。

01先看执行计划每节都给 EXPLAIN 的真实输出,对照着自己的 SQL 看 type 和 rows。
02再理解为什么索引、MVCC、锁三块是同一套页结构的推论,理解了就不用背结论。
03最后是排查顺序慢查询定位、死锁排查、分库分表判断,给的是可照着做的步骤。
1

一条 SQL 从发出去到出结果★

先知道有几个地方会慢,再谈优化
一条 SQL 在 MySQL 里要过五道关:连接器 → 解析器 → 优化器 → 执行器 → 存储引擎。你能插手的只有后面两道:让它走对索引(优化器的选择),让它别扫太多行(执行器的活)。MySQL 8.0 把查询缓存整个删掉了,所以"改一条 SQL 让缓存失效"这类老题已经过期。
实测输出(MySQL 8.0.41)第一个查询没有任何返回结果 —— 查询缓存已经不存在 transaction_isolation = REPEATABLE-READ autocommit = ON innodb_flush_log_at_trx_commit = 1 sync_binlog = 1 max_connections = 151 long_query_time = 10.000000 slow_query_log = OFF
面试怎么答一条 SQL 依次经过连接器、解析器、优化器、执行器和存储引擎。连接器管连接和权限,默认最大连接 151,权限在连接建立时就确定了,改了要重连。解析器只做语法分析。优化器根据统计信息在多个方案里挑一个,选哪个索引、JOIN 顺序都在这里定,所以统计信息不准就会选错索引。执行器按方案向引擎逐行取数据并过滤。优化能插手的只有优化器和执行器这两段。补充一点:MySQL 8.0 已经移除了查询缓存,因为失效粒度是整表,写多的场景命中率极低,现在不该再把查询缓存当成优化手段。
2

页与 B+ 树:三层为什么能放两千万行★

所有索引问题的物理基础
InnoDB 读写的最小单位是页,默认 16KB。索引是一棵 B+ 树,非叶子节点只存键值和页号,叶子节点才存数据并用双向链表串起来。这个结构决定了三件事:树很矮(三层就够放千万级)、范围查询很顺(叶子是链表)、每次查询的磁盘 IO 次数等于树高。
实测输出(1 万行的表)innodb_page_size = 16384 字节(16KB) t_user 表 行数 ≈ 10186 数据 1589248 字节 索引 2277376 字节 聚簇索引页数 = 97 二级索引页数 = 139
面试怎么答InnoDB 以 16KB 的页为读写单位,索引是 B+ 树,非叶子节点只存键值和页号,叶子节点存数据且用双向链表串联。按主键 4 字节估算,一个非叶子页能放约 1170 个指针,叶子页按每行 100 字节算放约 160 行,三层就能覆盖两千万行以上,所以查询的磁盘 IO 次数基本等于树高,通常两三次。相比红黑树,B+ 树单节点分支多所以更矮,IO 更少;相比 B 树,数据全在叶子层且叶子成链,范围查询是顺序扫描而不是中序跳跃。这也解释了主键要短、索引不能乱加:主键变长会抬高树高,每加一个索引就多维护一棵树。
3

聚簇索引、回表与覆盖索引

Extra 那一列里藏着的性能差异
InnoDB 的表本身就是一棵按主键排序的 B+ 树,叫聚簇索引,叶子节点直接存整行数据。二级索引的叶子只存"索引列 + 主键",所以用它查别的列时必须拿着主键再回聚簇索引查一次,这就是回表。如果需要的列全在索引里,就不用回表,EXPLAIN 的 Extra 会显示 Using index。
实测输出(只看关键三列)-- SELECT * → 需要回表 type=ref key=idx_name_age rows=1 Extra=(空) -- SELECT name, age → 覆盖索引,不回表 type=ref key=idx_name_age rows=1 Extra=Using index
面试怎么答InnoDB 的表本身就是按主键组织的 B+ 树,叫聚簇索引,叶子节点存整行。二级索引叶子只存索引列和主键,所以用它查非索引列时要拿主键再回聚簇索引查一次,这叫回表。回表的真实代价是随机 IO:二级索引按 name 排序、聚簇索引按主键排序,顺序无关,查出一批主键就要随机跳着读。EXPLAIN 的 Extra 显示 Using index 说明是覆盖索引,不用回表。优化上优先改成只查索引列,而不是无脑加索引;主键建议用短且递增的值,因为随机 UUID 会导致页分裂,而主键长度会放大每一个二级索引的体积。
4

最左前缀:key_len 是唯一可信的证据★

别再靠"感觉走了索引"下结论
联合索引 (a, b, c) 是按 a 排序、a 相同再按 b 排序、b 相同再按 c 排序。跳过 a 直接用 b,就像拿一本按姓氏排的电话簿去查名字,索引用不上。EXPLAIN 里的 key_len 精确告诉你用到了索引的哪几列,这比 key 有值更可信——key 有值但 key_len 很小,说明只用了一小段。
实测输出-- 只用 name type=ref key=idx_name_age key_len=202 rows=1 FORMAT=JSON: "used_key_parts": ["name"] -- name + age 都用 type=ref key=idx_name_age key_len=206 rows=1 FORMAT=JSON: "used_key_parts": ["name", "age"] -- 只用 age type=ALL key=NULL key_len=NULL rows=10186 Extra=Using where
面试怎么答联合索引按列的顺序逐级排序,所以必须从最左列开始用,跳过第一列后面全部失效。判断依据最硬的是 EXPLAIN 的 key_len:VARCHAR(50) 的 utf8mb4 非空列占 50×4+2=202 字节,INT 占 4 字节,202 说明只用了第一列,206 说明两列都用上了。EXPLAIN FORMAT=JSON 里的 used_key_parts 会直接列出用了哪几列,更好读。设计联合索引时等值条件放前面、范围条件放后面,因为范围列之后的列无法再用。另外 WHERE 条件的书写顺序不影响结果,优化器会重排,真正决定成败的是索引定义的列顺序。
5

索引失效的真凶,一次测清楚★

六种情况,每种都能在 EXPLAIN 里看到
索引失效不是玄学,本质只有两种:要么你让索引列"不再是它自己"(函数、运算、类型转换),要么你的条件覆盖了太多行(优化器算完觉得全表扫更便宜)。把这两种想明白,绝大部分"为什么不走索引"都能自己解释。
实测输出(1 万行,逐条对照)-- name = 'user123' type=ref key=idx_name_age rows=1 -- name LIKE 'user12%' type=range key=idx_name_age rows=111 Extra=Using index condition -- name LIKE '%123' type=ALL key=NULL rows=10186 Extra=Using where -- DATE(created_at) = '2026-01-01' type=ALL key=NULL rows=10186 Extra=Using where -- phone = 13800000123(传数字) type=ALL key=NULL rows=10186 Extra=Using where -- phone = '13800000123'(传字符串) type=ref key=idx_phone rows=1 -- city != '北京' type=ALL key=NULL rows=10186 Extra=Using where -- city = '北京' type=ref key=idx_city rows=2000 -- city = '北京' OR age = 30 type=ALL key=NULL rows=10186 Extra=Using where
面试怎么答索引失效可以归成两类原因。第一类是让索引列失去原有顺序:列上套了函数或运算、发生了隐式类型转换、LIKE 以通配符开头,这几种都会让 B+ 树的有序性用不上。第二类是优化器主动放弃:条件选择性太低、不等号覆盖了大部分行、OR 里有一边没索引,优化器算完成本发现全表扫更便宜。判断方法统一看 EXPLAIN,type 是 ALL 且 key 是 NULL 就说明没用索引,rows 是估算的扫描行数。修复上,函数改写成范围条件、字符串列传字符串、LIKE 改成前缀匹配、低选择性列放进联合索引的后置位置或者干脆不建索引。
6

explain 怎么读:type 的等级和 rows 的含义

一张表,四个字段,够定位九成慢查询
读 EXPLAIN 只需要看四列:type(访问方式,越靠前越好)、key(实际用了哪个索引)、rows(预估要扫多少行)、Extra(有没有回表、有没有额外排序)。其中 rows 是最容易被忽略但最有用的——它直接告诉你这个查询大概要付出多少行扫描的代价。
实测输出(只摘关键字段)-- 走 idx_name_age "table_name": "t_user" "key": "idx_name_age" "used_key_parts": ["name", "age"] "rows_examined_per_scan": 1 "query_cost": "0.35" -- 全表扫 "table_name": "t_user" "rows_examined_per_scan": 10186 "query_cost": "1042.85"
面试怎么答读 EXPLAIN 主要看 type、key、rows、Extra 四列。type 的优先级是 system、const、eq_ref、ref、range、index、ALL,看到 ALL 说明全表扫描要重点处理,看到 index 也要注意它只是改扫索引而不是真正定位。rows 是优化器基于统计信息的估算,只看数量级不看精确值。Extra 里 Using index 表示覆盖索引不用回表,Using index condition 是索引下推,Using filesort 和 Using temporary 是额外的排序和临时表开销。想看具体代价可以用 EXPLAIN FORMAT=JSON,里面的 query_cost 能直接对比优化前后,实测走索引 0.35、全表 1042.85,差了近三千倍。
7

索引下推:Extra 里的 Using index condition

把过滤提前到引擎层,少回表
索引下推(ICP,Index Condition Pushdown)是 MySQL 5.6 引入的优化:把本来要在 Server 层做的 WHERE 过滤,下推到存储引擎层,在遍历索引的时候就顺手判断掉。好处是不满足条件的行直接不回表。Extra 里出现 Using index condition 就说明它生效了。
实测输出-- ICP 关闭 type=range key=idx_name_age rows=111 Extra=Using where -- ICP 打开 type=range key=idx_name_age rows=111 Extra=Using index condition -- 实际命中 COUNT(*) = 3
面试怎么答索引下推是 5.6 引入的优化,把 Server 层的 WHERE 过滤下推到存储引擎,在遍历二级索引时就用索引里已有的列判断,不满足条件的行不再回表。判断是否生效看 EXPLAIN 的 Extra,出现 Using index condition 就是生效了。实测一个 LIKE 前缀加第二列等值的查询,候选 111 行最终只命中 3 行,没开 ICP 要回表 111 次,开了之后只回表 3 次。注意 rows 显示的还是索引扫描条目数,不会因为 ICP 变小,所以不能拿 rows 判断。它只减少回表不减少扫描,条件必须落在索引列上,默认开启不需要手动配置。
8

深翻页:从 9005 次回表降到 5 次★

LIMIT 大偏移量为什么越翻越慢
LIMIT 9000, 5 并不是"跳过 9000 行只读 5 行",而是先老老实实取出 9005 行,再把前 9000 行扔掉。如果每行还要回表,那就是 9005 次随机 IO。改成先在索引上取 5 个主键再回表,回表次数就只有 5 次,这个改法叫延迟关联。
实测输出-- 直接深翻 type=index key=PRIMARY rows=9005 Extra=(空) -- 延迟关联,子查询部分 type=index key=PRIMARY rows=9005 Extra=Using index ← 只扫索引,不回表 -- 延迟关联,外层关联 type=eq_ref key=PRIMARY rows=1 ← 只回表 5 次 -- 两者结果一致,首行 id 都是 9001
面试怎么答LIMIT offset, size 的语义是先取 offset+size 行再丢弃前 offset 行,所以偏移越深扫得越多。如果查询还要回表,深翻页就变成大量随机 IO。优化方法是延迟关联:子查询只在索引上取目标行的主键,EXPLAIN 里会显示 Using index 说明不回表,外层再用主键关联只回表 size 次。实测扫 9005 行的场景,直接查要回表 9005 次,延迟关联只回表 5 次。更彻底的是游标分页,用 WHERE id > 上一页最后 id 加 LIMIT,任何页都能直接定位,代价是不能跳页。如果排序字段没索引,得先解决排序问题,延迟关联不起作用。
9

MVCC:同一事务里为什么读出三个结果★

快照读、当前读,一次实验看清
InnoDB 靠 MVCC 实现"读不加锁也能不阻塞"。每行数据藏了两个隐藏字段:DB_TRX_ID(最后修改它的事务号)和 DB_ROLL_PTR(指向 undo log 里上一个版本)。普通 SELECT 是快照读,按事务开始时的 ReadView 挑一个可见的版本;FOR UPDATE 是当前读,永远读最新提交的版本并加锁。
实测输出(两个连接,RR 隔离级别,初始 balance=100)A 快照读 -> 100 B 执行 UPDATE 并提交 -> 999 A 再快照读 -> 100 ← 事务没结束,看到的还是老版本 A 当前读 FOR UPDATE -> 999 ← 当前读绕过快照,读最新 A 再快照读 -> 100 ← 又回到快照
面试怎么答InnoDB 每行有 DB_TRX_ID 和 DB_ROLL_PTR 两个隐藏字段,undo log 里保存旧版本并用回滚指针串成版本链。普通 SELECT 是快照读,按事务开始时的 ReadView 从版本链里挑可见版本;FOR UPDATE、UPDATE、DELETE 是当前读,直接读最新提交版本并加锁。实测在一个 RR 事务里,另一个事务提交了更新后,快照读仍是旧值,同一事务内执行 FOR UPDATE 能读到新值,之后再普通读又回到旧值,这正好证明快照和当前读是两套机制。RR 的 ReadView 在事务开始时创建一次,RC 每条语句都重建,这是两者可见性差异的根源。MVCC 只解决读写冲突,写写冲突仍然靠行锁。
10

四种隔离级别,RR 到底解决了什么★

同一个实验,RC 和 RR 三个结果全不同
四个隔离级别解决的是三类问题:脏读(读到别人没提交的)、不可重复读(同一事务两次读不一样)、幻读(同一范围两次读数变多)。MySQL 的默认级别是 RR(可重复读),靠 MVCC 的一致性读解决不可重复读,靠间隙锁解决当前读时的幻读。但 RR 下的快照读本身就不会有幻读,因为整张表看到的都是同一个快照。
实测输出(两个连接对照)=== READ COMMITTED === 快照读 id=1:10 -> B 改成 999 -> 999(不可重复读) 快照读 COUNT:2 -> B 插入 id=9 -> 3(出现幻影行) 当前读 COUNT(*):3 === REPEATABLE READ === 快照读 id=1:10 -> B 改成 999 -> 10(可重复读) 快照读 COUNT:2 -> B 插入 id=9 -> 2(没有幻影行) 当前读 COUNT(*):3(看到了幻影行)
面试怎么答四种隔离级别解决脏读、不可重复读和幻读三类问题。MySQL 默认 RR。实测同一段操作:RC 下两次快照读分别拿到 10 和 999,行数从 2 变成 3,说明既有不可重复读又有幻读;RR 下两次都是 10、行数都是 2,因为整个事务共用同一个 ReadView。但 RR 下把查询换成 FOR UPDATE 做当前读,行数变成 3,幻影行出现了。所以准确的说法是:RR 在快照读下靠 MVCC 避免幻读,在当前读下靠间隙锁避免幻读,两者缺一不可。RC 虽然没有间隙锁、一致性更弱,但并发更好、死锁更少,不少公司会主动选它。
11

间隙锁:为什么 RR 下插不进去★

从 data_locks 里看真实加了哪几把锁
InnoDB 的行锁有三种形态:记录锁(锁住某一行)、间隙锁(锁住两条记录之间的空隙,只阻止插入)、临键锁(记录锁 + 前面的间隙,范围查询时的默认形态)。间隙锁只在 RR 及以上级别存在,RC 下没有,这是很多人从 Oracle 或 PostgreSQL 转过来最容易困惑的地方。
实测输出=== REPEATABLE READ === A 锁住 id BETWEEN 2 AND 4,实际只命中 id=3 B 插 id=2(落在被锁区间内) -> 被锁挡住,错误 1205,等了 2.0 秒 B 插 id=6(落在区间外) -> 成功 === READ COMMITTED(同样语句)=== B 插 id=2(落在被锁区间内) -> 成功,没有间隙锁 B 插 id=6(落在区间外) -> 成功
面试怎么答InnoDB 行锁有记录锁、间隙锁、临键锁三种形态,范围查询在 RR 下默认加临键锁,也就是记录锁加左边的间隙。间隙锁只阻止插入不阻止读取,只在 RR 及以上级别存在,RC 没有。实测在 id 为 1、3、5、7、9 的表上对 BETWEEN 2 AND 4 加 FOR UPDATE,虽然只命中 id=3 一行,但插入 id=2 会被阻塞到锁等待超时,插入 id=6 则正常,说明锁的范围是 id=3 及其左边的空隙。用 performance_schema.data_locks 能看到真实的锁模式,X,REC_NOT_GAP 是纯记录锁,X,GAP 是纯间隙锁,没有后缀是临键锁。想减少锁冲突,优先用唯一索引做等值查询,或者在 RC 下运行。
12

死锁:谁被牺牲,怎么排查

不是"避免死锁",是"控制死锁的代价"
死锁是两个事务各持一把锁、又去等对方那把,形成环。InnoDB 有死锁检测(innodb_deadlock_detect,默认开),发现环后主动回滚其中一个事务,报 1213,另一个正常提交。所以死锁不是"数据库坏了",而是一种被设计的正常现象,关键是把它的代价控制在"重试一次就好"。
实测输出A 先锁 id=1:拿到 id=1 的锁 B 先锁 id=2:拿到 id=2 的锁 A 再锁 id=2:也拿到,正常提交(InnoDB 选了 B 做牺牲者) B 再锁 id=1:错误 1213,被选为牺牲者回滚 SHOW ENGINE INNODB STATUS 里的死锁记录: *** (1) TRANSACTION: LOCK WAIT 3 lock struct(s), 2 row lock(s), undo log entries 1 UPDATE t_lock SET v = v + 1 WHERE id = 1
面试怎么答死锁是两个事务各持有一把锁又互相等待形成的环。InnoDB 默认开启死锁检测,发现环后会主动回滚其中一个事务并报 1213,另一个正常提交。要和 1205 区分开:1213 是死锁,立刻重试通常成功;1205 只是锁等待超时,默认 50 秒,往往是长事务占锁造成,不一定是死锁。排查用 SHOW ENGINE INNODB STATUS 看 LATEST DETECTED DEADLOCK 段,里面有两个事务的完整现场。预防上最有效的是统一加锁顺序,批量更新前按主键排序;其次是缩短事务,不在事务里做 RPC、发消息这类耗时操作。高并发场景如果死锁检测消耗 CPU 明显,可以关掉检测让超时兜底。
13

三大日志与两阶段提交

redo、undo、binlog 各管一件事
三个日志分工完全不同:redo log 是 InnoDB 的物理日志,保证崩溃后已提交的数据不丢;undo log 是逻辑日志,用于事务回滚和 MVCC 的版本链;binlog 是 Server 层的逻辑日志,用于主从复制和数据归档。redo 和 binlog 必须保持一致,靠"两阶段提交"保证。
实测默认值(MySQL 8.0.41)innodb_flush_log_at_trx_commit 1 sync_binlog 1 binlog_format ROW
面试怎么答redo log 是 InnoDB 的物理日志,记录页级别的修改,用于崩溃恢复,采用 WAL 方式把随机写变成顺序写,所以事务提交只要 redo 落盘就算成功。undo log 是逻辑日志,记录反向操作,既用于事务回滚,也用于 MVCC 的版本链。binlog 是 Server 层的逻辑日志,用于主从复制和归档,格式推荐 ROW。redo 和 binlog 靠两阶段提交保持一致:先写 redo 并置为 prepare,再写 binlog,最后把 redo 改成 commit;崩溃恢复时如果 binlog 完整就提交,不完整就回滚。双 1 配置指 innodb_flush_log_at_trx_commit=1 和 sync_binlog=1,保证每次提交都刷盘,是数据安全性和吞吐量的权衡点。
14

count 与 NULL:四种写法差在哪

一个 5 行的表就能测出差异
COUNT(*) 和 COUNT(1) 在 InnoDB 里优化后完全等价,都统计行数;COUNT(列) 会跳过该列为 NULL 的行;COUNT(DISTINCT 列) 先对非 NULL 值去重再计数。所谓"count(1) 比 count(*) 快"是 MyISAM 时代的老说法,InnoDB 里不成立。
实测输出(5 行数据,其中 2 行 city 为 NULL)COUNT(*) = 5 COUNT(1) = 5 COUNT(city) = 3 COUNT(DISTINCT city) = 2
面试怎么答InnoDB 里 COUNT(*) 和 COUNT(1) 优化后等价,都统计行数,优化器会选最小的索引来扫,所以这两种是最快的。COUNT(列) 要跳过该列为 NULL 的行,必须把列值读出来判断,没法用只统计行数的优化,而且语义不同。实测一张 5 行、其中 2 行 city 为 NULL 的表,COUNT(*) 和 COUNT(1) 都是 5,COUNT(city) 是 3,COUNT(DISTINCT city) 是 2。InnoDB 不能像 MyISAM 那样直接返回总行数,是因为 MVCC 下不同事务看到的行数不同,没有全局唯一的总数。千万级表上 COUNT(*) 本身就慢,应该改用计数表或缓存,而不是换写法。
15

慢查询怎么定位:从日志到执行计划

别一上来就改 SQL
定位慢查询有固定顺序:先开慢日志找到是哪条 SQL → 用 explain 看执行计划 → 确认是索引问题还是数据量问题 → 再动手改。跳过前两步直接"我觉得加个索引会快"是最常见的错误,因为很多慢查询根本不是索引问题,而是数据量、锁等待或返回值太大。
实测默认值slow_query_log OFF long_query_time 10.000000 slow_query_log_file E:\...\data\DESKTOP-xxx-slow.log
面试怎么答定位慢查询有固定顺序:先确认慢日志开了没有,默认是关闭且阈值 10 秒,所以很多库说没有慢查询其实只是没记录。开了之后用 pt-query-digest 按总耗时聚合,重点找单次不慢但调用量极大的 SQL。拿到具体 SQL 后跑 explain,看 type、key、rows、Extra 判断是不是索引问题。要注意慢查询有三种不同原因:扫描行数多属于索引问题,扫描不多但慢往往是锁等待,返回数据量大是查询设计问题,三者解法完全不同。MySQL 8.0 还可以直接查 performance_schema 的 events_statements_summary_by_digest,按 SQL 指纹聚合,比慢日志更实时。优化后要用 query_cost 和 rows 验证效果,不能只凭感觉。
16

分库分表:什么时候该分,分的代价是什么

这是最后手段,不是第一选择
单表千万级、磁盘或连接数成为瓶颈,才考虑分库分表。在那之前有一整串成本更低的方案:加索引、覆盖索引、读写分离、归档历史数据、加缓存、改查询设计。分库分表一旦做了就几乎回不去,因为它把"一个事务能保证的事"打散到了多个库,分布式事务、跨库 JOIN、全局排序、分页、唯一 ID 全部要重新解决。
面试怎么答分库分表是最后手段,触发条件应该是单表数据量到千万级、磁盘 IO 或连接数成为实际瓶颈,而不是"表大了"这种模糊判断。在它之前有一段成本低得多的路:索引优化、读写分离、大字段拆表、历史数据归档、加缓存、改查询设计。分片键的选择最关键,必须覆盖大部分查询条件,否则按订单号查就要扫所有分片。分完之后有五个问题必须重新解决:跨库事务、跨库 JOIN、跨库分页排序、全局唯一 ID、扩容迁移。实践中建议先分表后分库,因为单库内分表还能用本地事务,成本最低。
17

MyBatis 的四个坑

用框架不等于不用想 SQL
MyBatis 层面最容易出问题的是四件事:${} 会拼接字符串导致注入和索引失效、N+1 查询、二级缓存在多表关联下返回脏数据、批量操作没走 batch。这四个都是"框架帮你做了决定,但你没意识到"的类型。
面试怎么答MyBatis 有四个常见坑。第一是 ${} 做字符串拼接,既有 SQL 注入风险又无法预编译,拼在 WHERE 里还会导致索引失效,只有表名列名这类结构才能用,而且必须白名单校验,参数值一律用 #{}。第二是 N+1,嵌套 collection 查询默认每行发一次 SQL,应该改成 JOIN 加 resultMap 组装,或者批量 IN。第三是二级缓存作用域是 Mapper 命名空间,多表关联写入时一个 Mapper 的更新清不掉另一个的缓存,会读到脏数据,所以不建议开。第四是批量操作要用 foreach 拼多值 INSERT 或 ExecutorType.BATCH,不要循环单条插入。
18

速答清单

十九条,考前扫一遍
01MySQL 8.0 还有查询缓存吗?
没有,8.0 已彻底移除。失效粒度是整张表,写多的场景命中率极低,维护成本高于收益。
02一条 SQL 在 MySQL 里经过哪些阶段?
连接器、解析器、优化器、执行器、存储引擎。能优化的只有后两段,优化器决定用哪个索引。
03为什么用 B+ 树不用红黑树?
B+ 树单节点分支多,树更矮,磁盘 IO 次数等于树高。红黑树二叉,同样数据高度高好几倍。
04三层 B+ 树能放多少行?
按主键 4 字节、每页放 1170 个指针、叶子每页约 100 行算,约 1.3 亿行;实际受单行大小影响。
05主键为什么建议短且递增?
短能让非叶子页放更多指针、降低树高;递增让新行总在最右追加,避免随机插入导致的页分裂。
06什么是回表?
二级索引叶子只存索引列和主键,查非索引列时要拿主键再查一次聚簇索引,这次随机 IO 就是回表。
07怎么避免回表?
用覆盖索引,即 SELECT 的列全在索引里,EXPLAIN 的 Extra 会显示 Using index。
08key_len 怎么算?
VARCHAR(50) 的 utf8mb4 非空列 = 50×4+2 = 202 字节,INT 占 4 字节。用它反推联合索引用到了哪几列。
09联合索引为什么跳列就失效?
B+ 树只保证在 a 相同的前提下 b 有序,a 不确定时 b 在整棵树上是乱序的,无法二分定位。
10哪些情况索引会失效?
列上套函数或运算、隐式类型转换、LIKE 以通配符开头、选择性太低、不等于覆盖大部分行、OR 里有一边没索引。
11字符串列传数字为什么会全表扫?
MySQL 会把字符串列逐行转成数字比较,等效于每行套了函数,索引顺序失效。字符串列必须传字符串。
12type 字段的优先级?
system、const、eq_ref、ref、range、index、ALL。看到 ALL 要处理,看到 index 也只是改扫索引。
13rows 是真实行数吗?
不是,是优化器基于统计信息的估算值,只能看数量级。比较优化前后用 EXPLAIN FORMAT=JSON 的 query_cost。
14索引下推是什么?
把 WHERE 过滤下推到存储引擎,在遍历索引时就用索引列判断,不满足的不回表。Extra 显示 Using index condition。
15深翻页为什么慢?怎么优化?
LIMIT offset 是先取 offset+size 行再丢弃,偏移越深扫得越多,还要逐行回表。用延迟关联只取主键,或改游标分页。
16MVCC 靠什么实现?
行上的 DB_TRX_ID 和 DB_ROLL_PTR 隐藏字段,undo log 里保存旧版本串成版本链,ReadView 决定哪个版本可见。
17快照读和当前读有什么区别?
普通 SELECT 是快照读,按 ReadView 读历史版本;FOR UPDATE、UPDATE、DELETE 是当前读,读最新提交版本并加锁。
18RR 怎么解决幻读?
快照读靠 MVCC 一致性读,整个事务共用一个 ReadView;当前读靠间隙锁阻止其他事务插入新行。两者缺一不可。
19间隙锁什么时候存在?
只在 RR 及以上级别存在,RC 没有。范围查询加临键锁(记录锁加左边间隙),唯一索引等值查询只加记录锁。
201213 和 1205 有什么区别?
1213 是检测到死锁,事务被主动回滚,立刻重试通常成功;1205 是锁等待超时,默认 50 秒,往往是长事务占锁。
21死锁怎么预防?
统一加锁顺序(批量更新前按主键排序)、缩短事务(不在事务里做 RPC 或发消息)、用唯一索引减少锁范围。
22redo、undo、binlog 各管什么?
redo 是 InnoDB 物理日志管崩溃恢复,undo 是逻辑日志管回滚和 MVCC,binlog 是 Server 层逻辑日志管主从复制和归档。
23什么是两阶段提交?
写 redo 置 prepare,写 binlog,再把 redo 改 commit。崩溃恢复时 binlog 完整就提交,不完整就回滚,保证两日志一致。
24COUNT(*) 和 COUNT(1) 有区别吗?
InnoDB 里优化后等价,都统计行数并选最小索引扫。COUNT(列) 会跳过 NULL,语义不同且更慢。
25为什么 InnoDB 不能直接返回总行数?
MVCC 下不同事务看到的行数不同,没有全局唯一的总数,只能现算。MyISAM 没这问题所以能缓存行数。
26分库分表前该先做什么?
索引优化、读写分离、大字段拆表、历史数据归档、加缓存、改查询设计。分片键必须覆盖主要查询条件。
27#{ } 和 ${ } 的区别?
#{} 是参数绑定走 PreparedStatement,安全且能预编译;${} 是字符串拼接,有注入风险且可能导致索引失效。