索引失效场景清单

共 17 题
📑 题目列表 17 题
#
★★★

1. 导致索引失效的完整场景清单,隐式类型转换、函数包裹列、前导通配符 LIKE、OR 连接非索引列、隐式排序规则不一致——各自的原理与改写方案?

导致索引失效的完整场景有哪些?隐式类型转换、函数包裹列、前导通配符 LIKE、OR 连接非索引列、排序规则不一致各自的原理与改写方案?

  • 隐式类型转换与函数包裹列:列被计算后 B-Tree 无法定位
  • 前导通配符 LIKE:无法确定范围起点;OR 非索引分支拖垮整体
  • 排序规则不一致:隐式转换使索引失效;各自改写方案

索引失效的完整清单与原理:① 隐式类型转换——WHERE int_col = '123' 时列被 CAST 包裹(等价函数包裹列),B-Tree 索引无法命中;改写为类型一致的比较(id = CAST('123' AS UNSIGNED) 或统一列类型)。② 函数包裹列——WHERE YEAR(created_at)=2024 使列成为函数参数,索引失效;改写为范围条件(created_at BETWEEN '2024-01-01' AND '2024-12-31')或建函数索引/表达式索引。③ 前导通配符 LIKE——LIKE '%abc' 无法确定范围起点(B-Tree 按前缀有序),只能全扫;'abc%' 则可走索引。④ OR 连接非索引列——WHERE idx_col=1 OR other_col=2 中非索引分支无法用索引,需索引合并或全表;改写为 UNION ALL 或 IN。⑤ 排序规则不一致——JOIN 两侧列 collation 不同(如 utf8mb4_0900_ai_ci vs utf8mb4_general_ci)时需对一侧做转换,索引失效;统一 collation 或显式 COLLATE 一致。

排查建议:EXPLAIN 看 key=NULL、type=ALL 或 index(全索引扫)结合 key_len 判断;每类场景都有对应的"改写优先、建索引兜底"方案。

本题考察索引失效的全景分类:五类场景的原理统一为"键序无法定位"或"分支无法命中",每类给出改写方案,形成清单式回答。

#
★★★

2. 为什么 IS NULL/IS NOT NULL、!= 不一定走索引?优化器基于成本的选择逻辑如何验证?

为什么 IS NULL、IS NOT NULL、!= 不一定走索引?优化器基于成本的选择逻辑如何验证?

  • 成本逻辑:匹配行占比高时回表代价大于全表扫描
  • 引擎差异:PG 与 MySQL 的 B-Tree 索引都存储 NULL(PG 中 NULL 排最大、InnoDB 中 NULL 排最左),IS NULL 均可匹配索引,是否使用由成本决定
  • 验证:EXPLAIN ANALYZE 对比强制索引与默认计划的耗时与行数

这三个条件"不一定走索引"的根本原因是成本:优化器基于统计估算选择率,若条件匹配行占比高(IS NOT NULL 在 NULL 极少时匹配几乎所有行、!= 匹配除等值外的全部行),索引扫描加逐行回表的随机 I/O 代价高于顺序全表扫描,因此选全表——这是"成本选择"而非"规则失效"。另有引擎语义细节:PostgreSQL 的 B-Tree 索引会存储 NULL 且默认把 NULL 视为最大值排在末尾(NULLS LAST),WHERE col IS NULL 可以用普通索引定位(PG 8.3 起);MySQL InnoDB 二级索引同样存储 NULL 且排在最左,IS NULL 也可走索引——两库的 IS NULL 都能匹配索引,最终是否使用仍取决于成本估算,NULL 占比高时同样可能被否决。

验证方法:EXPLAIN 查看 type/rows(估算回表行数)与 key;用 EXPLAIN ANALYZE 对比"强制索引(FORCE INDEX/IndexScan 提示)"与"默认计划"的实际耗时与行数,确认优化器判断正确;若数据分布变化(NULL 占比大幅下降)后计划未变,再检查统计是否陈旧(ANALYZE 后复测)。

本题考察"不走索引"的成本本质:选择率高→回表贵→全表优,叠加 PG/MySQL 的 NULL 排序差异,并用实测验证。

#
★★★

3. 复合索引的最左前缀原则,哪些查询模式会"部分使用"索引(如跳过中间列)?Index Skip Scan 如何缓解?

