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

时间:2009-09-09 07:37:05

标签: mysql sql select random

我有一张看起来像这样的表:

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个查询,那不是问题。

7 个答案:

答案 0 :(得分:3)

我尝试过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字段链接到table1_id。无论weighttable1的值是多少,我在table1_weight创建了多少链接记录。

测试

table1中有976条记录的数据集中,总重量为2031,因此table1_weight中有2031条记录,我运行了以下两个SQL:

  1. 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
    
  2. 加入辅助表进行加权

  3. 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很好,并且没有维护单独表的开销。

    如果在我的情况下,短暂的选择时间至关重要,那么我会推荐两种表格方法。

答案 1 :(得分:2)

这适用于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()的位置。

答案 2 :(得分:1)

一种简单的方法(避免连接或子查询)只是将权重乘以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倍,等等。)

更新:此方法实际上并未生成正确的发行版,因此现在不要使用它!(请参阅下面的评论)。我认为仍然应该有一个类似于上面的简单方法,但是现在下面更复杂的方法,包括连接,可能会更好。我将这个答案留下来是因为:(a)在下面的评论中有相关的讨论,(b)如果/当我有机会,我会尝试解决它。​​

答案 3 :(得分:1)

我认为最简单的方法实际上是使用加权储层采样:

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的最小值。

答案 4 :(得分:0)

这似乎很有效,但是我不确定它背后的数学原理。

SELECT RAND() / t.weight AS w, t.* 
FROM table t 
WHERE t.weight > 0
ORDER BY 1
LIMIT 1

我猜测它起作用的原因是,升序查找的结果最小,通过除以权重获得更高的权重,随机结果更紧密地聚集在零附近。

我测试了它(实际上与Postgresql中的算法相同),在3000行中进行了209000次查询,权重表示正确。

我的输入数据:

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输入行中的每行重新评估子选择。

答案 5 :(得分:-1)

也许这一个:

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

答案 6 :(得分:-4)

我不记得如何在mysql中使用RND(),但这里是MSSQL的工作示例:

SELECT TOP(1) (weight +RAND ()) r, id, content, weight FROM Table
ORDER BY 1 DESC

如果TOP(1)不适用,您只需从总结果集中获取第一条记录。