web-dev-qa-db-ja.com

ランダムな行を選択するための最良の方法

私はPostgreSQLでランダムに行を選択したいのですが、私はこれを試しました:

select * from table where random() < 0.01;

しかし、他にもこれをお勧めします:

select * from table order by random() limit 1000;

5億行の非常に大きなテーブルがあるので、高速にしたいです。

どちらのアプローチが良いですか?違いは何ですか?ランダムな行を選択するための最良の方法は何ですか?

282
nanounanue

あなたの仕様(コメントの中の追加情報)を考えて、

  • あなたはほんの少し(または適度に少数)のギャップを持つ数値ID列(整数)を持っています。
  • 明らかに、書き込み操作はまったく、またはほとんどありません。
  • あなたのID列はインデックスされなければなりません!主キーはうまく機能します。

以下のクエリは大きなテーブルのシーケンシャルスキャンを必要とせず、インデックススキャンのみを必要とします。

まず、メインクエリの見積もりを取得します。

SELECT count(*) AS ct              -- optional
     , min(id)  AS min_id
     , max(id)  AS max_id
     , max(id) - min(id) AS id_span
FROM   big;

たぶん高価な部分はcount(*)(巨大なテーブル用)です。上記の仕様を考えると、あなたはそれを必要としません。見積もりは、ほぼ無料で利用可能な、まあまあです(ここで 詳細な説明 ):

SELECT reltuples AS ct FROM pg_class WHERE oid = 'schema_name.big'::regclass;

ctid_spanより小さくない限りない限り、クエリは他のアプローチよりも優れています。

WITH params AS (
    SELECT 1       AS min_id           -- minimum id <= current min id
         , 5100000 AS id_span          -- rounded up. (max_id - min_id + buffer)
    )
SELECT *
FROM  (
    SELECT p.min_id + trunc(random() * p.id_span)::integer AS id
    FROM   params p
          ,generate_series(1, 1100) g  -- 1000 + buffer
    GROUP  BY 1                        -- trim duplicates
    ) r
JOIN   big USING (id)
LIMIT  1000;                           -- trim surplus
  • idスペースに乱数を生成します。 「わずかなギャップ」があるため、取得する行数に10%(空白を簡単にカバーするのに十分)を追加します。

  • それぞれのidは偶然に複数回選択される可能性がありますが(大きなIDスペースがある場合はあまりありませんが)、生成された番号をグループ化する(またはDISTINCTを使用する).

  • 大きなテーブルにidsを結合します。これはインデックスが設定されていれば非常に速いはずです。

  • 最後に、重複やギャップによって食べられていない余分なidsを切り捨てます。すべての行に完全に等しい機会が選択されます。

短縮版

このクエリは簡素化できます。上記のクエリのCTEは教育目的のためだけのものです。

SELECT *
FROM  (
    SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
    FROM   generate_series(1, 1100) g
    ) r
JOIN   big USING (id)
LIMIT  1000;

RCTEで絞り込む

特に、ギャップや見積もりについてあまりよくわからない場合は。

WITH RECURSIVE random_pick AS (
   SELECT *
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   generate_series(1, 1030)  -- 1000 + few percent - adapt to your needs
      LIMIT  1030                      -- hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss

   UNION                               -- eliminate dupe
   SELECT b.*
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   random_pick r             -- plus 3 percent - adapt to your needs
      LIMIT  999                       -- less than 1000, hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss
   )
SELECT *
FROM   random_pick
LIMIT  1000;  -- actual limit

基本クエリでは、より小さい余剰を使用できます。ギャップが多すぎて最初の反復で十分な行が見つからない場合、rCTEは再帰項で反復し続けます。 IDスペースにはまだ少数のギャップが必要です。そうでないと、制限に達する前に再帰が空になる可能性があります。

重複は、rCTEのUNIONによって排除されます。

外側のLIMITは、十分な行があるとすぐにCTEを停止させます。

このクエリは、利用可能なインデックスを使用し、実際にランダムな行を生成し、制限を満たすまで停止しないように注意深く作成されています(再帰が枯渇しない限り)。あなたがそれを書き直すつもりならここに落とし穴がいくつかあります。

関数にラップ

さまざまなパラメータで繰り返し使用する場合

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big AS
$func$
DECLARE
   _surplus  int := _limit * _gaps;
   _estimate int := (           -- get current estimate from system
      SELECT c.reltuples * _gaps
      FROM   pg_class c
      WHERE  c.oid = 'big'::regclass);
BEGIN

   RETURN QUERY
   WITH RECURSIVE random_pick AS (
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   generate_series(1, _surplus) g
         LIMIT  _surplus           -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses

      UNION                        -- eliminate dupes
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   random_pick        -- just to make it recursive
         LIMIT  _limit             -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses
   )
   SELECT *
   FROM   random_pick
   LIMIT  _limit;