最左前缀原则下哪些查询模式会部分使用索引(如跳过中间列)?Index Skip Scan 如何缓解?

  • 跳过中间列:前缀不连续,后续列退化为回表后过滤
  • 前缀缺失:完全无法用该索引
  • Index Skip Scan:枚举前缀列不同值多次探测,前缀列基数过大时无效

最左前缀原则:复合索引 (a,b,c) 只能按列序消费条件——WHERE a=1 AND c=2:a 用于索引定位,跳过 b 后 c 无法继续用索引(c 条件退化为回表后过滤,EXPLAIN 显示 Using where,key_len 只反映 a);WHERE b=2(缺 a):完全无法用该索引(除非建 (b) 索引或走 skip scan)。"部分使用"的两种形态:前缀不连续(跳列)与前缀缺失。

Index Skip Scan(MySQL 8.0.13+、Oracle 支持,PG 18 加入)的缓解原理:当 a 缺失但 a 的区分度低时,优化器枚举 a 的每个不同值,对每个值在 (a,b) 索引上做 b 的等值/范围扫描,最后合并结果——等价于"隐式按 a 分组多次索引探测",避免全表扫描;代价是探测次数等于 a 的去重值数,a 基数过大时反而更慢(优化器据此决定是否用 skip scan)。缓解替代:为高频查询调整列序(把真正过滤的列放前)或建新索引。

本题考察最左前缀的两种失效形态与 Skip Scan 的机制:跳列的部分使用、缺前缀的完全失效、以及"探测次数=前缀基数"的适用边界。

#
★★★

4. 索引失效的常见场景(函数、隐式转换、前导模糊、OR 等)如何系统排查?

如何系统排查索引失效场景(函数、隐式转换、前导模糊、OR 等)?排查流程与工具?

  • 定位慢 SQL:慢日志、performance_schema、pg_stat_statements
  • EXPLAIN 四要素:type/key/key_len/Extra 的判读
  • 对照失效清单归因,改写/建索引后回归验证

系统排查流程:① 定位——慢查询日志(MySQL slow_log)、performance_schema/pg_stat_statements 找出高耗时高频语句;② 读计划——EXPLAIN(MySQL/SQL Server)/EXPLAIN ANALYZE(PG)查看 type(ALL 全表、index 全索引扫、range、ref、const)、key(用到的索引)、key_len(用了多少列)、rows(估算行数)、Extra(Using where 说明索引未滤尽、Using filesort/Using temporary 说明排序分组未用索引);③ 对照失效清单归因——条件列是否被函数/表达式包裹(YEAR()、+1、隐式转换如字符串 vs 数字)、LIKE 是否前导通配、OR 分支是否含非索引列、JOIN 列 collation 是否一致、索引列序是否匹配最左前缀、统计是否陈旧(rows 与实际差数量级);④ 实验验证——改写 SQL(范围条件替代函数、UNION ALL 替代 OR、统一 collation)或用 FORCE INDEX/optimizer trace 观察优化器决策;⑤ 根治——建函数索引/表达式索引、调整索引列序、刷新统计/直方图;⑥ 回归——EXPLAIN 前后对比加线上 P95 监控。

关键点:EXPLAIN 的 rows 与 key_len 是"索引是否被充分利用"的最直接证据。

本题考察失效排查的方法论:六步流程(定位→读计划→归因→实验→根治→回归)与 EXPLAIN 四要素的判读。

#
★★★

5. IS NULL/IS NOT NULL 在 InnoDB 二级索引上的扫描方式,覆盖索引为何可以优化 NULL 查询

IS NULL/IS NOT NULL 在 InnoDB 二级索引上的扫描方式是什么?覆盖索引如何优化 NULL 查询?

  • InnoDB 二级索引存储 NULL 且 NULL 排在最左
  • IS NULL 走索引定位 NULL 区间,IS NOT NULL 从首个非 NULL 键开始扫
  • 覆盖索引使 NULL 查询零回表(Using index)

InnoDB 的二级索引会存储 NULL 值(NULL 在 B-Tree 中按最小值排序,位于键序最左端),因此:WHERE col IS NULL 可走二级索引——定位到 NULL 区间做范围扫描;WHERE col IS NOT NULL 等价于"col 大于 NULL 下限",从索引第一个非 NULL 键开始扫描。两者都能用索引,但性能取决于回表比例:NULL 或非 NULL 占比高时,索引扫描命中大量行、逐行回表读主键外的数据,成本逼近全表扫描,优化器可能改选全表。

