¿Cómo puedo optimizar aún más esta consulta MySQL?

9

Tengo una consulta que está tardando mucho en ejecutarse (más de 15 segundos) y solo empeora con el tiempo a medida que crece mi conjunto de datos. Lo he optimizado en el pasado, y he agregado índices, clasificación a nivel de código y otras optimizaciones, pero necesita un poco más de refinamiento.

SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM `sounds` 
INNER JOIN ratings ON sounds.id = ratings.rateable_id 
WHERE (ratings.rateable_type = 'Sound' 
   AND sounds.blacklisted = false 
   AND sounds.ready_for_deployment = true 
   AND sounds.deployed = true 
   AND sounds.type = "Sound" 
   AND sounds.created_at > "2011-03-26 21:25:49") 
GROUP BY ratings.rateable_id

El propósito de la consulta es obtener sound idla calificación promedio de los sonidos publicados más recientes. Hay alrededor de 1500 sonidos y 2 millones de clasificaciones.

Tengo varios índices sobre sounds

mysql> show index from sounds;
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+————+
| Table  | Non_unique | Key_name                                 | Seq_in_index | Column_name          | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+————+
| sounds |          0 | PRIMARY                                  |            1 | id                   | A         |        1388 |     NULL | NULL   |      | BTREE      |         | 
| sounds |          1 | sounds_ready_for_deployment_and_deployed |            1 | deployed             | A         |           5 |     NULL | NULL   | YES  | BTREE      |         | 
| sounds |          1 | sounds_ready_for_deployment_and_deployed |            2 | ready_for_deployment | A         |          12 |     NULL | NULL   | YES  | BTREE      |         | 
| sounds |          1 | sounds_name                              |            1 | name                 | A         |        1388 |     NULL | NULL   |      | BTREE      |         | 
| sounds |          1 | sounds_description                       |            1 | description          | A         |        1388 |      128 | NULL   | YES  | BTREE      |         | 
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+

y varios en ratings

mysql> show index from ratings;
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+————+
| Table   | Non_unique | Key_name                                | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+————+
| ratings |          0 | PRIMARY                                 |            1 | id          | A         |     2008251 |     NULL | NULL   |      | BTREE      |         | 
| ratings |          1 | index_ratings_on_rateable_id_and_rating |            1 | rateable_id | A         |          18 |     NULL | NULL   |      | BTREE      |         | 
| ratings |          1 | index_ratings_on_rateable_id_and_rating |            2 | rating      | A         |        9297 |     NULL | NULL   | YES  | BTREE      |         | 
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Aquí está el EXPLAIN

mysql> EXPLAIN SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM sounds INNER JOIN ratings ON sounds.id = ratings.rateable_id WHERE (ratings.rateable_type = 'Sound' AND sounds.blacklisted = false AND sounds.ready_for_deployment = true AND sounds.deployed = true AND sounds.type = "Sound" AND sounds.created_at > "2011-03-26 21:25:49") GROUP BY ratings.rateable_id;
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+——————+
| id | select_type | table   | type   | possible_keys                                    | key                                     | key_len | ref                                     | rows    | Extra       |
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+——————+
|  1 | SIMPLE      | ratings | index  | index_ratings_on_rateable_id_and_rating          | index_ratings_on_rateable_id_and_rating | 9       | NULL                                    | 2008306 | Using where | 
|  1 | SIMPLE      | sounds  | eq_ref | PRIMARY,sounds_ready_for_deployment_and_deployed | PRIMARY                                 | 4       | redacted_production.ratings.rateable_id |       1 | Using where | 
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+-------------+

Guardo en caché los resultados una vez obtenidos, por lo que el rendimiento del sitio no es un gran problema, pero mis calentadores de caché tardan más y más en ejecutarse debido a que esta llamada tarda tanto, y eso está comenzando a convertirse en un problema. Esto no parece una gran cantidad de números para resolver en una consulta ...