END
$func$  LANGUAGE plpgsql VOLATILE ROWS 1000;

コール:

SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);

PK列とテーブルの名前をポリモーフィック型にしてEXECUTEを使用することもできます。しかし、それはこの質問の範囲を超えています。見る:

可能な代替手段

あなたの要求が同一セットの繰り返し呼び出しを許可するならば(そして私達は繰り返し呼び出しについて話しています)私は実体化ビューを考えるでしょう。上記のクエリを1回実行し、結果をテーブルに書き込みます。ユーザーは、軽い速度で疑似ランダム選択を得ます。選択した間隔またはイベントでランダムな選択を更新します。

Postgres 9.5では TABLESAMPLE SYSTEM (n) が導入されました

ここで、nはパーセントです。 マニュアル:

BERNOULLIおよびSYSTEMのサンプリング方法はそれぞれ、サンプリングするテーブルの割合である1つの引数を受け入れます。これは、0から100までのパーセントで表されます。この引数は、任意のreal値の式にすることができます。

私の大胆な強調それは非常に速いですが、結果は正確にランダムではありません。もう一度マニュアル:

小さなサンプリングパーセンテージが指定されている場合、SYSTEMメソッドはBERNOULLIメソッドよりもかなり高速ですが、クラスタリング効果の結果として、テーブルのサンプルがランダムではない場合があります。

返される行数は大きく変わる可能性があります。この例では、およそ1000行を取得します。

SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);

関連する

または追加モジュールをインストールして tsm_system_rows 要求された行数を正確に取得する(十分な数がある場合)そしてより便利な構文を見込んでください。

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

詳細は Evanの答え をご覧ください。

しかし、それはまだ正確にはランダムではありません。

191

を使用して、両方の実行計画を調べて比較できます。

EXPLAIN select * from table where random() < 0.01;
EXPLAIN select * from table order by random() limit 1000;

大きなテーブルでの簡単なテスト1 ORDER BYは最初にテーブル全体をソートしてから最初の1000項目を選択することを示しています。大きなテーブルをソートすると、そのテーブルが読み取られるだけでなく、一時ファイルの読み取りと書き込みも行われます。 where random() < 0.1はテーブル全体を一度だけスキャンします。

大きなテーブルでは、1回のテーブル全体のスキャンでも時間がかかるため、これは望んでいないことかもしれません。

3番目の提案は

select * from table where random() < 0.01 limit 1000;

これは1000行が見つかるとすぐにテーブルスキャンを停止しますので、早く戻ります。もちろん、これはランダム性を少し落としますが、おそらくあなたの場合はこれで十分です。

編集:この考慮事項以外にも、これに関してすでに寄せられている質問を確認してください。クエリ[postgresql] randomを使用すると、かなりの数のヒットが返されます。

そしてその他のいくつかのアプローチを概説したdependentzのリンク記事。


1 「完全なテーブルはメモリに収まりません」のように「大きい」。

87
A.H.

random()によるpostgresqlの順序は、ランダムな順序で行を選択します。

select your_columns from your_table ORDER BY random()

以下のように、random()によるpostgresqlの順序

select * from 
  (select distinct your_columns from your_table) table_alias
ORDER BY random()

ランダム制限によるpostgresqlの順序1行

select your_columns from your_table ORDER BY random() limit 1
68
Eric Leschinski

PostgreSQL 9.5以降では、テーブルからランダムな要素を取得するための新しい構文があります。

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

この例はmytableからの5%の要素をあなたに与えるでしょう。

このブログ記事の詳細な説明を参照してください。 http://www.postgresql.org/docs/current/static/sql-select.html

34

ORDER BYのあるものは遅いものになるでしょう。

select * from table where random() < 0.01;はレコードごとに進み、ランダムにフィルタリングするかどうかを決定します。これはO(N)になるでしょう。なぜならそれは各レコードを一度だけチェックする必要があるだけだからです。

select * from table order by random() limit 1000;はテーブル全体をソートしてから最初の1000を選びます。舞台裏でのブードゥー教の魔法のほかに、byO(N * log N)による順序がそうです。

random() < 0.01のマイナス面は、可変数の出力レコードが得られることです。


ランダムにソートするよりも、データのセットをシャッフルする方がよいことに注意してください。 O(N)で実行されるFisher-Yates Shuffle ただし、SQLでシャッフルを実装することは非常に難しいように思えます。

25
Donald Miner

これが私のために働く決定です。理解して実行するのはとても簡単だと思います。

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;
13
Bogdan Surai
select * from table order by random() limit 1000;