覆盖索引的优化:若 SELECT 所需列(含主键)全部包含在索引中(EXPLAIN 显示 Using index),则 NULL 查询的扫描完全在索引内完成、零回表——此时即使命中行多,代价也只是索引顺序扫描,比"索引+大量回表"或全表扫描都优;这也是"低区分度列+覆盖列"组合索引的经典收益。实战:对高频 IS NULL 过滤的查询,设计"条件列+SELECT 列"的覆盖索引,并让选择性足够的列在前。

本题考察 NULL 在 InnoDB 索引中的物理行为:NULL 最左排序、两类条件的扫描方式、以及覆盖索引免回表的优化机制。

#
★★

6. IN 与 EXISTS 的选择,不同数据分布下优化器如何改写?半连接优化的适用条件?

IN 与 EXISTS 在不同数据分布下如何选择?优化器如何改写两者?半连接优化的适用条件?

  • IN/EXISTS 统一改写为半连接,写法差异不影响计划
  • MySQL 半连接策略:firstmatch、materialization 等按代价选择
  • 适用条件:无 LIMIT/聚合/副作用,可去相关

在主流数据库中,IN 与 EXISTS 的"手动选择"已被优化器改写取代:两者语义等价(去重语义下),优化器统一转换。MySQL 对 IN/EXISTS 子查询应用半连接优化(optimizer_switch 的 semijoin):根据代价在多种执行策略中选择——firstmatch(类似嵌套循环:外层行匹配即止,适合子查询小、索引好)、loosescan(松散扫描去重)、duplicateweedout(去重淘汰)、materialization(物化子查询结果集为临时表再 join,适合子查询结果集大且去重后小、外层无有效索引);未启用半连接时退化为物化或逐行相关执行。PG 把 IN 子查询去相关为 Semi Join,EXISTS 相关子查询同样去相关,计划形态取决于代价(NL 半连接 vs Hash 半连接)。

适用条件:子查询可安全改写(无 LIMIT、无聚合、无易变函数、无阻碍去相关的外层引用);不可改写时保留原执行形态。结论:SQL 写法(IN vs EXISTS)对现代优化器影响有限,关键是让统计准确以便代价模型选出正确策略——子查询基数、去重率与外层选择性才是决定因素。

本题考察 IN/EXISTS 的改写机制:半连接统一转换、MySQL 各策略的适用分布、以及"数据分布决定策略"的结论。

#
★★

7. ORDER BY 利用索引避免 filesort 的条件清单(方向一致性、最左匹配、覆盖)?

ORDER BY 利用索引避免 filesort 的条件清单是什么?方向一致性、最左匹配与覆盖如何影响?

  • 最左匹配:排序列与 WHERE 等值列构成连续前缀
  • 方向一致:全 ASC 或全 DESC 与索引定义一致
  • 范围截断与 NULL 排序/collation 的边界

ORDER BY 免 filesort 的完整条件:① 最左匹配——ORDER BY 列必须是索引前缀的一部分,且与 WHERE 中已用的等值列连续衔接:索引 (a,b,c),WHERE a=1 ORDER BY b 可免排序(a 等值锁定后 b 在索引内有序);WHERE a>1 ORDER BY b 则不能(范围条件截断,b 的排序被破坏);② 方向一致性——所有排序列方向与索引方向一致(全 ASC 或全 DESC);MySQL 8.0 支持降序索引定义、PG 同样支持 DESC 索引,方向不匹配则需排序;③ 覆盖性——若 SELECT 列也在索引中(覆盖索引),连回表都免(Using index),但"免 filesort"本身不依赖覆盖;④ 中间列等值可续接(WHERE a=1 AND b=2 ORDER BY c 可用),范围条件不可;⑤ 边界——NULL 排序(PG 的 NULLS FIRST/LAST 默认与索引顺序可能相反)、collation 需与索引一致。

验证:EXPLAIN Extra 无 Using filesort(MySQL)/无 Sort 节点(PG)即满足;有 Using filesort 时优先调整索引列序与方向。

本题考察免排序的条件清单:连续前缀、方向一致、范围截断与 NULL/collation 边界,用索引 (a,b,c) 举例验证。

#
★★

8. 索引列参与计算(WHERE id + 1 = 10)或表达式(对 created_at 使用 YEAR() 函数)为何失效?函数索引(Functional Index)如何补救?