¿Qué más puedo hacer para que esto funcione mejor ?

coneybeare
fuente
¿Puedes mostrar la EXPLAINsalida? EXPLAIN SELECT sounds.*, avg(ratings.rating) AS avg_rating, count(ratings.rating) AS votes FROM sounds INNER JOIN ratings ON sounds.id = ratings.rateable_id WHERE (ratings.rateable_type = 'Sound' AND sounds.blacklisted = false AND sounds.ready_for_deployment = true AND sounds.deployed = true AND sounds.type = "Sound" AND sounds.created_at > "2011-03-26 21:25:49") GROUP BY ratings.rateable_id
Derek Downey
@coneybeare ¡Este fue un desafío muy interesante para mí hoy! +1 para tu pregunta. Deseo que surjan más preguntas como esta en el futuro cercano.
RolandoMySQLDBA
@coneybeare Parece que el nuevo EXPLICAR solo lee 21540 filas (359 X 60) en lugar de 2,008,306. Ejecute EXPLAIN en la consulta que sugerí originalmente en mi respuesta. Me gustaría ver la cantidad de filas que provienen de eso.
RolandoMySQLDBA
@RolandoMySQLDBA La nueva explicación muestra que una cantidad menor de filas con el índice, sin embargo, el tiempo para ejecutar la consulta todavía era de unos 15 segundos, sin mostrar ninguna mejora
Coneybeare
@coneybeare Ajusté la consulta. Ejecute EXPLAIN en mi nueva consulta. Lo anexé a mi respuesta.
RolandoMySQLDBA

Respuestas:

7

Después de revisar la consulta, las tablas y las cláusulas WHERE AND GROUP BY, recomiendo lo siguiente:

Recomendación # 1) Refactorizar la consulta

Reorganicé la consulta para hacer tres (3) cosas:

  1. crear tablas temporales más pequeñas
  2. Procese la cláusula WHERE en esas tablas temporales
  3. Retrasa la unión hasta el último

Aquí está mi consulta propuesta:

SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

Recomendación # 2) Indice la tabla de sonidos con un índice que acomode la cláusula WHERE

Las columnas de este índice incluyen todas las columnas de la cláusula WHERE con valores estáticos primero y el objetivo móvil en último lugar

ALTER TABLE sounds ADD INDEX support_index
(blacklisted,ready_for_deployment,deployed,type,created_at);

Sinceramente creo que se sorprenderá gratamente. Darle una oportunidad !!!

ACTUALIZACIÓN 2011-05-21 19:04

Acabo de ver la cardinalidad. ¡Ay! Cardinalidad de 1 para rateable_id. Chico, me siento estúpido !!!

ACTUALIZACIÓN 2011-05-21 19:20

Quizás hacer el índice sea suficiente para mejorar las cosas.

ACTUALIZACIÓN 2011-05-21 22:56

Por favor ejecute esto:

EXPLAIN SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes FROM
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

ACTUALIZACIÓN 2011-05-21 23:34

Lo refactoré nuevamente. Prueba este por favor:

EXPLAIN
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes FROM
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
;

ACTUALIZACIÓN 2011-05-21 23:55

Lo refactoré nuevamente. Pruebe este por favor (la última vez):

EXPLAIN
  SELECT A.id,avg(B.rating) AS avg_rating, count(B.rating) AS votes FROM
  (
    SELECT BB.* FROM
    (
      SELECT id FROM sounds
      WHERE blacklisted = false 
      AND   ready_for_deployment = true 
      AND   deployed = true 
      AND   type = "Sound" 
      AND   created_at > '2011-03-26 21:25:49'
    ) AA INNER JOIN sounds BB USING (id)
  ) A INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) B
  ON A.id = B.rateable_id
  GROUP BY B.rateable_id;

ACTUALIZACIÓN 2011-05-22 00:12

¡Odio rendirme!

