你以为加了索引就能飞?SQL优化路上的那些自我感动

2026-05-10 14 0

你以为加了索引就能飞?SQL优化路上的那些自我感动

做后端开发的,谁没加过几个索引呢?慢查询来了,加索引。超时报警了,加索引。领导问为什么慢,加索引。

结果呢?该慢还是慢,数据库CPU还是飘红,有些表加完索引反而更慢了。

今天说点反直觉的——很多人以为在做SQL优化,实际上在搞破坏。或者说,在用正确的方法做错误的事情。

玄学一:索引越多越好

我见过一个表40多个索引的,每一个都是"需要"的。开发人员振振有词:这个字段要查,那个字段要排序,另一个字段要过滤。

听起来没问题。但你知道InnoDB的索引原理吗?每加一个索引,INSERT的时候就要多写一份B+树。UPDATE的时候每个索引都要更新。索引不是免费的,是要付出代价的。

更骚的操作是:建了个联合索引(idx_a_b_c, a, b, c),然后单独又建了idx_a。问就是"怕查单个字段慢"。实际上联合索引的最左前缀原则已经覆盖了这个场景,多余的索引就是在吃存储和更新开销。

正确的思路:索引要按查询模式来设计,不是按字段堆。如果你的查询是WHERE a = ? AND b > ? ORDER BY c,那就建(a, b, c)的联合索引,但只建这一个,而不是拆成三个单独索引。

玄学二:EXPLAIN看没看?看了,但看错了

说"我已经看过EXPLAIN了"的后端,十个有八个其实没看懂。常见错误:看到type是index就觉得快了,实际上index只是全表扫描的另一种形式。

看EXPLAIN要关注这几个字段:

  • type:至少要到ref,最好是const。ALL是全表扫描,index是全索引扫描,都不算快
  • key:实际用的索引名字,别光看possible_keys多
  • rows:扫描行数,100万行和100行的差距是质变
  • Extra:这个字段最容易忽略但信息量最大。Using filesort、Using temporary就是性能杀手

很多人口中的"已经优化过了",只是因为没看到全表扫描。实际上Extra里赫然写着Using temporary; Using filesort,只是不知道这俩玩意儿代表什么意思。

玄学三:分页偏移量大,就该用LIMIT OFFSET

分页是经典难题。很多人直觉上就这么写:

SELECT * FROM orders ORDER BY id DESC LIMIT 20 OFFSET 10000

一千条的时候还行,一万条开始卡,十万条直接超时。为什么?因为数据库要扫描10020行,然后扔掉前10000行,只留20行。

更深层的问题是:LIMIT OFFSET的深度分页是没法优化的,索引也救不了。只要OFFSET足够大,每一页都会越来越慢。

正确的方案是游标分页

SELECT * FROM orders WHERE id < #{last_id} ORDER BY id DESC LIMIT 20

这一页查完记住最后一个id,下一页用WHERE id < 那个id。无论翻多少页,时间都是稳定的O(1)。当然,前端得改分页交互,但这是值得的。

如果你的产品经理拒绝改前端交互怎么办?那就想办法让用户别翻那么深。Google搜索为什么只有前1000条?是有道理的。

玄学四:JOIN要少用,能拆就拆

有一种说法在江湖上流传:JOIN性能很差,要少用。解决方案是拆成多条SQL,在代码里拼结果。

这个说法对了一半。JOIN确实可能产生大表笛卡尔积,但问题在于"拆成多条SQL"也不一定快——你的代码跑多一次网络Round Trip,数据库少做一次优化器和Join执行的开销,总体未必省钱。

更重要的是:如果Join的两张表都有合适的索引,数据库执行器可以流水线执行,比你拆成两条SQL再在内存里聚合要快得多。

真实场景是:JOIN慢通常不是因为JOIN本身,而是因为关联字段没索引,或者参与JOIN的表太大了没建条件过滤。优化JOIN的第一步不是拆开,而是检查JOIN条件。

当然,那种SELECT * FROM large_table CROSS JOIN small_table(没有ON条件)的骚操作,确实该拆。那是业务逻辑问题,不是JOIN的问题。

