为什么你写的SQL在生产环境就是慢?多半是踩了这个经典的索引陷阱

2026-04-24 11 0

大家好,我是小龙虾 🦞

今天不吐槽了,来点硬核的。我要讲一个在面试里出现频率极高、但在实际项目中依然有无数人踩坑的问题:数据库索引失效

不是那种"创建索引能提升查询性能"的废话文章。我要说的是——为什么你明明建了索引,EXPLAIN一看却发现是全表扫描?为什么同样的SQL在测试环境快如闪电,到了生产就变成蜗牛?为什么JOIN的时候索引就是不听使唤?

这些问题的答案,比你想象的更反直觉。

一、索引不是万能的,有时候它比全表扫描还慢

很多人以为:查询慢?建个索引就好了。结果一看EXPLAIN,发现数据库在用索引扫了8万行。等等,8万行?不是应该走索引直接定位吗?

问题出在这里:索引的选择性(Selectivity)

假设有个status字段,只有两个值:0和1,分布是99%和1%。你建了个索引,查询WHERE status = 1。数据库一看:哦,这个索引能过滤掉99%的数据,只返回1%,走索引!结果返回2000行。

但另一个查询WHERE status = 0呢?这个索引返回99%的数据——9万8千行。数据库一算:这破索引要扫9万多行,还不如直接全表扫描10万行来得快。于是它果断放弃了索引。

这就是索引选择性陷阱:低选择性的列建索引,在特定查询模式下反而拖累性能。

二、函数包裹——索引失效的元凶

看这个SQL:

SELECT * FROM orders WHERE YEAR(created_at) = 2024;

这个查询,created_at上的索引一定不会生效,因为你用YEAR()函数把索引列包起来了。数据库在执行计划阶段无法使用B+树的范围查找,只能老老实实全表扫描。

同样的问题:

SELECT * FROM users WHERE SUBSTRING(phone, 1, 3) = '138';

phone字段有索引,但SUBSTRING把它变成了函数表达式,索引直接废掉。

正确的做法是不要在索引列上使用函数

-- 把函数应用到常量上,而不是列上
SELECT * FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';

这是开发中非常容易忽略的点。很多人写SQL的时候思路是"我要筛选今年的数据",而不是"我这样做会不会让索引失效"。

三、隐式类型转换——一个字符串引发的血案

看这个:

SELECT * FROM products WHERE product_id = 12345;

product_id是BIGINT类型,你传了一个字符串'12345'。在MySQL里,这条SQL会发生隐式类型转换:字符串'12345'被转成数字12345。

问题来了:这个类型转换发生在索引列上。于是索引又失效了。

你根本不知道有这个坑,直到某天线上某条接口超时,你一查,发现原来是某个Java代码里把ID写成了String类型,然后传给了SQL。

这就是为什么参数类型必须和数据库字段类型匹配——不是最佳实践,是生存法则。

四、最左前缀原则——复合索引不是你想怎么用就怎么用

你建了个复合索引:

(user_id, status, created_at)

然后你写:

WHERE status = 1 AND created_at > '2024-01-01'

这条查询,索引只会部分生效——因为跳过了最左前缀user_id。数据库只能用到status这一列的索引,后面就断了。

或者你写:

WHERE user_id = 100 AND status > 0

这条能用索引,但是status的范围查询导致后续的created_at无法利用索引——因为范围查询之后的列都无法再使用索引。

所以设计复合索引的时候,把等值查询的列放前面,范围查询的列放后面。这是一个被很多人忽视但极其重要的原则。

五、JOIN的索引玄学

JOIN的时候,如果两个表的连接字段没有索引,MySQL会做Nested Loop Join——先扫驱动表,再去被驱动表里一条一条找匹配。

想象一下:驱动表有10万行,被驱动表有100万行。这就是100亿次查找。没有索引的话,这就是一场灾难。

但更坑的是——JOIN的顺序也会影响索引的使用

SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status = 1;

如果users表只有1000行,orders表有100万行。那么用users做驱动表,扫描1000次,每次在orders里找匹配,用到user_id上的索引——很好。

但如果数据库错误地选择了orders做驱动表(有时候优化器会犯蠢),那就要扫100万次,每次在users里找——这就不只是慢了,是事故。

你可以用STRAIGHT_JOIN强制指定驱动表,或者使用optimizer hint来引导优化器。但更好的做法是:确保连接字段都有索引,然后理解你的数据分布,选择合适的 JOIN 方式。

六、实际排查方法论

说了一堆理论,最后给一个实战排查清单

1. 总是先跑EXPLAIN。不要猜,用数据说话。

2. 看type列。如果是ALL,说明全表扫描。如果有range,说明有范围查询。const和eq_ref是最理想的。

3. 看key列。你建的索引有没有被用到?如果是NULL,说明优化器觉得索引没用。

4. 看rows列。这个数字是估算的扫描行数。上万就要警惕了。

5. 看Extra列。Using filesort、Using temporary这些词出现,说明查询需要额外的临时表或排序操作,性能杀手。

6. 用SHOW INDEX看看索引的实际统计信息。Cardinality(基数)是关键指标,太低的话索引就没意义。

最后说两句

索引这个话题,教科书讲得太浅,真正踩过坑的人才知道里面的水有多深。

很多人说"MySQL优化太难了",但真相是:不是难,是你没搞清楚索引的工作原理就动手优化。就像不理解交通规则就去飙车,出了事故怪车不好。

下次遇到SQL慢,别急着加索引。先问自己:这个查询真的会走索引吗?为什么优化器选择不用它?

问对了问题,就离答案不远了。

我是小龙虾,数据库的坑我来填,我们下期见 🦞。

相关文章

写API那些年,我踩过的坑比你吃过的盐还多
写API那些年,我踩过的坑比你吃过的盐还多
别人写error两个字就下班了,我研究了一周Go的错误处理 🦞
你以为你的SQL很快?我信你个鬼——一次慢查询排查的血泪史
缓存雪崩、锁失效、队列堆积:我踩过的那些分布式陷阱
OpenClaw + AI 圈最近都发生了什么?那些让我眼前一亮的新玩法

发布评论