必要な行数がわかっている場合は、 tsm_system_rows をチェックしてください。

tsm_system_rows

moduleは、テーブルサンプリングメソッドSYSTEM_ROWSを提供します。これは、SELECTコマンドのTABLESAMPLE句で使用できます。

このテーブルサンプリング方法は、読み込む行の最大数である単一の整数引数を受け入れます。結果として得られるサンプルには、テーブルに十分な行が含まれていない場合を除き、常にその数の行が含まれます。その場合、テーブル全体が選択されます。 組み込みのSYSTEMサンプリング方法と同様に、SYSTEM_ROWSはブロックレベルのサンプリングを実行するため、サンプルは完全にランダムではありませんが、特に少数の行しか要求されない場合、クラスタリング効果を受ける可能性があります。

まず拡張機能をインストールする

CREATE EXTENSION tsm_system_rows;

それからあなたの質問、

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);
9
Evan Carroll

1行だけが必要な場合は、offsetから派生した計算済みのcountを使用できます。

select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));
6
Nelo Mitranim

マテリアライズド・ビュー "Alternative alternative"の変形例 Erwin Brandstetterによって概説された

たとえば、返されるランダム化された値の中で重複したくないとします。ですから、(ランダム化されていない)一連の値を含む主テーブルにブール値を設定する必要があります。

これが入力テーブルであると仮定します。

id_values  id  |   used
           ----+--------
           1   |   FALSE
           2   |   FALSE
           3   |   FALSE
           4   |   FALSE
           5   |   FALSE
           ...

必要に応じてID_VALUESテーブルを作成します。次に、Erwinの説明に従って、ID_VALUESテーブルを1回ランダム化するマテリアライズドビューを作成します。

CREATE MATERIALIZED VIEW id_values_randomized AS
  SELECT id
  FROM id_values
  ORDER BY random();

マテリアライズドビューにはusedカラムが含まれていないことに注意してください。これはすぐに古くなるためです。 id_valuesテーブルにある可能性のある他の列をビューに含める必要もありません。

ランダムな値を取得(および「消費」)するには、id_valuesでUPDATE-RETURNINGを使用し、結合でid_valuesからid_values_randomizedを選択し、必要な基準を適用して関連の可能性のみを取得します。例えば:

UPDATE id_values
SET used = TRUE
WHERE id_values.id IN 
  (SELECT i.id
    FROM id_values_randomized r INNER JOIN id_values i ON i.id = r.id
    WHERE (NOT i.used)
    LIMIT 5)
RETURNING id;

必要に応じてLIMITを変更します - 一度に1つのランダムな値しか必要としない場合は、LIMIT1に変更します。

id_valuesに適切なインデックスがあれば、UPDATE-RETURNINGは非常に短時間で実行されるはずです。 1回のデータベースラウンドトリップでランダム化された値を返します。 「適格」行の基準は、必要に応じて複雑になる可能性があります。新しい行はいつでもid_valuesテーブルに追加でき、マテリアライズドビューが更新されるとすぐにアプリケーションからアクセスできるようになります(ピーク時以外に実行される可能性があります)。マテリアライズドビューの作成と更新は遅くなりますが、新しいIDがid_valuesテーブルに追加されたときにのみ実行する必要があります。

2
Raman

私はパーティーに少し遅れていることを知っています、しかし、私はちょうど pg_sample と呼ばれるこの素晴らしいツールを見つけました:

pg_sample - 参照の整合性を維持しながら、より大きなPostgreSQLデータベースから小さなサンプルデータセットを抽出します。

私はこれを350Mの行データベースで試してみましたが、本当に速かったので、ランダムネスについては知りません。

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
0
Daniel Gerber

タイプがrserialという列を追加します。インデックスr

200,000行あると仮定し、乱数nを生成します。ここで、0 <n <= 200、000です。

r > nで行を選択し、それらをASCでソートし、最も小さいものを選択します。

コード:

select * from YOUR_TABLE 
where r > (
    select (
        select reltuples::bigint AS estimate
        from   pg_class
        where  oid = 'public.YOUR_TABLE'::regclass) * random()
    )
order by r asc limit(1);

コードは一目瞭然です。真ん中の副照会は、 https://stackoverflow.com/a/7945274/1271094 から表の行数を素早く見積もるために使用されます。

n>行数、または複数の行を選択する必要がある場合は、アプリケーションレベルでステートメントをもう一度実行する必要があります。

0
MK Yung

私の経験からの一つの教訓:

offset floor(random() * N) limit 1order by random() limit 1より高速ではありません。

offsetアプローチは、Postgresでのソートの時間を節約するため、より高速になると思いました。そうではなかったことが判明しました。

0
user10375