我有一张看起来像这样的表:
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个查询,那不是问题。
答案 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
。无论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很好,并且没有维护单独表的开销。
如果在我的情况下,短暂的选择时间至关重要,那么我会推荐两种表格方法。
答案 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)不适用,您只需从总结果集中获取第一条记录。