列表页越来越慢,最后到了 8 秒。这类问题几乎都有固定套路:先量,再改,别猜。
#第一步:打开慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SHOW VARIABLES LIKE 'slow_query_log_file';跑一段时间后拿到了罪魁祸首:
SELECT p.*, GROUP_CONCAT(t.name) AS tags
FROM posts p
LEFT JOIN post_tags pt ON pt.post_id = p.id
LEFT JOIN tags t ON t.id = pt.tag_id
WHERE p.status = 'published'
GROUP BY p.id
ORDER BY p.published_at DESC
LIMIT 20 OFFSET 400;#第二步:EXPLAIN 看真相
EXPLAIN SELECT ... 结果里 type=ALL、rows=51237,典型全表扫描。原因有两个:
status没有索引ORDER BY ... LIMIT ... OFFSET大偏移时要扫掉前面所有行
#第三步:三处改动
#1. 加索引
CREATE INDEX idx_posts_status_date ON posts(status, published_at DESC);#2. 用 EXISTS 替代 JOIN + GROUP BY
标签展示改成二次查询,主查询不再聚合:
SELECT p.* FROM posts p
WHERE p.status = 'published'
AND EXISTS (SELECT 1 FROM post_tags pt JOIN tags t ON t.id = pt.tag_id
WHERE pt.post_id = p.id AND t.slug = ?)
ORDER BY p.published_at DESC
LIMIT 20 OFFSET 400;#3. 深分页改成游标分页
OFFSET 400 要扫描 400 行,越往后越慢。改成"记住上一页最后一条":
SELECT p.* FROM posts p
WHERE p.status = 'published'
AND (p.published_at, p.id) < (?, ?)
ORDER BY p.published_at DESC, p.id DESC
LIMIT 20;#结果
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 首页列表 | 8.2s | 80ms |
| 第 20 页 | 12s | 95ms |
| 扫描行数 | 51237 | 20 |
#复盘
- 索引要跟着查询走,不是给每个字段都加
- 聚合尽量移到应用层,数据库擅长筛选不擅长拼字符串
- 分页别用大 OFFSET,游标分页几乎没有成本
优化最爽的地方在于:改动只有三处,性能提升一百倍。