1. 慢查询的噩梦:当索引突然失效时
凌晨三点,报警短信又一次把我从睡梦中惊醒。生产环境的订单查询接口响应时间突破了5秒,用户投诉像雪花一样涌来。这已经是本月第三次因为索引失效导致的慢查询事故了。作为经历过多次血泪教训的老DBA,我决定把这些年踩过的索引坑系统梳理出来。
MySQL索引就像数据库的导航系统,正常情况下能让查询速度提升百倍。但当索引失效时,系统会瞬间退化为全表扫描的"原始社会"。更可怕的是,这种性能劣化往往在特定条件下才会触发,测试环境很难复现。以下是五个最具迷惑性的索引失效场景,每个都曾让我付出过惨痛代价。
2. 五大索引失效陷阱详解
2.1 隐式类型转换:数字与字符串的暧昧关系
上周刚处理过一个典型案例:用户手机号字段varchar(20)上建有索引,但查询时错误使用了数值类型:
sql复制SELECT * FROM users WHERE phone = 13800138000; -- 致命错误!
这个查询看起来完全合理,但执行计划显示进行了全表扫描。原因是MySQL会将字符串字段phone隐式转换为数字类型,相当于在字段上使用了函数:
sql复制SELECT * FROM users WHERE CAST(phone AS signed) = 13800138000;
重要提示:所有类型转换都会导致索引失效,包括日期时间与字符串的隐式转换
解决方案:
- 严格保持字段与条件的类型一致
- 使用
EXPLAIN验证执行计划时,注意type列是否出现ALL - 推荐在代码审查时加入类型检查规则
2.2 最左前缀原则:联合索引的排列组合
我们的订单表有联合索引(user_id, status, create_time),某天发现这个查询突然变慢:
sql复制SELECT * FROM orders WHERE status = 'paid' AND create_time > '2023-01-01';
问题在于跳过了联合索引的第一个字段user_id。这就像查字典时直接翻到第100页找"张"姓——完全失去了索引的排序优势。
联合索引生效条件:
- 完整匹配最左列:
WHERE user_id = 100 - 最左列范围查询:
WHERE user_id > 100 - 最左列等值+次左列范围:
WHERE user_id = 100 AND status = 'paid'
血泪教训:联合索引字段顺序应该按照区分度从高到低排列
2.3 函数操作:索引不能承受之重
某次优化活动页查询时,发现这个简单查询异常缓慢:
sql复制SELECT * FROM products WHERE DATE(create_time) = '2023-06-01';
在字段上使用函数(包括DATE()、SUBSTRING()等)会导致索引完全失效,就像要求快递员根据收件人姓名拼音首字母送货。
替代方案:
sql复制SELECT * FROM products
WHERE create_time >= '2023-06-01 00:00:00'
AND create_time < '2023-06-02 00:00:00';
2.4 不等于(!=/<>)与NOT IN:索引的盲区
促销系统出现过这样的性能问题:
sql复制SELECT * FROM coupons WHERE status != 'expired';
不等于操作无法利用索引的有序性,优化器会直接选择全表扫描。同样的情况也发生在NOT IN和NOT LIKE操作。
优化策略:
- 改为正向查询:
WHERE status IN ('active', 'used') - 使用索引覆盖:
SELECT id FROM coupons WHERE status != 'expired' - 考虑使用
status = 'expired'先查出少量数据,再用程序排除
2.5 OR条件:索引的短路陷阱
用户中心有个多条件查询:
sql复制SELECT * FROM members
WHERE vip_level = 3 OR registration_channel = 'app';
当OR条件涉及不同列时,MySQL无法高效使用索引。我曾见过一个OR查询让800万行的表直接崩溃。
解决方案:
- 改用UNION ALL:
sql复制SELECT * FROM members WHERE vip_level = 3
UNION ALL
SELECT * FROM members WHERE registration_channel = 'app';
- 使用索引合并优化(需MySQL 5.0+)
- 考虑重构业务逻辑,拆分查询
3. 诊断与应急处理方案
3.1 慢查询实时捕获术
建立完善的监控体系:
sql复制-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒的查询
SET GLOBAL log_queries_not_using_indexes = ON;
推荐工具:
- Percona PMM:可视化慢查询分析
- pt-query-digest:日志分析工具
- 阿里云RDS的SQL审计功能
3.2 紧急止血方案
当线上出现索引失效导致慢查询时:
- 立即
KILL问题会话 - 使用
FORCE INDEX强制指定索引 - 临时增加
LIMIT限制影响范围 - 考虑降级方案或缓存兜底
4. 防患于未然的索引设计规范
-
EXPLAIN必查项:
- type列:确保不是ALL
- key列:确认使用了正确索引
- rows列:估算扫描行数
- Extra列:警惕"Using filesort"
-
索引设计黄金法则:
- 单表索引不超过5个
- 联合索引字段不超过3个
- 避免在低区分度字段建索引
- 更新频繁的字段谨慎建索引
-
定期索引健康检查:
sql复制SELECT * FROM sys.schema_unused_indexes; -- MySQL 8.0+
最后分享一个真实案例:某电商大促期间,因为WHERE create_time > DATE_SUB(NOW(), INTERVAL 7 DAY)这样的查询导致数据库CPU飙升至100%。改用create_time > '2023-06-01'后,QPS立即从200提升到5000。索引优化就像给数据库做心脏手术,稍有不慎就会引发系统性崩溃。