索引加对了,查询反而更慢?我被MySQL坑得差点删库跑路
事情是这样的。
那天深夜,监控大屏疯狂报警,P99延迟飙到了3秒。运维兄弟一个电话把我从被窝里拽起来,我迷迷糊糊打开电脑,心想着:肯定是哪个实习生又写了死循环。
结果呢?我看了半天才发现:这个慢查询,上面明明加过索引了。
是的,加了索引,查询反而更慢了。这不是我编的,这是MySQL在我脸上反复摩擦了3个小时才让我看清的事实。
先说结论:你以为的索引,不是MySQL以为的索引
很多人(包括以前的我)觉得索引就是一把瑞士军刀,加了就能快。我以前也这么想过,直到有一天我发现:加了索引的查询,有时候会走全表扫描。
来,上一个经典案例:
-- 用户表有个phone字段,加了索引ALTER TABLE users ADD INDEX idx_phone(phone);-- 查询语句SELECT * FROM users WHERE phone = 13800138000;-- 慢查询日志显示:执行时间2.3秒,全表扫描
你发现问题了吗?13800138000这个数字,MySQL会把它当成什么类型?
答案是:浮点数。而你的索引是建在 VARCHAR/CHAR 类型上的。
于是乎,MySQL默默做了个隐式类型转换:
SELECT * FROM users WHERE CAST(phone AS DOUBLE) = 13800138000.0;
加了索引?不好意思,用不了了。因为对索引列做任何函数操作或类型转换,索引都会失效。这就是传说中的索引失效第一定律:对索引列动手脚,索引就跟你拜拜。
第二个坑:范围查询会让后续索引列失效
这个坑更加隐蔽,看好了:
-- 建立复合索引ALTER TABLE orders ADD INDEX idx_status_created(status, created_at);-- 查询语句SELECT * FROM orders WHERE status = 'paid' AND created_at > '2026-01-01' AND amount > 100;-- 你以为会用到索引的status和created_at-- 实际上只有status用上了
为什么?因为范围查询(>、<、BETWEEN、LIKE '%xxx')会让右侧的索引列全部失效。
MySQL的索引是B+树,是按顺序组织的。当你在created_at上用了范围查询,MySQL就已经无法高效地继续往后扫描了。所以amount字段的索引根本不会被用到。
这个坑的恶心之处在于:你明明加了对的索引,EXPLAIN也显示用到了索引,但查询就是慢。因为它只用了部分索引。
正确的做法是:把区分度高的、经常用范围查询的列放在索引的最后面。比如上面这个例子,如果业务允许,应该把status = 'paid'这种等值查询放前面,范围查询放后面。
第三个坑:OR语句,索引并不是你想的那样
看这个:
-- 两个字段都有索引ALTER TABLE users ADD INDEX idx_email(email);ALTER TABLE users ADD INDEX idx_phone(phone);-- 查询SELECT * FROM users WHERE email = 'test@example.com' OR phone = '13800138000';
你可能觉得:两个字段都有索引,MySQL肯定会优化吧?
抱歉,MySQL的OR优化策略是:如果两个字段上任意一个没有索引,MySQL就会放弃索引扫描,直接全表扫描。
为什么?因为OR意味着要合并两个结果集,如果没有索引,MySQL认为全表扫描反而更省事。
解决方案有两个:
- 用UNION替代OR(注意是UNION,不是UNION ALL)
- 确保OR两边都有索引,并且MySQL的优化器认为索引扫描比全表扫描快
-- 优化方案SELECT * FROM users WHERE email = 'test@example.com'UNIONSELECT * FROM users WHERE phone = '13800138000';
第四个坑:JOIN查询的索引玄学
JOIN是我见过被误解最深的一个操作。很多人以为JOIN慢是因为数据量大,但实际上,JOIN慢的绝大多数原因是没有加对索引。
来一个经典场景:
-- 订单表和用户表SELECT o.*, u.name FROM orders o INNER JOIN users u ON o.user_id = u.idWHERE o.status = 'paid';
你发现没有?user_id字段上加索引了吗?
如果没有,那么每查一条订单记录,MySQL都要去users表里做一次全表扫描找对应用户。100条订单就是100次全表扫描,10000条就是10000次。
正确的做法是:在关联字段上建索引。
ALTER TABLE orders ADD INDEX idx_user_id(user_id);
这一条索引,能把JOIN的时间从几秒降到几毫秒。我亲手测过,200万订单表的JOIN,从4.2秒降到了0.03秒。
但问题是:不是所有JOIN字段都要加索引,也不是加了索引就一定用到。如果你的子查询返回的结果集很大,MySQL可能会选择先扫描小表,再去大表里查找,这时候关联字段的索引反而用不上。
回到开头那个故事
那天晚上的事故,最后怎么解决的?
我把查询条件从:
WHERE DATE(created_at) = '2026-03-15' AND status IN ('paid', 'shipped', 'completed')
改成了:
WHERE created_at >= '2026-03-15 00:00:00' AND created_at < '2026-03-16 00:00:00'AND status IN ('paid', 'shipped', 'completed')
就改了这一行,执行时间从3秒变成了12毫秒。
原因是什么?DATE()函数对索引列动手了,所以索引失效。而改成范围查询之后,索引就能正常使用了。
就这么简单。就这样。这么讽刺。
写给还在踩坑的你
MySQL的索引机制并不复杂,但里面藏着太多反直觉的东西。我总结了几条核心原则:
- 永远不要对索引列使用函数或类型转换,否则索引必挂
- 复合索引要遵循最左前缀原则,范围查询放最后
- OR查询要确保两边都有索引,否则不如不用
- JOIN之前先看子表大小,关联字段必须有索引
- EXPLAIN不是万能的,它只能告诉你索引有没有用到,不能告诉你用得好不好
索引是武器,但武器也得会用。用错了,轻则查询变慢,重则线上事故。
下次遇到慢查询,先别急着加索引。问自己三个问题:索引列有没有被动过手脚?查询条件有没有遵循最左前缀?关联字段有没有索引?
如果这三个问题都是肯定的,那再考虑加索引。
记住:索引加对了是神器,加错了是灾难。
完。