1. 先把话说透面试官问B树到底在考什么“MySQL索引为什么用B树”——这道题在技术面试里的出现频率大概相当于手写单例模式在十年前的地位。很多人背得滚瓜烂熟B树矮胖、非叶子节点不存数据、叶子节点有链表、范围查询快。背完面试官点点头然后追问一句“那你说说为什么不用B树”就开始卡壳了。我面过不少人也被人面过不少次慢慢发现这道题根本不是考背诵它是一块敲门砖敲的是你对存储引擎、磁盘IO、数据结构取舍这三件事的理解深度。先把结论摆出来MySQL的InnoDB引擎选择B树作为索引结构本质上是在磁盘这种慢速随机访问介质上用最少的IO次数定位到目标数据同时尽可能少地浪费存储空间。这句话里的每一个限定词都有用。换成内存数据库Redis用跳表就够了换成只做等值查询的KV场景哈希索引可能更快换成写多读少的日志型场景LSM树才是正解。B树不是什么“最优解”它是在“读多写少、范围查询多、数据量大到内存装不下”这个具体场景下的最优解而这个场景恰好覆盖了绝大多数业务系统的数据库需求。这篇文章我想按我自己的理解路径来写不搞那种“定义-特点-优点”的教科书式罗列。我会从磁盘IO的成本算起把B树的结构掰开揉碎然后落到InnoDB实际的索引组织方式上最后给一批面试里反复出现的实操题和排查思路。适合两类人看一类是正在准备面试、想从“背答案”升级到“讲清楚”的另一类是工作几年了索引建了一堆但说不清为什么这么建的。文章里涉及的数字和参数我都会给出计算过程你可以自己拿笔跟着算一遍算完基本就忘不掉了。1.1 一道被问烂的题其实是三层考点面试官抛出“为什么用B树”的时候他脑子里通常有三个评分档位。第一档是能说出B树的形态特征非叶子节点只存键和指针、所有数据都在叶子节点、叶子节点之间用双向链表串起来。这一档只是及格线说明你看过资料。第二档是能解释这些特征对应解决什么问题非叶子节点不存数据所以单页能放更多键树就更矮IO次数更少数据全在叶子所以每次查询的IO次数稳定叶子链表让范围查询和排序不用回到根节点重新走。能讲到这一层说明你理解了设计意图。第三档也是真正拉开差距的一层是能把这个选择和具体引擎、具体业务场景绑定起来讲。比如你能说清楚InnoDB的聚簇索引为什么把整行数据挂在主键B树的叶子节点上二级索引为什么要回表为什么主键建议用自增而不是UUID为什么联合索引的字段顺序会影响查询效率。这些问题背后是同一条主线B树是一个有序的、分层的、页为单位的结构你对它的所有困惑归根结底都是没把“有序”和“页”这两个词吃透。我个人的经验是面试时如果你只答第一档面试官可能会再给你一次机会追问如果你能主动过渡到第二档面试节奏就掌握在你手里了如果你能把第三档里跟你项目相关的那部分讲出来——比如“我们那套订单系统之前主键用的雪花ID插入的时候页分裂比较严重后来换成自增主键写入性能提升了大概多少”——那这道题基本就稳了。1.2 别急着背结论先建立“页”的直觉很多人学B树学得别扭是因为脑子里没有“页”这个概念把B树当成一个抽象的树结构在记。实际上InnoDB操作数据的最小单位是页默认大小16KB。你查一行数据引擎不是从磁盘上抠出这一行而是把包含这行的整个16KB页读到内存的Buffer Pool里。哪怕你只要一个字段也是读一整页。这个设定决定了后面所有的设计取舍。打个比方页就像是一本字典里的一页纸B树就是这套字典的目录体系。你要找一个字不是从第一页翻起而是先看总目录定位到大概在哪一册再看分册目录定位到哪一章最后翻到那一页。每一层目录都是一次“IO”而IO是这个体系里最贵的动作。所以整个数据结构的设计目标就一句话让目录层尽可能薄让每一页目录尽可能装下更多条目从而把“翻目录”的次数压到最低。理解了这个你再看B树非叶子节点为什么只存键不存数据就变得非常自然了——因为存了数据一页能放的键就少了目录就厚了树就高了IO就多了。2. B树的骨架从磁盘IO的成本算起聊数据结构不谈硬件特性就是耍流氓。内存里比较两个指针的开销大概是纳秒级而一次机械硬盘的随机读是毫秒级两者差了六个数量级。就算是现在普及的SSD随机读也在几十到上百微秒跟内存访问依然差着好几个量级。这意味着在数据库这个场景里“比较次数”几乎不值钱“IO次数”极其值钱。所有面向磁盘的数据结构优化的核心指标都是IO次数而不是时间复杂度里的比较次数。这就能解释一个很多人疑惑的点二叉搜索树查找的时间复杂度是O(log n)B树也是O(log n)凭什么B树更快因为这里的log底数不一样。二叉树每层只能把范围缩小一半100万条数据要走20层B树一个节点是一整页16KB能放几百上千个键每层能把范围缩小上千倍100万条数据三层就够了。20次IO和3次IO的差距在磁盘上就是几十毫秒和零点几毫秒的差距。复杂度记号相同实际代价天差地别。2.1 一次磁盘IO到底有多贵我们把数字摆出来看。假设一块普通的SATA SSD4K随机读的IOPS大概在几万这个量级换算下来单次随机读的延迟在几十微秒到一百微秒之间。听着不多但数据库查询往往不是一次IO就完事。如果没有索引一张一千万行的表做全表扫描按每页16KB、每行200字节算一页大概装80行总共需要读12.5万个页。就算全部走顺序读、就算有预读优化这个量级也是几百毫秒起步稍微并发高一点直接把磁盘打满。有索引的情况就完全不同了。一千万行的表三层B树定位一行只需要读三次页根页、中间页、叶子页。而且根页和中间页由于被频繁访问大概率常驻在Buffer Pool里实际的物理IO可能只有一次甚至零次。这就是索引存在的全部意义——把O(n)的扫描变成O(log n)的定位。这里有个容易忽略的细节InnoDB在读取页的时候除了读目标页还会触发预读机制。线性预读会在顺序访问一个区64个页中的一定数量页后把整个区预读到内存随机预读则在一个区中已经缓存了一定数量页时把剩余页读进来。这个机制对全表扫描和范围查询帮助很大但对点查基本没影响。面试里如果有人问你“B树点查一定要三次IO吗”你可以补一句“根节点常驻内存、加上预读和自适应哈希索引实际物理IO往往更少”这个回答的层次就上去了。2.2 B树、B树、哈希、红黑树横向对比光说B树好不如把候选方案摆一桌对比。面试里最常被拿来对照的是B树、哈希索引和二叉平衡树我做了个表把关键差异列清楚结构单节点容量树高1000万行范围查询等值查询是否适合磁盘二叉平衡树1个键2个指针约23层需中序遍历效率低O(log n)次IO不适合太高红黑树1个键2个指针约23层同上同上不适合同上B树键数据指针约3到4层需回溯较麻烦约3到4次IO较适合B树键指针非叶约3层叶子链表顺序扫高效约3次IO且稳定最适合哈希索引哈希桶1层完全不支持O(1)极快仅适合等值B树和B树最本质的区别在两点。第一B树的非叶子节点也存数据。这意味着一页16KB里能放的键数量会大幅减少。假设一个键4字节、指针6字节B树存了数据之后每条记录可能要占几百字节一页只能放几十个键B树非叶子节点只有键和指针一页能放上千个键。同样的数据量B树的树高会明显更高IO次数更多。第二B树的叶子节点之间用双向链表连接而且所有数据都集中在叶子层。这让范围查询变得极其自然定位到起始位置后顺着链表往后扫就行。B树因为数据分散在各层做范围查询需要中序遍历要在不同层级之间来回跳实现复杂且效率低。你想想where id between 100 and 200这种查询B树找到100所在的叶子然后顺着链表读几页就完事了B树得一层层回溯差距非常明显。哈希索引的问题更直接它只能做等值匹配。where name xxx飞快但你写个where age 18它直接废掉。而且哈希索引不支持最左前缀不支持排序一旦出现哈希冲突还要链表遍历。所以InnoDB只在特定场景下用自适应哈希索引作为辅助主索引结构从来不是哈希。Memory引擎倒是支持哈希索引但那是内存场景不涉及磁盘IO的取舍。2.3 三层B树到底能存多少行算给你看这个计算是面试里的加分项因为它能证明你不是在背书。假设InnoDB页大小16KB主键是bigint占8字节页内指针占6字节那么非叶子节点的一条索引记录是14字节。一页能放的记录数约等于 16384 / 14 ≈ 1170 条。也就是说每个非叶子节点可以指向1170个子节点。再看叶子节点。假设一行数据平均1KB这个估算对业务表来说偏大实际很多表一行几百字节一页能放16行。那么树高为2一个根节点一层叶子1170 × 16 ≈ 1.87万行树高为3根中间层叶子1170 × 1170 × 16 ≈ 2190万行树高为41170³ × 16 ≈ 256亿行结论就是一张两千万行左右的表B树只有三层任何一次点查最多三次页访问。这就是为什么InnoDB能扛住千万级数据量还保持毫秒级响应。你可以把这个计算过程记下来面试时主动算一遍效果比干说“B树很矮”强得多。顺便说一句如果一行数据平均只有200字节一页能放80行那三层B树能存到1.1亿行左右。所以真实业务里只要不是大字段特别多的表三层B树基本能覆盖绝大部分场景。这也解释了为什么DBA在评估分库分表时单表千万级是个常见阈值——不是技术硬限制而是留出余量、控制维护成本的经验值。3. InnoDB里的索引真相数据即索引理解了B树这个通用结构还得落到InnoDB的具体实现上不然面试官一问“聚簇索引和二级索引的区别”又要露馅。InnoDB有个说法叫索引组织表Index Organized Table意思是这张表的数据本身就是按主键B树组织存储的没有额外的“数据文件”和“索引文件”之分。你建表时指定的主键直接决定了数据在磁盘上的物理排列顺序。这个设计和MyISAM完全不同。MyISAM的索引文件和数据文件是分开的索引叶子节点存的是数据行的物理地址查的时候先查索引拿到地址再去数据文件里取。这种叫非聚簇索引。InnoDB的聚簇索引则把整行数据直接挂在主键B树的叶子节点上找到索引就找到了数据少一次寻址。这也是InnoDB在点查场景下通常比MyISAM快的原因之一。3.1 聚簇索引主键就是数据的物理顺序聚簇索引的叶子节点存的是完整的一行记录包括所有字段。这意味着两件事。第一一张表只能有一个聚簇索引因为数据只能有一种物理排列方式。第二如果你不指定主键InnoDB会自动选一个优先选第一个非空唯一索引如果没有就用内部生成的6字节隐藏列DB_ROW_ID。用隐藏列做聚簇索引是很糟糕的情况因为你完全无法控制数据的插入顺序而且二级索引会变得更大——这个后面讲。为什么主键推荐自增核心在于页分裂。B树的叶子节点是按主键顺序排列的新插入的记录如果主键是递增的那么它永远插在当前最后一页的末尾页写满了就申请新页这是纯粹的顺序写非常快。但如果主键是UUID这种随机值新记录会插到树的中间某个位置那个位置所在的页可能已经满了InnoDB就得把这个页拆成两半把一半数据挪到新页里再更新上层节点。这个动作叫页分裂代价包括额外的写IO、页空间利用率下降分裂后两个页都只用了50%左右、以及后续查询要访问更多页。我所在的一个项目早期就踩过这个坑。当时用的是随机字符串做主键写入压力一大监控上能看到明显的写入抖动磁盘写放大很严重。后来换成自增bigint同样的写入量P99延迟降了将近一半。这个案例我面试时讲过好几次面试官的反应基本都是认可的因为它证明你真的在生产环境里观察过这些现象。还有一个常被追问的点自增主键会不会有安全问题比如暴露业务量、被爬虫遍历。这个确实存在常见的处理是自增主键只做内部使用对外暴露一个独立的业务ID字段并加唯一索引。这样既不牺牲写入性能也不泄露信息。至于自增主键用完后溢出怎么办bigint无符号能到1844亿亿正常业务根本用不完不用操心这个。3.2 回表二级索引的代价到底在哪二级索引也叫辅助索引的叶子节点存的不是整行数据而是索引列的值 主键值。这个设计非常关键。查where name abc的时候先在name的二级索引上找到对应的主键id然后拿这个id回到聚簇索引上再查一次才能拿到完整行数据。这一步就叫回表。回表的代价是两次B树查找如果二级索引命中的记录很多就是N次回表每次都是三次页访问。这个成本累加起来相当可观。举个具体的一张表一千万行where status 1命中10万行如果status上有二级索引那么执行过程是先扫二级索引找到10万个主键再回表10万次。优化器一算成本很可能直接放弃索引走全表扫描——因为全表扫描是顺序读回表是随机读10万次随机读可能比扫12.5万个页还慢。这就是为什么区分度低的字段不适合单独建索引。性别、状态标志、是否删除这类字段值就那么几种索引扫出来的记录占比太高回表成本压过了索引的收益。优化器不傻它会算成本很多时候你建了索引它也不用。你可以用explain看key列是不是NULL来验证如果建了索引但key是NULL说明优化器放弃了。解决办法有两个方向。一是覆盖索引如果查询需要的字段全在二级索引里就不用回表了。比如select name from user where name abcname索引的叶子节点就有name值直接返回。二是联合索引把常用的过滤字段和返回字段组合进一个索引也能实现覆盖。后面我会详细讲这两种手法的实操。3.3 联合索引的物理排序与最左前缀联合索引(a, b, c)在B树里的排序规则是先按a排序a相同再按b排序b相同再按c排序。这个“字典序”的排列方式直接推导出了最左前缀原则。你可以把联合索引想象成一本书的目录先按章节排章节内按小节排小节内按页码排。你要找“第3章第2节”可以直接定位但你没法跳过章节直接找“所有第2节”因为第2节散落在各个章节里。同理where b 2用不上(a, b, c)索引因为b在全局上不是有序的。这里有个细节很多人搞混where a 1 and c 3能不能用上索引答案是能用上但只能用a这一列。因为a确定之后b是乱的c在b乱的情况下也不是全局有序的。所以索引只能用来过滤ac的部分要么靠索引下推在索引层过滤要么回表后过滤。这个区分在面试里经常被考答错了很掉分。还有个更细的点范围查询会截断后续列。where a 1 and b 2 and c 3索引能用上a和b但c用不上。因为b是范围在b的范围内c不是有序的。这个规则在MySQL 5.6引入索引下推ICP之后有所缓和——ICP可以在索引遍历阶段就用c来过滤减少回表次数但它不能把c变成索引查找的一部分。这个区别要讲清楚。关于字段顺序怎么定我给个实操原则等值查询的列放前面范围查询的列放后面排序的列跟着来最后考虑把select里常出现的列加进来做覆盖。比如一个查询是where user_id ? and create_time ? order by id那索引可以设计成(user_id, create_time, id)。注意order by id没法用上索引因为create_time是范围后面的id不保证有序。如果要排序走索引得把create_time换成等值或者调整业务逻辑。4. 索引失效与执行计划面试最爱追问的实操环节理论讲完了面试官最喜欢在这里切换模式“你平时怎么排查慢查询”这个问题看似开放其实是在验证你有没有真正用过explain、有没有调过索引。我的建议是准备一套固定的话术框架把排查流程讲成一条线而不是零散地蹦知识点。我自己的排查顺序是这样的先从慢查询日志里捞出具体的SQL然后explain看执行计划重点看type、key、key_len、rows、Extra这几列判断是没走索引、走了但选择性差、还是走了索引但回表太多。接着看表结构和现有的索引比对SQL的where条件和索引的最左前缀是否匹配看看有没有函数、隐式转换这类会导致失效的写法。最后才是考虑改SQL、加索引、还是调整业务逻辑。4.1 explain里必须盯紧的几个字段explain输出的列不少但真正决定判断的就这么几个。type列表示访问类型从好到坏大致是systemconsteq_refrefrangeindexALL。生产环境里至少要看到range如果出现index或者ALL基本就是全表或全索引扫描必须处理。index这个容易误解它表示扫描整棵索引树虽然比全表扫快一点因为索引页更小但数据量大了一样慢。key列显示实际使用的索引possible_keys是候选索引。如果possible_keys有值但key是NULL说明优化器算完成本觉得不划算放弃了索引。这种情况通常是因为索引选择性太差或者回表成本太高。key_len表示用到的索引字节数这个字段是判断“联合索引用了几列”的关键。rows是估算的扫描行数虽然是估算值但数量级有参考意义如果估算出来和实际返回行数差了几个数量级可能是统计信息过期需要analyze table。Extra列信息量最大。看到Using filesort说明排序没走索引需要额外的排序操作看到Using temporary说明用了临时表常见于group by或者distinct看到Using index是好消息说明用了覆盖索引没回表看到Using index condition说明用上了索引下推看到Using where则表示在存储引擎返回数据后Server层还要再过滤一遍。这几个标记的组合能快速定位问题所在。4.2 六种索引失效场景附实测验证下面这些场景我都实际建表验证过不是从文档上抄的。你可以自己在测试库上复现一遍印象会深得多。第一种左模糊或者全模糊匹配。where name like %abc用不上索引因为B树是按前缀有序的你不知道开头是什么就没法定位。like abc%是可以用上的。如果业务真的需要左模糊考虑全文索引或者搜索引擎别在MySQL里硬扛。第二种索引列上做函数运算。where date(create_time) 2024-01-01会让索引失效因为要对每一行算完函数才能比较。改成where create_time 2024-01-01 and create_time 2024-01-02就能走范围索引。这个改法在日志类查询里特别常见我几乎每次代码review都能看到几处。第三种隐式类型转换。字段是varchar你传个数字where phone 13800000000MySQL会把字段值转成数字再比较相当于在列上做了函数运算索引失效。反过来字段是int你传字符串MySQL会把字符串转成数字索引还能用。所以规则是字符串字段一定要传字符串。这个坑在Java项目里尤其常见因为参数类型不对编译器不报错。第四种or连接了非索引列。where a 1 or b 2如果b没索引整个查询就退化成全表扫描。哪怕a有索引也没用因为要保证结果完整必须扫全表。解决办法是给b也加索引或者用union all拆成两个查询。第五种不满足最左前缀。前面讲过(a, b, c)索引下where b 1用不上。这个不用多说但要注意一种隐蔽情况索引列的顺序和查询条件的顺序无关where b 1 and a 2和where a 2 and b 1是一样的优化器会自动调整。别在这上面纠结。第六种范围查询之后的列失效。前面讲过where a 1 and b 2 and c 3c用不上索引查找。这个不算严格意义上的“失效”但结果是一样的值得单独记一笔。4.3 覆盖索引、索引下推、MRR三件套这三个优化手段是面试的高频加分项理解它们能让你对索引的掌控力上一个台阶。覆盖索引最直接。查询要的列全在索引里就不需要回表。explain的Extra会显示Using index。实操上把select *改成只查需要的字段很多时候就能触发覆盖。我见过一个统计接口原来select *扫了十万行回表改成只查两个字段后走了覆盖索引耗时从两秒多降到几十毫秒。当然覆盖索引也不是免费的它会让二级索引变大写入成本上升得权衡。**索引下推ICP**是MySQL 5.6引入的。没有ICP的时候联合索引(a, b)遇到where a 1 and b like %x%只能先用a把记录捞出来然后全部回表回表后再判断b。有了ICPb的判断可以在索引遍历阶段就做掉不合格的记录直接跳过不回表。Extra里显示Using index condition就是启用了。这个优化对联合索引里的模糊匹配、范围条件特别有效能砍掉大量无效回表。**MRRMulti-Range Read**相对冷门一些。它的思路是把二级索引扫出来的主键先收集起来排序之后再批量回表。因为主键有序之后回表的磁盘访问就从随机变成了近似顺序对机械硬盘提升巨大对SSD也有帮助。可以通过optimizer_switch里的mrr参数控制默认是开的。面试里能提到这个基本能证明你不是只看过入门文章。5. 高频面试题速查与踩坑实录到这里原理和实操都覆盖了我整理一批实际被问过的题目配上我自己的回答思路。这些题不是让你背标准答案而是让你理解每道题在考哪个点然后组织自己的语言。5.1 常见问题速查表面试题考察点回答要点为什么用B树不用B树结构差异与IO成本非叶节点不存数据扇出更大树更矮叶子链表利于范围查询查询IO次数稳定聚簇索引和二级索引区别数据组织方式聚簇索引叶子存整行二级索引叶子存主键值二级索引查询需回表为什么主键推荐自增页分裂与写性能顺序插入避免页分裂减少随机IO二级索引更小什么是回表怎么避免覆盖索引二级索引查到主键再回聚簇索引取数据把查询字段并入索引实现覆盖最左前缀原则原理联合索引排序规则联合索引按字段顺序字典序排列跳过前导列无法定位索引失效有哪些场景实操经验左模糊、函数运算、隐式转换、or非索引列、不满足最左前缀、范围截断深分页怎么优化limit offset问题延迟关联或游标分页避免扫描大量无用记录索引是不是越多越好写放大与优化器成本每个索引都增加写开销和存储优化器选择成本上升需精简什么字段不适合建索引区分度与更新频率区分度低、更新频繁、大字段、很少作为查询条件的字段如何判断索引是否生效explain解读看type、key、key_len、rows、Extra重点看是否Using index关于深分页那道题展开说一句。limit 1000000, 10这种写法会扫描前100万零10行然后丢掉前100万行代价极高。优化思路有两个一是延迟关联先用覆盖索引查出主键再关联原表二是游标分页记住上一页最后一条的id用where id last_id limit 10。第二种最适合无限下拉的场景缺点是没法跳页。这个取舍要跟产品讲清楚不是纯技术问题。5.2 我实际踩过的几个坑第一个坑是给低区分度字段建了单列索引。当时做一个消息表想着“已读未读”查询频繁就给is_read加了索引。结果这张表八百万行未读消息只有几千条已读的占了99%。查询已读列表的时候优化器直接放弃索引走全表扫查询未读的时候虽然走了索引但因为表设计里select了太多字段回表次数多反而不如直接扫。后来改成联合索引(user_id, is_read, create_time)用user_id先把范围缩到单个用户问题才解决。教训是索引的选择性要结合查询条件看单看一个字段的选择性没意义。第二个坑是统计信息过期导致执行计划突变。有段时间线上一个查询突然变慢SQL没改、索引没动就是慢了。排查半天发现是表数据量在几天内涨了很多统计信息没更新优化器还以为只有几十万行选了个错误的执行计划。手动analyze table之后恢复正常后来加了定时任务定期更新统计信息。这个经历让我养成了看rows列和实际返回行数对比的习惯差距大就先去更新统计信息。第三个坑是字符集不一致导致join用不上索引。两张表关联一张是utf8mb4一张是utf8join的字段都建了索引但explain显示type是ALL。原因是字符集不同MySQL在比较前要做转换相当于在列上做了函数运算。把所有表的字符集统一成utf8mb4之后问题消失。这个坑很隐蔽因为两边单独查都正常只有join的时候才出问题。5.3 面试时怎么把答案讲出层次同样的知识点讲法不同效果差很多。我的建议是先给结论再讲原理最后落到你的实践。比如问“索引失效”别一上来就罗列六种情况先说“索引失效的本质是B树的有序性被破坏了”然后挑两三个最典型的展开最后补一句“我在项目里遇到过xxx当时是这么定位和解决的”。这个结构既显得有条理又有真实感。还有个小技巧主动暴露边界。比如讲覆盖索引的时候可以补一句“覆盖索引不是越多越好索引列太多会让二级索引变大写入成本上升我们之前有个表索引列加太多插入性能掉了三成后来砍掉了一些”。这种话一说面试官就知道你不是纸上谈兵。6. 从面试题到生产实践索引设计的落地方法面试题刷得再熟最终还是要落到怎么给一张真实的表设计索引。我给一套我自己在用的流程你可以直接拿去套。6.1 建索引前的四步判断第一步盘点查询。把这张表上所有的高频SQL列出来包括点查、范围查、排序、分组。别只盯着当前的代码也看看慢查询日志很多历史遗留的烂SQL就藏在那里。第二步看区分度。对每个候选字段算一下count(distinct col) / count(*)结果越接近1越好。低于0.1的字段单独建索引基本没意义除非能和其他字段组合成高区分度的联合索引。第三步合并联合索引。多个单列索引往往可以合并。比如有where a ?、where a ? and b ?、where a ? order by b三种查询一个(a, b)索引就能全部覆盖比建两个单列索引更省。合并的时候按“等值在前、范围在后、排序跟着”的原则排字段顺序。第四步评估写代价。每加一个索引插入、更新、删除都要多维护一棵B树。写多读少的表要克制一般控制在3到5个索引以内。加完索引后观察一段时间的写入延迟和磁盘增长有异常就回退。6.2 慢查询排查的固定套路我习惯按这个顺序走基本不会漏从慢查询日志拿到具体的SQL和执行时间、扫描行数explain看执行计划重点看type是不是ALL或indexkey是不是NULLrows是不是明显偏大看show index from 表名比对现有索引和SQL条件的匹配度如果是索引缺失评估加索引的收益和写代价如果是索引存在但不生效检查有没有函数、隐式转换、左模糊这类写法如果是索引生效但慢看是不是回表太多考虑覆盖索引或者调整查询字段改完之后用同样的数据量压测对比确认真的有提升再上线这套流程看着朴素但能覆盖九成以上的慢查询场景。真正难的不是排查是判断要不要改。有些查询慢是因为业务逻辑本身不合理比如一次查十万条数据做导出这种加索引也救不了得改成异步任务分批处理。技术方案要跟着业务场景走别为了优化而优化。最后再分享一个小技巧关于怎么记住这些知识。我发现单纯的文字记忆很容易忘但如果你亲手在测试库上建一张百万行的表把每种索引失效的场景都跑一遍看着explain的结果从ref变成ALL那个印象是刻在脑子里的。面试前与其翻笔记不如花一个小时在本地跑一遍效果会好得多。我自己到现在还保留着一个测试库里面存着各种边界情况的表结构遇到拿不准的写法就直接在上面试比查文档快。