如何使用唯一的user_id从前50个中选择8首随机歌曲?

时间:2012-07-30 23:05:15

标签: sql ruby-on-rails ruby-on-rails-3 postgresql

我想获得前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以任何方式做到这一切吗?

2 个答案:

答案 0 :(得分:3)

我想知道列user_id如何在表songs中结束?这意味着歌曲和用户的每个组合都有一行?在标准化模式中,这将是使用三个表实现的 n:m relationship

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或其他一些方法,例如aggregatewindow functions

您还需要使用子查询或CTEs,因为这不能一步完成。当您使用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. 获取最高download_count的50首歌曲。用户可以多次出现。
  2. 每位用户选择1首歌曲。随机或最受欢迎的,在你的问题中没有定义。
  3. 随机挑选8首歌曲user_id
  4. 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. 从中选择最高downloads_count的50。
    3. 随机挑选8首歌曲。
    4. 使用大表时,性能将 suck ,因为您必须先找到每个用户的最佳行,然后才能继续。多列索引会有所帮助,但它仍然不会很快:

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

      相同,更快

      如果重复user_id在热门歌曲可预见的罕见中,您可以使用技巧。选择足够的最高下载量,以便排在前50位的是唯一的user_id。完成此步骤后,按上述步骤操作。对于大表,这将快得多,因为前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;
      

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

      如果不到50个不同的用户属于前100首“歌曲”,那么这将失败。

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


      如果必须更快,那么,创建一个物化视图,其中包含预先选定的前50个,并定期重写该表或由事件触发。如果你大量使用这个,表格很大,我会去那。否则就不值得花费。

      广义的,改进的解决方案

      我后来将此方法进一步形式化和改进,以适用于此related question at dba.SE下的一类类似问题。

答案 1 :(得分:1)

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

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

虽然这不起作用,因为PostgreSQL要求在ORDER BY中找到SELECT中使用的列。你会收到像

这样的错误
ActiveRecord::StatementInvalid: PG::Error: ERROR:  for SELECT DISTINCT, ORDER BY expressions must appear in select list

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

我很乐意帮助提出SQL,虽然现在您知道RANDOM(),但这应该是非常简单的。