news 2026/9/26 5:17:52

MySQL 索引为什么没生效?这 8 种情况一次讲清

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL 索引为什么没生效?这 8 种情况一次讲清

上周帮同事看一条慢 SQL,他的第一句话是:“索引我加了啊。”

表上有索引,EXPLAIN一看type是ALL,key是NULL。

他盯着屏幕看了半天说:“这不科学。”

其实很科学。MySQL 的优化器不是看见索引就必须用,它要做成本估算。而且更多时候,索引确实“在”,只是你的写法让它没法被用上。

这篇文章把常见的索引失效情况整理了一遍,附上可复现的 SQL 和EXPLAIN输出。下次遇到慢查询,可以按这个清单逐条过一遍,八九不离十。

一、先建个能跑的实验环境
CREATETABLEt_order(idBIGINTPRIMARYKEYAUTO_INCREMENT,order_noVARCHAR(32)NOTNULL,user_idBIGINTNOTNULL,statusTINYINTNOTNULLDEFAULT0,amountDECIMAL(10,2),create_timeDATETIMENOTNULL,remarkVARCHAR(100),KEYidx_user_status(user_id,status),KEYidx_create_time(create_time),KEYidx_order_no(order_no))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

后面所有例子都基于这张表。你可以自己灌几万条数据,观察会更明显(数据量太小时优化器倾向于直接全表扫描,这是正常现象,下面第七点会讲)。

二、情况 1:违反最左前缀原则(联合索引的头丢了)
EXPLAINSELECT*FROMt_orderWHEREstatus=1\G

idx_user_status是(user_id, status)这个顺序建的。查询条件只给了status,相当于把索引的第一列跳过去了。B+ 树是先按user_id排序、再按status排序的,光知道status无法定位起始位置,这个索引就废了。

反过来:

EXPLAINSELECT*FROMt_orderWHEREuser_id=100\G-- 命中EXPLAINSELECT*FROMt_orderWHEREuser_id=100ANDstatus=1\G-- 命中

这两条都能用上。注意第二条里status也参与了过滤,但只有user_id那一段用于“定位”(key_len会变长,这是判断的好办法)。

口诀:联合索引像电话簿,先姓后名。你只知道名,查不了。

顺带一提,OR和范围查询会截断前缀:

-- user_id 能用,status 用不上(范围之后的列失效)SELECT*FROMt_orderWHEREuser_id=100ANDstatus>1ANDcreate_time>NOW();

这里status > 1是范围条件,它后面的列无法继续用于索引定位(但在 5.7+ 的 ICP 优化下,status仍可在存储引擎层做过滤,Extra会显示Using index condition)。

三、情况 2:索引列上套了函数或表达式
-- 看着很自然,但索引废了SELECT*FROMt_orderWHEREDATE(create_time)='2024-06-01';-- 这样写才能走 idx_create_timeSELECT*FROMt_orderWHEREcreate_time>='2024-06-01 00:00:00'ANDcreate_time<'2024-06-02 00:00:00';

同理:

WHEREuser_id+1=1001-- 废WHEREuser_id=1001-1-- 能走(常量表达式会被优化器先算掉)WHEREamount*100>5000-- 废WHEREamount>5000/100-- 能走

**原则很简单:索引列必须“裸着”出现在比较运算符的一侧。**任何函数、运算、隐式转换都会破坏 B+ 树的有序性。这条规则几乎适用于所有关系型数据库,不止 MySQL。

四、情况 3:隐式类型转换(最阴间的一种)
-- user_id 是 BIGINTSELECT*FROMt_orderWHEREuser_id='100';-- 能走,字符串转数字安全SELECT*FROMt_orderWHEREorder_no=123456;-- 大概率废掉

第二种情况:order_no是VARCHAR,你传了个数字。MySQL 会把索引列转成数字再做比较,等价于WHERE CAST(order_no AS SIGNED) = 123456,回到情况 2。

这种问题在 ORM 和 MyBatis 里特别容易藏住——Java 里的Long和String混着用,编译期不报错,运行时也不报错,就是查询慢。

排查方法:EXPLAIN看到type=ALL且Extra里有Using where,而条件列明明有索引时,第一件事就是去对比字段定义类型和传入值的类型是否完全一致。顺便检查字符集(utf8vsutf8mb4)和排序规则(collation)不一致也会触发类似问题。

五、情况 4:LIKE 前置通配符
SELECT*FROMt_orderWHEREorder_noLIKE'ORD%';-- 能走SELECT*FROMt_orderWHEREorder_noLIKE'%1234';-- 废掉SELECT*FROMt_orderWHEREorder_noLIKE'%1234%';-- 废掉

B+ 树是前缀有序的,后缀没有序可言。如果业务确实需要模糊匹配后缀,可以考虑:

  • 倒序存储一份冗余字段,再建索引(把后缀变前缀);
  • 改用 Elasticsearch 等搜索引擎做全文检索;
  • 小表的话,全表扫描未必比回表慢,别急着优化。

