¿Cómo puedo optimizar la función ORDER BY RAND () de MySQL?

90

Me gustaría optimizar mis consultas, así que analizo mysql-slow.log.

La mayoría de mis consultas lentas contienen ORDER BY RAND(). No puedo encontrar una solución real para resolver este problema. Hay una posible solución en MySQLPerformanceBlog, pero no creo que sea suficiente. En tablas mal optimizadas (o frecuentemente actualizadas, administradas por el usuario) no funciona o necesito ejecutar dos o más consultas antes de poder seleccionar mi PHPfila aleatoria generada.

¿Existe alguna solución para este problema?

Un ejemplo ficticio:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
ORDER BY
        RAND()
LIMIT 1
fabrik
fuente
Posible duplicado de MySQL seleccione 10 filas aleatorias de 600K filas rápidamente
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respuestas:

67

Prueba esto:

SELECT  *
FROM    (
        SELECT  @cnt := COUNT(*) + 1,
                @lim := 10
        FROM    t_random
        ) vars
STRAIGHT_JOIN
        (
        SELECT  r.*,
                @lim := @lim - 1
        FROM    t_random r
        WHERE   (@cnt := @cnt - 1)
                AND RAND(20090301) < @lim / @cnt
        ) i

Esto es especialmente eficiente en MyISAM(ya que COUNT(*)es instantáneo), pero incluso en InnoDBsus 10momentos es más eficiente que ORDER BY RAND().

La idea principal aquí es que no ordenamos, sino que mantenemos dos variables y calculamos el valor running probabilityde una fila que se seleccionará en el paso actual.

Consulte este artículo en mi blog para obtener más detalles:

Actualizar:

Si necesita seleccionar un solo registro aleatorio, intente esto:

SELECT  aco.*
FROM    (
        SELECT  minid + FLOOR((maxid - minid) * RAND()) AS randid
        FROM    (
                SELECT  MAX(ac_id) AS maxid, MIN(ac_id) AS minid
                FROM    accomodation
                ) q
        ) q2
JOIN    accomodation aco
ON      aco.ac_id =
        COALESCE
        (
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_id > randid
                AND ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        ),
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        )
        )

Esto supone que ac_idlos suyos se distribuyen de manera más o menos uniforme.

Quassnoi
fuente
¡Hola, Quassnoi! En primer lugar, ¡gracias por su rápida respuesta! Tal vez sea mi culpa, pero aún no está clara su solución. Actualizaré mi publicación original con un ejemplo concreto y estaré feliz si explica su solución en este ejemplo.
fabrik
hubo un error tipográfico en "JOIN alojamiento aco ON aco.id =" donde aco.id realmente es aco.ac_id. por otro lado, la consulta corregida no funcionó para mí porque arroja un error # 1241 - El operando debe contener 1 columna (s) en el quinto SELECT (la cuarta sub-selección). Intenté encontrar el problema entre paréntesis (si no me equivoco) pero todavía no puedo encontrar el problema.
fabrik
@fabrik: Probar ahora. Sería realmente útil si publicara los scripts de la tabla para que yo pudiera verificarlos antes de publicarlos.
Quassnoi
¡Gracias, funciona! :) ¿Puedes editar la parte JOIN ... ON aco.id para JOIN ... ON aco.ac_id para que pueda aceptar tu solución? ¡Gracias de nuevo! Una pregunta: me pregunto si es posible, ¿este es un peor aleatorio como ORDER BY RAND ()? Solo porque esta consulta repite algunos resultados muchas veces.
fabrik
1
@ Adam: no, eso es intencional, para que puedas reproducir los resultados.
Quassnoi
12

Depende de lo aleatorio que deba ser. La solución que vinculó funciona bastante bien en mi opinión. A menos que tenga grandes lagunas en el campo ID, sigue siendo bastante aleatorio.

Sin embargo, debería poder hacerlo en una consulta usando esto (para seleccionar un solo valor):

SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*MAX(id)) LIMIT 1

Otras soluciones:

  • Agregue un campo flotante permanente llamado randoma la tabla y rellénelo con números aleatorios. Luego puede generar un número aleatorio en PHP y hacer"SELECT ... WHERE rnd > $random"
  • Tome la lista completa de ID y almacénelos en un archivo de texto. Lea el archivo y elija una ID aleatoria.
  • Guarde en caché los resultados de la consulta como HTML y guárdelo durante unas horas.
Cabra descontenta
fuente
8
¿Soy solo yo o esta consulta no funciona? Lo probé con varias variaciones y todas arrojan "Uso no válido de la función de grupo" ..
Sophivorus
Puede hacerlo con una subconsulta, SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1pero esto no parece funcionar correctamente ya que nunca devuelve el último registro
Mark
11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1Parece que me está haciendo el truco
Mark
1

Así es como lo haría:

