1. 索引失效的本质:当优化器决定放弃索引
MySQL索引失效的根本原因在于查询优化器的成本计算机制。优化器会根据统计信息估算全表扫描和索引扫描的成本,当它认为全表扫描更高效时,就会放弃使用索引。这种"失效"实际上是优化器的主动选择,而非索引本身出现问题。
我曾在处理一个300万行的用户表时遇到典型场景:SELECT * FROM users WHERE status = 1这个简单查询本该使用status字段的索引,但EXPLAIN显示进行了全表扫描。通过SHOW INDEX FROM users查看索引统计信息,发现status字段的基数(Cardinality)值异常低,导致优化器误判。
关键提示:索引失效≠索引损坏,而是优化器基于成本模型的决策结果
2. 六大经典失效场景原理剖析
2.1 最左前缀原则与B+树结构
联合索引(a,b,c)的存储结构决定了它只能按a→b→c的顺序使用。当查询条件缺少a时,B+树的有序性被破坏,索引就会失效。例如:
-- 能使用索引 SELECT * FROM table WHERE a=1 AND b=2 -- 不能使用索引 SELECT * FROM table WHERE b=2底层原理在于B+树的叶子节点按(a,b,c)排序存储,缺少最左字段时无法利用有序性快速定位。
2.2 隐式类型转换的代价
当字段类型与条件值类型不匹配时,MySQL会进行隐式转换。例如字符串字段用数字查询:
-- phone是varchar类型 SELECT * FROM users WHERE phone = 13800138000这会导致索引失效,因为需要逐行执行CAST(phone AS signed)操作。我曾用性能测试对比:
- 使用正确类型:0.5ms
- 隐式转换:1200ms
2.3 函数操作破坏索引顺序
任何对索引列的函数操作都会使索引失效:
-- 失效案例 SELECT * FROM orders WHERE DATE_FORMAT(create_time,'%Y-%m')='2023-01'因为B+树存储的是原始值,而非函数计算后的结果。解决方案是改为范围查询:
-- 优化后 SELECT * FROM orders WHERE create_time >= '2023-01-01' AND create_time < '2023-02-01'2.4 范围查询后的索引列失效
对于联合索引(a,b,c),如果a使用范围查询,后续字段无法使用索引:
-- 只有a能用索引,b和c失效 SELECT * FROM table WHERE a > 1 AND b = 2这是因为B+树在范围扫描时,后续字段的值是无序的。
2.5 不等于(!=/<>)查询的全表扫描
优化器认为使用索引查不全值再回表的成本可能高于直接全表扫描:
-- 通常会导致全表扫描 SELECT * FROM products WHERE status != 12.6 OR条件的短路特性
当OR条件包含非索引列时,整个查询会失效:
-- 假设name有索引而age没有 SELECT * FROM users WHERE name='张三' OR age=20这是因为MySQL需要同时检查两个条件,无法有效利用索引。
3. 索引统计信息的幕后机制
3.1 基数(Cardinality)的影响
通过SHOW INDEX FROM table看到的Cardinality值是索引选择性的关键指标。当这个值严重偏离实际时(比如字段有大量重复值),优化器会错误估计扫描行数。
手动更新统计信息命令:
ANALYZE TABLE table_name;3.2 采样页数的配置
MySQL通过采样部分数据页来估算统计信息,innodb_stats_persistent_sample_pages参数控制采样数量。在数据分布不均匀时,增加该值可以提高准确性。
3.3 索引提示的使用技巧
当优化器选择错误时,可以用FORCE INDEX强制使用索引:
SELECT * FROM orders FORCE INDEX(idx_create_time) WHERE DATE(create_time) = '2023-01-01'但要注意这会使执行计划僵化,建议仅在确有必要时使用。
4. 实战中的特殊失效场景
4.1 ICP特性与失效边界
Index Condition Pushdown(ICP)是MySQL5.6引入的优化,它能在存储引擎层过滤数据。但当出现以下情况时ICP会失效:
- 使用子查询
- 使用存储函数
- 引用外部表的列
4.2 字符集与排序规则冲突
当关联字段的字符集或排序规则不同时,索引会失效:
-- utf8与utf8mb4的关联 SELECT * FROM t1 JOIN t2 ON t1.name = t2.name WHERE t1.name COLLATE utf8mb4_general_ci = t2.name4.3 分区表的索引陷阱
在分区表中,如果查询条件不包含分区键,所有分区都会被扫描。例如按月分区的orders表:
-- 没有使用分区键month SELECT * FROM orders WHERE user_id=1004.4 虚拟列索引的注意事项
虚拟列(Generated Column)上的索引在以下情况失效:
- 使用了非确定性函数如NOW()
- 虚拟列公式与查询条件不完全匹配
5. 系统化解决方案与最佳实践
5.1 EXPLAIN的深度解读
重点关注以下字段:
- type:const > ref > range > index > ALL
- key:实际使用的索引
- rows:估算扫描行数
- Extra:Using index(覆盖索引)、Using filesort(需要排序)
5.2 索引优化器提示
-- 推荐写法 SELECT /*+ INDEX(table_name index_name) */ * FROM table_name比FORCE INDEX更柔性的控制方式。
5.3 索引跳跃扫描优化
MySQL8.0新增的Index Skip Scan特性,可以在特定条件下突破最左前缀限制:
-- MySQL8.0+可能使用索引 SELECT * FROM table WHERE b=2 AND c=3前提是联合索引(a,b,c)且字段a的离散值较少。
5.4 索引选择策略
建立索引的黄金法则:
- 高选择性字段优先
- 常用查询条件组合
- 避免过度索引
- 定期检查冗余索引
检查冗余索引脚本:
SELECT * FROM sys.schema_redundant_indexes;6. 真实案例:电商系统优化实录
某电商平台的订单查询接口出现性能问题,原始SQL:
SELECT * FROM orders WHERE user_id=123 AND status IN (2,3) AND create_time > '2023-01-01' ORDER BY update_time DESC LIMIT 10问题诊断:
- 存在(user_id)单列索引和(status,create_time)联合索引
- 排序字段update_time没有索引
- IN条件导致范围查询
优化方案:
- 建立(user_id, status, create_time)的联合索引
- 添加update_time的倒序索引
- 重写为:
SELECT * FROM orders FORCE INDEX(idx_user_status_time) WHERE user_id=123 AND status = 2 AND create_time > '2023-01-01' UNION ALL SELECT * FROM orders FORCE INDEX(idx_user_status_time) WHERE user_id=123 AND status = 3 AND create_time > '2023-01-01' ORDER BY update_time DESC LIMIT 10优化后响应时间从1200ms降至35ms。这个案例展示了复合索引设计和查询重写的重要性。