質問

すべての候補行に適用された重みに基づいて、T-SQL でテーブル行をランダムに選択するにはどうすればよいでしょうか?

たとえば、テーブル内に 50、25、25 で重み付けされた行のセットがあり (合計すると 100 になりますが、そうする必要はありません)、それぞれの行と同等の統計結果を持つ行をランダムに 1 つ選択したいとします。重さ。

役に立ちましたか?

解決

Dane の答えには、二乗法則を導入する方法での自己結合が含まれています。 (n*n/2) テーブル内に n 行がある結合後の行。

より理想的なのは、テーブルを 1 回だけ解析できることです。

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), 0)

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 は無視されます)。

これはテーブル全体を 1 回だけ実行しますが、選択した値が最初のレコードであってもテーブル全体を実行する必要があります。平均位置はテーブルの途中にあるため (重みの昇順に並べた場合はそれより少なくなります)、ループを作成した方がおそらく高速になる可能性があります...(特に重み付けが共通のグループにある場合):

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
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), 0)

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

他のヒント

すべての候補行の重みを合計し、その合計内でランダムな点を選択し、その選択した点と一致するレコードを選択するだけです (各レコードには、累積された重みの合計が増分的に含まれます)。

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

「累積重量合計を段階的に運ぶ」 レコードがたくさんある場合、その部分は高価になります。すでに広範囲のスコア/重みを持っている場合 (例:範囲が十分に広いため、ほとんどのレコードの重みは一意になります。おそらく 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

これを実行し、返された重みより小さい最大スコアを持つレコードを選択します。複数のレコードがそのスコアを共有する場合は、ランダムに選択します。ここでの利点は、合計を維持する必要がなく、使用する確率方程式を好みに合わせて調整できることです。ただし、繰り返しになりますが、スコアの分布が大きいほど最適に機能します。

乱数発生器でこれを行う方法は、確率密度関数を統合することです。一連の離散値を使用して、プレフィックスの合計 (この値までのすべての値の合計) を計算して保存できます。これにより、乱数より大きい最小プレフィックス合計 (日付までの集計) 値を選択します。

データベースでは、挿入後の後続の値を更新する必要があります。相対的な更新頻度とデータ セットのサイズにより、これを実行するコストが法外に高くならない場合は、単一の s-argable (インデックス検索によって解決できる述語) クエリから適切な値を取得できることを意味します。 。

サンプルのグループを取得する必要がある場合 (たとえば、500 万行のコレクションから 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( ) 関数を使用することです。 ここ

ランダム関数の選択に関する参考資料は次のとおりです。 ここ そして ここ

あるいは、以下を使用することもできます。

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

チェックサムを次のようにキャストします BIGINT の代わりに INT のため これ 問題:

チェックサムはintを返し、intの範囲は-2^31(-2,147,483,648)に2^31-1(2,147,483,647)になるため、結果がたまたま-2,147,483,6483,6483,6483,6483,6483,6483,6483,6483,647個のabs()関数が返される可能性があります。 !可能性は明らかに非常に低く、40億人に約1人ですが、毎日約1.8bの列のテーブルでそれを実行していたので、週に1回行われていました!修正は、腹筋の前にチェックサムをbigintにキャストすることです。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top