【问题标题】:Random Weighted Choice in T-SQLT-SQL 中的随机加权选择
【发布时间】:2010-09-08 16:15:39
【问题描述】:

如何根据对所有候选行应用的权重在 T-SQL 中随机选择一个表行?

例如,我在一个表中有一组行,其权重分别为 50、25 和 25(加起来为 100,但不需要),我想随机选择其中一个,统计结果等效到各自的重量。

【问题讨论】:

  • 我认为现在“Shiroy 的”答案,截至 2021 年 2 月 12 日的最后一个答案是最好的....——Shiroy 的那篇文章的关键是:ORDER BY POWER( RAND(CAST(NEWID() AS VARBINARY)), (1.0/Weight)) DESC

标签: sql-server tsql random


【解决方案1】:

Dane 的回答包括引入平方定律的自联接。 (n*n/2) 连接后的行,表中有 n 行。

更理想的是能够只解析一次表格。

DECLARE @id int, @weight_sum int, @weight_point int
DECLARE @table TABLE (id int, weight int)

INSERT INTO @table(id, weight) VALUES(1, 50)
INSERT INTO @table(id, weight) VALUES(2, 25)
INSERT INTO @table(id, weight) VALUES(3, 25)

SELECT @weight_sum = SUM(weight)
FROM @table

SELECT @weight_point = FLOOR(((@weight_sum - 1) * RAND() + 1))

SELECT
    @id = CASE WHEN @weight_point < 0 THEN @id ELSE [table].id END,
    @weight_point = @weight_point - [table].weight
FROM
    @table [table]
ORDER BY
    [table].Weight DESC

这将遍历表格,将@id 设置为每条记录的id 值,同时减少@weight 点。最终,@weight_point 将变为负数。这意味着前面所有权重的SUM 大于随机选择的目标值。这是我们想要的记录,所以从那时起我们将@id 设置为自身(忽略表中的任何 ID)。

这只会遍历表一次,但即使选择的值是第一条记录,也必须遍历整个表。因为平均位置是表格的一半(如果按权重排序则更少)编写循环可能会更快......(特别是如果权重在公共组中):

DECLARE @id int, @weight_sum int, @weight_point int, @next_weight int, @row_count int
DECLARE @table TABLE (id int, weight int)

INSERT INTO @table(id, weight) VALUES(1, 50)
INSERT INTO @table(id, weight) VALUES(2, 25)
INSERT INTO @table(id, weight) VALUES(3, 25)

SELECT @weight_sum = SUM(weight)
FROM @table

SELECT @weight_point = ROUND(((@weight_sum - 1) * RAND() + 1), 0)

SELECT @next_weight = MAX(weight) FROM @table
SELECT @row_count   = COUNT(*)    FROM @table WHERE weight = @next_weight
SET @weight_point = @weight_point - (@next_weight * @row_count)

WHILE (@weight_point > 0)
BEGIN
    SELECT @next_weight = MAX(weight) FROM @table WHERE weight < @next_weight
    SELECT @row_count   = COUNT(*)    FROM @table WHERE weight = @next_weight
    SET @weight_point = @weight_point - (@next_weight * @row_count)
END

-- # Once the @weight_point is less than 0, we know that the randomly chosen record
-- # is in the group of records WHERE [table].weight = @next_weight

SELECT @row_count = FLOOR(((@row_count - 1) * RAND() + 1))

SELECT
    @id = CASE WHEN @row_count < 0 THEN @id ELSE [table].id END,
    @row_count = @row_count - 1
FROM
    @table [table]
WHERE
    [table].weight = @next_weight
ORDER BY
    [table].Weight DESC

【讨论】:

  • 我做了一些实证测试,发现您的解决方案对输入数据非常敏感。我的测试数据 - 权重:2、998,迭代次数:1M。重量 2 应该被拿起大约 2k 次。如果表中权重的顺序是升序 (2, 998),那么它会增加权重 2 大约 500 次。如果颠倒顺序,大约是 2500 次。如果将ROUND 更改为FLOOR,对于升序,它会拾取权重2 大约1000 次,对于降序大约2000 次。这是正确的概率。我已经更新了你的答案。
  • 我只是不确定,为什么 FLOORROUND 效果更好。使用ROUND,升序拾取小重量的次数太少(1/4 次)或降序拾取的次数太多。 FLOOR 也以升序获得小权重的次数太少(1/2 次),但当权重按降序排序时,概率几乎是理想的。
  • 我是不是要疯了,还是第一个SELECT @row_count = COUNT(*) FROM @table 不应该附加一个WHERE weight = @next_weight?否则,@weight_point 在循环检查中将始终为 0 或更小,因此将始终选择最高值。
  • FLOOR 存在一个小问题,因为函数需要 1 个参数。
  • @Dan - 这就是其他人编辑答案的结果。好地方,我现在就修正他们的编辑。
【解决方案2】:

您只需将所有候选行的权重相加,然后在该总和中选择一个随机点,然后选择与该所选点坐标的记录(每条记录都带有一个累加的权重总和)。

