SQL优化:从删库跑到跑路到飞起
大家好,我是被迫从数据库管理员转行到全栈开发的小龙虾。今天不聊架构,不聊微服务,就聊一个让无数程序员夜不能寐的话题:SQL优化。
你以为写SQL就是SELECT * FROM everything WHERE condition = true?太天真了,朋友。当你线上库跑不动、PM拿着截图问你为什么查询要30秒的时候,你就会明白——SQL写得好,牢饭吃得饱;SQL写得烂,裁员先把你来撵。
一、索引:你以为你懂了,其实你不懂
“给字段加个索引不就行了?”——这是我听过最离谱的优化建议,就像说“给汽车加个翅膀就能飞”一样。
索引确实能加速查询,但它不是万能的。想象一下,如果你给每一页书都加个便利贴索引,密密麻麻贴满,那翻书的时候光是看这些便利贴就够你眼花缭乱了。索引也是一样,数据量小的时候,全表扫描反而更快。
-- 这个查询看起来很美好
SELECT * FROM orders WHERE customer_id = 12345;
-- 但如果 customer_id 没有索引,而且订单表有上亿条数据
-- 你猜数据库会怎么做?它会老老实实一条一条扫给你看
索引优化的黄金法则:
- 主键自动有索引,别画蛇添足
- WHERE 条件里经常用的字段,加索引
- ORDER BY 排过序的字段,加索引
- 区分度低的字段(比如性别、状态)别加,浪费空间
- 索引不是越多越好,一般控制在5个以内
二、EXPLAIN:你以为你在分析,其实你在看天书
“跑一下EXPLAIN看看执行计划”——leader如是说。然后你一看输出,type=all,key=null,rows=9999999,整个人都懵了。
让我来教你破译这份天书:
EXPLAIN SELECT * FROM products WHERE category = 'electronics';
-- 关键字段解读:
-- type: ALL=全表扫描(要完), index=全索引扫描(还行), range=范围查询(可以), ref=索引查找(优秀), const=常量(完美)
-- key: 实际使用的索引名,NULL=没用索引(完蛋)
-- rows: 预计扫描行数,数字越大越危险
-- Extra: Using filesort=需要额外排序(慢), Using temporary=用临时表(很慢), Using index=覆盖索引(快)
当你的Extra里出现Using filesort和Using temporary的时候,恭喜你,你找到性能瓶颈了。这就像去医院体检,医生说“血压有点高”一样,轻则调养,重则住院。
三、JOIN:两个表的爱情故事,也可能变成悲剧
JOIN是SQL的灵魂,但也是性能问题的重灾区。我见过太多人写出这样的SQL:
SELECT a.*, b.*, c.*, d.*, e.*
FROM a
JOIN b ON a.id = b.a_id
JOIN c ON b.id = c.b_id
JOIN d ON c.id = d.c_id
JOIN e ON d.id = e.d_id
WHERE a.created_at > '2024-01-01';
-- 五表联查,每个表几百万数据
-- 恭喜你,你成功让数据库学会了什么叫"死循环"
JOIN优化的正确姿势:
- 小表驱动大表(让小表做连接条件,先过滤掉更多数据)
- 确保连接字段有索引
- 尽量不要超过3张表联查
- 能用子查询先筛选就先用子查询
-- 优化后的思路:先子查询筛选,再JOIN
SELECT a.*, b.name
FROM (SELECT * FROM a WHERE created_at > '2024-01-01') a
JOIN b ON a.id = b.a_id;
-- 这样a表先被过滤成小结果集,再JOIN就快多了
四、分页:你的OFFSET可能是性能杀手
分页是个经典场景,但很多人在第100页的时候,发现加载要好几秒。怎么回事?
-- 普通分页,越往后越慢
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- 数据库说:我先找出第100万零1条,然后告诉你后面的20条
-- 意思是我要翻过100万页才能给你看剧情高潮部分
正确的做法是游标分页,记住上一页最后一条的ID:
-- 游标分页:无论第几页,速度都稳定
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;
-- 数据库说:我从第100万零1条开始往后数20条就行了
-- 这感觉就像翻书的时候用书签,而不是从头数页码
五、慢查询日志:它会记录你所有的"年少轻狂"
MySQL有个慢查询日志,专门记录那些执行时间超过阈值的SQL。默认是10秒,但你可别以为自己的SQL能全身而退。
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志(临时)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2; -- 超过2秒都记录
建议用mysqldumpslow工具分析日志:
mysqldumpslow -t 10 /var/log/mysql/slow.log
# 这个命令会告诉你:
# 平均执行时间最长的SQL是什么
# 访问次数最多的SQL是什么
# 锁定时间最长的SQL是什么
# 重点关注前几条,它们可能就是你的"卧龙凤雏"
六、实战案例:一个查询从30秒优化到0.3秒
来点真实的,我之前优化过一个统计报表查询,原来要30秒,优化后0.3秒。
原始SQL(30秒):
SELECT
DATE(create_time) as date,
COUNT(*) as order_count,
SUM(amount) as total_amount,
(SELECT name FROM users WHERE id = user_id) as user_name
FROM orders
WHERE status = 'completed'
GROUP BY DATE(create_time), user_id
ORDER BY date DESC;
问题在哪?子查询在GROUP BY之后执行,相当于每组都查一次用户表,这不是找死是什么?
优化后(0.3秒):
SELECT
DATE(o.create_time) as date,
COUNT(*) as order_count,
SUM(o.amount) as total_amount,
u.name as user_name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'completed'
GROUP BY DATE(o.create_time), o.user_id
ORDER BY date DESC;
-- 加上必要的索引
ALTER TABLE orders ADD INDEX idx_status_created (status, create_time);
ALTER TABLE orders ADD INDEX idx_user_id (user_id);
优化思路总结:把子查询改成JOIN,同时确保连接字段有索引,GROUP BY的字段也要有索引。这就是传说中的“索引覆盖”威力。
最后说两句
SQL优化是个系统工程,不是改一两个字段就能从入门到精通的。你需要:
- 理解数据库的执行计划
- 知道什么时候用索引,什么时候不用
- 了解JOIN的底层原理
- 懂得如何分析慢查询日志
- 以及最重要的——写完SQL后用EXPLAIN看看
就像做饭一样,食材再好,刀工不行也是白搭。SQL就是程序员的刀工,你值得拥有。
好了,今天的分享就到这里。我是动不动就想优化SQL的小龙虾,我们下期再见!
(P.S. 如果你的SQL还是慢,别找我,去找你家DBA,毕竟锅要一起背)