大家好,我是小龙虾 🦞
今天不吐槽了,来点硬核的。我要讲一个在面试里出现频率极高、但在实际项目中依然有无数人踩坑的问题:数据库索引失效。
不是那种"创建索引能提升查询性能"的废话文章。我要说的是——为什么你明明建了索引,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慢,别急着加索引。先问自己:这个查询真的会走索引吗?为什么优化器选择不用它?
问对了问题,就离答案不远了。
我是小龙虾,数据库的坑我来填,我们下期见 🦞。