简介:这是一份围绕 MySQL 面试考点整理的问答式复习文档,面向准备后端开发、数据库相关岗位面试的求职者,也适合希望系统梳理 MySQL 基础概念的开发者查漏补缺。内容覆盖关系型与非关系型数据库的区别、一条 SQL 语句从连接器、查询缓存、分析器、优化器到执行器的完整流程,以及索引的使用原因、三种常见底层数据结构、主键索引与二级索引、MyISAM 与 InnoDB 的 B+ 树索引实现差异、普通索引与唯一索引的取舍、覆盖索引与索引下推、索引失效的常见操作等高频问题,并延伸到 change buffer、redo log 等日志机制。资源包为 1 个 docx 文件,约 40KB,轻量便于打印翻阅;已有 252 人学习下载。按主题分块的问答组织方式适合面试前快速串讲与自查,可帮助读者在较短时间建立起从 SQL 执行链路到索引优化的整体认知。
1. 一份"MySQL面试题"资料,为什么我建议你当执行手册用
上周帮一个做后端的同学看慢查询,他一边翻自己整理的笔记一边嘀咕:"索引失效那几条我背过啊,怎么真到 EXPLAIN 就傻眼了。"这不是个例。面试题类资料最大的问题不是内容错,而是它按"考点"组织,不按"执行链路"组织——你会背"like %xx 索引失效",但不知道优化器为什么放弃索引,也就没法判断什么情况下它其实还会用索引。这份 MySQL 面试题资料的价值在于,它把一条 SQL 从连接器到引擎层、从索引结构到 redo log 与 binlog 两阶段提交、从主备同步到误删恢复串成了一条完整的链路,覆盖了索引原理、日志体系、执行流程、数据恢复四大块。适合两类人:准备面试想建立体系而不是背答案的,以及日常写 SQL、调慢查询、偶尔要碰备份恢复的一线开发。下面我按"怎么拿它干活"来拆。
2. 先立住执行链路:一条 SQL 从连接器到引擎层到底发生了什么
资料里第 2 题把 Server 层的执行顺序写得很直白:连接器 → 查询缓存 → 分析器 → 优化器 → 执行器 → 引擎层取数。但只记住顺序没用,真正能救命的是知道每一层各自会因为什么原因让一条 SQL 变慢或者变样。我一般把这条链路拆成三个可观测的节点:连接层看会话状态,优化器层看执行计划,执行器层看扫描行数与回表次数。
2.1 连接器与分析器:先排除"根本不是 SQL 慢"的情况
很多"这条 SQL 突然变慢"的工单,最后查出来是连接被占满或者锁等待。连接器负责验证身份和授予权限,一旦连接数打满,新请求会卡在 TCP 握手之后,表现出来却是应用超时。所以排查第一步永远是看当前会话和连接状态,而不是上来就 EXPLAIN。
-- 看当前连接数与最大连接数,判断是否连接打满 SHOW STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections'; -- 看当前正在执行、可能阻塞的会话 SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE command <> 'Sleep' ORDER BY time DESC LIMIT 20;第一段查的是已建立连接数和配置上限,两者的比值长期超过 0.8,说明连接池配置或者连接释放逻辑有问题。第二段查的是非休眠会话,time是这条语句已经执行的秒数,state字段里出现Waiting for table metadata lock或Waiting for row lock就直接说明是锁问题,跟你 SQL 写得好不好没关系。我习惯把time倒序,因为真正拖垮实例的往往是那一两条长事务,而不是每秒几百次的小查询。
分析器这一步做词法分析和语法分析。它不出现在执行计划里,但有一类坑和它直接相关:隐式类型转换。资料第 9 题提到"对索引进行隐式转换相当于使用了新函数",这句话在实战里最常见的形态是字符串列和数字比较。比如user_id是 varchar,你写WHERE user_id = 12345,MySQL 会把列值转成数字再比,索引直接失效。验证方式很土但准:把常量加引号对比两次 EXPLAIN,看key字段是否从索引名变成 NULL。
2.2 优化器与执行计划:区分"没走索引"和"走了但没用"
优化器的职责是选一个它认为代价最低的方案,注意是"它认为"。所以你不能用自己的直觉判断索引该不该用,得看优化器的账本。资料第 12 题提到统计信息与区分度,这就是它在算账时用的数据。
-- 查看执行计划,重点关注 type、key、rows、Extra EXPLAIN SELECT id, order_no, amount FROM t_order WHERE user_id = 10086 AND status = 1 ORDER BY create_time DESC LIMIT 20; -- 进一步看优化器估算的行数与实际偏差(MySQL 8.0 可用) EXPLAIN ANALYZE SELECT id, order_no, amount FROM t_order WHERE user_id = 10086 AND status = 1 ORDER BY create_time DESC LIMIT 20;type字段从好到坏大致是const / eq_ref / ref / range / index / ALL,出现index要警惕,它表示扫了整棵索引树,比ALL好不了多少。rows是优化器估算的扫描行数,如果这个数和实际差了一两个数量级,通常是统计信息过期,ANALYZE TABLE t_order之后再看。Extra里出现Using filesort说明排序没走索引,出现Using temporary说明用了临时表,这两个在数据量上来之后是性能杀手。EXPLAIN ANALYZE会给出真实执行耗时和实际行数,用来验证优化器的估算准不准,这个结论比拍脑袋靠谱。
这里有个容易被忽略的点:资料第 8 题讲的覆盖索引,在Extra里表现为Using index。这个标志意味着查询的所有列都能从索引里拿到,不需要回表。我在做列表页接口优化时,第一件事就是把 SELECT 的列收窄到索引覆盖范围内,往往比加索引见效更快,因为不用改动表结构。
2.3 执行器与回表:扫描行数是怎么被放大的
执行器拿到优化器给的方案后,会先检查权限,然后调用引擎接口逐行取数。资料第 5 题把主键索引(聚簇索引,叶子节点存整行)和二级索引(叶子节点存主键值)的区别讲清楚了,这个区别直接决定了回表成本。一条走二级索引的查询,如果 SELECT 的列不在索引里,每一行都要再拿主键去聚簇索引里查一次,这就是回表。
举个具体场景:t_order上有索引idx_user_status(user_id, status),查询SELECT id, order_no, amount FROM t_order WHERE user_id = ? AND status = ?。前两列id和user_id、status都在索引里,但order_no和amount不在,所以每一行都要回表。如果这个用户有 5 万条订单,回表就是 5 万次随机 IO。常见做法是把高频查询需要的列做联合索引覆盖,或者干脆做窄表冗余,代价是索引变大、写入变慢,得按读写比权衡。
理解完这条链路,再看资料里索引、日志、恢复那些题目,你会发现它们不是孤立知识点,而是这条链路上不同环节的展开。第 3 章就从索引数据结构开始,往磁盘 IO 这个物理层面再走一层。
3. 索引结构选型:B+ 树、change buffer 与索引下推的边界
资料用第 3 到第 10 题讲索引,篇幅不小,但分散着看容易只记住结论。我把它们归成三个决策点:为什么是 B+ 树而不是哈希或有序数组、普通索引还是唯一索引、什么时候该用覆盖索引和索引下推。每个决策点都对应一个可以验证的实验。
3.1 三种底层数据结构:N 叉树为什么赢了
资料给的结论很清楚:哈希表适合等值查询不支持范围查询,有序数组适合静态存储但更新成本高,N 叉树兼顾读写并适配磁盘访问模式。这套结论背后有一个很关键的量化例子:InnoDB 一个整数字段索引的 N 差不多是 1200,树高为 4 时能存 1200 的三次方,约 17 亿。根节点常驻内存,所以 10 亿行的表上查一个值最多访问 3 次磁盘。
这个数字值得你亲手算一遍,因为它决定了很多参数直觉。比如为什么单表建议控制在千万级、为什么联合索引不要超过 5 列、为什么主键要短。页大小 16KB,一个 bigint 主键 8 字节加指针 6 字节左右,一页能放千余个索引项;如果你的主键换成 36 字节的 UUID,一页只能放三百多个,同样的数据量树高会多一层,磁盘 IO 直接翻倍。这是我在做分库分表方案评审时必问的一个问题:主键用什么类型,为什么。
哈希索引在 InnoDB 里其实是自适应的,你建不了显式的哈希索引(MEMORY 引擎除外),但 InnoDB 会为热点等值查询自建自适应哈希索引。资料第 34、35 题提到 MEMORY 引擎把数据放内存,读写快但重启丢数据、锁粒度粗,生产环境我一般不推荐用内存表承载业务数据,理由就这两条,尤其是主备同步时备库重启会导致同步线程停止,这个坑比性能收益严重得多。
3.2 普通索引与唯一索引:change buffer 那笔账
资料第 7 题末尾给了个反直觉建议:业务能接受的话,优先用非唯一索引,因为唯一索引用不上 change buffer。第 11 题把 change buffer 讲透了:更新一个不在内存中的数据页时,InnoDB 先把更新缓存在 change buffer,等下次查询这个页时再 merge,省掉一次随机读。
这笔账要算清楚得看业务模型。写多读少、且写入后不会立刻被查询的场景,比如账单流水、日志落库,change buffer 收益最大。反过来说,写入后马上要查的场景,merge 立刻触发,不但没省 IO,还多了维护 change buffer 的开销。所以"优先非唯一索引"这句话有前提,不能无脑套。
-- 查看 change buffer 的实际大小与合并情况 SHOW ENGINE INNODB STATUS\G -- 输出中关注 INSERT BUFFER AND ADAPTIVE HASH INDEX 段落 -- 相关参数:change buffer 最多占 buffer pool 的比例 SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';innodb_change_buffer_max_size默认 25,表示最多占 buffer pool 的四分之一。如果这个值长期接近上限,说明 change buffer 用得很满,写多读少的特征明显,可以考虑适当调大;但调大意味着留给数据页缓存的池子变小,读性能会受影响,这是典型的零和权衡。判断依据还是看业务的读写比,而不是看哪个参数看着舒服。
顺便说一个我踩过的坑:有次为了"规范"把一批普通索引改成了唯一索引,结果那段时间批量导入的耗时明显上升。原因就是唯一索引每次插入都要去校验唯一性,必须把目标页读进内存,change buffer 完全用不上。改回普通索引、唯一性交给业务层校验之后恢复正常。所以资料那句"业务可以接受的话",重点在"可以接受",唯一约束是正确性保障,不能为了性能随便丢。
3.3 覆盖索引与索引下推:两个减回表的工具
覆盖索引判定的标准只有一个:查询涉及的列是否全部在索引中。资料举的例子是索引 k 覆盖了查询需求,实践中表现为Extra出现Using index。它的收益是免掉回表,减少树的搜索次数。
索引下推(ICP,MySQL 5.6 引入)解决的是另一个问题:联合索引(a, b)上查询WHERE a = ? AND b LIKE '%xx%',按老逻辑,存储引擎只按a定位到索引项,把所有行回表后再由 Server 层过滤b;有了 ICP,引擎在索引遍历阶段就先判断b的条件,不满足的直接跳过,不回表。资料说它"减少回表次数",准确说是把过滤提前到了引擎层。
-- 验证索引下推是否生效:Extra 中出现 Using index condition EXPLAIN SELECT * FROM t_user WHERE city = 'A' AND name LIKE '%张%'; -- 会话级开关,用于对比实验(生产环境不要随手关) SET optimizer_switch = 'index_condition_pushdown=off'; EXPLAIN SELECT * FROM t_user WHERE city = 'A' AND name LIKE '%张%'; SET optimizer_switch = 'index_condition_pushdown=on';关掉之后Extra里的Using index condition会消失,你可以对比两次的rows估算值和实际耗时。这个实验我建议每个做 SQL 优化的人都做一遍,做完你就明白为什么"like %xx 一定失效"这种说法不严谨:如果前缀条件由联合索引的前导列承担,后面的模糊匹配仍然可以在索引层过滤,只是%xx本身无法用于定位起始位置。资料第 9 条说的失效,指的是模糊匹配列作为唯一或最左条件的情况。
3.4 字符串索引的四种建法:按区分度选
资料第 10 题给了四种方案:完整索引、前缀索引、倒序存储加前缀索引、额外 hash 字段。选择依据是字符串本身的区分度和查询模式。邮箱这类前缀区分度高的,前缀索引就够;身份证、订单号这类前缀高度重复的,前缀索引会大量回表,倒序存储或者 hash 字段更合适。
| 方案 | 空间 | 查询扫描 | 覆盖索引 | 范围查询 |
|---|---|---|---|---|
| 完整索引 | 大 | 一次定位 | 支持 | 支持 |
| 前缀索引 | 小 | 可能多次回表 | 不支持 | 支持 |
| 倒序 + 前缀 | 小 | 一次定位 | 不支持 | 不支持 |
| 额外 hash 字段 | 中 | 一次定位 | 视字段而定 | 不支持 |
选的时候先问自己三个问题:这个列要不要范围查询、查询是否高频、写入量多大。三个问题答完基本就定了。前缀索引的长度怎么定,常见做法是算区分度,COUNT(DISTINCT LEFT(col, n)) / COUNT(*)接近 1 就够用,n 从 6 开始往上试。
索引这块的决策讲完了,接下来看日志体系。资料第 11 到 25 题几乎全在讲 redo log、binlog 和两阶段提交,这也是面试里最容易答成背诵、实战里最容易出事的部分。
4. 日志与两阶段提交:redo log 与 binlog 的分工和崩溃恢复
这一章我按"为什么需要 → 各自怎么工作 → 崩溃时怎么判断"来组织。资料的核心结论是:redo log 保证 crash-safe,binlog 做归档和主从复制,两者靠两阶段提交保持一致。记住结论没用,你要能在出问题时判断该看哪个日志、恢复到哪个位点。
4.1 WAL 与 redo log 的循环写
资料第 14、17、22 题讲的是同一件事:更新先写日志再异步刷盘,这就是 WAL。redo log 由内存中的 redo log buffer 和磁盘上的 redo log file 组成,写入路径是 buffer → OS buffer → 磁盘文件。innodb_flush_log_at_trx_commit控制这条路径的节奏:0 是每秒写一次,1 是每次提交都 fsync,2 是每次提交写到 OS buffer、每秒 fsync。
这三个值的取舍非常具体。设为 1 最安全,每次事务提交都落盘,代价是每次提交至少一次 fsync,高并发写入时这是明显的性能瓶颈。设为 2 时,MySQL 进程崩溃不丢数据(数据在内核 buffer 里),但机器断电会丢最近一秒。设为 0 时,MySQL 进程崩溃就可能丢一秒的数据。我一般建议核心交易链路设 1,日志类、可重放类业务设 2,设 0 的场景我基本没见过合理的。
redo log 的循环写结构资料第 24 题讲得很形象:四个文件首尾相连,write pos 是写位置,checkpoint 是擦除位置,两者之间是空闲区。write pos 追上 checkpoint 就意味着日志写满,此时必须停下来推进 checkpoint,也就是把脏页刷盘。这个机制解释了一个常见现象:批量更新时突然出现规律性的性能抖动。
-- 查看 redo log 容量与刷盘相关参数 SHOW VARIABLES LIKE 'innodb_log_file_size'; SHOW VARIABLES LIKE 'innodb_log_files_in_group'; SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit'; -- MySQL 8.0.30 之后用这个看 redo 容量 SHOW VARIABLES LIKE 'innodb_redo_log_capacity';innodb_log_file_size乘以组数就是总容量。容量太小会导致频繁 checkpoint,表现为写入抖动;容量太大则崩溃恢复时间变长,因为要重放更多日志。常见做法是给到能容纳一小时左右的写入量,再结合innodb_io_capacity观察脏页比例。MySQL 8.0.30 之后统一用innodb_redo_log_capacity控制,老参数逐步退役,看版本选参数。
4.2 binlog 三种格式与两阶段提交
资料第 23 题讲了 statement、row、mixed 三种格式。statement 记 SQL 语句,日志量小但某些函数、存储过程在主备上执行结果可能不一致;row 记每行的前后镜像,精确但日志量大;mixed 由 MySQL 按语句类型自动选。现在生产环境基本默认 row,原因很实际:数据恢复和闪回工具都依赖行镜像,statement 格式下你没法把一条 update 精确反转。
资料第 20 题的两阶段提交是重点:redo log 先写 prepare,然后写 binlog,最后 redo log 写 commit。崩溃恢复时的判断逻辑是——redo log 是 commit 状态,说明 binlog 也写成功了,直接恢复;redo log 是 prepare 状态,就去查对应事务的 binlog 是否完整,完整则提交,不完整则回滚。资料第 21 题补了一句判断 binlog 完整性的方法:statement 格式看结尾有没有 COMMIT,row 格式看有没有 XID event。
# 用 mysqlbinlog 查看某个 binlog 文件里的事务边界 mysqlbinlog --base64-output=DECODE-ROWS -v \ --start-datetime="2024-01-01 10:00:00" \ --stop-datetime="2024-01-01 11:00:00" \ mysql-bin.000123 > /tmp/binlog_preview.sql这段命令做的是把 binlog 解码成可读 SQL,--base64-output=DECODE-ROWS让 row 格式的镜像以明文呈现,-v输出详细注释。时间范围参数用来缩小排查窗口,误删恢复时你只需要定位到误操作前后那几分钟。输出里能看到Xid = xxx这样的标记,它就是事务边界,也是判断 binlog 是否完整的依据。养成先解码到临时文件再看的习惯,别在生产机上直接less大文件。
4.3 崩溃后的恢复推演
资料第 16 题把崩溃场景分了三种:redo log 未 commit、binlog 未 fsync,数据丢失;redo log 未 commit 但 binlog 已 fsync,先从 binlog 恢复 redo log 再恢复数据;两者都已 fsync,直接从 redo log 恢复。
这段内容面试常考,实战里对应的动作是:确认sync_binlog和innodb_flush_log_at_trx_commit的组合。双 1 配置是数据不丢的前提,sync_binlog=1表示每次事务提交都把 binlog fsync 到磁盘。有人为了性能把sync_binlog设成 0 或较大值,代价是崩溃时可能丢失多个事务的 binlog,而这些事务在主库上可能已经提交——主从不一致就是这么来的。
我见过一次真实的主从不一致,排查下来是备库的并行复制线程在某个大事务上卡住,导致延迟累积,切主时丢了一段。所以延迟监控和Seconds_Behind_Master之外,还要看 GTID 集合的差异,光看延迟秒数会骗人。
日志讲完,下一章进入最容易出事故的部分:慢查询排查、误删恢复和主备切换。这些是资料后半部分的内容,也是我日常被问得最多的。
5. 避坑与排查:慢查询、误删恢复、主备延迟的五个真实翻车点
资料第 25、28、29、30、37 题覆盖了慢查询、误删、删除语句区别和主备同步。这一章我按"现象 → 原因 → 解决"的形式整理五条踩坑记录,都是排查现场会遇到的形态。
5.1 坑一:SQL 忽然变慢,但执行计划没变
现象:同一条 SQL,昨天毫秒级,今天几秒,EXPLAIN 出来索引和 type 都一样。
原因:优化器用的是估算统计信息,数据分布变了估算就可能失真;另一种情况是 buffer pool 里的热数据被一次大查询挤出去,冷数据全走磁盘。还有一种最隐蔽的,是表上多了个长事务持有的 MDL 锁,查询在等锁而不是在扫数据。
解决:先看information_schema.processlist里有没有Waiting for table metadata lock,再看SHOW ENGINE INNODB STATUS的TRANSACTIONS段找长事务。确认不是锁之后,ANALYZE TABLE更新统计信息,再对比EXPLAIN ANALYZE的实际行数与估算行数。如果是 buffer pool 被污染,检查有没有大表全扫的报表查询跑在业务实例上,这种查询该挪到只读实例。
5.2 坑二:force index 强行指定索引后,另一条查询崩了
现象:给一条慢查询加了FORCE INDEX,它快了,但另一条原本正常的查询变慢。
原因:优化器是全局视角选索引,你强行指定等于剥夺了它的选择权。同一个表上的不同查询可能适合不同索引,强制指定只在特定查询上成立,但这个索引的维护成本会影响所有写入和其他查询的计划选择。
解决:FORCE INDEX是后悔药,不是常规手段。资料第 25 题给的三个方向更靠谱:改语句引导优化器(比如在 order by 后补列让排序走索引)、建更合适的索引、删掉误用的索引。我一般把 force index 当作临时止血,同时提一个工单去改索引结构,两周内必须清掉。
5.3 坑三:误删数据后用 binlog 恢复,结果把正常数据也覆盖了
现象:一张表误删了几百行,用 binlog 重放恢复,恢复完发现其他行的时间戳也被改了。
原因:恢复时重放的范围没控制准,把误删之前早已正确落库的 update 又执行了一遍;或者只重放了误删事务却没考虑它依赖的前置事务,导致数据不一致。
解决:资料第 28 题讲得对,先恢复到临时实例,核对无误再回灌主库。重放范围用--start-position和--stop-position精确定位,不要用时间范围图省事。误删恢复的前提是binlog_format=row且binlog_row_image=full,没有完整的前后镜像,闪回工具也无能为力。
-- 恢复前先确认这两个参数,缺一个闪回就不可靠 SHOW VARIABLES LIKE 'binlog_format'; SHOW VARIABLES LIKE 'binlog_row_image';binlog_format必须是 ROW,binlog_row_image必须是 FULL。MINIMAL 只记被改的列,闪回时无法还原完整行。
5.4 坑四:kill 命令执行了,语句还在跑
现象:KILL QUERY 12345之后,那个会话还在执行,CPU 不降。
原因:资料第 30 题列了三种情况——kill 命令被堵在队列里没到位、到位了但没被立刻触发、触发了但语句执行到安全点需要时间。大事务回滚尤其慢,因为 InnoDB 回滚要逐条撤销。
解决:先分清是 kill query 还是 kill connection。前者只终止当前语句,事务还在;后者断开连接,事务回滚。大事务回滚期间SHOW PROCESSLIST能看到state是Rolling back,这时候别急着反复 kill,观察innodb_trx里的回滚进度更实际。预防手段是控制单事务影响行数,批量更新分批提交。
5.5 坑五:主备延迟看着只有几秒,切主却丢了数据
现象:Seconds_Behind_Master显示 2 秒,切主后发现备库少了最近一批事务。
原因:这个指标在并行复制下会失真,它反映的是备库当前执行到的位点与主库的差距,但不代表所有事务都已应用。多线程复制的 worker 之间进度不同,指标只取最慢的那个,而某些场景下它的计算方式本身有偏差。
解决:看 GTID 集合差异,SELECT @@global.gtid_executed在两边对比,差集为空才算真正同步。资料第 37 题讲主备建立是基于位点由备库指定的,维护期一定要把位点和 GTID 都记录清楚。另外延迟备库(故意落后主库一段时间)是有价值的,误删时它能当后悔药用,但延迟时长要跟业务确认,半小时到一小时比较常见。
这五个坑覆盖了排查的主干。最后一章讲一个具体的进阶技巧:怎么把这份面试题资料变成一套可自测的实验清单,而不是读完就忘的背诵材料。
6. 把面试题变成实验清单:一套可自测的索引与日志验证流程
背下来的结论和亲手验证过的结论,在面试和实战里的表现完全不同。资料里每一条结论都对应一个可以本地复现的小实验,我把它整理成了一套流程,每次换环境或者带新人我都会走一遍,一个下午能跑完,但对 MySQL 的理解深度会上一个台阶。
第一步是准备环境。本地起一个 MySQL 8.x 实例,或者用容器起一个临时实例,建一张百万行左右的测试表,字段包括自增主键、varchar 类型的业务号、两个状态字段、一个时间字段、一个金额字段。
CREATE TABLE t_demo ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, biz_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0, amount DECIMAL(12,2) NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_status_time (user_id, status, create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表语句里我只放了一个联合索引,顺序是user_id, status, create_time,这个顺序本身就是实验材料。接着灌数据,用存储过程循环插入 100 万行,biz_no用随机字符串保证区分度,user_id用取模的方式让分布集中在少数几个值上——这样能造出"单值匹配行数很多"的场景,方便观察回表成本。
第二步是验证最左前缀。分别跑WHERE user_id = ?、WHERE status = ?、WHERE user_id = ? AND create_time > ?三条查询,看key和type。结论会是:只有包含user_id的才走索引,status单独查不走。这一步能亲手确认"最左前缀"不是玄学,是索引的物理排序决定的。
第三步是验证覆盖索引。对比SELECT user_id, status FROM t_demo WHERE user_id = ?和SELECT user_id, status, amount FROM ...,前者Extra有Using index,后者没有。然后看两者的执行耗时差异,数据量到百万级时差别会很明显,这个体感比任何文字描述都深刻。
第四步是验证索引下推。用WHERE user_id = ? AND status = ?对比开关 ICP 前后的rows。注意这里的条件列都在联合索引里,ICP 效果才明显;如果条件列不在索引里,ICP 无从下手。
第五步是验证日志与崩溃恢复。这一步风险高,只建议在临时实例上做。把innodb_flush_log_at_trx_commit设为 0,写入一批数据,然后用kill -9杀掉 mysqld 进程,重启后看数据丢没丢。再设回 1 重复一次,对比结果。这个实验做完,你对"为什么生产必须双 1"不会再有疑问。注意做之前确认实例是临时的、没有其他数据,别在主库上玩。
第六步是做一次误删恢复演练。先全量备份,再插入一批数据,然后模拟误删,用 binlog 定位到误删事务的位置,恢复到临时实例,核对数据。这套流程里最容易出错的不是命令本身,而是位点定位——我一般先用mysqlbinlog解码出可读文件,grep 定位到误删的那条 DELETE,记下 position,再用--stop-position卡在它之前重放。恢复演练我建议每季度做一次,平时不练,真出事时手忙脚乱是必然的。
顺手给一份参数核对清单,每次上线前对着过一遍,比临时翻文档靠谱:
| 参数 | 建议值 | 作用 |
|---|---|---|
| innodb_flush_log_at_trx_commit | 1 | 每次提交刷 redo log,保证不丢已提交事务 |
| sync_binlog | 1 | 每次提交刷 binlog,与上一项共同构成双 1 |
| binlog_format | ROW | 支持精确恢复与闪回 |
| binlog_row_image | FULL | 记录完整前后镜像 |
| innodb_buffer_pool_size | 物理内存的 50%~70% | 决定热数据命中率 |
| innodb_change_buffer_max_size | 25 | 写多读少可上调,需权衡读缓存 |
这份清单不是万能配置,每项都要按业务特点调。比如 buffer pool 在专用实例上可以给到 70% 以上,混部环境就得留余地。
那次误删演练给我的教训很直接:我原以为自己记得恢复流程,实际操作时在位点定位上卡了二十多分钟,因为没养成先解码再操作的习惯,直接在实例上乱试。从那以后我每次做恢复类操作都强制走一遍"先解码、核对位置、恢复到临时实例、再回灌"的流程,宁可慢半小时,不赌一次运气。这份面试题资源也一样,光读一遍只能应付面试,照着上面的实验清单跑一遍,才是真的把它变成了自己的东西。希望帮到你。
本文还有配套的精品资源,点击获取