SET @r := (SELECT ROUND(RAND() * (SELECT COUNT(*)
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != 'draft'
        AND c.acat_slug != 'vendeglatohely'
        AND a.ac_images != 'b:0;';

SET @sql := CONCAT('
  SELECT  a.ac_id,
        a.ac_status,
        a.ac_name,
        a.ac_status,
        a.ac_images
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != ''draft''
        AND c.acat_slug != ''vendeglatohely''
        AND a.ac_images != ''b:0;''
  LIMIT ', @r, ', 1');

PREPARE stmt1 FROM @sql;

EXECUTE stmt1;
Bill Karwin
fuente
Véase también stackoverflow.com/questions/211329/…
Bill Karwin
mi tabla no es continua porque se edita a menudo. por ejemplo, actualmente la primera identificación es 121.
fabrik
3
La técnica anterior no se basa en que los valores de id sean continuos. Elige un número aleatorio entre 1 y COUNT (*), no 1 y MAX (id) como algunas otras soluciones.
Bill Karwin
1
El uso de OFFSET(para qué @rsirve) no evita un escaneo, hasta un escaneo de tabla completo.
Rick James
@RickJames, eso es correcto. Si tuviera que responder a esta pregunta hoy, haría la consulta por clave principal. El uso de un desplazamiento con LIMIT escanea muchas filas. La consulta por clave primaria, aunque es mucho más rápida, no garantiza la posibilidad de elegir cada fila, sino que favorece las filas que siguen espacios.
Bill Karwin
1

(Sí, me van a pegar por no tener suficiente carne aquí, pero ¿no puedes ser vegano por un día?)

Caso: AUTO_INCREMENT consecutivo sin espacios, 1 fila devuelta
Caso: AUTO_INCREMENT consecutivo sin espacios, 10 filas
Caso: AUTO_INCREMENT con espacios, 1 fila devuelta
Caso: Columna FLOAT adicional para aleatorizar
Caso: UUID o columna MD5

Esos 5 casos se pueden hacer muy eficientes para mesas grandes. Vea mi blog para más detalles.

Rick James
fuente
0

Esto le dará una única subconsulta que usará el índice para obtener una identificación aleatoria y luego la otra consulta se activará al obtener su tabla unida.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
AND accomodation.ac_id IS IN (
        SELECT accomodation.ac_id FROM accomodation ORDER BY RAND() LIMIT 1
)
Karl Mikko
fuente
0

La solución para su ejemplo ficticio sería:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation,
        JOIN 
            accomodation_category 
            ON accomodation.ac_category = accomodation_category.acat_id
        JOIN 
            ( 
               SELECT CEIL(RAND()*(SELECT MAX(ac_id) FROM accomodation)) AS ac_id
            ) AS Choices 
            USING (ac_id)
WHERE   accomodation.ac_id >= Choices.ac_id 
        AND accomodation.ac_status != 'draft'
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
LIMIT 1

Para leer más sobre alternativas a ORDER BY RAND(), debe leer este artículo .

tereško
fuente
0

Estoy optimizando muchas consultas existentes en mi proyecto. ¡La solución de Quassnoi me ha ayudado a acelerar mucho las consultas! Sin embargo, encuentro difícil incorporar dicha solución en todas las consultas, especialmente para consultas complicadas que involucran muchas subconsultas en múltiples tablas grandes.

Entonces estoy usando una solución menos optimizada. Fundamentalmente, funciona de la misma manera que la solución de Quassnoi.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / [accomodation_table_row_count]
LIMIT $size

$size * $factor / [accomodation_table_row_count]calcula la probabilidad de elegir una fila aleatoria. El rand () generará un número aleatorio. La fila se seleccionará si rand () es menor o igual a la probabilidad. Esto efectivamente realiza una selección aleatoria para limitar el tamaño de la mesa. Dado que existe la posibilidad de que devuelva menos que el recuento límite definido, debemos aumentar la probabilidad para asegurarnos de que estamos seleccionando suficientes filas. Por lo tanto, multiplicamos $ size por un $ factor (normalmente establezco $ factor = 2, funciona en la mayoría de los casos). Finalmente hacemos ellimit $size

El problema ahora es resolver el conteo de filas de mesas de alojamiento . Si conocemos el tamaño de la tabla, PODEMOS codificar el tamaño de la tabla. Este sería el más rápido, pero obviamente no es lo ideal. Si está utilizando Myisam, obtener el recuento de tablas es muy eficiente. Como estoy usando innodb, solo estoy haciendo una selección simple de conteo +. En su caso, se vería así:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / (select (SELECT count(*) FROM `accomodation`) * (SELECT count(*) FROM `accomodation_category`))
LIMIT $size

La parte complicada es calcular la probabilidad correcta. Como puede ver, el siguiente código en realidad solo calcula el tamaño aproximado de la tabla de temperatura (de hecho, ¡demasiado aproximado!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))Pero puede refinar esta lógica para dar una aproximación más cercana del tamaño de la tabla. Tenga en cuenta que es mejor SOBRE-seleccionar que sub-seleccionar filas. es decir, si la probabilidad es demasiado baja, corre el riesgo de no seleccionar suficientes filas.

Esta solución funciona más lentamente que la solución de Quassnoi, ya que necesitamos recalcular el tamaño de la tabla. Sin embargo, encuentro esta codificación mucho más manejable. Esta es una compensación entre precisión + rendimiento y complejidad de codificación . Dicho esto, en tablas grandes esto sigue siendo mucho más rápido que Order by Rand ().

Nota: Si la lógica de la consulta lo permite, realice la selección aleatoria lo antes posible antes de cualquier operación de combinación.

Lawrenceshen
fuente
-1
function getRandomRow(){
    $id = rand(0,NUM_OF_ROWS_OR_CLOSE_TO_IT);
    $res = getRowById($id);
    if(!empty($res))
    return $res;
    return getRandomRow();
}

//rowid is a key on table
function getRowById($rowid=false){

   return db select from table where rowid = $rowid; 
}
Rokhayakebe
fuente