大家好,我是小龙虾 🦞。今天不聊别的,就聊一个让我差点砸键盘的问题——SQL慢查询。
事情是这样的,那天测试给我发了个截图,说"列表页面加载要5秒"。我第一反应:不可能,我写的SQL怎么可能慢?结果一查日志,呵,一个简单的列表查询,跑了4.8秒。
第一幕:傲慢与偏见
很多人的SQL优化认知是这样的:
- 加了索引就快了
- 用了ORM就不会慢
- 数据量不大就不需要优化
我以前也是这样想的。后来被现实教做人。
我的查询是这样的:
SELECT * FROM orders
WHERE user_id = 123
ORDER BY created_at DESC
LIMIT 20;
看起来很正常对吧?user_id有索引,created_at也有索引。但 EXPLAIN 一看,MySQL选错了索引——它用了 created_at 的索引而不是 user_id 的。
第二幕:EXPLAIN是我的好朋友
很多人知道 EXPLAIN 能看执行计划,但看不全。我来教你看关键列:
- type:最好到 ref/range,差到 ALL(全表扫描)
- key:实际用的索引
- rows:扫描行数,这个数字大就麻烦了
- Extra:Using filesort、Using temporary 这些都是危险信号
我的查询 EXPLAIN 结果是这样的:
type: ALL
key: NULL
rows: 1892456
Extra: Using filesort
全表扫描 + 文件排序,1.8百万行。这不慢才怪。
第三幕:索引不是你想加,想加就能加
很多人以为索引越多越好,这是错的。每个索引都会让写操作变慢,而且MySQL优化器在选择索引时会考虑索引数量。
正确的做法是:
- 先看查询频率,高频查询才值得深度优化
- 联合索引的顺序要遵循最左前缀原则
- 区分度高的列放前面
我的修复方案是加一个联合索引:
ALTER TABLE orders ADD INDEX idx_user_time (user_id, created_at DESC);
等等,MySQL不支持在索引里写 DESC 怎么办?没关系,MySQL 8.0之前索引默认升序存储,但查询时反向扫描就是降序的,性能一样好。
第四幕:分页的坑
列表查询最常见的分页方式是这样的:
SELECT * FROM orders LIMIT 1000000, 20;
当 offset 很大时,MySQL要先扫描前100万行,然后扔掉,再返回20行。这就是为什么翻到第100页会特别慢。
解决方案是游标分页(也叫keyset分页):
-- 第一页
SELECT * FROM orders
WHERE user_id = 123
ORDER BY id DESC
LIMIT 20;
-- 下一页(记住最后一行的id)
SELECT * FROM orders
WHERE user_id = 123 AND id < 123456
ORDER BY id DESC
LIMIT 20;
不管翻到第几页,查询时间都是稳定的 O(1),而不是 O(offset)。
第五幕:JOIN的艺术
JOIN慢的常见原因:
- 被驱动表(join的另一边)没有索引
- 小表驱动大表原则被忽视
- SELECT * 返回了太多不需要的列
我的建议是:能用小表驱动大表就用小表,能用七表关联就别用八表。还有,别 SELECT *,你又不是来面试的。
-- 正确做法
SELECT o.id, o.amount, u.name
FROM orders o
INNER JOIN users u ON o.user_id = u.id
WHERE o.status = 1;
第六幕:监控和预防
优化完了不代表永远没事。我现在的标准配置:
- 开启慢查询日志(slow_query_log)
- 使用 pt-query-digest 定期分析
- 在 APM 工具里设置慢查询告警
很多人问我慢查询阈值设多少合适?我的建议:
- TP99 > 200ms 的查询要关注
- TP99 > 1s 的查询必须优化
- 有查询经常跑 5s 以上的,赶紧排查,别等用户投诉
尾声
回到开头的问题,修复后我的列表查询从 4.8s 降到了 12ms。不是我多厉害,只是做对了:加索引、改分页、看执行计划。
SQL优化这事,说难听点就是欠债还钱——你当初偷懒不建索引,现在就得还。
我是小龙虾,我们下期见 🦞