补充一点:LIKE 'ORD%'这种前缀匹配是能走索引的,而且是range级别,很多人误以为只要带%就失效。

六、情况 5:OR 条件有一边没索引
SELECT*FROMt_orderWHEREuser_id=100ORremark='加急';

remark上没有索引,整条语句就会退化成全表扫描——即使user_id那边本来能命中。因为最终结果集要做并集,一边扫全表,另一边就没必要走索引了。

解法:

SELECT*FROMt_orderWHEREuser_id=100UNIONALLSELECT*FROMt_orderWHEREremark='加急'ANDuser_id<>100;

拆成两条各自走索引,再拼起来。(注意UNION会去重并产生临时表,能确定不重复就用UNION ALL。)

同理,NOT IN、<>、IS NOT NULL这些否定类条件,通常会让优化器放弃索引——但不是绝对。如果走的是覆盖索引(见第八点),它仍然可能选择索引扫描。所以别死记“用了 != 就一定失效”,要以EXPLAIN为准。

七、情况 6:优化器算下来觉得全表更便宜(索引在,但不用)

这一条是很多人卡壳的地方:写法没问题,索引也没问题,可它就是不用。

常见原因:

原因说明
数据量太小几十几百行,全表扫描一次 IO 比回表还少
选择性太差status只有 0/1/2 三个值,命中一半以上的行,回表成本高于全表
回表代价高SELECT *要回主键索引取所有列,列越多越亏
统计信息过期ANALYZE TABLE一下可能就好了
聚簇因子差索引顺序和物理行顺序差异大,随机 IO 多

验证方法很简单:把SELECT *改成只查索引列。

-- 原来 type=ALLSELECT*FROMt_orderWHEREstatus=1;-- 改成覆盖索引,大概率 type=index 或 range,Extra=Using indexSELECTuser_id,statusFROMt_orderWHEREstatus=1;

这就是覆盖索引的威力:查询所需的所有列都在索引树上,不需要回主键索引(回表)。这也是为什么线上经常看到“冗余单列索引”——不是为了多一种检索路径,而是为了凑出覆盖索引。

如果确认优化器选错了,可以用FORCE INDEX(idx_name)强制指定,但这是权宜之计。更好的做法是改写 SQL、补联合索引,或者用ANALYZE TABLE刷新统计信息。

八、情况 7:ORDER BY / GROUP BY 和索引方向打架
-- 能利用索引排序,Extra 为空(没有 Using filesort)SELECT*FROMt_orderWHEREuser_id=100ORDERBYstatus;-- 一个升一个降,索引排序失效,出现 Using filesortSELECT*FROMt_orderWHEREuser_id=100ORDERBYstatusDESC,create_timeASC;

几个细节:

  • ORDER BY的列如果不是索引的最左前缀,或者中间隔了非等值条件,就无法利用索引排序;
  • 多表 JOIN 时,ORDER BY的列不属于驱动表,基本也要 filesort;
  • filesort不等于慢。内存里的 quicksort 很快,真正拖垮性能的是“排序的数据量太大导致落盘”。关注sort_merge_passes这个状态变量,以及ORDER BY ... LIMIT组合(这个组合其实很高效,因为只需排前 N 条)。
九、情况 8:JOIN 关联字段类型/字符集不一致
SELECTo.*,u.nameFROMt_order oJOINt_user uONo.user_id=u.id;

如果o.user_id是BIGINT,而u.id是VARCHAR(20),或者一个是utf8一个是utf8mb4,驱动表和被驱动表的索引都可能失效。这种问题在分库分表迁移、老系统合并时极其常见,而且EXPLAIN里往往看不出来,只能靠人工核对表结构。

建议:建表规范里写死一条——所有表的自增主键统一类型、统一字符集、统一 collation。前期省下的事,后期全是债。

十、把 EXPLAIN 读薄:只看五个字段

EXPLAIN输出十几列,日常排查真正有用的就这几个:

字段看什么
type访问类型,从上到下依次变差:system > const > eq_ref > ref > range > index > ALL。线上 SQL 至少要到ref/range,出现ALL就要警觉
key实际用到的索引。NULL就是没用索引
possible_keys候选索引。有值但key=NULL,说明优化器放弃了,多半是第七点
key_len索引使用的字节数。用来判断联合索引用到了几列(越长用得越多)
Extra关键信息集中地:Using index=覆盖索引(好);Using where=需要回表或额外过滤;Using filesort=外部排序(警惕);Using temporary=临时表(高度警惕)

一个经验:rows × rows的乘积大致反映 JOIN 的扫描量级。多表 JOIN 时每一行的rows都要看,不能只看第一行。

另外两个很有用的变体:

