web-dev-qa-db-ja.com

MySQL:ランダムエントリを選択しますが、特定のエントリに重みを付けます

多数のエントリを含むMySQLテーブルと、「Multiplier」という列があります。この列のデフォルト(および最も一般的な)値は0ですが、任意の数値にすることができます。

私がする必要があるのは、そのテーブルからランダムに1つのエントリを選択することです。ただし、行は「乗数」列の数値に従って重み付けされます。値0は、まったく重み付けされていないことを意味します。値1は、エントリがテーブルに2回あるかのように、2倍の重みが付けられることを意味します。値2は、エントリがテーブルに3回あるかのように、3倍の重みが付けられることを意味します。

開発者から提供されたものを変更しようとしているので、セットアップがあまり意味をなさない場合は申し訳ありません。私はおそらくそれを変更することができますが、既存のテーブル設定を可能な限り維持したいと思います。

SELECTとRand()を使用してこれを行う方法を理解しようとしていますが、重み付けを行う方法がわかりません。出来ますか?

38
John

この男 同じ質問をします。彼はフランクと同じことを言っていますが、重みが正しく出ていないので、コメントで誰かがORDER BY -LOG(1.0 - Rand()) / Multiplierを使用することを提案しています。これは、私のテストではほぼ完璧な結果をもたらしました。

(これが正しい理由を説明したい数学者がいる場合は、私に教えてください!しかし、それは機能します。)

不利な点は、ゼロで除算することになるため、オプションを一時的に無効にするために重みを0に設定できないことです。ただし、WHERE Multiplier > 0を使用していつでも除外できます。

41
limos

はるかに優れたパフォーマンス(特に大きなテーブル)の場合、最初に重み列にインデックスを付けるそして次のクエリを使用します:

SELECT * FROM tbl WHERE id IN 
    (SELECT id FROM (SELECT id FROM tbl ORDER BY -LOG(1-Rand())/weight LIMIT x) t)

MySQLは最初のサブクエリでLIMITをまだサポートしていないため、2つのサブクエリが使用されます。

40MBのテーブルでは通常のクエリは1秒かかります私のi7マシンではこれは0.04秒かかります

11
Ali

0、1、2ではなく1、2、3を使用してください。次に、この値を乗数として使用できます。

SELECT * FROM tablename ORDER BY (Rand() * Multiplier);
6
Frank Heikens

さて、私は重みのロジックをPHPに入れます:

<?php
    $weight_array = array(0, 1, 1, 2, 2, 2);
    $multiplier = $weight_array[array_Rand($weight_array)];
?>

とクエリ:

SELECT *
FROM `table`
WHERE Multiplier = $multiplier
ORDER BY Rand()
LIMIT 1

私はそれがうまくいくと思います:)

3
Silver Light

この主題をグーグルで検索している他の人にとっては、次のようなこともできると思います。

SELECT strategy_id
FROM weighted_strategies AS t1 
WHERE (
   SELECT SUM(weight) 
   FROM weighted_strategies AS t2 
   WHERE t2.strategy_id<=t1.strategy_id
)>@Rand AND 
weight>0
LIMIT 1

すべてのレコードの重みの合計必須はn-1であり、@ Randは0からn-1までのランダムな値である必要があります。

@Randは、SQLで設定することも、呼び出し元のコードから整数値として挿入することもできます。

副選択は、先行するすべてのレコードの重みを合計し、指定されたランダム値を超えていることを確認します。

1
wally
SELECT * FROM tablename ORDER BY -LOG(Rand()) / Multiplier;

あなたに正しい分布を与えるものです。

SELECT * FROM tablename ORDER BY (Rand() * Multiplier);

間違った分布を与えます。

たとえば、テーブルには2つのエントリAとBがあります。 Aの重みは100で、Bの重みは200です。最初の(指数確率変数)の場合、Pr(A勝ち)= 1/3になり、2番目の重みは1/4になりますが、これは正しくありません。私はあなたに数学を見せたいと思います。ただし、関連するリンクを投稿するのに十分な担当者がいません。

1
yoloDave
_<?php
/**
 * Demonstration of weighted random selection of MySQL database.
 */
$conn = mysql_connect('localhost', 'root', '');

// prepare table and data.
mysql_select_db('test', $conn);
mysql_query("drop table if exists temp_wrs", $conn);
mysql_query("create table temp_wrs (
    id int not null auto_increment,
    val varchar(16),
    weight tinyint,
    upto smallint,
    primary key (id)
)", $conn);
$base_data = array(    // value-weight pair array.
    'A' => 5,
    'B' => 3,
    'C' => 2,
    'D' => 7,
    'E' => 6,
    'F' => 3,
    'G' => 5,
    'H' => 4
);
foreach($base_data as $val => $weight) {
    mysql_query("insert into temp_wrs (val, weight) values ('".$val."', ".$weight.")", $conn);
}