索引列参与计算(id+1=10)或表达式(YEAR(created_at))为何失效?函数索引/表达式索引如何补救?

  • 原理:索引键按列原始值有序,计算后无法二分定位
  • 补救:函数/表达式索引(MySQL 8.0 (expr)、PG 表达式索引)
  • 优先改写为范围条件或可索引形式

失效原理:B-Tree 索引的键是列的原始存储值,键序按原值排列;WHERE id+1=10 意味着比较对象是"计算后的值",索引树中不存在该键序(每个索引项的键需先做运算才能比较),优化器无法用二分定位,只能全表扫描——等价于"列被函数包裹"(YEAR(created_at) 同理:树的键是时间戳原值,无法按年份定位)。补救手段:① 函数/表达式索引——MySQL 8.0 直接建 (expr) 索引(如 CREATE INDEX idx ON t ((YEAR(created_at))),内部用隐藏生成列实现),PG 用 CREATE INDEX ON t (YEAR(created_at)),查询保持原写法即可命中;② 改写为可索引形式——YEAR(created_at)=2024 改为 created_at BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59'(范围条件走普通索引);id+1=10 改写为 id=9。

原则:优先改写(索引通用性强、无需额外存储),无法改写才建函数索引(函数索引对写入有额外成本,且查询表达式必须与索引表达式完全一致才能命中)。

本题考察计算/函数包裹的失效原理与双通道补救:键序无法定位的本质、表达式索引与改写范围的对比。

-- MySQL 8.0
CREATE INDEX idx_year ON orders ((YEAR(created_at)));
-- PostgreSQL
CREATE INDEX idx_year ON orders (YEAR(created_at));
-- 或改写为范围条件
SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
#
★★

9. 字符集/排序规则不一致(utf8mb4 vs utf8)导致 JOIN 时索引失效的排查与修复?

JOIN 条件两侧字符集/排序规则不一致为什么导致索引失效?如何排查与修复?

  • 原理:collation 不同触发隐式转换,等价函数包裹使索引失效
  • 排查:SHOW CREATE TABLE、information_schema.columns、EXPLAIN type=ALL
  • 修复:统一列定义(CONVERT TO CHARACTER SET)、显式 COLLATE、DDL 规范防复发

原理:MySQL 中 JOIN/比较时,若两侧列的字符集(charset)或排序规则(collation)不同,服务器必须先将一侧列转换为另一侧的 collation 才能比较——转换相当于对列施加表达式(CONVERT/CAST),使该侧列"被计算",其上的索引无法命中,EXPLAIN 表现为 type=ALL、key=NULL。常见触发:新库 utf8mb4_0900_ai_ci(8.0 默认)与旧表 utf8mb4_general_ci 混用、utf8mb4 与 utf8 混用(迁移遗留)。

排查:SHOW CREATE TABLE 对比两侧列定义;information_schema.columns 查 COLLATION;EXPLAIN 确认是否全表;用 SELECT ... COLLATE utf8mb4_0900_ai_ci 实验验证。修复:① 根治——统一列定义(ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci,注意数据长度与索引前缀限制);② 临时——显式 COLLATE 使两侧一致(但需两侧都一致才不失效);③ 防复发——DDL 规范与巡检脚本检查列 collation 一致性。

本题考察 collation 不一致的失效链路:隐式转换=函数包裹、排查三入口、根治与防复发手段。

#
★★

10. "索引未失效但走全表"的统计信息问题,直方图、采样与 optimizer 选择?

"索引未失效但优化器走全表"的统计信息原因有哪些?直方图、采样与 optimizer 如何影响?

  • 原因:统计陈旧、采样不足、缺直方图高估选择率、低基数列、random_page_cost 高估回表
  • 手段:刷新统计、建直方图、列级 STATISTICS、采样页、代价参数
  • 验证:EXPLAIN ANALYZE 对比估算与实际,区分"误判"与"正确选择"

"索引明明有效(条件可匹配索引)却走全表"的根因在成本与统计,常见:① 统计陈旧/采样不足——rows 估算远大于实际(未 ANALYZE 或采样页过少低估基数),优化器按错误高选择率放弃索引;② 缺乏分布信息——无直方图时优化器按均匀假设高估选择率(尤其倾斜列),MySQL 8.0 用 ANALYZE TABLE ... UPDATE HISTOGRAM ON 列 补充分布;③ 低基数列条件——回表行数占比高,全表更便宜,这是"正确选择"而非失效;④ 代价参数——SSD 上 random_page_cost 过高会系统性高估"索引+回表"代价(PG 调至 1.1 左右);⑤ NULL 占比未反映——NULL 大量存在时等值估算偏差。