EXPLAINFORMAT=JSONSELECT...\G-- 看优化器的详细决策、cost、chosen 索引EXPLAINANALYZESELECT...\G-- 8.0+,真实执行一遍,给出实际行数和耗时

EXPLAIN ANALYZE是排查“估计不准”的神器,它能告诉你优化器预估的行数和实际返回的行数差了多少倍——差距过大基本就是统计信息的问题。

十一、一份可抄的检查清单

遇到慢查询,按这个顺序过:

  1. 有没有索引?看possible_keys。没有就考虑加。
  2. 有没有被用上?看key。NULL就往下逐条排查。
  3. 是不是最左前缀断了?检查联合索引列顺序和条件列。
  4. 索引列是不是“裸”的?找函数、运算、隐式转换。
  5. 类型和字符集是否完全一致?尤其 ORM 传参和 JOIN 关联字段。
  6. 有没有%xxx、OR、NOT IN 这类结构?尝试改写。
  7. 是不是SELECT *导致回表太贵?试试缩小列,看能否变成覆盖索引。
  8. 数据量和选择性是否合理?跑ANALYZE TABLE,看看是不是统计信息过期。
  9. 能不能改成覆盖索引?这是性价比最高的优化手段之一。
  10. 最后才考虑FORCE INDEX,并且要注释写明原因和日期。
写在最后

回到开头那个同事的问题。他那条 SQL 的问题出在第三点:order_no是VARCHAR,代码里传了个Long,发生了隐式类型转换。改完类型之后,type从ALL变成ref,耗时从 3.2s 降到 18ms。

他改完说了一句挺有意思的话:“原来索引不是加了就有用。”

对,索引是一种需要被“配合”的结构。它不会主动工作,它只在你的查询条件和它的组织方式对齐时才生效。所以调优的第一原则从来不是“加索引”,而是:

先看执行计划,再动表结构。

花五分钟读懂EXPLAIN那五个字段,比你盲目加十个索引管用得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 5:17:50

分布式鲁棒优化微电网单元分配的Python复现全解析

接到这个活的时候&#xff0c;客户丢过来的需求就一句话&#xff1a;把这篇论文里的分布式鲁棒优化微电网单元分配方法用Python复现出来&#xff0c;代码能跑、结果对得上。乍一听很常规&#xff0c;但真正动手才发现&#xff0c;光是“分布式鲁棒优化”这几个字就够你琢磨两天…

作者头像 李华
网站建设 2026/9/26 5:16:32

WinForms+SQL Server外卖系统开发:订单、库存与事务设计

简介&#xff1a;这是一份基于 WinForm 与 SQL Server 的外卖系统完整项目&#xff0c;面向学习 C# 桌面开发与数据库设计的高校学生、课程设计者或初级开发者。项目按角色拆分为用户端、商家端、骑手端和管理员四个子系统&#xff0c;用户端实现跨店铺加购、结算下单、订单管理…

作者头像 李华
网站建设 2026/9/26 5:15:09

Word粘贴到富文本编辑器样式丢失?一套可落地的清洗流程

距离我上一次被“样式丢失”这个需求折腾到抓狂&#xff0c;其实还不到两个月。事情本身不复杂&#xff1a;用户把一份排好版的Word文档复制到后台富文本编辑器&#xff0c;点发布&#xff0c;结果正文里的标题、加粗、颜色、行距全部变成默认值&#xff0c;表格线也断了大半。…

作者头像 李华
网站建设 2026/9/26 5:14:51

JSP+Servlet教学设备报修系统(含源码/论文/部署指南)

简介&#xff1a;本资源是一套面向高校计算机专业本科生的毕业设计实战项目&#xff0c;聚焦教学设备报修场景&#xff0c;解决教师报修流程繁琐、学生参与度低等实际管理痛点。系统基于JSPMySQL开发&#xff0c;采用B/S架构&#xff0c;涵盖用户注册、报修提交、状态跟踪、公告…

作者头像 李华
网站建设 2026/9/26 5:14:49

Flask+Vue打造家电维修服务系统:从架构设计到部署实战

在家电维修这个行业&#xff0c;线上化一直是个老大难问题。用户找不到靠谱师傅&#xff0c;师傅接单靠熟人介绍&#xff0c;维修记录全靠纸质单据。我之前接过一个项目&#xff0c;要搭一套家电维修服务系统&#xff0c;技术栈限定为Python生态。当时就在Flask和Django之间反复…

作者头像 李华
网站建设 2026/9/26 5:12:56

英文AIGC检测率过高?从检测原理到实战降AI率完整指南

1. 项目概述&#xff1a;AIGC检测是什么&#xff0c;英文文本为何容易“中招”先说结论&#xff1a;AI检测率过高&#xff0c;本质上不是“内容不够好”&#xff0c;而是文本在统计学特征上太像机器生成的。2026年这个时间节点&#xff0c;AIGC检测工具的底层逻辑、模型能力和应…

作者头像 李华