// calculate the sum of weight.
$rs = mysql_query('select sum(weight) as s from temp_wrs', $conn);
$row = mysql_fetch_assoc($rs);
$sum = $row['s'];
mysql_free_result($rs);

// update range based on their weight.
// each "upto" columns will set by sub-sum of weight.
mysql_query("update temp_wrs a, (
    select id, (select sum(weight) from temp_wrs where id <= i.id) as subsum from temp_wrs i 
) b
set a.upto = b.subsum
where a.id = b.id", $conn);

$result = array();
foreach($base_data as $val => $weight) {
    $result[$val] = 0;
}
// do weighted random select ($sum * $times) times.
$times = 100;
$loop_count = $sum * $times;
for($i = 0; $i < $loop_count; $i++) {
    $Rand = Rand(0, $sum-1);
    // select the row which $Rand pointing.
    $rs = mysql_query('select * from temp_wrs where upto > '.$Rand.' order by id limit 1', $conn);
    $row = mysql_fetch_assoc($rs);
    $result[$row['val']] += 1;
    mysql_free_result($rs);
}

// clean up.
mysql_query("drop table if exists temp_wrs");
mysql_close($conn);
?>
<table>
    <thead>
        <th>DATA</th>
        <th>WEIGHT</th>
        <th>ACTUALLY SELECTED<br />BY <?php echo $loop_count; ?> TIMES</th>
    </thead>
    <tbody>
    <?php foreach($base_data as $val => $weight) : ?>
        <tr>
            <th><?php echo $val; ?></th>
            <td><?php echo $weight; ?></td>
            <td><?php echo $result[$val]; ?></td>
        </tr>
    <?php endforeach; ?>
    <tbody>
</table>
_

n行を選択する場合...

  1. 合計を再計算します。
  2. 範囲をリセットします(「最大」列)。
  3. _$Rand_が指している行を選択します。

以前に選択した行は、各選択ループで除外する必要があります。 where ... id not in (3, 5);

1
sukhoi

これはMySQLに関する質問だと思いますが、RANDOMとLOGの実装が微妙に異なるSQLite3を使用している人には、次のことが役立つ場合があります。

SELECT * FROM table ORDER BY (-LOG(abs(RANDOM() % 10000))/weight) LIMIT 1;

weightは、整数を含むテーブルの列です(テーブルの範囲として1〜100を使用しました)。

SQLiteのRANDOM()は、-9.2E18から+ 9.2E18までの数値を生成します(詳細については、 SQLite docs を参照してください)。モジュロ演算子を使用して、数値の範囲を少し下げました。

abs()は、ゼロ以外の正の数のみを処理するLOGの問題を回避するために、負の数を削除します。

LOG()は、SQLite3のデフォルトのインストールには実際には存在しません。 php SQLite3 CreateFunction呼び出しを使用して、SQLでphp関数を使用しました。詳細については、 PHP docs を参照してください。

0
Graeme Hilton

擬似コードの結果(Rand(1, num) % Rand(1, num))は0に向かって多くなり、numに向かって少なくなります。 numから結果を引くと、逆になります。

したがって、私のアプリケーション言語がPHPの場合、次のようになります。

$arr = mysql_fetch_array(mysql_query(
    'SELECT MAX(`Multiplier`) AS `max_mul` FROM tbl'
));
$MaxMul = $arr['max_mul']; // Holds the maximum value of the Multiplier column

$mul = $MaxMul - ( Rand(1, $MaxMul) % Rand(1, $MaxMul) );

mysql_query("SELECT * FROM tbl WHERE Multiplier=$mul ORDER BY Rand() LIMIT 1");

上記のコードの説明:

  1. Multiplier列の最大値をフェッチします
  2. ランダムな乗数値を計算します([乗数]列の最大値に向かって重み付けされます)
  3. その乗数の値を持つランダムな行をフェッチします

また、MySQLを使用するだけでも実現できます。

擬似コード(Rand(1, num) % Rand(1, num))は0に向かって重みが付けられます:次のPHPコードを実行して、理由を確認します(この例では、16が最大数です)。

$v = array();

for($i=1; $i<=16; ++$i)
    for($k=1; $k<=16; ++$k)
        isset($v[$i % $k]) ? ++$v[$i % $k] : ($v[$i % $k] = 1);

foreach($v as $num => $times)
        echo '<div style="margin-left:', $times  ,'px">
              times: ',$times,' @ num = ', $num ,'</div>';
0
Dor

何をするにしても、それは以下を含むのでひどいことです:*すべての列の合計「重み」を1つの数値として取得する(乗数の適用を含む)。 * 0からその合計までの乱数を取得します。 *すべてのエントリを取得して実行し、乱数から重みを差し引いて、アイテムがなくなったときに1つのエントリを選択します。

平均して、テーブルの半分に沿って走ります。パフォーマンス(テーブルが小さい場合を除いて、メモリ内のmySQLの外部で実行)は遅くなります。

0
TomTom