处理:先 EXPLAIN ANALYZE 对比估算与实际行数定位偏差来源,再按原因分别处理(刷新统计、建直方图、调采样页/列级 STATISTICS、调代价参数);确认"优化器判断正确"的情况(真选择率高)则不该强制索引,而应改查询形态(覆盖索引、改写)。

本题考察"该走索引却没走"的统计归因:五类原因、对应修复手段,以及"误判与正确选择"的区分。

#
★★

11. 复合索引的列顺序与过滤/排序/覆盖的匹配规则,如何设计最优列序?

复合索引列序与过滤/排序/覆盖的匹配规则是什么?如何设计最优列序?

  • 设计顺序:等值列(常用高区分度在前)→ 排序列(方向一致)→ 范围列 → 覆盖列
  • 等值列顺序可互换;范围列后的列无法继续定位
  • 用 key_len 与 Extra 验证设计

复合索引列序设计的三原则:① 等值条件列放最前——WHERE a=1 AND b=2 中 a、b 都是等值条件,顺序可互换(等值不打断前缀连续性),优先把"查询最常用、区分度高"的列放前以提高扫描效率与索引复用度;② 排序列紧跟其后——ORDER BY/GROUP BY 列若与等值列连续构成前缀且方向一致,可直接免 filesort/临时表——把排序列放在等值列之后、范围列之前;③ 范围列最后、覆盖列兜底——范围条件(>、<、BETWEEN)之后的列无法继续用索引过滤,因此放最后;SELECT 中的剩余列可追加到索引尾部形成覆盖索引(免回表),但每列都有写入成本,需权衡。

反例诊断:若 WHERE a=1 AND c 且 ORDER BY b 高频,则 (a,b,c) 优于 (a,c,b)。落地:列出高频查询的"等值列→排序列→范围列→覆盖列"清单,合并同类项、避免冗余前缀索引,最后用 EXPLAIN 的 key_len 与 Extra 验证。

本题考察列序设计的方法论:四类列的顺序规则、等值可互换的例外、以及用 EXPLAIN 验证的闭环。

#
★★

12. 索引下推(ICP)与覆盖索引在 InnoDB 中的实际收益场景?

索引下推(ICP)与覆盖索引在 InnoDB 中的实际收益场景是什么?如何发挥作用?

  • ICP:索引扫描时提前过滤非前缀条件,减少回表(Using index condition)
  • 覆盖索引:全部列在索引内,零回表(Using index)
  • 收益场景:大表、行宽、回表随机 I/O 昂贵

索引下推(ICP)的收益场景:查询条件同时包含"索引能定位的部分"与"索引无法定位但属于索引列的非前缀部分"(如复合索引 (a,b),WHERE a=1 AND b LIKE '%x'——b 在索引内但非前缀定位):传统执行需把 a=1 命中的每行回表后再过滤 b 条件;启用 ICP 后,存储引擎在索引扫描时就按 b 条件过滤,只有通过过滤的行才回表,EXPLAIN 显示 Using index condition,显著减少随机回表 I/O。

覆盖索引的收益场景:SELECT 列全部在索引中(Using index),索引扫描即完成查询、零回表——适合高频点查/统计类查询与"索引列多、表行宽"的场景(行宽时回表 I/O 大)。两者协同:ICP 减少"回表行数",覆盖索引减少"回表行为",都是对"二级索引回表"这一 InnoDB 固定成本的优化;设计时先看 EXPLAIN 的 Extra(Using index condition/Using index),再针对回表热点补索引。

本题考察两类回表优化的分工:ICP 减行数、覆盖免行为,结合 Extra 判读与场景设计。

#
★★

13. OR 条件与索引,什么情况下 OR 会导致索引失效,如何改写?

OR 条件在什么情况下导致索引失效?如何改写?

  • 全部分支可走索引:Index Merge Union/BitmapOr
  • 任一分支无索引:整体退化为全表扫描
  • 改写:等值同列合并 IN、异列拆 UNION ALL