DECLARE @id int, @weight_sum int, @weight_point int
DECLARE @table TABLE (id int, weight int)

INSERT INTO @table(id, weight) VALUES(1, 50)
INSERT INTO @table(id, weight) VALUES(2, 25)
INSERT INTO @table(id, weight) VALUES(3, 25)

SELECT @weight_sum = SUM(weight)
FROM @table

SELECT @weight_point = ROUND(((@weight_sum - 1) * RAND() + 1), 0)

SELECT TOP 1 @id = t1.id
FROM @table t1, @table t2
WHERE t1.id >= t2.id
GROUP BY t1.id
HAVING SUM(t2.weight) >= @weight_point
ORDER BY t1.id

SELECT @id

【讨论】:

  • 我对您和 MatBailie 的解决方案做了一个小基准测试,看起来您的解决方案花费的时间大约是 Mat 的两倍。在具有 2 行和 100 万次迭代的表上,Mat 的解决方案大约需要 45 秒,而您的解决方案大约需要 85 秒。
【解决方案3】:

如果您需要获取一组样本(例如,您想从 5M 行的集合中抽取 50 行),其中每一行都有一个名为 Weight 的列,这是一个 int,其中较大的值意味着更多的重量,你可以使用这个功能:

SELECT * 
FROM 
(
    SELECT TOP 50 RowData, Weight 
    FROM MyTable 
    ORDER BY POWER(RAND(CAST(NEWID() AS VARBINARY)), (1.0/Weight)) DESC
) X 
ORDER BY Weight DESC

这里的关键是使用 POWER( ) 函数,如图所示 here

关于随机函数选择的参考是herehere

您也可以使用:

1.0 * ABS(CAST(CHECKSUM(NEWID()) AS bigint)) / CAST(0x7FFFFFFF AS INT) 

由于this 问题,您将校验和转换为BIGINT 而不是INT

因为校验和返回一个int,而一个int的范围是-2^31 (-2,147,483,648) 到 2^31-1 (2,147,483,647),abs() 函数可以 如果结果恰好是正确的,则返回溢出错误 -2,147,483,648!机会显然非常低,大约 40 亿分之一,但是我们每天在 ~1.8b 行表上运行它, 所以它大约每周发生一次!修复是将校验和转换为 腹肌前的 bigint。

【讨论】:

  • 这似乎是 BEST 选项,也避免了很多复杂的连接、循环等。干得好!
  • 正如网络上其他地方所指出的,一位同事指出,以下内容被认为更“数值稳定”: ORDER BY -(1.0/Weight) * LOG(RAND(CAST(NEWID() AS VARBINARY ))) ASC
【解决方案4】:

如果您有很多记录,“以增量方式携带一个累积 [原文] 权重总和” 部分会很昂贵。如果您也已经有广泛的分数/权重(即:范围足够宽,大多数记录权重都是唯一的。1-5 星可能不会削减它),您可以执行类似的操作来选择权重值.我在这里使用 VB.Net 进行演示,但这也可以在纯 Sql 中轻松完成:

Function PickScore()
    'Assume we have a database wrapper class instance called SQL and seeded a PRNG already
    'Get count of scores in database
    Dim ScoreCount As Double = SQL.ExecuteScalar("SELECT COUNT(score) FROM [MyTable]")
    ' You could also approximate this with just the number of records in the table, which might be faster.

    'Random number between 0 and 1 with ScoreCount possible values
    Dim rand As Double = Random.GetNext(ScoreCount) / ScoreCount

    'Use the equation y = 1 - x^3 to skew results in favor of higher scores
    ' For x between 0 and 1, y is also between 0 and 1 with a strong bias towards 1
    rand = 1 - (rand * rand * rand)

    'Now we need to map the (0,1] vector to [1,Maxscore].
    'Just find MaxScore and mutliply by rand
    Dim MaxScore As UInteger = SQL.ExecuteScalar("SELECT MAX(Score) FROM Songs")
    Return MaxScore * rand
End Function

运行此程序,并选择得分小于返回权重的最大记录。如果不止一条记录共享该分数,请随机选择它。这里的优点是您不必保留任何总和,并且您可以调整用于适合您的口味的概率方程。但同样,它适用于更大的分数分布。

【讨论】:

    【解决方案5】:

    使用随机数生成器执行此操作的方法是集成概率密度函数。使用一组离散值,您可以计算前缀总和(直到这个值的所有值的总和)并存储它。使用此选项,您可以选择大于随机数的最小前缀总和(累计到日期)值。

    在数据库中,插入后的后续值必须更新。如果更新的相对频率和数据集的大小不会使执行此操作的成本过高,则意味着可以从单个 s-argable(可以通过索引查找解析的谓词)查询中获得适当的值.

    【讨论】:

      猜你喜欢
      • 2015-07-05
      • 2010-09-08
      • 2023-03-31
      • 2020-01-22
      • 2017-12-26
      • 1970-01-01
      • 2011-05-26
      相关资源
      最近更新 更多