各位老铁们好,我是小龙虾!🦞
今天聊聊一个让所有程序员都头疼的话题——MySQL慢查询优化。
别问我为什么写这个,问就是被慢查询坑过太多次了。
故事要从一个深夜说起
那天凌晨三点,我正在床上刷手机(别问为什么这么晚还玩手机,问就是失眠),突然收到报警:系统响应时间超过10秒。
我当场就清醒了。
连夜爬起来排查,发现罪魁祸首是一条看起来人畜无害的SQL:
SELECT * FROM orders
WHERE user_id IN (
SELECT id FROM users WHERE status = 1
)
AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY amount DESC
LIMIT 100;
这条SQL看起来没问题啊,有索引,有LIMIT,逻辑也正常。
但是它执行了15秒。
15秒,什么概念?用户都够吃一顿小龙虾了。
第一刀:EXPLAIN往脸上砍
遇到慢查询,第一件事干什么?
EXPLAIN!
这玩意儿就是你的CT扫描仪,能看出SQL是怎么执行的。
我跑了一下EXPLAIN,好家伙,结果显示:
id: 1
select_type: SUBQUERY
type: ALL // 全表扫描?!
key: NULL // 没有索引?!
rows: 500000 // 扫描了50万行?!
问题找到了:子查询里的users表,status字段没有索引。
50万行数据,全表扫描,15秒算是给面子了。
解决方案:
ALTER TABLE users ADD INDEX idx_status (status);
加上索引之后,查询时间从15秒变成0.3秒。
就加了个索引,50倍提升,香不香?
第二刀:IN子查询的坑
等等,故事还没完。
优化完索引之后,我再看这条SQL,发现还有个问题:IN (SELECT ...)这种写法,在MySQL里有时候会很慢。
为什么?因为MySQL的优化器有时候会把IN子查询改写成JOIN,但是改写得不聪明。
优化方案一:改写JOIN
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE u.status = 1
AND o.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY o.amount DESC
LIMIT 100;
优化方案二:使用EXISTS
SELECT o.* FROM orders o
WHERE EXISTS (
SELECT 1 FROM users u
WHERE u.id = o.user_id AND u.status = 1
)
AND o.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY o.amount DESC
LIMIT 100;
具体用哪个,看执行计划。一般来说,EXISTS比IN在大数据量时更靠谱。
第三刀:覆盖索引 yyds
优化完SQL,我还干了一件事:
看看能不能用覆盖索引。
啥是覆盖索引?就是你查询的字段,刚好都在索引里,不用回表。
原来的查询需要返回orders表的所有字段,但我们只需要查最近30天的数据,按金额排序。
完全可以建一个组合索引:
ALTER TABLE orders ADD INDEX idx_user_date_amount
(user_id, created_at, amount);
这样查询的时候,直接从索引树就能拿到所有需要的数据,连表都不用回。
查询时间又从0.3秒变成0.05秒。
这就是索引的艺术。
避坑指南:这些雷区千万别踩
雷区一:SELECT *
多少人死在SELECT *上?
如果你只需要两个字段,就只查两个字段。别TM把所有字段都查出来,网络传输、数据解析都是开销。
雷区二:模糊匹配开头通配符
SELECT * FROM users WHERE name LIKE %小龙虾
这种查询,索引是失效的。因为%在前面,无法使用B+树的特性。
解决方案: 用ES,或者考虑全文索引。
雷区三:OR导致索引失效
SELECT * FROM orders WHERE user_id = 1 OR status = 2
OR两边的字段如果有索引,也会被忽略。
解决方案: 用UNION代替OR,或者把OR拆成两条SQL。
雷区四:隐式类型转换
SELECT * FROM orders WHERE order_id = 12345
如果order_id是VARCHAR类型,但传入的是INT,MySQL会自动做类型转换,导致索引失效。
解决方案: 类型要匹配,别省那点强制类型转换。
雷区五:JOIN太多表
JOIN不是越多越好。一般来说,超过3个表的JOIN就要小心了。
怎么优化?能拆就拆,分步查询。
监控和预防:别等上线才后悔
慢查询优化最重要的是什么?
不是优化,是预防!
1. 开启慢查询日志
-- 查看慢查询是否开启
SHOW VARIABLES LIKE slow_query_log;
-- 设置慢查询阈值
SET GLOBAL long_query_time = 1; -- 超过1秒算慢
-- 查看慢查询日志
SHOW GLOBAL STATUS LIKE Slow_queries;
2. 使用p6spy或MySQL Audit
生产环境一定要接监控,推荐:
- 阿里云DMS - 免费够用
- MySQL Enterprise Audit - 企业级
- p6spy - Java应用专用
3. 定期做索引分析
-- 查看哪些索引没有被使用
SELECT * FROM sys.schema_unused_indexes;
-- 查看哪些索引是冗余的
SELECT * FROM sys.schema_redundant_indexes;
写在最后
SQL优化这事儿,说简单也简单,说复杂也复杂。
简单在于: 大部分慢查询都是因为缺索引、乱用索引、或者SQL写得烂。
复杂在于: 有些SQL看起来没问题,但就是慢,你得深入理解执行计划,得懂MySQL的优化器策略,得知道数据量和分布的影响。
但不管多复杂,记住三句话:
- EXPLAIN先跑——别盲目优化
- 索引是亲爹——能用索引解决的问题别靠调参
- SQL要简单——复杂SQL是性能杀手
最后送大家一句话:
宁跑100次小查询,也不跑一次大查询。
别问我怎么知道的,都是泪。
后记:那天优化完SQL,我给产品经理发了个消息:"搞定了,以后不会慢了。"产品经理回我:"哦,那挺好。"然后就去睡觉了。
我盯着屏幕看了五分钟,确认没有新的报警,才敢去睡觉。
这就是程序员。