ブランコが受け入れた解決策は素晴らしいです(ありがとう!)。ただし、(私のテストによると)パフォーマンスが同じで、おそらく視覚化が容易な代替案を提供したいと思います。
要約しましょう。元の質問は、おそらく次のように一般化できます。
IDと相対的な重みのマップが与えられた場合、マップ内のランダムなIDを返すクエリを作成しますが、確率はその相対的な重みに比例します。
パーセントではなく、相対的な重みに重点が置かれていることに注意してください。ブランコが彼の答えで指摘しているように、相対的な重みを使用することは、パーセントを含むすべてに有効です。
ここで、一時テーブルに配置するいくつかのテストデータについて考えてみます。
CREATE TEMP TABLE test AS
SELECT * FROM (VALUES
(1, 25),
(2, 10),
(3, 10),
(4, 05)
) AS test(id, weight);
元の質問よりも複雑な例を使用していることに注意してください。これは、合計が100になると便利ではなく、同じ重み(20)が複数回使用されている(ID 2および3の場合)という点です。後で説明するように、これを考慮することが重要です。
最初に行う必要があるのは、重みを0から1までの確率に変換することです。これは、単純な正規化(weight / sum(weights))にすぎません。
WITH p AS ( -- probability
SELECT *,
weight::NUMERIC / sum(weight) OVER () AS probability
FROM test
),
cp AS ( -- cumulative probability
SELECT *,
sum(p.probability) OVER (
ORDER BY probability DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumprobability
FROM p
)
SELECT
cp.id,
cp.weight,
cp.probability,
cp.cumprobability - cp.probability AS startprobability,
cp.cumprobability AS endprobability
FROM cp
;
これにより、次の出力が得られます。
id | weight | probability | startprobability | endprobability
----+--------+-------------+------------------+----------------
1 | 25 | 0.5 | 0.0 | 0.5
2 | 10 | 0.2 | 0.5 | 0.7
3 | 10 | 0.2 | 0.7 | 0.9
4 | 5 | 0.1 | 0.9 | 1.0
上記のクエリは確かに私たちのニーズに厳密に必要な以上の作業を行っていますが、この方法で相対確率を視覚化することは有用であり、idを選択する最後のステップは簡単です。
SELECT id FROM (queryabove)
WHERE random() BETWEEN startprobability AND endprobability;
それでは、クエリが期待される分布のデータを返していることを確認するテストと一緒にすべてをまとめましょう。を使用generate_series()
して、乱数を100万回生成します。
WITH p AS ( -- probability
SELECT *,
weight::NUMERIC / sum(weight) OVER () AS probability
FROM test
),
cp AS ( -- cumulative probability
SELECT *,
sum(p.probability) OVER (
ORDER BY probability DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumprobability
FROM p
),
fp AS ( -- final probability
SELECT
cp.id,
cp.weight,
cp.probability,
cp.cumprobability - cp.probability AS startprobability,
cp.cumprobability AS endprobability
FROM cp
)
SELECT *
FROM fp
CROSS JOIN (SELECT random() FROM generate_series(1, 1000000)) AS random(val)
WHERE random.val BETWEEN fp.startprobability AND fp.endprobability
;
これにより、次のような出力が得られます。
id | count
----+--------
1 | 499679
3 | 200652
2 | 199334
4 | 100335
ご覧のとおり、これは予想される分布を完全に追跡します。
パフォーマンス
上記のクエリは非常にパフォーマンスが高いです。私の平均的なマシンでも、PostgreSQLがWSL1インスタンスで実行されている場合(ホラー!)、実行は比較的高速です。
count | time (ms)
-----------+----------
1,000 | 7
10,000 | 25
100,000 | 210
1,000,000 | 1950
テストデータを生成するための適応
ユニット/統合テストのテストデータを生成するときに、上記のクエリのバリエーションをよく使用します。アイデアは、現実を追跡する確率分布を近似するランダムデータを生成することです。
そのような状況では、開始分布と終了分布を1回計算し、その結果をテーブルに保存すると便利です。
CREATE TEMP TABLE test AS
WITH test(id, weight) AS (VALUES
(1, 25),
(2, 10),
(3, 10),
(4, 05)
),
p AS ( -- probability
SELECT *, (weight::NUMERIC / sum(weight) OVER ()) AS probability
FROM test
),
cp AS ( -- cumulative probability
SELECT *,
sum(p.probability) OVER (
ORDER BY probability DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) cumprobability
FROM p
)
SELECT
cp.id,
cp.weight,
cp.probability,
cp.cumprobability - cp.probability AS startprobability,
cp.cumprobability AS endprobability
FROM cp
;
次に、これらの事前計算された確率を繰り返し使用できるため、パフォーマンスが向上し、使用が簡単になります。
ランダムなIDを取得したいときにいつでも呼び出すことができる関数ですべてをラップすることもできます。
CREATE OR REPLACE FUNCTION getrandomid(p_random FLOAT8 = random())
RETURNS INT AS
$$
SELECT id
FROM test
WHERE p_random BETWEEN startprobability AND endprobability
;
$$
LANGUAGE SQL STABLE STRICT
ウィンドウ関数フレーム
上記の手法は、非標準のフレームでウィンドウ関数を使用していることに注意してくださいROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
。これは、一部の重みが繰り返される可能性があるという事実に対処するために必要です。そのため、最初に重みが繰り返されるテストデータを選択しました。