别再全表扫描:索引失效8大典型场景与排查清单
本期《性能诊断室》邀请资深数据库性能工程师陈默,围绕“索引失效与全表扫描”做一次系统访谈。陈默长期负责交易系统、订单库和报表库的性能治理,他的观点很明确:索引失效不是玄学,绝大多数都能从SQL写法、索引结构、统计信息和成本模型四条线找到证据。以下为对话整理,文末给出8大场景速查与排查清单。
问:先请给读者一个判断框架:什么叫索引失效? 答:严格说,索引失效不是“索引坏了”,而是优化器没有选择索引作为访问路径,或者选择了低效索引,最终表现为全表扫描、全分区扫描、回表过多。优化器基于成本做选择,数据量、选择性、回表代价、统计信息都会影响结果。因此排查时不要只问“为什么没走索引”,而要问“为什么不值得走索引”或“为什么走了错误索引”。执行计划要看访问类型、实际行数、预估行数、过滤比例、回表次数和额外排序。目标不是强迫每条SQL都走索引,而是避免无谓的全表扫描。
问:第1个典型场景是什么? 答:隐式类型转换。最常见的是字符串列用数字比较,例如手机号字段是varchar,SQL写成where phone = 13800000000。数据库可能把列转换成数字,索引有序性被破坏,从而退化为全表扫描。正确写法是where phone = '13800000000'。连接字段字符集或排序规则不一致也会发生隐式转换。排查时核对字段类型、参数类型、绑定变量类型,以及ORM是否把字符串传成数字。很多“昨天还好、今天突然慢”的问题,根源就是新版本代码改了参数类型。
问:第2个场景呢? 答:函数或表达式包裹索引列。比如where date(created_at) = '2026-10-28',列被函数包住后,普通B+树索引很难直接定位范围,应改写为created_at >= '2026-10-28 00:00:00' and created_at < '2026-10-29 00:00:00'。再如where upper(name) = 'ABC',可以考虑函数索引或生成列;where amount + 100 > 500,应把计算移到常量侧。注意,函数索引能解决问题,但会增加写入成本和维护复杂度,必须结合查询频率评估。
问:第3个场景是模糊查询吗? 答:前导通配符是典型代表。like '%关键词'无法利用B+树的有序性,因为不知道前缀从哪里开始,容易变成全表扫描;like '关键词%'通常还能走范围扫描。治理思路不是硬扛,而是按业务选择:能前缀匹配就前缀匹配;需要全文检索就用全文索引、倒排索引或搜索服务;需要任意位置匹配,就评估独立搜索集群。还要注意转义符和大小写规则,避免为了“安全”写出让索引完全失效的表达式。
问:第4个场景,联合索引最左前缀断裂怎么理解? 答:联合索引(a,b,c)不是三个独立索引。查询条件必须从最左列开始连续使用,才能充分利用索引定位。where b = ? and c = ?通常无法有效使用该索引;where a = ? and c = ?只能用到a。更隐蔽的是范围条件:where a > ? and b = ?,a用于范围扫描后,b往往不能继续用于精确定位。设计原则通常是等值条件在前、范围条件在后、排序字段与过滤字段协同。不要迷信“列都出现在索引里就行”,顺序和条件形态同样重要。
问:第5个场景涉及OR、否定条件和空值吗? 答:是的。OR连接不同列时,优化器可能选择索引合并,但成本常常很高,尤其是两边选择性都一般时。可以评估改写成union all,或拆成两次查询。not in、!=、not like、is null并非绝对失效,但在数据分布不利时会放大扫描。空值还涉及索引存储和统计信息,不能简单认为“建了索引就一定走”。治理时要看业务语义:能否把否定条件改成正向范围,能否把空值转成默认值,能否通过分区减少扫描范围。
问:第6个场景是排序和分组吗? 答:排序分组与索引顺序不匹配,是深分页和报表查询的重灾区。比如索引是(a,b),查询where a = ? order by b,通常较友好;如果order by b,a或混合升降序,就可能产生额外排序。group by同样依赖索引顺序。还要警惕limit深分页:limit 100000,20并不是只读20行,前面大量行可能被扫描和丢弃。治理方式包括让索引顺序匹配过滤与排序、使用覆盖索引、基于游标分页,以及为报表场景设计专用索引或预聚合表。
问:第7个场景是统计信息问题吗? 答:统计信息陈旧、数据倾斜和低选择性,都会让优化器做出“看似错误”的选择。比如status只有几个值,某个值占90%数据,走索引回表反而更贵,优化器可能选择全表扫描;统计信息过期时,预估行数可能与实际行数相差几个数量级。要关注统计信息更新时间、采样比例、直方图是否覆盖倾斜列,以及绑定变量是否存在参数敏感问题。治理手段包括更新统计信息、为倾斜列建立更合适的索引、使用分区或改写查询,而不是一上来就force index。
问:第8个场景呢? 答:回表成本过高与覆盖索引缺失。二级索引只包含索引列和主键,查询需要其他列时就要回表。大范围扫描时,大量随机回表可能比顺序全表扫描更贵,优化器因此放弃索引。典型信号是扫描行数很多、回表次数高、选择列很宽。治理方式包括减少select *、建立覆盖索引、把高频小字段纳入索引、利用索引条件下推,以及评估MRR等能力。但覆盖索引不是越宽越好,索引宽度会增加页数、降低缓存命中率,必须做权衡。
问:如果线上已经出现慢查询,排查清单应该怎么走? 答:我建议按九步走。第一步,固化问题SQL和真实参数,别拿脱敏后的假参数做判断。第二步,获取真实执行计划,优先看实际执行信息,而不是只看预估。第三步,核对访问路径、使用的索引、预估行数与实际行数、过滤比例、回表次数和额外排序。第四步,检查字段类型、字符集、排序规则、函数和表达式。第五步,检查联合索引顺序与最左前缀,确认范围条件位置。第六步,检查统计信息、直方图和数据倾斜。第七步,评估回表成本与覆盖索引。第八步,做SQL改写或索引调整的A/B验证,记录前后指标。第九步,把结论沉淀为SQL审核规则和监控基线。排查时至少保留执行计划、慢日志、表结构、索引定义和数据分布五类证据。
问:能举一个综合案例吗? 答:可以。订单表有一个联合索引(status, merchant_id, create_time),查询是where merchant_id = ? and status = ? order by create_time desc。这个索引的最左列是status,而查询把merchant_id放在前面,虽然两个等值条件都能用上部分能力,但排序和定位并不理想。更匹配的索引是(merchant_id, status, create_time),让等值列在前、排序列在后,减少额外排序和回表。再叠加一个手机号查询,如果手机号列是varchar却传了数字,就会再多一个隐式转换问题。治理这类系统不能只改一条SQL,而要把“字段类型规范、索引设计规范、SQL审核、慢查询复盘”连成闭环。
问:常见误区有哪些? 答:第一,索引越多越好。索引会增加写入、更新和存储成本,还会干扰优化器。第二,全表扫描一定坏。小表、低选择性查询、宽范围报表,全表扫描可能更合理。第三,只看执行计划里的key,不看rows和实际耗时。第四,出了问题就force index,短期救火可以,长期会掩盖统计信息和索引设计问题。第五,忽略数据增长。今天走索引,数据量上来后回表成本可能反转。第六,只优化查询不治理写入,最后索引维护成为新瓶颈。
问:落地治理机制上有什么建议? 答:到2026年,云数据库和分布式数据库的能力越来越强,但基本原则没有变:让证据驱动优化。上线前做SQL审核,重点拦截隐式转换、函数包裹、前导通配符、联合索引顺序错误;上线后做慢日志聚合和执行计划基线;定期更新统计信息,关注数据倾斜;对高频SQL建立索引生命周期管理,新增索引必须说明收益和写入代价;每季度复盘一次全表扫描TOP清单。团队要形成共同语言:不是“这条SQL没走索引”,而是“这条SQL在当前数据分布下,走索引的回表成本高于全表扫描,我们准备用覆盖索引或改写降低代价”。
关键经验:第一,索引失效多数不是数据库“抽风”,而是列被转换、被函数包裹、条件形态不匹配、索引顺序不对、统计信息失真或回表成本过高。第二,排查顺序应固定为:真实SQL与参数、真实执行计划、类型与函数、联合索引、统计信息、数据分布、回表成本、改写验证、监控回归。第三,8大典型场景要熟记:隐式类型转换、函数包裹列、前导通配符、最左前缀断裂、OR与否定条件、排序分组不匹配、统计信息与数据倾斜、回表成本与覆盖索引缺失。第四,优化目标不是“每条SQL都必须走索引”,而是“避免无谓的全表扫描,并让访问路径与数据分布相匹配”。第五,最好的排查清单不是写在文档里,而是固化到SQL审核、监控告警和发布流程中。做到这五点,全表扫描才会从“偶发事故”变成“可发现、可解释、可治理的工程问题”。