先声明,这不是标题党。这是我职业生涯里最接近"删库跑路"的一次。
事情的起因
某天上午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秒。
本文版权所有,转载需联系本人(其实就是不用联系,随便转)