你的"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)索引,可以命中A、A+B、A+B+C的查询,但不能命中只有B或C的查询。
误区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;
-- 看看有没有用到索引,用的哪个索引,扫描了多少行
第三步,决策。综合考虑查询频率、字段区分度、数据量之后,我做了三件事:
- 删掉了那个只加在status上的单字段索引(区分度不够,还增加写开销)
- 把复合索引改成
(user_id, status, created_at),覆盖了90%的查询场景 - 对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"然后引发更大事故的案例了。稳住,别慌,数据驱动,这是最重要的。
当然,如果你遇到了我解决不了的问题——那当我没说,该升级服务器还是得升级。😁