Comment puis-je optimiser davantage cette requête MySQL?

9

J'ai une requête qui prend un temps particulièrement long à exécuter (15+ secondes) et elle ne fait qu'empirer avec le temps à mesure que mon ensemble de données se développe. J'ai optimisé cela dans le passé, et j'ai ajouté des indices, un tri au niveau du code et d'autres optimisations, mais il a besoin d'être affiné.

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

Le but de la requête est de me donner le son sound idet la note moyenne des sons les plus récents et sortis. Il y a environ 1500 sons et 2 millions de notes.

J'ai plusieurs indices sur 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      |         | 
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+

et plusieurs sur 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      |         | 
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Voici la 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 | 
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+-------------+

Je cache les résultats une fois obtenus, les performances du site ne sont donc pas vraiment un problème, mais mes cache-cache prennent de plus en plus de temps à s'exécuter en raison de la longueur de cet appel, et cela commence à devenir un problème. Cela ne semble pas beaucoup de chiffres à croquer dans une seule requête…

Que puis-je faire de plus pour améliorer les performances ?

coneybeare
la source
Pouvez-vous montrer la EXPLAINsortie? 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 Ce fut un défi très intéressant pour moi aujourd'hui !!! +1 pour votre question. Je souhaite que d'autres questions comme celle-ci se présentent dans un avenir proche.
RolandoMySQLDBA
@coneybeare Il semble que le nouvel EXPLAIN ne lit que 21540 lignes (359 X 60) au lieu de 2 008 306. Veuillez exécuter EXPLAIN sur la requête que j'ai initialement suggérée dans ma réponse. J'aimerais voir le nombre de lignes qui en découlent.
RolandoMySQLDBA
@RolandoMySQLDBA La nouvelle explication montre en effet qu'une plus petite quantité de lignes avec l'index, cependant, le temps pour exécuter la requête était encore d'environ 15 secondes, sans amélioration
coneybeare
@coneybeare J'ai affiné la requête. Veuillez exécuter EXPLAIN sur ma nouvelle requête. Je l'ai annexé à ma réponse.
RolandoMySQLDBA

Réponses:

7

Après avoir examiné la requête, les tables et les clauses WHERE AND GROUP BY, je recommande ce qui suit:

Recommandation n ° 1) Refactoriser la requête

J'ai réorganisé la requête pour faire trois (3) choses:

  1. créer des tables temporaires plus petites
  2. Traiter la clause WHERE sur ces tables temporaires
  3. Retarder jusqu'au dernier

Voici ma requête proposée:

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

Recommandation n ° 2) Indexer la table des sons avec un index qui tiendra compte de la clause WHERE

Les colonnes de cet index incluent toutes les colonnes de la clause WHERE avec les valeurs statiques en premier et la cible en mouvement en dernier

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

Je crois sincèrement que vous serez agréablement surpris. Essaie !!!

MISE À JOUR 2011-05-21 19:04

Je viens de voir la cardinalité. AIE !!! Cardinalité de 1 pour rateable_id. Garçon, je me sens stupide !!!

MISE À JOUR 2011-05-21 19:20

Peut-être que faire l'index sera suffisant pour améliorer les choses.

MISE À JOUR 2011-05-21 22:56

Veuillez exécuter ceci:

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

MISE À JOUR 2011-05-21 23:34

Je l'ai refactorisé à nouveau. Essayez celui-ci s'il vous plaît:

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
;

MISE À JOUR 2011-05-21 23:55

Je l'ai refactorisé à nouveau. Essayez celui-ci s'il vous plaît (dernière fois):

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;

MISE À JOUR 2011-05-22 00:12

Je déteste abandonner !!!!

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;

MISE À JOUR 2011-05-22 07:51

Cela me dérange que les notes reviennent avec 2 millions de lignes dans EXPLAIN. Ensuite, ça m'a frappé. Vous pourriez avoir besoin d'un autre index sur le tableau des notes qui commence par rateable_type:

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

Le but de cet indice est de réduire la table temporaire qui manipule les notes afin qu'elle soit inférieure à 2 millions. Si nous pouvons réduire considérablement la taille de cette table temporaire (au moins la moitié), nous pouvons avoir un meilleur espoir dans votre requête et la mienne fonctionner plus rapidement également.

Après avoir créé cet index, veuillez réessayer ma requête initiale proposée et essayez également la vôtre:

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

MISE À JOUR 2011-05-22 18:39: MOTS FINAUX

J'avais refactorisé une requête dans une procédure stockée et ajouté un index pour aider à répondre à une question sur l'accélération des choses. J'ai reçu 6 votes positifs, la réponse a été acceptée et j'ai reçu 200 primes.

