3

我试图获得前 50 名的下载量,然后对 8 个结果进行洗牌(随机化)。另外,8 个结果必须是唯一的 user_id。到目前为止,我想出了这个:

Song.select('DISTINCT songs.user_id, songs.*').where(:is_downloadable => true).order('songs.downloads_count DESC').limit(50).sort_by{rand}.slice(0,8)

我对此唯一的抱怨是,最后一部分.sort_by{rand}.slice(0,8)是通过 Ruby 完成的。有什么办法可以通过 Active Record 来完成这一切?

4

2 回答 2

3

我想知道该列是如何user_id出现在表格中的songs?这意味着您对于歌曲和用户的每种组合都有一行?在规范化模式中,这将是使用三个表实现的n:m 关系:

song(song_id, ...)
usr(usr_id, ...)    -- "user" is a reserved word
download (song_id, user_id, ...) -- implementing the n:m relationship

您问题中的查询会产生不正确的结果。相同的user_id可以弹出多次。DISTINCT没有做你似乎期望的事情。您需要DISTINCT ON或其他一些方法,如聚合窗口函数

您还需要使用子查询或CTE,因为这不能一步完成。使用DISTINCT时不能同时使用ORDER BY random(),因为排序顺序不能与所规定的顺序不一致DISTINCT。这个查询当然不是微不足道的。

简单案例,前50首歌曲

如果您很高兴只选择前 50 首歌曲(不知道其中有多少重复的 user_id),那么这个“简单”的案例就可以了:

WITH x AS (
    SELECT *
    FROM   songs
    WHERE  is_downloadable
    ORDER  BY downloads_count DESC
    LIMIT  50
    )
    , y AS (
    SELECT DISTINCT ON (user_id) *
    FROM   x
    ORDER  BY user_id, downloads_count DESC -- pick most popular song per user
--  ORDER  BY user_id, random() -- pick random song per user
    )
SELECT *
FROM   y
ORDER  BY random()
LIMIT  8;
  1. 获得最高的 50 首歌曲download_count。用户可以多次出现。
  2. 为每位用户选择 1 首歌曲。随机或最受欢迎的,在你的问题中没有定义。
  3. user_id随机选择现在不同的 8 首歌曲。

您只需要一个索引songs.downloads_count可以快速:

CREATE INDEX songs_downloads_count_idx ON songs (downloads_count DESC);

具有唯一 user_id 的前 50 首歌曲

WITH x AS (
    SELECT DISTINCT ON (user_id) *
    FROM   songs
    WHERE  is_downloadable
    ORDER  BY user_id, downloads_count DESC
    )
    , y AS (
    SELECT *
    FROM   x
    ORDER  BY downloads_count DESC
    LIMIT  50
    )
SELECT *
FROM   y
ORDER  BY random()
LIMIT  8;
  1. download_count获取每位用户最高的歌曲。每个用户只能出现一次,所以它必须是最高的一首歌download_count
  2. 从中选出最高的 50 个downloads_count
  3. 从中随机选择 8 首歌曲。

使用大表,性能会很差,因为您必须先为每个用户找到最佳行,然后才能继续。多列索引会有所帮助,但仍然不会很快:

CREATE INDEX songs_u_dc_idx ON songs (user_id, downloads_count DESC);

一样,更快

如果user_id热门歌曲中的重复s 可以预见的很少,您可以使用一个技巧。从热门下载中挑选出足够多的下载量,这样具有独特user_id性的前 50 名肯定就在其中。在这一步之后,像上面一样继续。对于大表,这会快得多,因为可以从索引顶部快速读取前 n 行:

WITH x AS (
    SELECT *
    FROM   songs
    WHERE  is_downloadable
    ORDER  BY downloads_count DESC
    LIMIT  100 -- adjust to your secure estimate
    )
    , y AS (
    SELECT DISTINCT ON (user_id) *
    FROM   x
    ORDER  BY user_id, downloads_count DESC
    )
    , z AS (
    SELECT *
    FROM   y
    ORDER  BY downloads_count DESC
    LIMIT  50
    )
SELECT *
FROM   z
ORDER  BY random()
LIMIT  8;

上面简单案例的索引足以使其几乎与简单案例一样快。

如果前 100 首“歌曲”中的不同用户少于 50 个,这将达不到要求。

所有查询都应适用于 PostgreSQL 8.4 或更高版本。


如果它必须更快,但是,创建一个包含预选前 50个的物化视图,并定期重写该表或由事件触发。如果你大量使用它并且桌子很大,我会去的。否则不值得开销。

通用的、改进的解决方案

后来,我将这种方法形式化并进一步改进,以适用于 dba.SE的这个相关问题下的一整类类似问题。

于 2012-07-31T00:40:47.857 回答
1

您可以按顺序使用 PostgreSQL 的RANDOM()函数,使其

___.order('songs.downloads_count DESC, RANDOM()').limit(8)

虽然这不起作用,因为 PostgreSQL 需要ORDER BYSELECT. 你会得到一个错误,比如

ActiveRecord::StatementInvalid: PG::Error: ERROR:  for SELECT DISTINCT, ORDER BY expressions must appear in select list

在 SQL (使用 PostgreSQL)中完成你所要求的所有事情的唯一方法是使用子查询,这对你来说可能是也可能不是更好的解决方案。如果是,最好的办法是使用find_by_sql写出完整的查询/子查询。

我很高兴能帮助提出 SQL,尽管现在您知道了RANDOM(),它应该是非常微不足道的。

于 2012-07-31T00:32:29.063 回答