如何在考虑重量的情况下随机选择一行?

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 日期选择器

生成具有概率的随机整数

如何在不从参考节点获取所有数据的情况下获取 Firebase 数据库中的随机键?

如何在不替换条件的情况下随机化?

如何仅在列更改其值的情况下选择行(从第一行到最后一行)?

在给定特征数量的情况下找到随机森林的最大深度