示例输入:
SELECT * FROM test; id | percent ----+---------- 1 | 50 2 | 35 3 | 15 (3 rows)
你会如何写这样的查询,平均50%的时间我可以获得id = 1的行,35%的时间行id = 2,15%的时间行id = 3 ?
我尝试了类似SELECT id FROM test ORDER BY p * random() DESC LIMIT 1
的内容,但却给出了错误的结果。在10,000次运行后,我得到了一个类似于{1=6293, 2=3302, 3=405}
的分布,但我预计分布几乎为:{1=5000, 2=3500, 3=1500}
。
有什么想法吗?
答案 0 :(得分:22)
这应该可以解决问题:
WITH CTE AS (
SELECT random() * (SELECT SUM(percent) FROM YOUR_TABLE) R
)
SELECT *
FROM (
SELECT id, SUM(percent) OVER (ORDER BY id) S, R
FROM YOUR_TABLE CROSS JOIN CTE
) Q
WHERE S >= R
ORDER BY id
LIMIT 1;
子查询Q
给出以下结果:
1 50
2 85
3 100
然后,我们只需在[0,100]范围内生成一个随机数,然后选择该数字处或之外的第一行(WHERE
子句)。我们使用公用表表达式(WITH
)来确保随机数只计算一次。
BTW,SELECT SUM(percent) FROM YOUR_TABLE
允许您在percent
中拥有任何权重 - 它们不一定是百分比(即加起来为100)。 < / p>
答案 1 :(得分:3)
ORDER BY random()^(1.0 / p)
来自Efraimidis和Spirakis描述的算法。
答案 2 :(得分:2)
您提出的查询似乎有效;见this SQLFiddle demo。它创造了错误的分布;见下文。
为了防止PostgreSQL优化子查询,我将其包装在VOLATILE
SQL函数中。 PostgreSQL没有办法知道你打算让子查询为外部查询的每一行运行一次,所以如果你不强制它挥发它只会执行一次。另一种可能性 - 虽然查询计划程序可能在将来优化 - 是使它看起来像一个相关的子查询,就像这个使用always-true where子句的hack一样,如下所示:http://sqlfiddle.com/#!12/3039b/9
猜测(在您更新之前解释为什么它不起作用)您的测试方法有问题,或者您将其用作PostgreSQL注意到的外部查询中的子查询它不是一个相关的子查询,只执行一次,就像在this example中一样。
更新:产生的分配不是您所期望的。这里的问题是你通过采用random()
的多个样本来扭曲分布;你需要一个单个样本。
此查询生成正确的分布(SQLFiddle):
WITH random_weight(rw) AS (SELECT random() * (SELECT sum(percent) FROM test))
SELECT id
FROM (
SELECT
id,
sum(percent) OVER (ORDER BY id),
coalesce(sum(prev_percent) OVER (ORDER BY id),0) FROM (
SELECT
id,
percent,
lag(percent) OVER () AS prev_percent
FROM test
) x
) weighted_ids(id, weight_upper, weight_lower)
CROSS JOIN random_weight
WHERE rw BETWEEN weight_lower AND weight_upper;
不用说,表演太可怕了。它使用两组嵌套的窗口。我正在做的是:
答案 3 :(得分:2)
Branko接受的解决方案很棒(谢谢!)。但是,我想提供一种性能一样(根据我的测试),并且可能更易于可视化的替代方案。
回顾一下。最初的问题可能可以概括如下:
给出ID和相对权重的映射,创建一个查询,该查询返回映射中的随机ID,但概率与其相对权重成正比。
请注意强调相对重量,而不是百分比。正如Branko在回答中所指出的那样,使用相对权重将对包括百分数在内的任何内容都有效。
现在,考虑一些测试数据,我们将它们放在一个临时表中
CREATE TEMP TABLE test AS
SELECT * FROM (VALUES
(1, 25),
(2, 10),
(3, 10),
(4, 05)
) AS test(id, weight);
请注意,我使用的是比原始问题中的示例更复杂的示例,因为它不方便地加起来等于100,并且< em>相同的重量(20)被多次使用(emid 2和3) (重要的是要考虑的,您将在后面看到)。
我们要做的第一件事是将权重从0变为1,这不过是简单的归一化(权重/总和(权重)):
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()
生成一个随机数一百万次:
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
在为单元/集成测试生成测试数据时,我经常使用上面查询的变体。这个想法是生成近似于追踪现实的概率分布的随机数据。
在这种情况下,我发现一次计算开始和结束分布并将结果存储在表中很有用:
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
的窗口函数。处理某些权重可能会重复这一事实是必要的,这就是为什么我首先选择具有重复权重的测试数据的原因!
答案 4 :(得分:1)
以下是您可以使用的内容:
select t1.id as id1
, case when t2.id is null then 0 else t2.id end as id2
, t1.percent as percent1
, case when t2.percent is null then 0 else t2.percent end as percent2
from "Test1" t1
left outer join "Test1" t2 on t1.id = t2.id + 1
where random() * 100 between t1.percent and
case when t2.percent is null then 0 else t2.percent end;
基本上执行左外连接,以便您有两列来应用between子句。
请注意,只有以正确的方式订购您的桌子才能使用它。
答案 5 :(得分:0)
基于布兰科·迪米特里耶维奇(Branko Dimitrijevic)的回答,我编写了此查询,使用分层窗口函数(与percent
不同)使用ROLLUP
的总和可能会或可能不会更快。
WITH random AS (SELECT random() AS random)
SELECT id FROM (
SELECT id, percent,
SUM(percent) OVER (ORDER BY id) AS rank,
SUM(percent) OVER () * random AS roll
FROM test CROSS JOIN random
) t WHERE roll <= rank LIMIT 1
如果排序不重要,则SUM(percent) OVER (ROWS UNBOUNDED PRECEDING) AS rank,
可能更可取,因为它避免了必须先对数据进行排序。
我还尝试了Mechanic Wei的答案(as described in this paper, apparently),它的性能似乎很有希望,但是经过一些测试,the distribution appear to be off:
SELECT id
FROM test
ORDER BY random() ^ (1.0/percent)
LIMIT 1