玄学五:COUNT(*) = 0就是空表,可以安全删数据

这条很多人可能不服气。COUNT(*)明明很准啊?

问题在于MVCC。InnoDB是支持多版本并发控制的,普通SELECT不加锁是读快照,而COUNT(*)是实时统计行数,需要计算当前可见的版本。

高并发场景下,一个大表的COUNT(*)可能跑几十秒,因为它需要遍历所有可见版本。而WHERE条件的SELECT如果能利用索引,直接定位到一条记录就返回,快得多。

更坑的是,有些运维脚本会执行DELETE FROM table WHERE COUNT(*) > 0这种操作——以为删起来会很轻量,结果把数据库打死了。

如果只是想判断是否有数据,用SELECT 1 LIMIT 1比COUNT(*)快。不关心具体有多少行,只关心有没有行的时候,别用COUNT(*)。

玄学六:覆盖索引就是最优解,无脑怼上去

覆盖索引确实是个好东东——查询的字段都在索引里,不用回表,直接从索引里返回结果,爽得很。

但很多人不知道的是:如果你的索引是(idx_a, idx_b),但WHERE条件是WHERE b = ?,这个索引用不上。因为最左前缀原则要求查询条件必须包含idx_a。

另一种翻车现场:WHERE a > 100 ORDER BY b。这个查询覆盖索引用不了,因为a是范围查找,索引在a>100之后的顺序是乱的,无法保证b的有序。

覆盖索引是好东西,但要用对场景:

  • 等值查找的字段放前面
  • ORDER BY的字段放后面
  • 范围查找通常会中断索引的有序性

写SQL的时候脑子里要有一张索引B+树的图,想象数据是怎么排列的。索引设计本质上是排列数据,让查询能沿着路径高效走下去。

玄学七:SQL优化是DBA的事,业务代码不用管

这大概是流传最广的误解。业务工程师觉得:SQL嘛,交给DBA优化就好了,我写业务逻辑就行。

实际上DBA能优化的是索引配置和服务器参数,真正的病根往往在SQL本身。你一个SELECT *把五十个字段都拉出来,然后让DBA优化这个慢查询?对不起,无解。

或者WHERE条件里用了函数:WHERE YEAR(created_at) = 2026。这种写法索引直接失效,再牛的DBA也救不了。

正确姿势是:写业务代码的时候就脑子里过一遍SQL:查询条件有没有索引?返回字段是不是必要的?有没有隐式类型转换?有没有函数调用?这些DBA管不了,得自己管。

最后说个真实的段子:有个项目慢查询告警,DBA加了两个索引,优化了三天没效果。后来定位到问题是一个工程师写的SQL:WHERE phone = #{phone},而phone参数传的是字符串"13800138000",数据库做隐式转换,索引失效。改成显式转换类型后,0.3秒变0.01秒。DBA内心:???

总结一下

SQL优化这事儿,说难也难说简单也简单。难在很多坑藏得很深,简单在套路就那些。

记住几个原则:

  • 索引是手段不是目的,不是越多越好
  • EXPLAIN要真看懂,别光看个type列
  • 深度分页是性能杀手,能不用LIMIT OFFSET就不用
  • JOIN不是洪水猛兽,滥用才是
  • COUNT(*)不一定是最好用的判断方式
  • 覆盖索引要用对场景,不是无脑怼
  • SQL优化是每个人的责任,不只是DBA的

下次遇到慢查询,别急着加索引。先问自己一句:这个SQL,真的写对了吗?

毕竟,加索引治标不治本,写好SQL才是真的优化。

相关文章

API设计避坑指南:那些让前端想打人的操作
【AI探索】最近AI圈发生了什么?这些新鲜事我不允许你不知道!
AI编程工具横评:我让Copilot、Claude和GPT-4同时写代码,结果笑死我了
我以为我的接口很安全,直到黑客用这5招把我按在地上摩擦
AI圈最近又发生了什么?我来给你盘一盘
🤖 懒人福音:让AI工具从”装不上”变成”一键起飞”,我们帮你搞定!

发布评论