你的SQL正在被你的”优化”悄悄杀死

2026-04-04 12 0

你的"SQL优化"正在悄悄杀死你的数据库

上周三凌晨两点,被一阵急促的钉钉警报吵醒。生产库CPU飙到98%,查询延迟从2ms变成8000ms。用户下单页面直接超时,那一刻我感觉自己的职业生涯要交代了。

查了一圈,发现罪魁祸首是我两周前加的一个索引。

你没看错——是索引,而且是我亲手加的"优化索引"。


事情的经过是这样的

那张order订单表有个status字段,查询订单列表时经常按status筛选。开发环境跑了一下,加个索引,EXPLAIN一看,rows直接从10万变成200——漂亮,性能提升500倍。

于是我信心满满地部署到了生产。

两周后,在某个促销活动的流量高峰,这个索引开始作妖。

问题在于:索引是有代价的。每次INSERT/UPDATE/DELETE,数据库不仅要维护数据本身,还要更新所有相关索引。索引越多,维护成本越高。

-- 我加的"优化"索引大概长这样
CREATE INDEX idx_order_status ON orders(status);

-- 每次订单状态变更时的代价:
-- 1. 更新orders主表
-- 2. 更新idx_order_status索引
-- 3. 如果还有复合索引idx_order_status_created之类的,还要再更新
-- 4. 数据量大了之后,这些索引文件可能比主表还大

你以为索引是免费的午餐?Too young too simple.


几个让人血压升高的认知误区

误区1:索引越多越好

这是最经典的误解。我见过很多项目,数据库里有几十个索引,业务代码里用到的可能就三四个。

怎么判断该不该加索引?先问自己几个问题:

  • 这个字段的区分度够不够?性别字段加索引?恭喜你,索引扫描比全表扫描还慢
  • 这个查询的频率够不够高?如果一天查一次,加索引的维护成本可能划不来
  • 有多大?小表全表扫描可能比走索引还快
-- 看字段区分度
SELECT status, COUNT(*) FROM orders GROUP BY status;

-- 结果可能是:
-- status=1 (待支付): 5000条
-- status=2 (已支付): 80000条  
-- status=3 (已取消): 3000条

-- 区分度 = MAX(count) / MIN(count) = 80000/3000 ≈ 27
-- 这个区分度其实还行,但如果status=1和status=3合并查询,
-- 索引效果就大打折扣了

误区2:复合索引顺序无所谓

复合索引是有顺序的,而且这个顺序必须是等值查询在前,范围查询在后

-- 你的查询
WHERE user_id = 123 AND created_at > 2024-01-01

-- 正确的索引顺序
CREATE INDEX idx_user_time ON orders(user_id, created_at);

-- 错误示范:把范围条件放前面
CREATE INDEX idx_time_user ON orders(created_at, user_id);
-- 这个索引对上面的查询几乎没用!

记住:数据库索引遵循最左前缀原则。如果你有(A, B, C)索引,可以命中AA+BA+B+C的查询,但不能命中只有BC的查询。

误区3:覆盖索引就是多加几个字段

覆盖索引的意思是:索引包含了查询所需的所有字段,数据库直接读索引就能返回结果,不需要回表。

-- 查询:SELECT id, user_name, status FROM orders WHERE user_id = 123

-- 普通索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 需要回表:先通过user_id找到id,再去主表拿user_name和status

-- 覆盖索引
CREATE INDEX idx_user_cover ON orders(user_id, user_name, status);
-- 不需要回表,直接从索引返回结果

但覆盖索引不是加越多字段越好。索引越宽,占用空间越大,维护成本越高。这是一个 tradeoff,别走极端。


我是怎么修复这次事故的

第一步,先观察。开了慢查询日志,把执行时间超过100ms的查询都捞出来:

-- MySQL开启慢查询
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.1; -- 100ms
SET GLOBAL slow_query_log_file = /var/log/mysql/slow.log;

-- 查看慢查询
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

第二步,分析。用EXPLAIN看这些慢查询的执行计划:

EXPLAIN SELECT * FROM orders WHERE status = 2 AND created_at > 2024-01-01;
-- 看看有没有用到索引,用的哪个索引,扫描了多少行

第三步,决策。综合考虑查询频率、字段区分度、数据量之后,我做了三件事:

  1. 删掉了那个只加在status上的单字段索引(区分度不够,还增加写开销)
  2. 把复合索引改成(user_id, status, created_at),覆盖了90%的查询场景
  3. 对status加了个低区分度的草率索引,专门服务那10%的特殊查询(用force index绕过优化器误判)
-- 最终的索引方案
-- 复合索引:覆盖高频查询
CREATE INDEX idx_main ON orders(user_id, status, created_at);

-- 单字段索引:低频但必须的查询
CREATE INDEX idx_status ON orders(status);

-- 对于确实需要强制走某个索引的查询
SELECT * FROM orders FORCE INDEX(idx_status) 
WHERE status IN (1, 3);

CPU从98%降到12%,查询延迟恢复到2ms。凌晨三点,我终于可以回去睡觉了。


最后说几句真心话

数据库优化这东西,没有银弹。别信什么"加个索引就能解决一切"的鬼话。

真正有用的优化,永远是:

  • 测量,别瞎猜
  • 执行计划,别凭感觉
  • 考虑整体,别只盯着单个查询
  • 相信数据,别相信直觉

我见过太多"三更半夜加索引改SQL"然后引发更大事故的案例了。稳住,别慌,数据驱动,这是最重要的。

当然,如果你遇到了我解决不了的问题——那当我没说,该升级服务器还是得升级。😁

相关文章

别让你的API成为同事的噩梦:RESTful设计踩坑实录
你的后端正在被”超时”慢慢杀死:80%的人都在犯同一个致命错误
上线五分钟,排查两小时:你需要分布式链路追踪
「懒人福音」AI工具一键部署,自己折腾还是花钱搞定?
RESTful API设计:那些年我们一起踩过的坑,今天一次说清楚
你的数据库事务可能是定时炸弹:没人告诉你的隔离级别真相

发布评论