我想获得前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以任何方式做到这一切吗?
答案 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
或其他一些方法,例如aggregate或window functions。
您还需要使用子查询或CTEs,因为这不能一步完成。当您使用DISTINCT
时,您无法同时ORDER BY random()
,因为排序顺序不能与DISTINCT
指示的顺序不一致。这个查询当然不是一件容易的事。
如果您乐意选择前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;
download_count
的50首歌曲。用户可以多次出现。user_id
。 songs.downloads_count
只需索引就可以加快速度:
CREATE INDEX songs_downloads_count_idx ON songs (downloads_count DESC);
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;
download_count
最高的歌曲。每个用户只能出现一次,因此必须是最高download_count
的一首歌。downloads_count
的50。使用大表时,性能将 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()
,但这应该是非常简单的。