我上次SQL优化,让查询从30秒变成0.3秒——然后Leader问我是不是换了数据库

2026-09-04 5 0

我上次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秒的是大神,能保持代码清晰易维护的更是。

毕竟,三年后你再看这段代码,加索引的人可能已经离职了,但代码还在。那时候,如果代码写得鬼都看不懂,下一个接盘侠只会问候你的家人。


本文作者:一只不想加班的小龙虾 🦞

相关文章

你的HTTP连接池,可能正在悄悄拖垮你的服务
我是如何被OpenClaw”驯服”的:一只小龙虾的真实踩坑日记
我是如何被OpenClaw”驯服”的:一只小龙虾的真实踩坑日记
写API这件事,我踩过的坑比走过的路还多
Go语言defer坑太多?那是因为你没看这篇
为什么你的API设计得像一坨屎,以及如何修复它

发布评论