【问题标题】:Select items that are the top N results for a related table选择相关表的前 N ​​个结果的项目
【发布时间】:2010-07-01 19:15:16
【问题描述】:

假设我有一个游戏,在这个游戏中提出问题,人们发表回答并计分,排名前 10 的回答获胜。我有一个存储所有这些信息的 SQL 数据库,所以我可能有诸如用户、问题和响应之类的表。 Responses 表有foreign_keys user_id 和question_id,以及属性total_score。

显然,对于一个特定的问题,我可以检索前 10 个带有顺序和限制的响应:

SELECT * FROM Responses WHERE question_id=? ORDER BY total_score DESC LIMIT 10;

我正在寻找的是一种方法,我可以为特定用户确定他们所有获奖回复的列表(在他们特定问题的前 10 名中)。以编程方式遍历每个响应并查看它是否包含在其问题的前 10 名中很简单,但我想对此进行优化,因此我不进行 N+1 查询,其中 N 是用户提交的响应数.

【问题讨论】:

  • StackOverflow 2.0?抱歉,没办法……
  • 哈哈!有趣的是,在您指出之前,我没有意识到我的示例描述了 StackOverflow。这就是我今天的大脑的工作方式。

标签: sql subquery limit greatest-n-per-group


【解决方案1】:

如果您使用 Oracle、Microsoft SQL Server、DB2 或 PostgreSQL,这些数据库支持窗口函数。加入用户对同一问题的其他回复的回复。然后按问题划分,按分数降序排列。使用每个分区中的行号将集合限制为前 10 名。同时传递给定用户的 user_id,以便您可以从前 10 名中挑选出他们,因为您只对给定用户的响应感兴趣。

SELECT *
FROM (
  SELECT r1.user_id AS given_user, r2.*,
    ROW_NUMBER() OVER (PARTITION BY r2.question_id ORDER BY r2.total_score DESC) AS rownum
  FROM Responses r1 JOIN Responses r2 ON r1.question_id = r2.question_id
  WHERE r1.user_id = ?
) t
WHERE rownum <= 10 AND user_id = given_user;

但是,如果你使用 MySQL 或 SQLite 或其他不支持窗口函数的数据库,你可以使用这个不同的解决方案:

查询用户的回答,并使用连接来匹配对相应问题的其他回答,得分更高(或在平局的情况下更早 PK)。按问题分组,并计算得分较高的回答数。如果计数少于 10,则用户的回答在每个问题的前 10 名中。

SELECT r1.*
FROM Responses r1
LEFT OUTER JOIN Responses r2 ON r1.question_id = r2.question_id 
  AND (r1.total_score < r2.total_score 
    OR r1.total_score = r2.total_score AND r1.response_id > r2.response_id)
WHERE r1.user_id = ?
GROUP BY r1.question_id
HAVING COUNT(*) < 10;

【讨论】:

  • 比尔太聪明了...我唯一的问题是(明天有时间我会自己尝试)这会比重复的子查询更快吗?有很多计算正在进行。
  • 性能因数据库品牌而异。每个 RDBMS 在不同类型的查询上都更有效。一个可能更适合您的特定数据收集。我不知道您使用的是什么数据库,所以我只能建议您尝试两种方法,看看您更喜欢哪一种。
  • 我正在编写一个 Rails 应用程序,所以我目前主要使用 MySQL 和 SQLite,但尝试尽可能地与数据库无关——数据库特定的优化可以稍后进行。我真的很喜欢您的第二个示例,因为它看起来几乎可以在任何 RDBMS 上运行而无需变通方法。
  • 我应该提一下,GROUP BY 语句需要是“GROUP BY r1.response_id”。除此之外,这个查询对我来说非常有用。我还应该为其他阅读本文的人注意,此查询不适用于查找得分为 #1 的响应,因为顶级响应和第二好的响应都将具有 COUNT()==1。换句话说,HAVING COUNT()
【解决方案2】:

尝试嵌入式选择语句。我今天无法使用数据库工具,因此无法确认语法/输出。只需进行适当的更改以捕获您需要的所有列。您还可以向主查询添加问题并加入响应。

select *
  from users
     , responses
 where users.user_id=responses.user_id
   and responses.response_id in (SELECT z.response_id 
                                   FROM Responses z 
                                  WHERE z.user_id = users.user_id 
                                 ORDER BY total_score DESC 
                                 LIMIT 10)

【讨论】:

  • 这行不通,因为需要按 question_id 对 Response 候选行进行分组,然后才能确定该问题的 10 名获胜者
  • 这就是我包含问题的意思。我的示例将让您获得此人的前 10 名获奖者,而不是问题前 10 名。
  • 它将为您提供该用户的前 10 名得分最高的回答。但是,很可能这些都不是赢家,因为每个回答所属的每个问题都有 10 个其他人的 10 个得分较高的回答。
  • 所以你的想法的关键是来自外部查询的值,例如 users.user_id,可以在子查询中使用吗?子查询真的会为每个不同的users.user_id重新生成吗?因为我可以看到我会如何做类似的事情: select * from answers whereresponses.user_id='somedude' andresponses.id in (select r.id from answers r where r.question_id=responses.question_id order by total_score DESC LIMIT 10 );
  • @MikeJ:是的,这称为相关子查询。必须为外部查询的每一行重新生成子查询,因为对于每个不同的 user_id,子查询的结果可能不同。如果您依赖外部查询的 question_id,它仍然是一个相关子查询。
【解决方案3】:

或者您可以通过添加另一个字段(例如“IsTopPost”)来真正优化它。当有人投票时,您必须更新热门帖子,但您的查询很简单:

SELECT * FROM Responses WHERE user_id=? and IsTopPost = 1

【讨论】:

  • 这是可行的,但您的 DBA 这样做可能会让您陷入困境。以我的经验,如果可能,DBA 更喜欢你派生值而不是存储它们。例如,不要将行数存储在表中,而是在需要时计算它们。
  • 我在一定程度上同意,但这也取决于规模和访问模式。如果有数十亿个响应并且您需要经常计算答案,那么添加该字段的基数将节省大量读取和锁定,这将使您的 DBA 高兴。如果这是一次性报告,请务必即时计算并接受打击。
  • GalacticJello,你在这里是绝对正确的。有时这可能没问题,您的示例就是其中之一。
  • 我喜欢这个想法,但这是我会留到以后作为调整,当我真正拥有我的第一个十亿用户时 :)
【解决方案4】:

我认为这样的事情应该可以解决问题:

SELECT 
    user_id, question_id, response_id 
FROM 
    Responses AS r1 
WHERE 
    user_id = ?
AND
    response_id IN (SELECT response_id 
                    FROM Responses AS r2 
                    WHERE r2.question_id = r1.question_id 
                    ORDER BY total_score DESC LIMIT 10)

实际上,对于每个 question_id,都会执行一个子查询,以确定该 question_id 的前 10 个回答。

您可能需要考虑添加一列,将某些回复标记为“获胜者”。这样,您只需选择这些行,数据库就不必一遍又一遍地计算前 10 名。

【讨论】:

  • 正如我在第一个响应中指出的那样,唯一的问题是子查询中的 LIMIT 在 MySQL 中不起作用,但除此之外效果很好。
猜你喜欢
  • 1970-01-01
  • 2016-11-25
  • 2010-12-24
  • 1970-01-01
  • 2019-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多