《那个让数据库蹦迪的夜晚:慢SQL排查惊魂记》
事故背景
某个月黑风高的周四凌晨1点,我正在梦里和Java祖师爷James Gosling切磋武功,突然被连环夺命call惊醒——生产环境响应时间曲线比过山车还刺激。监控大屏上,支付接口TP99从优雅的50ms直接表演垂直落体运动,成功突破3000ms大关。更刺激的是,DBA老王的咖啡杯监控系统显示,他的马克杯已经连续3小时处于空杯状态…
第一幕:午夜凶铃
抄起笔记本连上VPN的瞬间,20条报警短信同时在手机屏幕上蹦迪。打开APM监控,发现有个SQL语句正在数据库里开个人演唱会:
SELECT * FROM order_table
WHERE user_id IN (SELECT id FROM user WHERE vip_level BETWEEN 3 AND 5)
AND create_time > '2023-01-01'
ORDER BY total_amount DESC
LIMIT 1000;
这个看起来人畜无害的查询,此刻正在以每秒200次的频率对数据库进行"爱的魔力转圈圈"。CPU使用率曲线已经cosplay起珠穆朗玛峰,连接池里的线程们排起了春运般的队伍。
第二幕:福尔摩斯附体
祭出数据库慢查询日志,发现这条SQL的平均执行时间高达2.8秒。用EXPLAIN掀开它的底裤:
EXPLAIN SELECT * FROM order_table...
执行计划显示它正在表演"全表扫描极限运动",type列赫然写着ALL,rows列的数字比双十一销售额还壮观。更绝的是Extra列温馨提示:“Using temporary; Using filesort”,这相当于数据库同时在做俯卧撑和引体向上。
第三幕:解剖课现场
把SQL拖进解剖室:
- 子查询陷进:IN子句里的子查询每次执行都要重新问候user表
- 排序狂欢:在没有索引的情况下对total_amount进行排序
- 时间穿越:create_time条件跨度三年却无索引支撑
- 字段海王:SELECT * 一次性娶了所有列回家
此时DBA老王发来贺电:“你们这查询是在给数据库做心肺复苏呢?”
第四幕:手术进行时
抄起SQL改造工具包:
-- 先给order_table做个微整形
ALTER TABLE order_table
ADD INDEX idx_user_create_amount (user_id, create_time, total_amount DESC);
-- 再把子查询改成联姻模式
SELECT o.* FROM order_table o
JOIN user u ON o.user_id = u.id
WHERE u.vip_level BETWEEN 3 AND 5
AND o.create_time > '2023-01-01'
ORDER BY o.total_amount DESC
LIMIT 1000;
顺手把SELECT *改成指定字段,像给数据库做了个瘦身手术。再用连接查询取代子查询,让两个表的关系从网恋奔现变成明媒正娶。
第五幕:黎明曙光
改造后的执行计划跳起了优雅的华尔兹:
- type变成ref,rows降到两位数
- Using filesort和Using temporary消失不见
- 执行时间从2800ms直降到35ms
监控大屏上的响应时间曲线恢复了大家闺秀的矜持,DBA老王的咖啡杯水位开始稳步回升。关键时刻,发现某个实习生写的ORM代码里藏了个N+1
查询炸弹,赶紧补上一个@BatchSize
注解:
@Entity
@BatchSize(size = 50)
public class User {
//...
}
最终战报
次日晨会数据:
- 数据库QPS从12000降到800
- CPU使用率从95%回归到20%水平线
- 支付接口TP99重回50ms俱乐部
- 运维组咖啡消耗量下降60%
血泪总结
- 索引三件套:WHERE、ORDER BY、JOIN字段请自觉穿好索引外衣
- SELECT * 是原罪:只带走需要的字段,别把整张桌子都搬走
- 子查询变形记:能用JOIN就别搞地下恋情
- ORM防坑指南:N+1问题要像防蟑螂一样警惕
- DBA友谊法则:优化SQL前请先续杯咖啡
最后友情提醒:下次写SQL时,请想象你在给十年后的自己写情书——越简洁优雅越好,毕竟凌晨三点爬起来修BUG的时候,流的泪都是当初写SQL时脑子进的水啊!