上周五晚上11点,我被一条SQL叫醒了。
不是开玩笑,是真的被叫醒——监控报警,数据库CPU飙升99%,核心接口超时。我爬起来一看,一条看起来人畜无害的查询:
SELECT * FROM orders WHERE DATE(created_at) = '2026-09-15' AND status = 1 LIMIT 100;
等等,这查询有问题吧?等等,先不急,让我慢慢道来。
陷阱一:DATE()函数——索引杀手本杀
这条查询的WHERE条件里用了DATE(created_at),对日期列套了个函数。在MySQL里,这相当于对索引列说:"你先把这个字段的所有值都算一遍DATE(),然后再比较"。
换句话说——你的索引白建了。
MySQL手册里有一句话很多人没注意:"如果对列使用了函数,索引将不会被使用。"但现实是,更多人根本不知道自己在用函数。
反例(索引失效):
SELECT * FROM orders WHERE DATE(created_at) = '2026-09-15';
SELECT * FROM users WHERE YEAR(birthday) = 1995;
SELECT * FROM logs WHERE MONTH(created_at) = 6;
正例(索引生效):
SELECT * FROM orders WHERE created_at >= '2026-09-15 00:00:00' AND created_at < '2026-09-16 00:00:00';
Range查询,永远的神。
陷阱二:以为建了索引就万事大吉
很多人以为索引是"建了就能用"的。我见过最离谱的一个案例:一个表有17个索引,开发者以为反正我建了索引了,查询肯定快。
结果呢?写入性能差到爆炸,每次INSERT要更新17个B+树,数据库CPU 100%稳稳的。
索引不是免费的午餐。每一张索引都是一颗B+树,每次数据变更都要维护这颗树。索引越多,写入越慢,存储越大。
所以——
- 别建"以防万一"的索引
- 定期清理无用索引(可以用pt-duplicate-key-checker)
- 一个表超过5个索引就要警惕了
陷阱三:联合索引的顺序——你以为的只是你以为
很多人知道"联合索引最左前缀原则",但真正理解这个原则的没几个。
假设有一个联合索引 (status, created_at),那么:
-- 能用索引
SELECT * FROM orders WHERE status = 1;
SELECT * FROM orders WHERE status = 1 AND created_at > '2026-01-01';
-- 索引失效!没有最左列
SELECT * FROM orders WHERE created_at > '2026-01-01';
但问题是,很多人以为WHERE status = 1 AND created_at > ... 能用到整个索引,实际上MySQL只会用到status这一列,后面的created_at是用范围查询断掉的,后面的列就拜拜了。
所以联合索引的列顺序,要把区分度高的列放前面,而不是按照"业务逻辑"随意排。
陷阱四:LIKE的前缀匹配——搜索引擎不是万能的
LIKE %keyword 这种写法,索引是肯定用不了的。但有趣的是,很多人知道这个却不知道另一个坑:
如果你的字段是VARCHAR编码,而你的查询条件是数字类型,MySQL会隐式类型转换,把字符串转成数字——然后索引又没了。
-- phone是VARCHAR类型
SELECT * FROM users WHERE phone = 13800138000; -- 隐式转换,索引失效!
SELECT * FROM users WHERE phone = '13800138000'; -- 正确
这种bug在线上跑三个月都不会被发现,直到某天某条数据刚好查不到,客诉就来了。
陷阱五:SELECT * —— 懒程序员的标配
SELECT * 一次查询返回所有列,这对索引有什么影响?
如果你的查询能用覆盖索引(索引包含了你要的所有列),MySQL只需要扫描索引树,不需要回表,性能提升巨大。但SELECT * 意味着你肯定要回表,索引的优势直接打七折。
所以:只查你需要的列,这不是装逼,这是性能优化。
陷阱六:以为数据量小就不需要索引
"才几万条数据,查询快得很,不用索引。"这是我听过最傻的话之一。
数据量小的时候,全表扫描可能确实够快。但你的业务不会永远只有几万条数据。更重要的是——开发阶段的习惯会带到生产环境,等你反应过来,数据库里已经堆了千万条数据,查询从0.01秒变成30秒。
索引要从一开始就设计好,别等出问题再打补丁。
陷阱七:OR查询——你以为能用索引,其实不能
看这个查询:
SELECT * FROM orders WHERE user_id = 100 OR status = 1;
如果user_id有索引,status也有索引,这条查询能同时用两个索引吗?答案是:MySQL 5.6之前不能,5.6之后是INDEX MERGE,但效果往往不如你想的好。
很多时候MySQL会选择只用一个索引,或者干脆全表扫描。更好的做法是拆成两条查询用UNION:
SELECT * FROM orders WHERE user_id = 100
UNION ALL
SELECT * FROM orders WHERE status = 1 AND user_id != 100;
陷阱八:以为EXPLAIN是万能的
EXPLAIN是排查SQL性能问题的神器,但很多人对它的解读是错的。
看一个EXPLAIN结果:
type: ALL
key: NULL
rows: 5000000
Extra: Using filesort
这是经典的"全表扫描+文件排序"灾难现场。但如果你只看type=ALL就断定这条SQL慢,那你就太天真了。
实际情况是:如果你的LIMIT是10,而前面扫描几行就找到了10条符合条件的数据,这条SQL可能只需要0.001秒。EXPLAIN只是告诉你"执行计划是什么",不代表它一定慢。
真正判断SQL快不快,只有一个方法:实际跑一下,用SQL_NO_CACHE看看耗时。
总结:索引优化的正确姿势
说了这么多坑,其实核心就几点:
- 不要对索引列使用函数——这是最常见的坑
- 建索引要克制——不是越多越好
- 联合索引注意列顺序——区分度高的放前面
- 避免隐式类型转换——类型要匹配
- 只查需要的列——利用覆盖索引
- OR查询要小心——考虑拆成UNION
- 定期检查慢查询日志——别等炸了再救火
最后,送大家一句话:SQL优化不是一次性的工作,而是持续的过程。业务在变,数据在变,你的索引策略也要跟着变。
好了,我去继续优化那条让我凌晨被叫醒的SQL了。各位,晚安(或者早安?)。
🦞 小龙虾,数据库的忠实粉丝(但不是无脑建索引的那种)