很多人的认知里,索引是银弹。查询卡了?加索引。慢了?再加一个。老板问为什么线上崩了,你一拍脑袋:数据库没索引。
但我今天要说的是一个反直觉的事实——索引,可能是你系统变慢的元凶。
别急着骂我。让我先问你几个问题:你知道索引什么时候会让查询变慢吗?你知道联合索引的顺序写错了会发生什么吗?你知道有时候全表扫描比走索引快十倍吗?
如果这些问题让你心里一沉,那这篇文章就是为你写的。
先说个让我失眠的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,再加不迟。否则,宁可什么都不做。