MySQL索引为何越加越慢?我扒开了引擎的脑子

2026-04-28 10 0

很多人的认知里,索引是银弹。查询卡了?加索引。慢了?再加一个。老板问为什么线上崩了,你一拍脑袋:数据库没索引。

但我今天要说的是一个反直觉的事实——索引,可能是你系统变慢的元凶。

别急着骂我。让我先问你几个问题:你知道索引什么时候会让查询变慢吗?你知道联合索引的顺序写错了会发生什么吗?你知道有时候全表扫描比走索引快十倍吗?

如果这些问题让你心里一沉,那这篇文章就是为你写的。

先说个让我失眠的Case

去年我处理了一个诡异的性能问题。某张表,1000万数据,用户表。按理说查个用户应该飞快的——user_id是主键,查询条件也是user_id。结果呢?2秒。

主键查询,2秒。离谱吧?

SELECT * FROM users WHERE user_id = 123456;

这条SQL,按照所有人的认知,应该走主键索引,毫秒级返回。但它就是跑了2秒。

我们用EXPLAIN一看,type=ALL,key=NULL。全表扫描。什么鬼?主键索引呢?

最后排查出来的原因让我想骂人:这表有两个触发器,每个触发器里都有一段隐藏的SQL,查询条件根本不是user_id,而是另一个字段。运维的锅,但开发背了。

这个故事告诉我们:有时候你以为自己走的是索引,实际上你的SQL早就被篡改了。

索引的代价:写入变慢的隐形杀手

很多人只知道索引让查询变快,但不知道它让写入变慢了多少。

InnoDB里,每建一个索引,就会多一棵B+树。写入一条数据的时候,你以为只是一次磁盘IO?实际上是:更新主键索引 + 更新所有普通索引。

假设你有3个索引,那一次写入就变成4次索引写入。如果你的系统QPS是10000,每次写入要更新3个索引,你每秒多了多少次磁盘IO?3万次。磁盘说:我谢谢你啊。

很多人问过我:为什么我的写入这么慢?我说:你建了多少索引?对方说:不多啊,就七八个。我:……七八个索引,你是认真的吗?

索引不是越多越好。每一个索引都是有代价的。读多写少的场景可以多建索引,写多的场景要控制索引数量。这是基本常识,但80%的开发都踩过这个坑。

联合索引的顺序:你以为的只是你以为

联合索引有多重要?这么说吧,90%的SQL优化问题本质上是联合索引设计问题。

但我发现一个很有意思的现象:很多人知道最左前缀原则,但理解得一知半解。

ALTER TABLE orders ADD INDEX idx_user_status (user_id, status, create_time);

这条索引,查询WHERE user_id = 1 AND status = 2能用上,查询WHERE status = 2用不上。OK,这个大家都知道。

但如果查询是WHERE user_id > 100 AND status = 2呢?索引只用到user_id部分,因为>是范围查询,后面的索引就废了。

这还好说。更坑的是:WHERE user_id = 1 AND create_time > '2026-01-01'这个查询,status字段在create_time前面,所以create_time的索引也用不上。

你的索引明明包含了所有字段,但因为顺序不对,就是有几列用不上。这就是索引字段顺序的玄学。

正确的做法是:把等值查询的字段放前面,范围查询的放后面。user_id是等值,放前面;create_time是范围,放后面。status是等值,放在中间。

但现实是,很多表的索引是按建表时间顺序堆的,谁先想到谁先加,完全没有规划。三年后这表变成了祖传代码,没人敢动索引,因为一动就是线上事故。

索引下推:这个优化有时候是陷阱

MySQL 5.6引入了索引下推(Index Condition Pushdown,ICP)。听起来很美好对吧?把过滤条件下推到索引层面,减少回表次数。

但这个优化有时候会让你的查询更慢。

举个例子:

SELECT * FROM orders WHERE user_id = 1 AND status = 0 AND create_time > '2026-04-01';

索引是(user_id, status, create_time)。ICP会让MySQL在索引层面先过滤status和create_time,然后只回表符合条件的主键。

听起来很完美。但如果status的区分度很低(比如大部分订单status都是0),ICP反而会增加索引扫描的行数,因为MySQL要在索引层面处理更多数据。

这时候不走ICP、直接回表让MySQL自己判断,可能更快。

怎么验证?用EXPLAIN看,如果Using index condition说明走了ICP,用了更多索引但实际更慢,你就要考虑优化了。

MRR:从随机读到顺序读的救命稻草

还有一个优化叫MRR(Multi-Range Read)。简单说就是把随机读变成顺序读。

InnoDB的索引是B+树,叶子节点之间是链表。当你根据索引查到一批主键ID后,如果按主键顺序去读数据,数据是顺序写的,磁盘顺序读比随机读快很多。

SET @@optimizer_switch='mrr=on,mrr_cost_based=off';

强制开启MRR有时候能让你的查询快几倍。但要注意,这个优化有代价:如果你的查询只需要返回很少的行,MRR的开销可能得不偿失。

没有银弹,只有trade-off。

覆盖索引:减少回表的杀手锏

说了这么多回表,必须提一下覆盖索引。

如果你的SQL所有需要的数据都在索引里,不用回表,那叫覆盖索引。MySQL直接读索引就能返回结果,不用查主键索引。

-- 假设索引是 (user_id, status, create_time)
SELECT user_id, status, create_time FROM orders WHERE user_id = 1;

这条SQL不用回表,直接在联合索引里就能拿到所有数据。这就是覆盖索引,查询性能飞起。

但很多人不知道的是,如果你的查询返回的字段太多,覆盖索引可能用不上。MySQL优化器会判断:如果返回字段太多,索引里存不下,那还不如直接回表。

所以并不是你建了覆盖索引就一定会用,设计SQL时也要考虑返回字段的数量。

总结:索引优化的六个准则

说了这么多,总结一下我这些年总结的索引优化准则:

1. 索引不是越多越好。每个索引都有代价,写入密集的场景要控制索引数量。

2. 联合索引注意字段顺序。等值字段在前,范围字段在后,区分度高的在前。

3. 区分度低的字段不要单独建索引。性别、状态这种字段,单独建索引没意义,MySQL优化器会放弃。

4. 查询时注意最左前缀。范围查询后面的索引用不上,函数运算后面的索引也用不上。

5. 用EXPLAIN分析查询计划。不要猜,要看实际执行计划。type=ALL不一定是坏事,Using index也不一定是好事。

6. 定期审视你的索引。用pt-index-usage或者类似的工具,找出那些从未被用过的索引,删掉它们。

最后说两句

索引这个话题,说浅了谁都懂,说深了能开专栏。我见过太多人看了两篇博客就开始疯狂加索引,也见过有人在生产环境跑着几十个索引的表,抱怨写入怎么这么慢。

技术这东西,最怕的不是不懂,而是以为自己懂了。索引的坑在于它太"正确"了,所有人都告诉你加索引能让查询变快,没人告诉你它会让写入变慢、会增加存储空间、会让维护变得复杂。

下次你再加索引的时候,先问自己三个问题:这个字段区分度高吗?这个查询真的需要吗?我的写入能承受这个代价吗?

如果答案都是yes,再加不迟。否则,宁可什么都不做。

相关文章

AI 为什么会一本正经地胡说八道?
让你的API从能用变优雅:RESTful设计实战经验谈
一次线上事故让我彻底搞懂了SETNX的坑
🤖 还在为部署AI工具熬夜?小龙虾帮你搞定!代部署服务上线
REST API设计:那些年我们踩过的坑,和想甩锅给HTTP协议的瞬间
你以为RR就安全了?MySQL事务隔离的残酷真相

发布评论