OR 的索引行为:优化器只有确认"每个 OR 分支都能用索引"时才会走"索引合并/位图合并"路径(MySQL 的 Index Merge Union 或 PG 的 BitmapOr——分别扫描各分支的索引位图再求并集回表);只要有一个分支无法用索引(条件列无索引、函数包裹列、前导模糊等),优化器通常放弃索引直接全表扫描,因为无法保证每个分支都有索引路径。

改写方案:① 等值同列 OR 合并为 IN——WHERE a=1 OR a=2 → a IN (1,2),可直接走索引范围,比 index merge 更高效;② 异列 OR 且各分支行数都大——拆成两个查询 UNION ALL(各自走最优路径,避免合并位图后的大回表);③ 无法用索引的分支——为该列补索引,或改写业务逻辑把无索引分支的过滤提前/拆开。验证:EXPLAIN 看 type(index_merge/ALL)与 Extra(Using union);PG 看 BitmapOr 节点是否出现。

本题考察 OR 的失效条件与改写:合并路径的前提(全分支可索引)、三类改写方案、以及 EXPLAIN 验证。

#
★★

14. MySQL 前缀索引(INDEX(col(10)))的失效边界与选择依据

MySQL 前缀索引(INDEX(col(10)))的失效边界是什么?选择前缀长度的依据?

  • 边界:无法全列排序、无法覆盖、选择性按前缀去重
  • 选择依据:COUNT(DISTINCT LEFT(col,N))/COUNT(*) 接近全列比例的最小 N
  • 字节上限(3072)与适用场景(长文本列)

前缀索引 INDEX(col(N)) 只把列的前 N 个字符(utf8mb4 下为 N 个字符)作为索引键,节省空间、加快插入,但有一组边界:① 无法用于 ORDER BY/GROUP BY 的完整排序(索引只含前缀,无法确定全列顺序);② 无法成为覆盖索引(索引中不含完整列值,SELECT 该列必须回表,EXPLAIN 不会出现 Using index);③ 选择性受限——若前缀区分度差(如公共前缀),扫描范围大,可能不如全列索引甚至不如不加;④ 等值/范围/最左前缀匹配本身可用,但匹配基于前缀(WHERE col LIKE 'abc%' 可命中)。

选择依据:用 SELECT COUNT(DISTINCT LEFT(col, N)) / COUNT(*) 计算各 N 的去重比例,选择"接近全列选择率的最小 N"(如 5、10、15),同时考虑字节上限(InnoDB 索引键 ≤ 3072 字节,utf8mb4 下字符数×4)与写放大。适用:长字符串列(URL、邮箱、身份证)且查询模式为前缀等值/范围;对"精确匹配整个长串"且高频的场景,可搭配哈希列(存储 col_hash 并建索引)替代。

本题考察前缀索引的边界学:三类失效边界、去重比例选择法、字节上限与哈希列替代方案。

SELECT COUNT(DISTINCT LEFT(url, 10)) / COUNT(*) AS sel FROM pages;
CREATE INDEX idx_url ON pages (url(10));
#

15. 统计信息过期导致优化器误判索引选择,如何通过 ANALYZE TABLE / pg_stat_statements 发现并修复?

统计信息过期如何导致优化器误判索引?如何用 ANALYZE TABLE 与 pg_stat_statements 发现并修复?

  • 误判链路:统计旧 → rows 误估 → 索引/join 误选
  • 发现:EXPLAIN ANALYZE 对比、n_mod_since_analyze、pg_stat_statements 耗时突变
  • 修复:手动 ANALYZE、阈值与采样调参、直方图/扩展统计

误判链路:大量 DML 后统计未刷新,优化器用旧行数/基数估算选择率——低估则错过索引、高估则滥用索引(如把 10 万行估算成 1 行选主键点查路径),最终表现为"同样 SQL 突然变慢"或"计划与数据规模明显不符"。发现手段:① EXPLAIN ANALYZE 对比 estimated rows 与 actual rows(差数量级即统计失真);② pg_stat_user_tables 的 n_mod_since_analyze(PG 14+)与 last_analyze——修改量远超自动阈值却长期未 analyze,说明 autovacuum 滞后或阈值不当;③ pg_stat_statements——对同一语句统计 total_time 的突变(配合计划变化);④ MySQL——performance_schema 的语句统计 + 对比 ANALYZE TABLE 前后 EXPLAIN 计划与 rows。

