如何在考虑重量的情况下随机选择一行?
Posted
技术标签:
【中文标题】如何在考虑重量的情况下随机选择一行?【英文标题】:How to select one row randomly taking into account a weight? 【发布时间】:2010-11-26 18:06:24 【问题描述】:我有一张看起来像这样的桌子:
id: primary key
content: varchar
weight: int
我想做的是从这张表中随机选择一行,但要考虑到重量。例如,如果我有 3 行:
id, content, weight
1, "some content", 60
2, "other content", 40
3, "something", 100
第一行有 30% 的几率被选中,第二行有 20% 的几率被选中,第三行有 50% 的几率被选中。
有没有办法做到这一点?如果我必须执行 2 或 3 个查询,这不是问题。
【问题讨论】:
看到这个问题:***.com/questions/58457/… 【参考方案1】:我不记得如何在 mysql 中使用 RND(),但这里是 MSSQL 的工作示例:
SELECT TOP(1) (weight +RAND ()) r, id, content, weight FROM Table
ORDER BY 1 DESC
如果 TOP(1) 不适用,您只需从总结果集中获取第一条记录。
【讨论】:
这种方式随机超过任何重量;-) 嗯.. 现在随机性只对权重最高的行起作用。 ;-) 现在您将 0 到 1 随机添加到 1000 的权重。这并没有真正的帮助。在这种简单程度下,您可以得到的最接近的方法是,如果您将权重乘以随机数。但是它是否真的符合规格需要更多的考虑,我现在生病了不能给它;-) 好吧,重量是 100,抱歉,这并不重要 ;-)SELECT * FROM table ORDER BY weight*random() DESC LIMIT 1
看起来更好、更短并且传输的数据更少;-)【参考方案2】:
也许这个:
SELECT * FROM <Table> T JOIN (SELECT FLOOR(MAX(ID)*RAND()) AS ID FROM <Table> ) AS x ON T.ID >= x.ID LIMIT 1;
或者这个:
SELECT * FROM tablename
WHERE somefield='something'
ORDER BY RAND() LIMIT 1
【讨论】:
您忽略了权重,- 权重较高的记录应该更频繁地出现在结果中。【参考方案3】:这在 MSSQL 中有效,我确信应该可以更改几个关键字以使其在 MySQL 中也有效(甚至更好):
SELECT TOP 1 t.*
FROM @Table t
INNER JOIN (SELECT t.id, sum(tt.weight) AS cum_weight
FROM @Table t
INNER JOIN @Table tt ON tt.id <= t.id
GROUP BY t.id) tc
ON tc.id = t.id,
(SELECT SUM(weight) AS total_weight FROM @Table) tt,
(SELECT RAND() AS rnd) r
WHERE r.rnd * tt.total_weight <= tc.cum_weight
ORDER BY t.id ASC
想法是对每一行(subselect-1)有一个累积权重,然后在这个累积范围内找到跨越的RAND()的位置。
【讨论】:
【参考方案4】:一种简单的方法(避免连接或子查询)是将权重乘以 0 到 1 之间的随机数来生成临时权重以进行排序:
SELECT t.*, RAND() * t.weight AS w
FROM table t
ORDER BY w DESC
LIMIT 1
要理解这一点,请考虑RAND() * 2x
将在大约三分之二的时间里大于RAND() * x
的值。因此,随着时间的推移,每行的选择频率应与其相对权重成正比(例如,选择权重为 100 的行的频率将是选择权重为 1 的行的大约 100 倍,等等)。
更新:这种方法实际上并不能产生正确的分布,所以现在不要使用它!(参见下面的 cmets)。我认为仍然应该有一个类似于上面的简单方法可以工作,但现在下面更复杂的方法,涉及连接,可能会更好。我留下这个答案是因为:(a)下面的 cmets 中有相关讨论,以及(b)如果/当我有机会时,我会尝试修复它。
【讨论】:
当您从较少的行数中选择时(最好的 2 个),它工作得很好。我需要从 50 行中随机选择。 1 的权重为 32,1 的权重为 3,48 的权重为 1,总权重为 83。所以我的 32 行应该有 38.6% 的机会被选中,但是使用这种方法,它有 32 个更多的机会选择所有重量为 1 的人。有没有办法将总重量考虑在内?谢谢!! 这不适用于您的情况吗?在您的情况下,选择权重为 32 的行的机会应该是 32/83(0.386,或 38.6%)。选择权重为 1 的行的机会应该是 1/83(0.012,或 1.2%)。但是由于 32/83 = 32 * 1/83,重量为 32 的东西的选择频率仍然是重量为 1 的东西的 32 倍! 我可能在我的脚本中犯了一个错误,但我有 30 次以上的行,权重为 32 和其他人偶尔一次。它被选中的频率是其他所有的 32 倍。我结束了创建一个带有总重量的临时表,使用它来获得重量百分比(SELECT id FROM near50, total_weight ORDER BY Random()*(1/(WEIGHT*100/total_weight.weight)) LIMIT 1)。 我明白你在说什么。当然,它应该有 32 倍的被选中的机会,而任何其他的权重为 1。我的意思是,在我的剧本中,它被选中的次数增加了 32 倍,并且所有其他人都团结起来。在 1000 次测试中,我的体重是 32 的 960 倍,其余的是 40。我应该选择它大约 386 次。我的评论是基于我的观察。 很确定这不会给你预期的分布。考虑 3 行,权重分别为 80、10 和 10。我们预计第一行将在 80% 的时间内被选中,而其他行的概率相同,剩下的 20% 的时间。如果 rand()*80 > 10,那么我们必须选择第一行。如果 rand()*80 在 [0, 80] 之间平均分布,则超过 10 的几率为 69/81,即 85%。它将被过度代表。即使我在这里犯了一些错误。【参考方案5】:我尝试过 van 的解决方案,虽然可行,但速度并不快。
我的解决方案
我解决此问题的方法是为权重维护一个单独的链接表。基本的表结构是这样的:
CREATE TABLE `table1` (
`id` int(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`name` varchar(100),
`weight` tinyint(4) NOT NULL DEFAULT '1',
);
CREATE TABLE `table1_weight` (
`id` bigint(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`table1_id` int(11) NOT NULL
);
如果我在 table1
中有一个权重为 3 的记录,那么我在 table1_weight
中创建 3 条记录,通过 table1_id
字段链接到 table1
。无论weight
的值在table1
中是多少,这就是我在table1_weight
中创建的链接记录数。
测试
在table1
中有 976 条记录的数据集上,总权重为 2031,因此table1_weight
中有 2031 条记录,我运行了以下两个 SQL:
van 的解决方案的一个版本
SELECT t.*
FROM table1 t
INNER JOIN
( SELECT t.id,
SUM(tt.weight) AS cum_weight
FROM table1 t
INNER JOIN table1 tt ON tt.id <= t.id
GROUP BY t.id) tc ON tc.id = t.id,
( SELECT SUM(weight) AS total_weight
FROM table1) tt,
( SELECT RAND() AS rnd) r
WHERE r.rnd * tt.total_weight <= tc.cum_weight
ORDER BY t.id ASC
LIMIT 1
加入辅助表进行加权
SELECT t.*
FROM table1 t
INNER JOIN table1_weight w
ON w.table1_id = t.id
ORDER BY RAND()
LIMIT 1
SQL 1 始终需要 0.4 秒。
SQL 2 需要 0.01 到 0.02 秒。
结论
如果选择随机加权记录的速度不是问题,那么 van 建议的单表 SQL 就可以了,并且没有维护单独表的开销。
如果在我的情况下,选择时间很短,那么我会推荐两表方法。
【讨论】:
主要缺点是大桌子的桌子大小:) 或者对于高权重...并且不支持分数权重。【参考方案6】:这个似乎可行,但我不确定它背后的数学原理。
SELECT RAND() / t.weight AS w, t.*
FROM table t
WHERE t.weight > 0
ORDER BY 1
LIMIT 1
我对它起作用的原因的猜测是升序查找最小的结果,并且通过除以权重以获得更高的权重,随机结果更紧密地聚集在零附近。
我用超过 3000 行的 209000 个查询对其进行了测试(实际上与 postgresql 中的算法相同),并且权重表示正确。
我的输入数据:
select count(*),weight from t group by weight
count | weight
-------+--------
1000 | 99
1000 | 10
1000 | 100
(3 rows)
我的结果:
jasen=# with g as ( select generate_series(1,209000) as i )
,r as (select ( select t.weight as w
FROM t
WHERE t.weight > 0
ORDER BY ( random() / t.weight ) + (g.i*0) LIMIT 1 ) from g)
select r.w, count(*), r.w*1000 as expect from r group by r.w;
w | count | expect
-----+-------+--------
99 | 98978 | 99000
10 | 10070 | 10000
100 | 99952 | 100000
(3 rows)
+(g.i*0)
对算术结果没有影响,但需要外部引用来强制规划器重新评估在 g
中生成的每个 209K 输入行的子选择
【讨论】:
【参考方案7】:我认为最简单的其实是使用加权水库采样:
SELECT
id,
-LOG(RAND()) / weight AS priority
FROM
your_table
ORDER BY priority
LIMIT 1;
这是一种很棒的方法,可以让您从 N 个元素中选择 M 个,其中每个元素被选择的概率与其权重成正比。当您碰巧只需要一个元素时,它也同样有效。 该方法在this article 中有描述。注意他们选择POW(RAND(), 1/weight)的最大值,相当于选择-LOG(RAND())/weight的最小值。
【讨论】:
这是一个绝妙的答案!谢谢!只需加上我的两分钱:写 log(1-rand()) 来避免 log(0) 会不会更优雅,因为随机值可能在 [0,1[ (但未检查)中? 这看起来是个不错的方法,但分布可能非常不平衡。我尝试了几行的权重,其中所有权重都是 67 或 33(即大约 2/3 或 1/3),在我的例子中,所有选择的行都具有更高的权重。不知道为什么。以上是关于如何在考虑重量的情况下随机选择一行?的主要内容,如果未能解决你的问题,请参考以下文章
如何在不改变位置的情况下调整同一行中的两个 React 日期选择器