【问题标题】:Avoiding the “n+1 selects” problem when I want a subset of related resources当我想要相关资源的子集时避免“n+1 选择”问题
【发布时间】:2020-01-16 17:59:15
【问题描述】:

想象一下,我正在设计一个多用户博客,我有 userpostcomment 表,这些表具有明显的含义。在主页上,我想显示最近的十个帖子以及所有相关的 cmets。

天真的方法是SELECT 是最近的十个posts,可能是JOINed 和写它们的users。然后我可以通过它们循环到SELECT cmets,再次,可能是JOINed 与编写它们的用户。这需要 11 次选择:1 次用于帖子,10 次用于他们的 cmets,因此著名的反模式的名称是:n+1 次选择

避免这种反模式的通常建议是使用第一个查询中的 ID 在第二个查询中获取所有相关的 cmets,如下所示:

SELECT
    *
FROM
    comments
WHERE
    post_id IN (/* A comma separated list of post IDs returned from the first query */)

只要逗号分隔的列表相当短,我们就设法通过仅发出两个SELECT 查询而不是十一个查询来获取我们需要的所有数据。太好了。

但是,如果我只想要每个帖子的前三个 cmets 怎么办?我没有尝试,但我可能会想出一些 LEFT JOIN 诡计来在单个查询中获取最新帖子及其前三个 cmets,但我不确定它是否具有可扩展性。例如,如果我想要超过典型 MySQL 安装的 61 个表的连接限制的前 100 个 cmets,该怎么办?

除了恢复到 n+1 选择反模式之外,通常的解决方案是什么?在这种相当典型的场景中,使用与每个项目相关的项目子集来获取项目的最有效方法是什么?

【问题讨论】:

  • 你的 rdbms 是什么? Sql Server、postgres、oracle?什么版本?
  • @JuanCarlosOropeza 我认为这在这里并不重要,因为这是一个理论问题,我最感兴趣的是通用解决方案。但也欢迎针对特定 RDBM 的解决方案。

标签: mysql sql join select greatest-n-per-group


【解决方案1】:

运行尽可能少的查询通常是一个更好的选择,然后在需要时在其上实现一些应用程序逻辑。在您的用例中,我将构建一个返回最新帖子和最新关联 cmets 的查询,并使用正确的顺序使应用程序处理更容易。然后您的应用程序可以处理显示它们。

假设您使用 MySQL(因为您在问题中提到了它),让我们从一个查询开始,它会为您提供 10 个最新帖子:

SELECT * FROM posts ORDER BY post_date DESC LIMIT 10

那你就可以用对应的cmets来加入这个了:

SELECT
    p.*,
    c.*
FROM 
    (SELECT * FROM posts ORDER BY post_date DESC LIMIT 10) p
    INNER JOIN comments c ON c.post_id = p.id

最后,让我们对每个帖子的 cmets 数量进行限制。为此,您可以使用ROW_NUMBER()(在 MySQL 8.0 中可用)对每个帖子的 cmets 进行排名,然后仅过滤给定数量的 cmets。这将为您提供 10 个最近的帖子以及他们最近的 3 个 cmets:

SELECT *
FROM (
    SELECT
        p.*,
        c.*,
        ROW_NUMBER() OVER(PARTITION BY p.post_id ORDER BY c.comment_date DESC) rn
    FROM 
        (SELECT * FROM posts ORDER BY post_date DESC LIMIT 10) p
        INNER JOIN comments c ON c.post_id = p.id
) x
WHERE rn <= 3
ORDER BY p.post_date DESC, c.comment_date DESC

查询结果按帖子排序,然后按评论日期排序。因此,当您的应用程序获取结果时,您会按顺序在每个帖子中获得 1 到 3 条记录。

【讨论】:

  • 非常感谢并 +1。我接受了另一个答案,因为它暗示了几分钟前发布的几乎相同的东西。
【解决方案2】:

如果你想要最后 10 个帖子

 SELECT p.post_id
 FROM post p
 ORDER BY p.publish_date DESC
 LIMIT 10

现在,如果您想要这些帖子的评论:

SELECT c.comment_id, u.name
FROM comments c
JOIN users u
  on c.user_id = u.user_id
WHERE c.post_id IN ( SELECT p.post_id
                     FROM post p
                     ORDER BY p.publish_date DESC
                     LIMIT 10 )

现在最后 3 个 cmets 是 rdbms 版本很重要的地方,因此您可以使用 row_number 或不使用:

SELECT *
FROM (
    SELECT c.comment_id, u.name, 
           row_number() over (partition by c.post_id order by c.comment_date DESC) as rn
    FROM comments c
    JOIN users u
      on c.user_id = u.user_id
    WHERE c.post_id IN ( SELECT p.post_id
                         FROM post p
                         ORDER BY p.publish_date DESC
                         LIMIT 10 )
    ) x
WHERE x.rn <= 3

【讨论】:

  • 酷,我不知道row_count,它确实可以解决问题,并且似乎在大多数流行的 RDBMS 的最新版本中得到了很好的支持。
【解决方案3】:

您可以在一个查询中完成此操作:

select . . .   -- whatever columns you want here
from (select p.*
      from posts p
      order by <datecol> desc
      fetch first 10 rows only
     ) p join
     users u
     on p.user_id = u.user_id join
     comments c
     on c.post_id = p.post_id;

这将返回一个表中的帖子/用户/cmets,混合列。但它只需要一个查询。

【讨论】:

  • 这并没有提供限制每个帖子的 cmets 数量的解决方案。
  • @GMB 。 . .那是因为问题是:“我想显示最近的 10 个帖子及其所有相关的 cmets。”
  • @GordonLinoff 是的,但只是为了建立问题的前提。实际问题在底部。
猜你喜欢
  • 2011-12-07
  • 1970-01-01
  • 2012-09-29
  • 2015-11-28
  • 2012-05-19
  • 1970-01-01
  • 2011-07-12
  • 1970-01-01
  • 2010-12-03
相关资源
最近更新 更多