2010-03-10 13 views
30

Tengo una tabla MySQL con un montón de entradas y una columna llamada "Multiplicador". El valor predeterminado (y el más común) para esta columna es 0, pero podría ser cualquier número.MySQL: Seleccione entrada aleatoria, pero peso hacia ciertas entradas

Lo que tengo que hacer es seleccionar una sola entrada de esa tabla al azar. Sin embargo, las filas se ponderan según el número en la columna "Multiplicador". Un valor de 0 significa que no está ponderado en absoluto. Un valor de 1 significa que se pondera el doble, como si la entrada estuviera en la tabla dos veces. Un valor de 2 significa que se pondera tres veces más, como si la entrada estuviera en la tabla tres veces.

Estoy tratando de modificar lo que mis desarrolladores ya me han dado, así que lo siento si la configuración no tiene mucho sentido. Probablemente podría cambiarlo pero quiero mantener la mayor cantidad posible de la configuración de la tabla existente.

He estado tratando de encontrar la manera de hacer esto con SELECT y RAND(), pero no sé cómo hacer la ponderación. ¿Es posible?

+0

"Como si la entrada estuviera en la mesa dos veces" suena como un buen punto de partida. Repita cada fila 'Multiplicador' veces, y haga la selección al azar como lo haría normalmente. – bzlm

+1

Cuando dices "repetir cada fila", ¿qué quieres decir? – John

Respuesta

0

Hagas lo que hagas, es terrible porque implicará: * Obteniendo el total de "pesos" para todas las columnas como UN número (incluyendo la aplicación del multiplicador). * Obteniendo un número aleatorio entre 0 y ese total. * Obteniendo todas las entradas y ejecutándolas, deduciendo el peso del número aleatorio y eligiendo una entrada cuando se queden sin artículos.

En promedio, se ejecutará a lo largo de la mitad de la tabla. Rendimiento - a menos que la tabla sea pequeña, hágalo fuera de mySQL en la memoria - será LENTO.

3

Bueno, me gustaría poner la lógica de pesos en PHP:

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

y la consulta:

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

Creo que va a trabajar :)

+0

¡Interesante! El valor posible para el multiplicador podría ser teóricamente cualquier cosa, pero probablemente sea tan alto como 20. ¿Eso no haría que la matriz sea enorme? ¿Eso esta bien? – John

+0

Bueno, puedes hacer que $ weight_array sea dinámico, para que no tengas que escribir todos los números a mano. No se preocupe por los recursos: mil de int no es mucho. –

+0

@John, luego crea la matriz de pesas dinámicamente con un ciclo for, colocando un segundo bucle for dentro de – TravisO

7

No utilice 0 , 1 y 2 pero 1, 2 y 3. Entonces puede usar este valor como un multiplicador:

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

o simplemente agregue 1: SELECCIONE * FROM tablename ORDER BY (RAND() * (Multiplier + 1)); –

+0

Pensé en hacer algo como esto, pero no veo cómo multiplicar un número aleatorio por otro número da como resultado que se pondere algo. Además, ¿cómo sabe de qué entrada se toma el valor del multiplicador? – John

+0

@John: RAND() te da un número aleatorio entre 0 y 1. Un multiplicador más grande te da más posibilidades de obtener el mayor resultado. Ordenar en este resultado tiene sentido. Realice algunas pruebas con un gran conjunto de datos y vea los resultados. –

0

El resultado del pseudo-código (rand(1, num) % rand(1, num)) obtendrá más hacia 0 y menos hacia num. Reste el resultado de num para obtener lo opuesto.

Así que si mi idioma de la aplicación es PHP, debe verse algo como esto:

$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"); 

Explicación del código anterior:

  1. buscar el valor más alto en la columna de la Multiplicador
  2. calcular un valor multiplicador aleatorio (ponderado hacia el valor máximo en la columna Multiplicador)
  3. Obtenga una fila aleatoria que tenga ese valor Multiplicador

También se puede lograr simplemente mediante el uso de MySQL.

Demostrando que la pseudo-código (rand(1, num) % rand(1, num)) será peso hacia 0: ejecutar el siguiente código PHP para ver por qué (en este ejemplo, 16 es el número más alto):

$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

Estoy tratando de entender lo que este código está haciendo, pero veo algunas cosas que no había visto antes. ¿Podrías explicarlo en términos simples? – John

+0

Sí :) He editado mi publicación con una explicación para el código PHP. – Dor

+0

Se ve bien, pero la mayoría de las entradas tendrá un multiplicador de 0 y no parece que este código alguna vez las seleccione. – John

0

Para otros Googling este tema, creo que también se puede hacer algo como esto:

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 

La suma total de los pesos de todos los registros debe ser N-1, y @RAND debe ser un valor aleatorio entre 0 y n-1 inclusive.