修复:① 立即——手动 ANALYZE/ANALYZE TABLE 刷新;② 防复发——调整 autovacuum_analyze 阈值与采样(PG 列级 STATISTICS、MySQL innodb_stats_persistent_sample_pages)、对倾斜列建直方图/扩展统计;③ 验证——重跑 EXPLAIN ANALYZE 确认估算收敛,监控计划回归。

本题考察统计过期的发现与修复闭环:误判链路、四类发现手段、三层修复动作。

#

16. "优化器选错索引"的案例排查,force index、统计信息刷新与直方图?

优化器选错索引的典型案例如何排查?force index、统计刷新与直方图各有什么作用?

  • 典型案例:多索引可匹配时选了选择性差的索引
  • 排查:EXPLAIN key 与 rows、EXPLAIN ANALYZE、optimizer trace 归因
  • 修复顺序:刷新统计 → 直方图/扩展统计 → 代价参数 → FORCE INDEX 临时止血

典型案例:表上有 (status, created_at) 与 (user_id, created_at) 两个索引,查询 WHERE status='A' AND user_id=123 ORDER BY created_at 时,优化器因 status 列统计(陈旧或无直方图)高估其选择性而选了错误驱动索引,导致回表与排序激增。排查步骤:① EXPLAIN 看 key 与 rows,用 EXPLAIN ANALYZE/optimizer trace(MySQL SET optimizer_trace='enabled=on' 后 SHOW WARNINGS 查看 JSON)确认"为什么选它";② 对比候选索引的实际表现——FORCE INDEX 实验:分别强制各索引跑 EXPLAIN ANALYZE,比较耗时与行数;③ 归因——统计陈旧(last_analyze 旧、rows 离谱)、缺直方图(倾斜列均匀假设)、索引冗余导致选择混乱。

修复顺序(由根因到止血):① ANALYZE/ANALYZE TABLE 刷新统计;② 倾斜列建直方图(MySQL UPDATE HISTOGRAM)/扩展统计(PG CREATE STATISTICS)+ 列级 STATISTICS;③ 仍不稳则调整代价参数(SSD 的 random_page_cost);④ 最后用 FORCE INDEX/IndexScan 提示临时锁定并登记跟踪,待统计稳定后评估移除。原则:FORCE INDEX 是证据与止血工具,根治靠统计质量与索引治理(删冗余索引减少误选空间)。

本题考察选错索引的完整排查法:案例构造、三步排查、按"根因→止血"排序的修复链路。

#

17. 使用 EXPLAIN ANALYZE 与 optimizer trace 定位索引选择的完整流程

使用 EXPLAIN ANALYZE 与 optimizer trace 定位索引选择的完整流程是什么?

  • 流程:捕获慢 SQL → EXPLAIN 初判 → EXPLAIN ANALYZE 实测对比 → optimizer trace 深挖 → 归因修复 → 回归
  • 线上采集:auto_explain 自动记录慢查询计划
  • 要点:先实测后猜根因,用 trace 理解优化器逻辑

完整定位流程:① 捕获——慢日志/performance_schema/pg_stat_statements/auto_explain(PG 配 auto_explain.log_min_duration 自动记录慢查询的计划与执行统计)找出目标 SQL;② 初判——EXPLAIN(不执行)看计划形态(key、type、rows、Extra),形成假设;③ 实判——EXPLAIN ANALYZE 真实执行,逐节点对比"估算 rows vs 实际 rows、估算耗时 vs 实际耗时",定位偏差最大的节点(通常即索引选择错误处);④ 深挖——MySQL 开 optimizer trace(SET optimizer_trace='enabled=on',SHOW WARNINGS 查看 JSON),逐条看"候选索引的 rows/成本比较"与"为何放弃某索引";PG 用 EXPLAIN (ANALYZE, BUFFERS) 看缓冲命中、排序与哈希落盘细节;⑤ 归因与验证——对照"统计陈旧/直方图缺失/代价参数/索引设计(列序、冗余)"四类根因执行对应修复(ANALYZE、建直方图、调参数、调整/新增索引),再跑 EXPLAIN ANALYZE 确认估算收敛、耗时下降;⑥ 回归——压测加线上监控(计划形态、P95),并考虑用计划缓存/SPM 防回退。

要点:先"实测数据"后"猜根因",用 optimizer trace 理解优化器逻辑而非对抗它。

本题考察定位流程的完整闭环:六步链路、线上采集工具、以及"实测先行"的方法论要点。