J'avais également refactorisé une autre requête (résultats marginaux) et ajouté un index (résultats spectaculaires). J'ai reçu 2 votes positifs et j'ai accepté la réponse.

J'ai ajouté un index pour un autre défi de requête et j'ai été voté une fois

et maintenant votre question .

Le fait de vouloir répondre à toutes les questions comme celles-ci (y compris la vôtre) a été inspiré par une vidéo YouTube que j'ai regardée lors de la refactorisation des requêtes.

Merci encore, @coneybeare !!! Je voulais répondre à cette question dans toute la mesure du possible, pas seulement accepter des points ou des distinctions. Maintenant, je sens que j'ai gagné des points !!!

RolandoMySQLDBA
la source
J'ai ajouté l'index, aucune amélioration sur le temps. Voici le nouvel EXPLAIN: cloud.coneybeare.net/6y7c
coneybeare
EXPLAIN sur la requête de la recommandation 1: cloud.coneybeare.net/6xZ2 Il a fallu environ 30 secondes pour exécuter cette requête
coneybeare
J'ai dû modifier un peu votre syntaxe pour une raison quelconque (j'ai ajouté un FROM avant la première requête et j'ai dû me débarrasser de l'alias AAA). Voici l' EXPLICATION : cloud.coneybeare.net/6xlq La requête réelle a pris environ 30 secondes pour s'exécuter
coneybeare
@RolandoMySQLDBA: EXPLIQUEZ votre mise à jour de 23h55: cloud.coneybeare.net/6wrN La requête réelle a duré plus d'une minute, j'ai donc tué le processus
coneybeare
La deuxième sélection interne ne peut pas accéder à la table de sélection A, donc A.id génère une erreur.
coneybeare
3

Merci pour la sortie EXPLAIN. Comme vous pouvez le constater à partir de cette déclaration, la raison pour laquelle cela prend si longtemps est le tableau complet du tableau des notes. Rien dans l'instruction WHERE ne filtre les 2 millions de lignes.

Vous pouvez ajouter un index sur ratings.type, mais je suppose que la CARDINALITÉ va être vraiment faible et que vous continuerez à scanner pas mal de lignes ratings.

Alternativement, vous pouvez essayer d'utiliser des indices pour forcer mysql à utiliser les index des sons.

Mise à jour:

Si c'était moi, j'ajouterais un index sounds.createdcar cela a la meilleure chance de filtrer les lignes et forcera probablement l'optimiseur de requête mysql à utiliser les index de la table des sons. Méfiez-vous des requêtes qui utilisent de longues périodes de temps créées (1 an, 3 mois, cela dépend juste de la taille de la table des sons).

Derek Downey
la source
On dirait que votre suggestion était notable pour @coneybeare. +1 de moi aussi.
RolandoMySQLDBA
L'index créé n'a pas été rasé à tout moment. Voici l'EXPLAIN mis à jour. cloud.coneybeare.net/6xvc
coneybeare
2

Si cela doit être une requête disponible "à la volée" , cela limite un peu vos options.

Je vais suggérer de diviser pour mieux régler ce problème.

--
-- 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
la source
semble que @coneybeare a vu quelque chose dans votre suggestion. +1 de moi !!!
RolandoMySQLDBA
En fait, je ne pouvais pas faire fonctionner cela. J'obtenais des erreurs SQL que je ne savais pas comment aborder. Je n'ai jamais vraiment travaillé avec des tables temporaires
coneybeare
Je l'ai finalement obtenu (j'ai dû ajouter FROM sounds, ratingsà la requête du milieu), mais il a verrouillé ma boîte sql et j'ai dû tuer le processus.
coneybeare
0

Utilisez des JOIN, pas des sous-requêtes. Une de vos tentatives de sous-requête a-t-elle aidé?

AFFICHER CRÉER TABLE sons \ G

AFFICHER CRÉER LE TABLEAU \ G

Il est souvent avantageux d'avoir des index "composés", pas des index à colonne unique. Peut-être INDEX (type, created_at)

Vous filtrez sur les deux tables dans un JOIN; cela risque d'être un problème de performances.

Il y a environ 1500 sons et 2 millions de notes.

Vous recommandons d'avoir un identifiant auto_increment ratings, de créer un tableau récapitulatif et d'utiliser l'identifiant AI pour garder une trace de l'endroit où vous vous êtes «arrêté». Cependant, ne stockez pas de moyennes dans un tableau récapitulatif:

avg (ratings.rating) AS avg_rating,

Au lieu de cela, conservez le SUM (ratings.rating). La moyenne des moyennes est mathématiquement incorrecte pour calculer une moyenne; (somme des sommes) / (somme des comptes) est correct.

Rick James
la source