MySQL 索引失效的 10 个场景
MySQL 索引失效的 10 个场景踩过才知道有多坑索引建了不等于索引会用。很多开发者加了索引还是慢原因就是索引悄悄失效了。本文总结 10 个最常见的索引失效场景每个都附可复现的 SQL对照检查你的代码。场景 1对索引列做函数运算-- 失效对 created_at 做了函数运算SELECT*FROMordersWHEREYEAR(created_at)2024;-- 正确改成范围查询SELECT*FROMordersWHEREcreated_at2024-01-01ANDcreated_at2025-01-01;原理MySQL 无法对函数结果使用索引必须全表扫描每一行计算函数值。场景 2隐式类型转换-- 假设 phone 字段是 VARCHAR 类型索引存在-- 失效传入数字触发隐式转换SELECT*FROMusersWHEREphone13812345678;-- 正确类型匹配SELECT*FROMusersWHEREphone13812345678;原理MySQL 会把 VARCHAR 列的值转为数字再比较导致索引失效全表扫描。这是生产环境最常见的坑之一。场景 3LIKE 左模糊查询-- 失效左边有通配符SELECT*FROMproductsWHEREnameLIKE%手机%;-- 部分有效只有右边有通配符才走索引SELECT*FROMproductsWHEREnameLIKE华为%;原理B 树索引按前缀排序左模糊无法利用有序性只能全表扫描。如果必须支持全文搜索考虑 MySQL 全文索引或 Elasticsearch。场景 4联合索引违反最左前缀-- 联合索引INDEX(a, b, c)-- 有效从最左列开始SELECT*FROMtWHEREa1;SELECT*FROMtWHEREa1ANDb2;SELECT*FROMtWHEREa1ANDb2ANDc3;-- 失效跳过了 aSELECT*FROMtWHEREb2;SELECT*FROMtWHEREb2ANDc3;-- 部分有效a 有效c 失效SELECT*FROMtWHEREa1ANDc3;原理联合索引的排序是先按 a再按 b再按 c。跳过前面的列后面的列在索引里是无序的无法利用。场景 5用 OR 连接非索引列-- 假设只有 user_id 有索引status 没有索引-- 失效OR 右边没索引导致整体失效SELECT*FROMordersWHEREuser_id1001ORstatus1;-- 解决方案一给 status 也加索引-- 解决方案二改写成 UNIONSELECT*FROMordersWHEREuser_id1001UNIONSELECT*FROMordersWHEREstatus1;场景 6使用 ! 或 -- 失效不等于条件通常不走索引SELECT*FROMordersWHEREstatus!0;-- 优化改成 IN 或范围查询如果数据分布合适SELECT*FROMordersWHEREstatusIN(1,2,3);注意数据分布影响很大。如果 status ! 0 的数据占 99%MySQL 优化器会认为走索引还不如全表扫描直接放弃索引。场景 7IS NULL / IS NOT NULL-- 在某些 MySQL 版本和场景下会失效SELECT*FROMordersWHEREdeleted_atISNULL;建议避免用 NULL 做业务状态判断改用 0/1 或默认值代替既能走索引又避免 NULL 的各种坑。场景 8在索引列上做计算-- 失效对索引列做了加法运算SELECT*FROMordersWHEREid11001;-- 正确把运算移到等号右边SELECT*FROMordersWHEREid1000;场景 9字符集不一致导致隐式转换-- orders.user_id 是 utf8mb4-- users.id 是 utf8-- 联表时发生隐式字符集转换索引失效SELECT*FROMorders oJOINusers uONo.user_idu.idWHEREu.name张三;解决方案统一全库字符集为 utf8mb4建库建表时养成习惯避免混用。场景 10数据量太少优化器放弃索引-- 表里只有 100 行数据-- MySQL 优化器认为全表扫描比走索引还快直接放弃SELECT*FROMsmall_tableWHEREstatus1;这不是 bug是优化器的正常行为。小表不需要纠结索引等数据量上来了再说。快速排查清单怀疑索引失效时按以下顺序检查EXPLAIN看key字段是否为 NULL检查 WHERE 条件里有没有对索引列做函数或运算检查字段类型和传入参数类型是否一致联合索引检查是否满足最左前缀查看数据分布确认走索引确实比全表扫描快养成写完 SQL 就 EXPLAIN 的习惯索引失效的问题大多在开发阶段就能发现。