ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

索引命中却查询慢?揭秘回表代价与优化器选错索引的完整排查链路

2026/9/30 8:14:35 拓冰建站 浏览量
索引命中却查询慢?揭秘回表代价与优化器选错索引的完整排查链路 1. 这题看似考索引实际考的是代价模型——先说说我面试时的真实见闻在阿里二面被问到“明明加了索引查询为什么还是慢”时我第一反应其实是松了口气的因为索引这块平时项目里没少折腾自认为能答得不错。但等我看到面试官紧接着追问的几个方向才发现这题远不是背几条“索引失效”口诀就能糊弄过去的。后来我自己也参与过不少技术面试站在面试官的角度看候选人这个问题确实能筛掉一大批人。先说一个普遍现象当面试官抛出“加了索引还是慢”这个问题时90%的候选人第一句话必然是“索引失效了”。然后开始背八股隐式转换、最左前缀、like通配符、函数运算、not in、or连接、is null……背得倒是挺顺溜但面试官通常不会满意原因很简单——“索引失效”只是表层答案它回答的是“为什么查询计划没选索引”而问题问的是“为什么明明有索引查询还是慢”。这两者之间有质的区别甚至可以说大多数“加了索引还是慢”的场景索引其实并没有失效它是被用上了的但用得非常亏。面试官真正想听到的东西我后来复盘下来大概是这五个层次能够区分“索引没被用上”和“索引被用上了但代价很高”这两种完全不同的情况明白索引和数据存储结构之间的关系——InnoDB的B树聚簇索引和二级索引之间靠什么搭建联系回表操作是什么理解优化器选择执行计划时依赖的统计信息和成本估算模型以及基于采样数据的估算误差带来的选错索引风险能定位慢查询的真正瓶颈是在 SQL 执行内部扫描行数、排序、临时表、锁等待还是在服务端资源层面最后落到实操——给出从慢查询日志到 EXPLAIN、到 optimizer trace再到具体索引调整的完整排查手段。这篇文章我会把这些内容一条条拆开讲清楚。不管你是在准备面试还是手头正好有线下生产环境里的慢 SQL 不知道怎么下手这篇文章都可以直接当排查手册来用。2. 常规背的“索引失效”清单为什么只能算半对先快速过一遍大家都会背的基础部分但我会直接告诉你哪些场景讲出来面试官会觉得“还行”哪些场景一旦你当成主要论点会被追问到哑口无言。2.1 索引失效场景清单区分“真失效”和“假失效”经典八股里说的情况其实可以分成两类。第一类是索引“真失效”——查询计划里压根没进二级索引直接走了全表扫描或者全索引扫描。这类场景包括对索引列使用了函数或表达式计算比如WHERE DATE(create_time) 2024-01-01等价于WHERE create_time 2024-01-01 AND create_time 2024-01-02才能走索引前者会迫使每一行都套一层函数B树没法基于函数结果排序只能放弃索引隐式类型转换最典型的是WHERE phone 13800138000phone 字段是 varchar 类型MySQL 会把字符串列转成数字做比较相当于套了一层 cast索引失效违反最左前缀原则复合索引(a, b, c)上直接查 b 或 c跳过开头的 a 等值条件like 通配符在左侧%keyword摸不着 B 树排序后的前缀路径索引列参与!、、NOT IN、IS NOT NULL等范围否定判断时优化器预期扫描比例过大而放弃。第二类是索引“假失效”——索引其实走了但查询依然慢。这类是最容易让面试者产生困惑的也是最需要细讲的部分。举例来说走了二级索引但查询列里有大量结果集需要回表回表次数多到还不如直接全表扫走了二级索引但优化器用的索引不是最贴合这个查询条件的那个扫描行数依然巨大索引本身没问题但基数估算偏差大优化器被误导选择了错误索引。区分这两类的唯一方法就是看执行计划只看现象猜是猜不准的。如果possible_keys和key都有值说明索引确实进了候选也最终被选上了那就别再背“索引失效”了方向要切换到“索引代价太高”上。2.2 为什么背熟了失效列表面试还是挂这里我说句可能不太中听的话背八股是面试准备的最低效路径。因为“索引失效”这件事本身在 MySQL 8.0 和 MySQL 5.7 中都会有一些细节差异而且每一条失效条件背后都有具体的优化器逻辑支撑。你背了结论但解释不了结论的产生原因面试官一击就能打穿。举个例子WHERE a 1 OR b 2这种 OR 条件老八股说“会导致索引失效”但实际上 MySQL 是支持 index merge 的——它会分别走 a、b 两个索引取结果集求并集在某些场景下甚至比全表扫更快。关键在于optimizer_switch里 index_merge 开关以及两个索引扫描结果集的占比。你会背“OR 导致索引失效”这句话但如果遇到一个实际生产案例同样的 SQL 在同版本库上一条走 index merge、一条走全表扫描你怎么跟同事解释这就是背概念和真正理解的差距。再比如隐式类型转换核心原因在于“索引列参与了运算”。MySQL 优化器在面对cast(phone as signed) 13800138000这种表达式时无法利用 B 树键值的排序关系做范围裁剪因为树的节点上存的是 varchar 原始值不是转换后的数值。但如果左值是一个常量右边是索引列比如WHERE id 123主键是 int 类型MySQL 反而会把字符串常量转换成数字再比较因为把常量 cast 一下不影响索引列本身的排序结构。这个细节九成背八股的人都说不出来。所以我建议你把这部分内容当成“背景常识”而不是“答案核心”。真正能让你答到点上的是下面这一层索引被用上了查询还是很慢问题到底出在哪。3. 真正让“索引被用上还是很慢”的三个底层机制面试官的追问往往会落在这个地方如果执行计划明明显示索引命中了查询耗时还是下不来接下来你会从哪些方向分析这需要你理解 InnoDB 存储引擎的索引实现细节。我会用三个机制来解释为什么索引命中不等于查询快这三个机制我后面会根据项目经验补充几个真实案例。3.1 聚簇索引与二级索引的差异回表代价InnoDB 里主键对应的那棵 B 树的叶子节点保存的是整行数据这叫聚簇索引clustered index。二级索引secondary index的叶子节点保存的却不是行数据而是索引键值 主键值。所以当你通过二级索引查询时通常需要先在这棵 B 树上找到满足条件的叶子节点拿到主键值再回到聚簇索引树上去查一次完整行数据这个过程就是回表random read by primary key。回表一次的代价看似很小但代价取决于回表次数。如果一条 SQL 命中了 10 万行二级索引记录那就意味着最多 10 万次的主键随机查找。这和无索引的全表扫描相比全表扫描是顺序读机械硬盘或 SSD 对顺序 IO 的吞吐是非常高的而回表是随机 IO每次都要从 B 树根节点走到叶子节点。数据量一大、buffer pool 装不下热点页的时候随机 IO 的代价会指数级上升。一个很经典的场景SELECT * FROM orders WHERE user_id 12345user_id 上有二级索引。假设这个用户下了 5 万条订单那这条查询走了索引但需要回表拿 5 万行完整数据。如果表里总行数是 500 万这个查询可能耗时 2 到 3 秒而如果你换成SELECT order_id, amount FROM orders WHERE user_id 12345并且在 user_id 上建立(user_id, order_id, amount)的复合索引二级索引的叶子节点已经包含这三个字段不需要回表——这就是覆盖索引的威力。提示判断是否回表看执行计划里的 Extra 列。如果出现Using index condition说明在索引层面做了条件下推但还是要回表出现Using index说明查询列全部被索引覆盖不需要回表。前者中的 “condition” 才是关键差异点很多人把这两者混淆。3.2 优化器成本模型走索引不一定比全表扫便宜第二个机制是 MySQL 优化器的成本模型。优化器决定走哪条路并不是“有索引就选索引”而是估算不同执行计划的代价选代价最小的。这个代价包括 IO 成本和 CPU 成本InnoDB 里对应 read 的页数量、访问的行记录数量等。关键问题在于这些估算依赖的是统计信息而统计信息是基于采样得到的。InnoDB 在表数据变化超过一定阈值时会自动重新计算基数cardinality——索引列上不同值的个数。但采样不是全量扫描它是随机抽取若干数据页估算的这就可能造成统计信息和实际情况偏差很大。举个真实发生过的例子。线上有个表存用户行为日志其中状态字段 status 只有三个值0、1、2。某一次某个状态值突然快速增长比如 status 1 的记录占比从原来的 1% 涨到了 80%。但优化器手里的统计信息还没刷新它基于旧的基数估算以为 status 1 只有很少的记录于是高高兴兴地选择了 status 索引结果实际执行时扫出了百万行还得逐行回表查询慢到超时。这种场景你去看执行计划索引确实被选上了但你不能说索引是“失效”的它是被选了但是选错了。另一个调度优化器的案例MySQL 8.0.18 之后支持 hash join优化器在评估关联查询时可能会觉得走某个索引还是全表扫 hash join 更划算。如果统计信息不准它做的成本决策也会跟着不准。有时候你手动ANALYZE TABLE刷新统计信息后执行计划立刻变正常就是这个道理。3.3 随机 IO、排序和临时表真正拖垮 SQL 的隐形凶手第三个机制要承认一个查询的耗时不是只有“取行数据”这一个环节。读过索引之后SQL 还要做别的操作这些操作的代价往往比索引本身更大。比如排序。ORDER BY create_time DESC LIMIT 20如果 create_time 不是索引的一部分那么 MySQL 需要先把命中的数据行取出来再放到 sort buffer 里做 filesort。命中的数据行如果非常大排序就得走磁盘临时文件——这个代价跟索引有没有命中完全是两码事索引的命中也救不了 filesort 的物理开销。比如临时候表。GROUP BY、DISTINCT、UNION等操作可能会创建内部临时表。尤其是GROUP BY非索引列MySQL 可能要建一个临时表来做分组聚合大字段列TEXT、BLOB还会导致临时表落在磁盘上性能暴跌。你加再多索引只要分组聚合本身需要建临时表查询都慢。再比如锁等待。如果你在事务里执行一条索引命中的 UPDATE但是恰好其他事务已经在这批索引记录上加上了行锁你的 SQL 即使执行计划很完美也只能在锁等待队列里排队。慢查询日志里记录的 query time 会包含这段时间但你去分析执行计划是看不出来任何异常的。遇到过两次线上慢查询查了半天索引最后发现是另一个跑批任务锁了一批行索引走到了但锁等了好久。这三个机制合起来才构成了一个完整的“索引命中但查询慢”的解释框架。面试的时候你把这个逻辑讲清楚面试官才会认可你是真正理解数据库工作方式的人。4. 定位慢查询的完整排查链路——从慢日志到 EXPLAIN 再到 optimizer trace理论讲清楚了接下来进入实战环节。无论你是排查线上问题还是准备面试掌握一套完整的定位链路比背多少条失效规则都重要。我按排查顺序逐步说。4.1 第一步慢查询日志——先确认“慢”的定义和数据慢查询日志是排查入口。MySQL 默认慢查询阈值是 10 秒实际业务系统里这个值通常要调小一般 1 秒就足以捞到值得关注的 SQL。你可以这样配置SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1; SET GLOBAL log_queries_not_using_indexes ON;log_queries_not_using_indexes会把所有没有用索引的查询都记下来这在做索引优化时很有用。不过要注意线上开启这个参数一定要谨慎它可能瞬间产生大量日志所以通常是在短时间窗口内开着收集完样本就关。拿到慢日志之后要看几个关键指标Query_time总耗时、Lock_time锁等待耗时、Rows_sent返回行数、Rows_examined扫描行数。如果有人跟你说查询慢你先问一句Rows_examined是多大扫描行数和返回行数差距越大说明越可能存在“大范围扫描 大量无效回表”的问题。4.2 第二步EXPLAIN 的每个字段到底在告诉你什么拿到具体 SQL 后立刻用EXPLAIN查看执行计划。我见过很多人只用 EXPLAIN 看 type 列是不是 ref或者 key 列有没有值这远远不够。我建议你按下面的顺序读一份执行计划table执行计划里每一步对应哪张表关联查询时读取顺序是什么type访问类型从好到坏大致是 system const eq_ref ref range index ALL。注意出现了 ALL 不代表天塌了小表全表扫可能比走索引还快但大表出现 ALL 且 rows 巨大就是问题possible_keys优化器考虑过哪些索引。如果这个字段是 NULL说明它压根没考虑索引你的索引要么字段不匹配、要么失效重点查 SQL 写法key最终选中的索引。经常有人混淆 possible_keys 和 key其实前者是“备选项”后者是“最终选择”两者的差距可以暴露选错索引的问题rows优化器估算的扫描行数。这是基于统计信息的估算值不准是常态但它能反映优化器的“心理活动”filtered经过索引条件过滤之后剩余行占比注意是估算值。如果 rows 很大但 filtered 很小说明索引的过滤效果不好Extra最重要的细节区。Using index是覆盖索引Using index condition是 ICP 条件下推但可能要回表Using where表示在存储引擎层拿回数据后还要再过滤Using temporary说明用了临时表Using filesort说明 SQL 里带排序但没有合适的索引利用。真正有经验的人会重点看rows和Extra两列而不是只盯着 type 看。possible_keys user_id_idx, key user_id_idx不代表万无一失如果rows 800000这个索引的过滤性就很差回表代价高到离谱。4.3 第三步optimizer trace——让优化器把心里话全部说出来如果你已经看完了 EXPLAIN还是不明白为什么优化器选了这条路最直接的办法是用 optimizer trace 查看优化器生成执行计划的完整决策过程SET optimizer_trace enabledon; SELECT * FROM orders WHERE user_id 12345 AND status 1; SELECT * FROM information_schema.OPTIMIZER_TRACE; SET optimizer_trace enabledoff;OPTIMIZER_TRACE 表的输出会包含几个关键段落rows_estimation对每个索引做扫描行数估算、considered_execution_plans考虑过的执行计划及代价、attached_condition条件下推细节。从这里你能看到优化器到底是怎么“想”的——它认为走索引 at_idx 需要扫描多少行、每行代价多大走全表扫描又是什么代价最终为什么选了现在的路径。这里有个真实排查案例我之前遇到过一个慢 SQLSELECT * FROM coupon WHERE status 1 AND expire_at NOW()。表里有 status 索引也有 expire_at 索引。执行计划选的是 status 索引但扫描了 40 万行才过滤出 3 万条有效数据。通过 optimizer trace 发现优化器估算 expire_at 索引需要扫 55 万行status 索引只需要扫 41 万行于是选了 status。但实际 exprie_at 索引的选择性远高于估算它之所以估算偏差大是因为 expire_at 字段的统计信息长期未更新数据分布变化后基数已经失真。ANALYZE TABLE之后优化器立刻改为选择 expire_at 索引SQL 从 3 秒降到 0.2 秒。这种案例里索引重来没有真失效纯粹是统计信息失真误导了优化器。4.4 第四步profile 定位耗时阶段执行计划正常但 SQL 仍然慢的场景可以用SHOW PROFILE把查询各阶段的耗时列出来精确到每个环节花了多少时间。比如是executing阶段高还是Sending data阶段高这直接决定了问题在哪一层。MySQL 8.0 中SHOW PROFILE已标记为废弃deprecated更推荐用performance_schema的events_statements_history_long等表来分析但 5.7 环境用SHOW PROFILE依然顺手。一条命令SET profiling 1; SELECT * FROM orders WHERE user_id 12345; SHOW PROFILE FOR QUERY 1;输出里你会看到awaiting handler commit、statistics、executing、Sending data等阶段。如果executing时间特别长说明存储引擎层的数据读取是瓶颈要往索引和数据量方向深挖如果Sending data时间最长可能是网络传输和结果集处理的问题比如SELECT *取了一堆大字段。4.5 第五步服务端资源层别忘了走到这一步如果 SQL 层级一切看起来都正常——执行计划合理、扫描行数不多、没有排序临时表——但查询还是慢那就必须把视野放大到 MySQL 实例本身。排查这几项连接数是否打满是的话 SQL 在排队获取连接innodb_buffer_pool_size是否太小导致热数据频繁淘汰、频繁发生磁盘 IOinnodb_io_capacity设置是否合理刷脏能力跟不上写入速度是否出现了大事务持有大量行锁或间隙锁阻塞了其他查询。我曾经碰到过一个“十分钟前还正常十分钟后突然所有查询都慢”的问题。查慢日志发现扫描行数很正常EXPLAIN 也没毛病后来用SHOW ENGINE INNODB STATUS看事务列表发现一个无人关注的定时任务开启了一个大事务改了上百万行持有大量锁没提交导致其他所有涉及这些记录的 DML 全部排队锁等待。这种问题你抓破头皮优化索引也没用得先解决事务隔离和提交策略。这条链路走完你基本能定位绝大多数“加了索引还慢”的问题。面试里如果你能把这个排查链路有条有理地讲出来比干巴巴背出八条失效规则要加分太多。5. 两个高频场景的根因复盘与优化实践——案例比理论更有说服力讲完排查链路我把日常开发里最常踩、面试里最高频的两个慢查询场景单独拎出来复盘一遍。每个场景我都会给出完整的现象描述、排查过程、优化方案和调整后的效果你可以直接参考。5.1 场景一深分页翻页慢——LIMIT 偏移量太大导致回表爆炸这个场景太普遍了。SELECT * FROM user_orders WHERE user_id 12345 ORDER BY create_time DESC LIMIT 100000, 20。这条 SQL 的执行计划看起来人畜无害命中了 index_user_id_create_timeuser_id, create_timetype 为 rangekey 非空。但真实执行就是慢后端接口超时分页越往后越卡。我看过很多人的第一反应是“加索引”实际上索引已经在了。真正的瓶颈在深分页机制MySQL 没有“直接跳到某一行”的能力LIMIT 100000, 20意味着它要把满足条件的前 100020 条记录全部找出来然后丢掉前 100000 条只把最后 20 条发给客户端。而且为了排序它还得先把所有候选行的主键加 create_time 载入到 sort buffer排序后再取那 20 条回表。优化方案有几种按性价比排序延迟关联deferred join先查主键拿到目标主键后再回表取完整行。SQL 改成SELECT t.* FROM user_orders t INNER JOIN ( SELECT id FROM user_orders WHERE user_id 12345 ORDER BY create_time DESC LIMIT 100000, 20 ) tmp ON t.id tmp.id ORDER BY t.create_time DESC;内层子查询只查了二级索引列 id 和 order_create_time不需要回表排序只需处理索引数据速度增幅几倍到几十倍。外层再按主键回表 20 行回表次数从 100020 次降到了 20 次核心代价一下子降下来了。游标分页keyset pagination页面参数不传页码而是传上一页最后一条记录的(create_time, id)作为游标。SQL 变成SELECT * FROM user_orders WHERE user_id 12345 AND (create_time 2024-01-01 10:00:00 OR (create_time 2024-01-01 10:00:00 AND id 123456)) ORDER BY create_time DESC, id DESC LIMIT 20;这个写法的核心是让条件直接落在索引范围上完全用 B 树的范围扫描能力不管数据翻到多深每一页的代价都是恒定的 20 行扫描不再跟偏移量挂钩。这个优化适合绝大多数线上列表分页场景。比较建议优先做游标分页如果产品形态非要支持“跳到第 N 页”退而求其次用延迟关联。5.2 场景二复合索引设计不合理——明明有索引匹配上却像没有另一个高频场景是复合索引顺序设计有问题。很多人建索引喜欢“想查哪个字段就给哪个字段建索引”导致一张表上堆了很多单列索引然后查询各种碰壁。举个例子订单表有三个常见过滤条件卖家seller_id、买家buyer_id、创建时间create_time。开发人员给seller_id、buyer_id、create_time分别建了单列索引。一条查询SELECT * FROM orders WHERE seller_id 1001 AND buyer_id 8888 AND create_time BETWEEN 2024-01-01 AND 2024-01-31 ORDER BY create_time DESC三个单列索引都可能进入 possible_keys但优化器最终只能选一个——选基数最高、估算过滤性最好的那个走。比如它选了 seller_id 索引拿到大量 seller_id 1001 的订单回表之后再将行数据逐一过滤 buyer_id 和 create_time。这时候你从 EXPLAIN 看它确实用到了索引但扫描行数可能非常高因为你建立的所有单列索引都无法利用“多个等值条件同时在索引树内裁剪”的能力。优化方案是重建复合索引。这里的关键是设计索引字段顺序。经验口诀等值条件字段放前面范围条件字段放后面。因为只有等值条件能最大程度压缩 B 树搜索范围范围条件会中断后续字段的排序效用。针对以上 SQL建(seller_id, buyer_id, create_time)或者(buyer_id, seller_id, create_time)具体看哪个字段的分辨率更高、哪个字段更常用于独立查询。同时由于查询列里有SELECT *走复合索引仍需要回表但回表的行数从几万降到几百效果立竿见影。如果查询还带着排序那最好把 create_time 也放进索引里使排序直接通过索引顺序完成避免 filesort。比如把 create_time 作为复合索引的第三列此查询不仅过滤条件在索引内剪枝连 ORDER BY 也能直接用索引顺序满足。我在实际项目里见过一个更极端的案例有一张 2000 万行规模的订单表线上一条统计 SQL 跑了 30 秒几个单列索引全部失效因为查询条件全是多字段组合。后来只新建了一个复合索引覆盖了组合查询里等值和范围所有列SQL 直接降到 200 毫秒。同一条 SQL索引利用率天差地别问题不在有没有索引而在索引设计是否匹配查询模式。5.3 场景三数据分布倾斜——热点值带偏了优化器再多说一个比较隐蔽的场景它常被当作“玄学”数据分布严重倾斜导致优化器选错索引。用户表里有个字段source_channel记录注册渠道取值一共 5 种其中渠道 A 覆盖了 95% 的用户其余 4 种只有 5%。你在 source_channel 上建了索引。执行SELECT * FROM user WHERE source_channel A优化器走到全表扫描执行SELECT * FROM user WHERE source_channel B优化器却会走索引。同样一个索引同样结构的 SQL一个走全表一个走索引这不是“索引失效”而是优化器基于数据分布做出的合理选择——渠道 A 的值命中 95% 的行走索引要大量回表干脆全表扫顺序读更快渠道 B 命中 1% 的行走索引加少量回表更优。这种问题不需要修索引但如果你不清楚优化器的成本模型很容易被误导。遇到类似情况不要急着 DROP 索引重建先用优化器 trace 看清楚它计算的行数和成本再用ANALYZE TABLE刷新统计信息排除统计失真。如果业务上必须让某种低基数值也能快速筛选可以考虑覆盖索引加条件下推或者拆分查询模式。6. 面试里怎么答能命中“点子上”——区分背答案和真理解既然标题是面试题我最后专门聊聊面试表达。同样是答这个问题不同深度的回答在面试官心里的评分完全不同。6.1 差劲的回答长什么样“索引失效了比如隐式转换、对索引列做了函数操作、违反了最左前缀……所以要避免这些写法。”说完就等着面试官点头。这种回答只能说明你背过一些面试题但暴露的问题是你没有建立完整的执行流程概念也缺少排查问题的系统性思路甚至你都不清楚“加了索引但慢”和“加了索引但失效”之间的边界。6.2 合格的回答长什么样“加索引不代表稳。要看执行计划确认 type 和 rows。如果索引没进 possible_keys重点检查 SQL写法是否让索引列参与了运算比如函数和隐式转换如果 possible_keys 里有但 key 是另一个索引或者 rows 很大重点分析优化器选的索引是否合适检查已用索引是否覆盖了查询条件如果 key 命中了但 Extra 出现了 Using filesort 或 Using temporary重点优化排序和分组逻辑。最后还要看表数据大小和统计信息是否过期需要 ANALYZE TABLE 刷新。回表多的话改覆盖索引或延迟关联。”——讲到这个程度面试官基本知道你平时真的排查过慢查询。6.3 优秀的回答长什么样在合格的基础上再加一层系统性。比如你可以在结尾总结这类问题本质上是“索引访问路径的成本优化”问题要从三个视角理解存储引擎视角B 树的结构决定了需要范围扫描能力二级索引和聚簇索引间存在回表代价优化器视角它依赖统计信息做成本估算统计过旧会导致选错执行计划这跟“索引是否失效”无关执行器视角即便走对索引排序、分组、临时表、锁等待都可能在索引之后拖慢查询。这个回答框架涵盖了原理、实践和排查方法论面试官几乎没有继续追问的余地因为你把这条链路从上到下都堵满了。就算他再追问某个细节你也能顺着链路往下深挖。提示面试时候如果有机会主动说出你自己经历过的一个慢查询案例哪怕是“线上一个统计接口每晚到凌晨才跑完通过 optimizer trace 发现是统计信息失真ANALYZE 之后恢复”这种小案例比任何理论都更能证明你的实战能力。没有真实案例讲清楚一个 oltp 系统常见的“深分页回表”场景的排查过程也完全可以。7. 说说我踩坑之后沉淀下来的几条实用经验最后这部分我不再做总结性发言就分享一下我在实际工作中反复用到的几条索引优化经验每一条都是从线上事故里换来的希望能帮你少走弯路。第一条建索引之前先用真实生产数据量评估选择性。很多同事喜欢在小表上建索引数据量只有几百行任何索引看起来都快。等你数据量涨到千万级才发现当初建的低选择性索引完全没用。判断一个索引有没有存在价值在关键数据量下看一眼区分度SELECT COUNT(DISTINCT col) / COUNT(*) FROM table。比值太低的列单独建索引基本是浪费空间还拖慢插入。第二条调整完索引务重复查执行计划和实测耗时。我见过不少人改完索引跑一次查询觉得“好像快了一点”就算完事结果第二天凌晨批量任务一跑新索引不仅没帮助反而因为额外维护索引导致写入性能下降。索引是拿写入 IO 换查询速度的收益不明显就要果断放弃。第三条别迷信“索引越多越好”。每张表多一个索引每次 INSERT、UPDATE、DELETE 就要多维护一棵 B 树。线上有一个反馈“加了索引后数据导入变慢了”其实就是因为索引数量过多导致写入路径频繁更新索引页。正常情况下单表索引数量控制在 5 个以内覆盖核心查询模式就足够了。第四条把慢查询日志和监控告警打通不要等人报障。我自己的习惯是每天扫一遍慢日志看看有没有新出现的、之前没见过的 SQL 模式并结合优化器 trace、performance_schema 的数据把常见的坏模式沉淀到团队的 SQL 规范里。一个规范的“慢查询巡检清单”比任何大牛的一次性调优都管用。回到开头那道面试题。当你真正把索引理解为一套“数据分布 存储结构 成本估算”共同作用的系统时“加了索引为什么还慢”就不再是一道要背答案的面试题而是一个只要按方法论排查就一定能找到根因的工程问题。希望这篇整理对你的面试准备和日常排障都有实实在在的帮助。