@RAND podría establecerse en SQL o insertarse como un valor entero a partir del código de llamada.

La subselección sumará todos los pesos de los registros precedentes, verificando que excede el valor aleatorio proporcionado.

30

This guy formula la misma pregunta. Él dice lo mismo que Frank, pero las ponderaciones no salen bien y en los comentarios alguien sugiere usar ORDER BY -LOG(RAND())/Multiplier, que en mi prueba dio resultados casi perfectos.

(Si alguna matemáticos por ahí quieren explicar por qué esto es correcto, por favor me ilumine! Pero funciona.)

La desventaja sería que no se podía establecer la ponderación de 0 a desactivar temporalmente una opción , ya que terminarían dividiéndose por cero. Pero siempre puedes filtrarlo con un WHERE Multiplier > 0.

+4

'1 - RAND()' es equivalente a 'RAND()', que es (idealmente) Uniform entre 0 y 1. '-LOG (RAND())/weight' es exponencial con la tasa' weight'. Piense en una Expo como el momento a partir de ahora hasta que reciba un correo electrónico de un tipo particular, y la tasa es qué tan rápido llega cada tipo de correo electrónico. 'LIMIT 1' solo selecciona el siguiente correo electrónico. –

+0

Gracias! - Editado – limos

+0

¡Genial! Modifiqué esto para ponderarlo hacia un valor agregado de una tabla relacionada. SELECT l.nombre, COUNT (l.id) FROM envíos c INNER JOIN ubicaciones l ON c.current_location_id = l.id GROUP BY l.id ORDER BY -LOG (RAND())/COUNT (l.id) DESC – khany

1
<?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> 

si desea seleccionar N filas ...

  1. re-calcular la suma.
  2. rango de reinicio (columna "hasta").
  3. seleccione la fila que apunta $rand.

filas anteriores se deben excluir en cada ciclo de selección. where ... id not in (3, 5);

+0

¿No produciría esta solución una cantidad sustancial de gastos generales? No estoy seguro de cuán intensivo en recursos sería la creación de una tabla completa, la manipulación de esa tabla y su eliminación en el sistema. ¿Un conjunto de valores ponderados, generados dinámicamente, sería más simple, menos propenso a errores y menos intensivo en recursos? – Nathan

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

Es el que le da la distribución correcta.

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

Te da una distribución incorrecta.

Por ejemplo, hay dos entradas A y B en la tabla. A tiene un peso de 100 mientras que B tiene un peso de 200. Para la primera (variable aleatoria exponencial), le da Pr (A ganador) = 1/3, mientras que la segunda le da 1/4, lo cual no es correcto. Ojalá pueda mostrarle los cálculos. Sin embargo, no tengo suficiente representante para publicar el enlace relevante.

0

Aunque me doy cuenta de que esta es una pregunta en MySQL, lo siguiente puede ser útil para alguien que usa SQLite3 que tiene implementaciones sutilmente diferentes de RANDOM y LOG.

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

peso es una columna de la tabla que contiene los números enteros (He usado 1-100 como el rango en mi mesa).

RANDOM() en SQLite produce números entre -9.2E18 y + 9.2E18 (consulte SQLite docs para obtener más información). Usé el operador de módulo para bajar el rango de números un poco.

abs() eliminará los negativos para evitar problemas con LOG que solo maneja números positivos distintos de cero.

LOG() no está realmente presente en una instalación predeterminada de SQLite3. Usé la llamada php SQLite3 CreateFunction para usar la función php en SQL. Consulte the PHP docs para obtener información sobre esto.

3

Para una un rendimiento mucho mejor (especialmente en grandes mesas), primera índice de la columna y el peso utilizar esta consulta:

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

dos subconsultas se utilizan porque MySQL no soporta LIMIT en el primera subconsulta todavía.

En la tabla 40 MB la consulta habitual toma 1s en mi máquina i7 y este se lleva 0.04s.

+0

¿Puede explicar la importancia de las subconsultas? ¿Por qué no 'SELECT *' en la subconsulta más interna y elimina a los otros dos? Esa es la forma de la consulta habitual. – concat

+0

@concat Esto se debe a cómo funciona SQL: cuando haces un pedido en una gran mesa, carga la información completa y luego ordena de acuerdo con la cláusula order by, pero aquí la subconsulta solo funciona en datos indexados que están disponibles en la memoria. vea estas pruebas: usual> https://i.stack.imgur.com/006Ym.jpg, subconsulta> https://i.stack.imgur.com/vXU8e.jpg el tiempo de respuesta se resalta. – Ali

+0

Ahora puedo confirmar, y aunque es muy inesperado, creo que ahora entiendo cómo funciona esto. Gracias por mostrarme algo nuevo hoy! – concat

Cuestiones relacionadas