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?つまり、曲とユーザーの組み合わせごとに 1 つの行があるということですか? 正規化されたスキーマでは、次の3 つのテーブルで実装された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も使用する必要があります。これは 1 つのステップでは実行できないためです。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ユーザーあたりの最高の曲を取得します。すべてのユーザーは 1 回しか表示できないため、download_count.
  2. その中から高いものを50個選びdownloads_countます。
  3. その中からランダムに8曲選んでください。

大きなテーブルでは、続行する前にすべてのユーザーに最適な行を見つける必要があるため、パフォーマンスが低下します。複数列のインデックスは役に立ちますが、それでもあまり高速ではありません。

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

同じ、より速い

user_id上位の曲の重複が予想通りまれである場合は、トリックを使用できます。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)

ORDER BYただし、PostgreSQL では で使用される列が で見つかる必要があるため、これは機能しませんSELECT。次のようなエラーが表示されます

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 に答える