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,并且相同的权重(20)被使用了
多次(对于id 2和3),这很重要,稍后你会看到。
我们需要做的第一件事是将权重转换为0到1之间的概率,这只是一个简单的归一化过程(weight / sum(weights))。
WITH p AS (
SELECT *,
weight::NUMERIC / sum(weight) OVER () AS probability
FROM test
),
cp AS (
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 (
SELECT *,
weight::NUMERIC / sum(weight) OVER () AS probability
FROM test
),
cp AS (
SELECT *,
sum(p.probability) OVER (
ORDER BY probability DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumprobability
FROM p
),
fp AS (
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
如您所见,这完美地跟踪了预期的分布。
性能
上述查询性能相当高。即使在我的平均机器上,使用运行在WSL1实例中的PostgreSQL(可怕!),执行速度也相对较快:
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 (
SELECT *, (weight::NUMERIC / sum(weight) OVER ()) AS probability
FROM test
),
cp AS (
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
的窗口函数。这是必要的,以处理一些权重可能重复的情况,这也是我选择测试数据中包含重复权重的原因!