一次SQL慢查询排查,让我差点被开除

2026-06-17 9 0

先声明,这不是标题党。这是我职业生涯里最接近"删库跑路"的一次。

事情的起因

某天上午10点,产品经理神神秘秘地凑过来:"用户反馈咱们的搜索特别慢,你能看看吗?"

我当时还心想,不就是搜索慢吗,Elasticsearch调一调,缓存加一加,小场面。

然后我打开数据库,一查——好家伙,一个用户的搜索列表查询,跑了8秒

8秒。用户在厕所蹲完回来了还没看到结果。

初步排查

我先查了执行计划:

EXPLAIN SELECT * FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN products p ON o.product_id = p.id
WHERE u.status = "active" AND o.created_at > "2024-01-01"
ORDER BY o.created_at DESC
LIMIT 20;

结果出来的那一刻,我沉默了。数据库在全表扫描

三个表JOIN,几十个字段,没有任何索引。就这,8秒都算给面子了。

问题一:缺失的索引

我先问了开发的同事:"这查询为什么没索引?"

他理直气壮地说:"我以为MySQL会自动优化……"

我当时就想把键盘扔他脸上。MySQL是数据库,不是你肚子里的蛔虫。你不告诉它按什么字段查,它就只能一行行去读。

加索引:

ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
ALTER TABLE orders ADD INDEX idx_created (created_at);
ALTER TABLE products ADD INDEX idx_name (name);

问题二:SELECT * 的诱惑

我看了下代码,查询用的是 SELECT *。三个表JOIN,返回了几十个字段,但实际用到的就五六个。

这里涉及一个很重要的概念:数据以页为单位加载到内存。你SELECT的字段越多,需要读取的页就越多,内存压力就越大。

改成精确字段:

SELECT o.id, o.total_amount, o.status, u.name, p.title
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN products p ON o.product_id = p.id
WHERE u.status = "active" AND o.created_at > "2024-01-01"
ORDER BY o.created_at DESC
LIMIT 20;

问题三:分页的坑

改完以上两点,查询降到200毫秒了。但产品经理还是皱眉:"200毫秒也慢啊,Google都是50毫秒。"

我:"……你去用Google吧。"

不过他说的也不完全错。200毫秒确实还有优化空间。

问题出在分页方式上:

--  OFFSET 分页(OFFSET越大越慢)
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

OFFSET分页的原理是:数据库先跳过前100000条,再读20条。随着OFFSET增大,需要扫描的数据量线性增长。

改成游标分页

-- 游标分页(性能恒定)
SELECT * FROM orders
WHERE id > :last_seen_id
ORDER BY id
LIMIT 20;

原理很简单:不再数 skip 了多少行,而是直接从目标位置开始读。不管你翻到第几页,性能始终是稳定的20条。

问题四:N+1查询

你以为这就完了?Too young。

我看了下代码的另一处:

users = db.query("SELECT * FROM users WHERE status = 1")
for user in users:
    orders = db.query(f"SELECT * FROM orders WHERE user_id = {user.id}")
    print(user.name, orders)

这是一个经典的N+1问题。查1个用户,顺便查了他所有的订单。100个用户,就是101次数据库请求。

改成JOIN或批量查询:

SELECT u.*, o.* FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 1;

一次查询,全部搞定。

最终结果

经过以上优化,查询从8秒降到了50毫秒

提升160倍。产品经理露出了满意的微笑,然后问我:"能不能再快一点?"

我温和地请他离开了我的工位。

总结

这次排查让我总结出几条血泪教训:

  • 索引不是万能的,但没有索引是万万不能的。JOIN和WHERE字段一定要建索引。
  • 永远不要SELECT *。这不只是性能问题,也是数据安全问题。你不知道自己会泄露什么字段。
  • 大翻页用游标。OFFSET分页是性能杀手,越翻越慢。
  • 批量操作代替循环查询。N+1问题听起来低级,但实际项目中极其常见。
  • 慢查询日志要定期看。别等用户投诉了你才知道慢。

最后送大家一句话:写代码的时候想着数据库,数据库才能让你好过

不然下一个8秒,就是你的8秒。


本文版权所有,转载需联系本人(其实就是不用联系,随便转)

相关文章

当AI开始整活:我和OpenClaw的日常
当AI开始整活:我和OpenClaw的日常
被一只小龙虾支配的日常:我用 OpenClaw 这几个月的真实体验
被一只小龙虾支配的日常:我用 OpenClaw 这几个月的真实体验
RESTful API 设计那些事儿:别让你的接口变成一场灾难
为什么你写的数据库连接池总在泄漏?我从Stack Overflow抄的答案居然是错的

发布评论