EXPLAIN
  SELECT A.*,avg(B.rating) AS avg_rating, count(B.rating) AS votes FROM
  (
    SELECT BB.* FROM
    (
      SELECT id FROM sounds
      WHERE blacklisted = false 
      AND   ready_for_deployment = true 
      AND   deployed = true 
      AND   type = "Sound" 
      AND   created_at > '2011-03-26 21:25:49'
    ) AA INNER JOIN sounds BB USING (id)
  ) A,
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
    AND AAA.rateable_id = A.id
  ) B
  GROUP BY B.rateable_id;

ACTUALIZACIÓN 2011-05-22 07:51

Me ha estado molestando que las calificaciones vuelvan con 2 millones de filas en el EXPLICAR. Entonces, me golpeó. Es posible que necesite otro índice en la tabla de calificaciones que comience con rateable_type:

ALTER TABLE ratings ADD INDEX
rateable_type_rateable_id_ndx (rateable_type,rateable_id);

El objetivo de este índice es reducir la tabla temporal que manipula las clasificaciones para que sea inferior a 2 millones. Si podemos obtener esa tabla temporal significativamente más pequeña (al menos la mitad), entonces podemos tener una mejor esperanza en su consulta y la mía trabajando más rápido también.

Después de hacer ese índice, vuelva a intentar mi consulta propuesta original y también pruebe la suya:

SELECT
  sounds.*,srkeys.avg_rating,srkeys.votes
FROM
(
  SELECT AA.id,avg(BB.rating) AS avg_rating, count(BB.rating) AS votes
  (
    SELECT id FROM sounds
    WHERE blacklisted = false 
    AND   ready_for_deployment = true 
    AND   deployed = true 
    AND   type = "Sound" 
    AND   created_at > '2011-03-26 21:25:49'
  ) AA INNER JOIN
  (
    SELECT AAA.ratings,AAA.rateable_id
    FROM ratings AAA
    WHERE rateable_type = 'Sound'
  ) BB
  ON AA.id = BB.rateable_id
  GROUP BY BB.rateable_id
) srkeys INNER JOIN sounds USING (id);

ACTUALIZACIÓN 2011-05-22 18:39: PALABRAS FINALES

Refactoré una consulta en un procedimiento almacenado y agregué un índice para ayudar a responder una pregunta sobre cómo acelerar las cosas. Recibí 6 votos a favor, la respuesta fue aceptada y obtuve una recompensa de 200.

También había refactorizado otra consulta (resultados marginales) y agregué un índice (resultados dramáticos). Recibí 2 votos a favor y la respuesta fue aceptada.

Agregué un índice para otro desafío de consulta y fui votado una vez

y ahora su pregunta .

El deseo de responder a todas las preguntas como estas (incluida la suya) se inspiró en un video de YouTube que vi en consultas de refactorización.

Gracias de nuevo, @coneybeare !!! Quería responder a esta pregunta en la mayor medida posible, no solo aceptar puntos o elogios. ¡Ahora, puedo sentir que gané los puntos!

RolandoMySQLDBA
fuente
Agregué el índice, sin mejoras a tiempo. Aquí está la nueva EXPLICACIÓN: cloud.coneybeare.net/6y7c
coneybeare
El EXPLICAR sobre la consulta de la recomendación 1: cloud.coneybeare.net/6xZ2 Se tardó unos 30 segundos en ejecutar esta consulta
coneybeare
Tuve que editar un poco su sintaxis por alguna razón (agregué un FROM antes de la primera consulta y tuve que deshacerme del alias AAA). Aquí está el EXPLICAR: cloud.coneybeare.net/6xlq La consulta real tardó unos 30 segundos en ejecutarse
coneybeare
@RolandoMySQLDBA: EXPLICAR en su actualización 23:55: cloud.coneybeare.net/6wrN La consulta real se ejecutó durante un minuto, así que
terminé
La segunda selección interna no puede acceder a la tabla de selección A, por lo tanto, A.id arroja un error.
coneybeare
3

