文章

sql是如何被需求一步一步拖慢的

sql是如何被需求一步一步拖慢的

1.起点:一条完全合理、没有任何问题的 SQL

一开始,一个sql特别简单,需求就是一句话:

按时间排序,展示最新内容

SQL如下:

1
2
3
4
5
SELECT id, content, update_time
FROM post
WHERE status = 1
ORDER BY update_time DESC
LIMIT 20;

查询条件简单,排序字段明确,limit很小,update_time有索引。

一条极其合理规范的sql,不存在“优化”的讨论空间。


2.第一次需求变化:加一个“时间范围”

过了一段时间,业务提了个很常见的需求:

“太老的内容没意义,只查看最近七天的”

SQL变成这样:

1
2
3
4
5
6
SELECT id, content, update_time
FROM post
WHERE status = 1
  AND update_time > NOW() - INTERVAL 7 DAY
ORDER BY update_time DESC
LIMIT 20;

到这一步位置,系统仍旧非常健康:

  • 条件更收敛了
  • 扫描数据更少
  • 查询成本反而更低

这一阶段,没有任何理由去怀疑这条 SQL。


3.第二次需求变化:开始引入“互动”概念(拐点出现)

来自一个看起来也很合理的需求:

“最近的不一定最好。”
“点赞多一点的内容,应该靠前一些。”

这句话非常”产品”,但也开始让事情变得糟糕。

实现方式通常是这样想的:

1
2
那就把点赞表 join 进来,
按点赞数排一下。

SQL于是变成如下:

1
2
3
4
5
6
7
8
SELECT p.id, p.content, p.update_time, COUNT(l.id) AS like_cnt
FROM post p
LEFT JOIN post_like l ON l.post_id = p.id
WHERE p.status = 1
  AND p.update_time > NOW() - INTERVAL 7 DAY
GROUP BY p.id
ORDER BY like_cnt DESC, p.update_time DESC
LIMIT 20;

此时的SQL状态:

  • 数据量不算大。
  • explain看起来也“还行”。
  • 线上暂时没报警。

SQL还能跑,所以在当时,这一步几乎不可能被否掉。

但事后看,这一步已经发生了一个本质变化:

排序依据,从“字段值”变成了“字段值”&&“统计结果”。

4.为什么这一步很容易被接受?

因为它踩中了几个“工程幻觉”:

  • LIMIT 20 给人一种“数据量很小”的错觉
  • LEFT JOIN + GROUP BY 在中小数据量下还能扛
  • 点赞表当时也不算大

所以看到这条 SQL 时,大概率会想:

“虽然复杂了一点,但也没到不能用的程度吧?”

这正是问题的温床。


5.第三次需求变化:叠加纬度(质变完成)

此时业务有新想法了:

“光点赞不够。”
“评论、转发量高的内容,也应该更靠前。”

于是,SQL再次“顺理成章”地演化:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT p.id, p.content,
       COUNT(DISTINCT l.id) AS like_cnt,
       COUNT(DISTINCT c.id) AS comment_cnt
       COUNT(DISTINCT f.id) AS forward_cnt
FROM post p
LEFT JOIN post_like l ON l.post_id = p.id
LEFT JOIN post_comment c ON c.post_id = p.id
LEFT JOIN post_forward f ON f.post_id = p.id
WHERE p.status = 1
  AND p.update_time > NOW() - INTERVAL 7 DAY
GROUP BY p.id
ORDER BY (like_cnt + comment_cnt + forward_cnt) DESC
LIMIT 20;

到这里其实已经很清楚了:

这条SQL,不实在“查数据”,而是在“算数据”。

问题在于——已经回不去了,如果后续纬度继续叠加,join依旧继续叠加。


6.为什么这时才开始出问题?

因为这条 SQL 在做的事情是:

  • 扫描最近 7 天的所有 post
  • join 点赞表
  • join 评论表
  • join 转发表

对每一条 post:

  • 统计点赞数
  • 统计评论数
  • 统计转发数

对所有 post:

  • 计算排序权重
  • 再排序
  • 最后,才取前 20 条

也就是说:

哪怕你只要 20 条结果,
MySQL 也必须“算完再挑”。

在数据量继续增长后:

  • join 成本开始失控
  • group by 成本指数级上升
  • order by 彻底不可控

慢查询,就是从这里开始的。


7.最终的解决方式:把“算”移走

最终的解决方案并不复杂:

  • 点赞、评论、转发发生时
  • 增量维护统计数据

维护一张内容统计表:

1
2
3
4
5
6
7
post_stat (
  post_id,
  like_cnt,
  comment_cnt,
  forward_cnt,
  score
)

score在写入期更新,查询阶段,不join行为表、不group by、不在查询期算排名。


8.结语

这类问题最危险的地方在于:没有哪一步是明显错误的 它是被一连串“看起来都很合理”的需求,一步一步拖成系统问题的。 下次在改SQL的时候,多考虑一步:是否越界了? 这篇文章的目的都达到了

本文由作者按照 CC BY 4.0 进行授权