MySQL: выберите случайную запись, но вес к определенным записям

У меня есть таблица MySQL с кучей записей в ней и столбец с названием "Множитель". Значение по умолчанию (и наиболее распространенное) для этого столбца равно 0, но это может быть любое число.

Что мне нужно сделать, это выбрать одну запись из этой таблицы наугад. Однако строки взвешиваются в соответствии с номером в столбце "Множитель". Значение 0 означает, что он не взвешен вообще. Значение 1 означает, что он взвешивается в два раза больше, как если бы запись была в таблице дважды. Значение 2 означает, что он взвешивается в три раза больше, как если бы запись была в таблице три раза.

Я пытаюсь изменить то, что мои разработчики уже дали мне, поэтому извините, если настройка не имеет большого смысла. Возможно, я мог бы изменить его, но хочу сохранить как можно больше существующей настройки таблицы.

Я пытался выяснить, как это сделать с помощью SELECT и RAND(), но не знаю, как сделать взвешивание. Возможно ли это?

Ответ 1

Этот парень задает тот же вопрос. Он говорит то же самое, что и Фрэнк, но весовые коэффициенты не получаются правильными, и в комментариях кто-то предлагает использовать ORDER BY -LOG(1.0 - RAND()) / Multiplier, который в моем тестировании дал почти идеальные результаты.

(Если кто-нибудь из математиков захочет объяснить, почему это правильно, пожалуйста, просветите меня! Но это работает.)

Недостатком было бы то, что вы не могли установить весовое значение 0, чтобы временно отключить опцию, так как в итоге вы бы делили на ноль. Но вы всегда можете отфильтровать это с помощью WHERE Multiplier > 0.

Ответ 2

Для гораздо лучшей производительности (особенно для больших таблиц) сначала индексируйте столбец веса и используйте этот запрос:

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

Два подзапроса используются, потому что MySQL не поддерживает LIMIT в первом подзапросе.

В таблице 40 МБ обычный запрос занимает 1 с на моей машине i7, а - 0.04 с.

Ответ 3

Не используйте 0, 1 и 2, но 1, 2 и 3. Затем вы можете использовать это значение как множитель:

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

Ответ 4

Ну, я бы поставил логику весов в 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

Я думаю, что это сработает:)

Ответ 5

<?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 строк...

  • пересчитать сумму.
  • reset range (колонка "upto" ).
  • выберите строку, которая указывает $rand.

ранее выбранные строки должны быть исключены в каждом цикле выделения. where ... id not in (3, 5);

Ответ 6

SELECT * FROM tablename ORDER BY -LOG(RAND()) / Multiplier;

Это тот, который дает вам правильное распределение.

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

Дает неправильное распределение.

Например, в таблице есть две записи A и B. A имеет вес 100, а B - с массой 200. Для первой (экспоненциальной случайной величины) она дает вам Pr (выигрыш A) = 1/3, а второй дает вам 1/4, что неверно. Хотел бы я показать вам математику. Однако мне не хватает репутации, чтобы опубликовать соответствующую ссылку.

Ответ 7

Независимо от того, что вы делаете, это giong ужасно, потому что это будет включать: * Получение общих "весов" для всех столбцов как ОДИН номер (включая применение множителя). * Получение случайного числа от 0 до этой суммы. * Получение всех записей и их запуск, вычитание веса из случайного числа и выбор одной записи, когда у вас заканчиваются пункты.

В среднем вы будете бегать по половине стола. Производительность - если таблица не мала, то сделайте это за пределами mySQL в памяти - будет SLOW.

Ответ 8

Результат псевдокода (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");

Объяснение приведенного выше кода:

  • Получить наибольшее значение в столбце "Множитель"
  • вычислить случайное значение множителя (взвешенное по отношению к максимальному значению в столбце Множитель)
  • Получить случайную строку с таким значением множителя

Это также достижимо просто с помощью 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>';

Ответ 9

Для других людей, изучающих эту тему, я считаю, что вы также можете сделать что-то вроде этого:

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 или вставлен как целое значение из вызывающего кода.

Подзапрос суммирует все весовые значения предыдущих записей, проверяя, что он превышает предоставленное случайное значение.

Ответ 10

Хотя я понимаю, что это вопрос в MySQL, следующее может быть полезно для тех, кто использует SQLite3, который имеет тонко различные реализации RANDOM и LOG.

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

weight - столбец в таблице, содержащий целые числа (я использовал 1-100 в качестве диапазона в моей таблице).

RANDOM() в SQLite производит номера между -9.2E18 и + 9.2E18 (см. SQLite docs для получения дополнительной информации). Я использовал оператор modulo, чтобы немного уменьшить диапазон чисел.

abs() удалит негативы, чтобы избежать проблем с LOG, который обрабатывает ненулевые положительные числа.

LOG() на самом деле не присутствует в установке SQLite3 по умолчанию. Я использовал вызов php SQLite3 CreateFunction для использования функции php в SQL. Для получения информации об этом см. документы.