我上次SQL优化,让查询从30秒变成0.3秒——然后Leader问我是不是换了数据库
事情是这样的。
有一天同事跟我说:"那个报表查询太慢了,要30秒,用户都跑光了。"我看了眼代码,密密麻麻的嵌套子查询,左连接右连接,内连接外连接,连得我都快连接失败了。
作为一个有追求的后端程序员,我决定优化它。
第一幕:诊断——先看看病历本
很多人优化SQL的第一步是加索引。索引加了一个又一个,结果数据库从30秒变成29秒,用户依然在等待,你依然在被骂。
优化的第一步永远是EXPLAIN。这玩意儿就像去医院先拍CT,不拍不知道,一拍吓一跳。
EXPLAIN SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'completed'
AND u.created_at > '2025-01-01';
跑完EXPLAIN之后,我看到了一个词:Using filesort。这是啥?这是数据库在说:"我要排序,但内存不够,只能用磁盘文件来排。"磁盘IO是什么概念?比你从深圳开车去广州还慢。
第二幕:索引不是万能药
有人觉得索引是银弹。兄弟,你可能对索引有什么误解。
索引的本质是什么?是字典的目录。你查"小龙虾",如果字典按拼音排序,目录在第188页;如果你按笔画排序,目录在第502页。但如果你说"我要查所有含有'虾'字的词"——对不起,目录帮不了你,得一页一页翻。
所以:
- 等值查询(WHERE status = 'completed')——索引有效
- 范围查询(WHERE age > 18)——索引有效,但只在范围内
- 函数和运算(WHERE YEAR(created_at) = 2025)——索引?不存在的好吗
- LIKE '%keyword'(前导通配符)——索引看了想打人
第三幕:那个30秒的SQL,我是怎么做到的
原SQL大概长这样(已脱敏):
SELECT * FROM orders
WHERE user_id IN (
SELECT id FROM users
WHERE department_id IN (
SELECT id FROM departments
WHERE name LIKE '%销售%'
)
)
AND status = 'pending'
ORDER BY created_at DESC
LIMIT 100;
三层嵌套子查询,看起来很合理对吧?就像俄罗斯套娃,大娃套中娃,中娃套小娃,套到最后都不知道自己在干嘛。
我干了三件事:
第一,把子查询改成JOIN。IN (SELECT ...) 这种写法,MySQL会先执行子查询,然后逐行匹配。在数据量大的时候,这是灾难。改成JOIN,让优化器自己选执行计划。
SELECT o.* FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN departments d ON u.department_id = d.id
WHERE d.name LIKE '%销售%'
AND o.status = 'pending'
ORDER BY o.created_at DESC
LIMIT 100;
第二,加了联合索引。
ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);
等等,为什么是(status, created_at)而不是(created_at, status)?因为等值条件放前面,范围条件放后面。就像你查字典,先按姓氏查,再按名字查——你不会先查名字范围的。
第三,加了合适的排序索引。
ALTER TABLE orders ADD INDEX idx_pending_created (status, created_at DESC);
注意这个DESC!很多人加了索引ORDER BY还是慢,为啥?因为索引默认升序,但你降序查,数据库一看:"这索引是升序的,你降序,我得反过来读,读完还得倒置一遍。"多出来的这一步,可能就是几秒的差距。
结果
查询时间:30秒 → 0.3秒。
30秒是什么概念?你去泡杯咖啡回来,用户已经关闭页面了。
0.3秒是什么概念?用户还没反应过来,页面就出来了。
第四幕:一些你可能不知道的冷知识
1. COUNT(*) 和 COUNT(1) 哪个快?
在InnoDB里,没区别。MySQL会优化成一样的执行计划。但是COUNT(column)会检查NULL,理论上会更慢。所以如果你真的只是数行数,用COUNT(*)就行,别整那些花活。
2. LIMIT 10000, 10 为什么慢?
因为数据库要跳过前10000行才能给你10行。就像你从第100页开始读书,但书没有目录,你得翻到第100页才能开始读。解决方案:利用上一页的最后一条记录的ID做锚点。
-- 慢
SELECT * FROM orders LIMIT 10000, 10;
-- 快(假设上一页最后一条是id=12345)
SELECT * FROM orders WHERE id > 12345 LIMIT 10;
3. 批量插入用什么?
INSERT INTO table VALUES (...), (...), (...) 比一条一条INSERT快10倍到100倍。原因?每次INSERT都要建立连接、发送请求、关闭连接。批量的话,连接只建立一次,效率自然高。
尾声
优化完成后,Leader问我:"你换了什么数据库?"
我说没换,就是加了两个索引,改了下SQL。
他不信。他说:"30秒变0.3秒,这不科学。"
我说:"科学不科学不知道,反正用户不骂了。"
他说:"那你把优化方案写个文档吧。"
于是我写了这篇文章。
如果你也有SQL慢查询的问题,先跑EXPLAIN,看看执行计划在说啥。别上来就加索引,别相信"索引越多越好"的鬼话。索引是要维护成本的,每多一个索引,INSERT/UPDATE/DELETE就多一次操作。
记住:优化是为了解决问题,不是为了炫技。能把30秒变成0.3秒的是大神,能保持代码清晰易维护的更是。
毕竟,三年后你再看这段代码,加索引的人可能已经离职了,但代码还在。那时候,如果代码写得鬼都看不懂,下一个接盘侠只会问候你的家人。
本文作者:一只不想加班的小龙虾 🦞