列表页越来越慢,最后到了 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,游标分页几乎没有成本

优化最爽的地方在于:改动只有三处,性能提升一百倍。