web-dev-qa-db-ja.com

2つのテーブル(a、b)から(a.id、b.id)ペアのランダムサンプルを(最適に)取得する方法は?

非常に単純なテーブルが必要だとしましょう

CREATE TABLE a(id integer PRIMARY KEY, 
       t timestamp default now(), 
       sensor_readings real[]);
CREATE TABLE b(id integer PRIMARY KEY, 
       t timestamp default now(), 
       sensor_readings real[]);

それらのいくつかのデータで

INSERT INTO a(id) SELECT generate_series(    1,   100);
INSERT INTO b(id) SELECT generate_series(10001, 10100);

実際には、テーブルaには約100_000行、テーブルbには約50_000の行があります。また、practiveでは、idシーケンスにギャップ(数%のオーダー)がある場合があります。したがって、デカルト積a x bのカーディナリティは数十億です。

1_000の並べ替えられたペア(a.id、b.id)のランダムなサンプルを取得します。次のようなクエリを使用できます。

SELECT  
    *
FROM
(
    SELECT
        *
    FROM
        (
        SELECT 
            a.id AS a_id, b.id AS b_id
        FROM
            a CROSS JOIN b
        ORDER BY
            random()
        ) AS s0
    LIMIT
        1000 
) AS s1
ORDER BY
    a_id, b_id ;

...しかし、(CROSS JOINの増加により)aまたはbの行数が増加するとすぐに、非常に非効率になります。

これと同等のことを最適な方法で行う方法はありますか?つまり、実際にインスタンス化する必要なく、a x bリレーションから行のランダムなサンプルを取得する実用的な方法があります。

注:a.idまたはb.idを繰り返すことができるという事実に制限はありません。ペア(a.id、b.id)はできませんが。

これを命令型言語でプログラミングしようとした場合、おそらくループを使用して、次の疑似コードのようなことを行います(そして、統計学者にチェックしてもらい、実際にサンプルを取得するようにします)すべてのペアが選択される確率は同じです):

start with a result set equal to {} (empty set)
while size of result set < 1000
    Pick the id value from a random row from table a -> Rand_id_a
    Pick the id value from a random row from table b -> Rand_id_b
    If (Rand_id_a, Rand_id_b) not in result set
        append (Rand_id_a, Rand_id_b) to result set
    end if
end while
sort result set and return it

ループに頼らずに同等の結果を達成する方法はありますか?そうでない場合、plpgSQLを使用してそれを行う効率的な方法はありますか? (またはその他の言語)

3
joanolo

最良の解決策は、セットアップのexact定義に依存します。設定例ではそれは取るに足らないことです:

  • ギャップのないシリアル整数列。
_SELECT DISTINCT
           1 + trunc(random() * 100)::int AS a_id
     , 10001 + trunc(random() * 100)::int AS b_id
FROM   generate_series(1, 1100) g  -- enough excess to make up for possible dupes
LIMIT  1000;  -- only take 1000
_

唯一の興味深い質問:デュープを効率的に折りたたむ方法。解決策:Postgresに決定を任せます。単にDISTINCTを使用します。
テーブルを関与させる必要すらありません。超早い。

random()は( ドキュメントごと )を生成することに注意してください:

0.0 <= x <1.0の範囲のランダムな値

したがって、1 + trunc(random() * 100)::intは、1100の間のID番号を正確にカバーします。

実際のセットアップ?

実際の設定についてより具体的にする必要があります。 ID列だけでなく、各テーブルに少なくともpayload列があると仮定します。

_CREATE TABLE a(a_id integer PRIMARY KEY, a text);
CREATE TABLE b(b_id integer PRIMARY KEY, b text);

INSERT INTO a(a_id, a)
SELECT g, 't' || g FROM generate_series(    1,   100) g;
INSERT INTO b(b_id, b)
SELECT g, 't' || g FROM generate_series(10001, 10100) g;
_

クエリ:

_SELECT a.a_id, a.a, b.b_id, b.b
FROM  (
    SELECT DISTINCT
               1 + trunc(random() * 100)::int AS a_id  -- cover *whole* key space
         , 10001 + trunc(random() * 100)::int AS b_id  -- maybe add reserve for new rows
    FROM   generate_series(1, 1100) g
    LIMIT  1000
    ) ra
JOIN   a USING (a_id)
JOIN   b USING (b_id);
_

完全にランダムで、非常に高速で、実際のテーブルサイズにほとんど依存しません。

必要なのは、a(a_id)およびb(b_id)のインデックスです。あるいは、インデックスのみのスキャンを許可するための複数列インデックス。


ソリューションは、スキップされたnextval()呼び出しからのfew gapsに対しても機能します。 よりもギャップが多くない限り、ギャップによる損失をカバーするのに十分な組み合わせを生成することは依然として非常に安価です。 (大きなテーブルのデカルト積で作業したり、いずれにせよORDER BY random()で大きなテーブル全体をソートしたりするよりもはるかに安い。)十分な組み合わせを生成するようにしてください。

_SELECT a.a_id, a.a, b.b_id, b.b
FROM  (
    SELECT DISTINCT
               1 + trunc(random() * 100)::int AS a_id
         , 10001 + trunc(random() * 100)::int AS b_id
    FROM   generate_series(1, 1100) g  -- enough to cover dupes *and* gaps
    ) ra
JOIN   a USING (a_id)
JOIN   b USING (b_id)
LIMIT  1000;  -- LIMIT moves to outer query to cover gaps
_

いくつかのギャップよりも多くの場合、95%の時間で十分な数の組み合わせから始め、必要に応じて行を追加するための再帰的なステップを追加します及ばない。関連する回答に、このソリューションのレシピ(単一テーブル用)があります。また、より多くの説明とバリエーション:

4

1000ソートされたペアのランダムサンプル(a.id、b.id)を取得します。

randomの意味に常に依存しますが、必要な行数を定義している場合は、拡張機能 _tsm_system_rows_ が必要になる可能性があります。

tsm_system_rows

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

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

最初に拡張機能をインストールします

_CREATE EXTENSION tsm_system_rows;
_

次に、クエリ

_SELECT *
FROM a
CROSS JOIN b
TABLESAMPLE SYSTEM_ROWS(1000);
_

ここで重要なのは、それが常に1000 ROWSを提供するであるということです。これは、random() <= 0.10または_TABLESAMPLE BERNOULLI_の場合は言うことができません。

それが良くない場合」

本当にランダムが必要で、クラスタリングの欠点を受け入れることができない場合、私は使用します

_ORDER BY random()
LIMIT x;
_

重複を取り除く必要がある場合

重複を削除し(_a.id_および_b.id_がUNIQUEでない場合)、結果セットをランダムに保つ唯一の健全な方法は、事前に行うことです。 。TABLESAMPLEは仮想テーブルではまだ機能しないため、厄介になる可能性があります。そのため、一時テーブルを作成する必要があります(まだメモリに保持される場合があります)。その恥ずかしがり屋ですが、遅くて醜い他の方法を使うことができますが、少なくともそれを書く必要はありません

_SELECT *
FROM (
  SELECT DISTINCT ON(a.id, b.id) a.id, b.id
  FROM a
  CROSS JOIN b
) AS t
ORDER BY random()
FETCH FIRST 1000 ROWS ONLY;
_
0
Evan Carroll