Gracias por la salida EXPLICAR. Como se puede deducir de esa declaración, la razón por la que está tardando tanto es el escaneo completo de tablas en la tabla de calificaciones. Nada en la instrucción WHERE está filtrando las 2 millones de filas.

Podría agregar un índice en ratings.type, pero supongo que la CARDINALIDAD será muy baja y seguirá escaneando bastantes filas ratings.

Alternativamente, puede intentar usar pistas de índice para obligar a mysql a usar los índices de sonidos.

Actualizado:

Si fuera yo, agregaría un índice, sounds.createdya que tiene la mejor oportunidad de filtrar las filas y probablemente obligará al optimizador de consultas mysql a usar los índices de la tabla de sonidos. Solo tenga cuidado con las consultas que usan marcos de tiempo creados durante mucho tiempo (1 año, 3 meses, solo depende del tamaño de la tabla de sonidos).

Derek Downey
fuente
Parece que su sugerencia fue notable para @coneybeare. +1 de mi parte también.
RolandoMySQLDBA
El índice en creado no se redujo en ningún momento. Aquí está la EXPLICACIÓN actualizada. cloud.coneybeare.net/6xvc
coneybeare
2

Si tiene que ser una consulta disponible "sobre la marcha" , eso limita un poco sus opciones.

Voy a sugerir dividir y vencer por este problema.

--
-- Create an in-memory table
CREATE TEMPORARY TABLE rating_aggregates (
rateable_id INT,
avg_rating NUMERIC,
votes NUMERIC
);
--
-- For now, just aggregate. 
INSERT INTO rating_aggregates
SELECT ratings.rateable_id, 
avg(ratings.rating) AS avg_rating, 
count(ratings.rating) AS votes FROM `sounds`  
WHERE ratings.rateable_type = 'Sound' 
GROUP BY ratings.rateable_id;
--
-- Now get your final product --
SELECT 
sounds.*, 
rating_aggregates.avg_rating, 
rating_aggregates.votes AS votes,
rating_aggregates.rateable_id 
FROM rating_aggregates 
INNER JOIN sounds ON (sounds.id = rating_aggregates.rateable_id) 
WHERE 
ratings.rateable_type = 'Sound' 
   AND sounds.blacklisted = false 
   AND sounds.ready_for_deployment = true 
   AND sounds.deployed = true 
   AND sounds.type = "Sound" 
   AND sounds.created_at > "2011-03-26 21:25:49";
randomx
fuente
Parece que @coneybeare vio algo en tu sugerencia. +1 de mi parte !!!
RolandoMySQLDBA
En realidad no pude hacer que esto funcione. Estaba recibiendo errores de sql que no estaba seguro de cómo abordar. Realmente nunca he trabajado con tablas temporales
coneybeare
Eventualmente lo obtuve (tuve que agregar FROM sounds, ratingsa la consulta del medio), pero bloqueó mi casilla sql y tuve que matar el proceso.
coneybeare
0

Use JOINs, no subconsultas. ¿Alguno de sus intentos de subconsulta ayudó?

MOSTRAR CREAR TABLA sonidos \ G

MOSTRAR CREAR TABLA ratings \ G

A menudo es beneficioso tener índices "compuestos", no índices de una sola columna. Quizás INDEX (tipo, created_at)

Está filtrando en ambas tablas en un JOIN; es probable que sea un problema de rendimiento.

Hay alrededor de 1500 sonidos y 2 millones de clasificaciones.

Le recomendamos que tenga una identificación de aumento automático ratings, cree una tabla de resumen y use la identificación de AI para realizar un seguimiento de dónde la "dejó". Sin embargo, no almacene promedios en una tabla de resumen:

avg (ratings.rating) AS avg_rating,

En cambio, mantenga la SUMA (rating.rating). El promedio de promedios es matemáticamente incorrecto para calcular un promedio; (suma de sumas) / (suma de conteos) es correcta.

Rick James
fuente