Wie kann ich diese MySQL-Abfrage weiter optimieren?

9

Ich habe eine Abfrage, deren Ausführung besonders lange dauert (15+ Sekunden) und die mit der Zeit immer schlimmer wird, wenn mein Datensatz wächst. Ich habe dies in der Vergangenheit optimiert und Indizes, Sortierung auf Codeebene und andere Optimierungen hinzugefügt, aber es muss noch weiter verfeinert werden.

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

Der Zweck der Abfrage ist es, mir die sound idund die durchschnittliche Bewertung der neuesten, veröffentlichten Sounds zu geben. Es gibt ungefähr 1500 Sounds und 2 Millionen Bewertungen.

Ich habe mehrere Indizes 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      |         | 
+--------+------------+------------------------------------------+--------------+----------------------+-----------+-------------+----------+--------+------+------------+---------+

und mehrere auf 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      |         | 
+---------+------------+-----------------------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Hier ist der 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 | 
+----+-------------+---------+--------+--------------------------------------------------+-----------------------------------------+---------+-----------------------------------------+---------+-------------+

Ich speichere die einmal erhaltenen Ergebnisse im Cache, daher ist die Leistung der Website kein großes Problem, aber die Ausführung meiner Cache-Wärmer dauert immer länger, da dieser Aufruf so lange dauert, und das wird langsam zu einem Problem. Dies scheint nicht viele Zahlen zu sein, die in einer Abfrage zusammengefasst werden müssen…

Was kann ich noch tun, um die Leistung zu verbessern ?

coneybeare
quelle
Können Sie die EXPLAINAusgabe zeigen ? 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 Das war heute eine sehr interessante Herausforderung für mich !!! +1 für deine Frage. Ich wünsche mir, dass in naher Zukunft weitere Fragen wie diese auftauchen.
RolandoMySQLDBA
@coneybeare Es sieht so aus, als würde der neue EXPLAIN nur 21540 Zeilen (359 x 60) anstelle von 2.008.306 lesen. Bitte führen Sie EXPLAIN für die Abfrage aus, die ich ursprünglich in meiner Antwort vorgeschlagen habe. Ich würde gerne die Anzahl der Zeilen sehen, die daraus entstehen.
RolandoMySQLDBA
@ RolandoMySQLDBA Die neue Erklärung zeigt in der Tat, dass eine geringere Anzahl von Zeilen mit dem Index, jedoch die Zeit zum Ausführen der Abfrage noch etwa 15 Sekunden betrug, was keine Verbesserung zeigt
coneybeare
@coneybeare Ich habe die Abfrage fein abgestimmt. Bitte führen Sie die EXPLAIN für meine neue Abfrage aus. Ich habe es meiner Antwort beigefügt.
RolandoMySQLDBA

Antworten:

7

Nachdem Sie sich die Abfrage, die Tabellen und die WHERE AND GROUP BY-Klauseln angesehen haben, empfehle ich Folgendes:

Empfehlung 1) Refaktorieren Sie die Abfrage

Ich habe die Abfrage neu organisiert, um drei (3) Dinge zu tun:

  1. Erstellen Sie kleinere temporäre Tabellen
  2. Verarbeiten Sie die WHERE-Klausel für diese temporären Tabellen
  3. Verzögerung beim Beitritt bis zum letzten

Hier ist meine vorgeschlagene Abfrage:

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

Empfehlung Nr. 2) Indizieren Sie die Sounds-Tabelle mit einem Index, der die WHERE-Klausel enthält

Die Spalten dieses Index enthalten alle Spalten aus der WHERE-Klausel, wobei die statischen Werte zuerst und das Ziel zuletzt verschoben werden

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

Ich glaube aufrichtig, dass Sie angenehm überrascht sein werden. Versuche es !!!

UPDATE 2011-05-21 19:04

Ich habe gerade die Kardinalität gesehen. Autsch !!! Kardinalität von 1 für rateable_id. Junge, ich fühle mich dumm !!!

UPDATE 2011-05-21 19:20

Vielleicht reicht es aus, den Index zu erstellen, um die Dinge zu verbessern.

UPDATE 2011-05-21 22:56

Bitte führen Sie dies aus:

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

UPDATE 2011-05-21 23:34

Ich habe es wieder überarbeitet. Versuchen Sie dies bitte:

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
;

UPDATE 2011-05-21 23:55

Ich habe es wieder überarbeitet. Versuchen Sie dies bitte (letztes Mal):

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;

UPDATE 2011-05-22 00:12

Ich hasse es aufzugeben !!!!

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;

UPDATE 2011-05-22 07:51

Es hat mich gestört, dass die Bewertungen mit 2 Millionen Zeilen in der EXPLAIN zurückkommen. Dann traf es mich. Möglicherweise benötigen Sie einen anderen Index in der Bewertungstabelle, der mit rateable_type beginnt:

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

Das Ziel dieses Index ist es, die temporäre Tabelle, die die Bewertungen manipuliert, so zu reduzieren, dass sie weniger als 2 Millionen beträgt. Wenn wir diese temporäre Tabelle deutlich kleiner machen können (mindestens die Hälfte), können wir eine bessere Hoffnung auf Ihre und meine Abfrage haben, die auch schneller funktioniert.

Nachdem Sie diesen Index erstellt haben, wiederholen Sie bitte meine ursprünglich vorgeschlagene Abfrage und versuchen Sie auch Ihre:

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

UPDATE 2011-05-22 18:39: SCHLUSSWÖRTER

Ich hatte eine Abfrage in einer gespeicherten Prozedur überarbeitet und einen Index hinzugefügt, um eine Frage zur Beschleunigung zu beantworten. Ich bekam 6 positive Stimmen, ließ die Antwort akzeptieren und nahm ein Kopfgeld von 200 auf.

Ich hatte auch eine andere Abfrage überarbeitet (marginale Ergebnisse) und einen Index hinzugefügt (dramatische Ergebnisse). Ich bekam 2 positive Stimmen und hatte die Antwort akzeptiert.

Ich habe einen Index für eine weitere Abfrage-Herausforderung hinzugefügt und wurde einmal hochgestuft

und jetzt deine Frage .

Der Wunsch, alle Fragen wie diese (einschließlich Ihrer) zu beantworten, wurde von einem YouTube-Video inspiriert, das ich mir beim Refactoring von Abfragen angesehen habe.

Nochmals vielen Dank, @coneybeare !!! Ich wollte diese Frage so weit wie möglich beantworten und nicht nur Punkte oder Auszeichnungen akzeptieren. Jetzt kann ich fühlen, dass ich die Punkte verdient habe !!!

RolandoMySQLDBA
quelle
Ich habe den Index hinzugefügt, keine zeitliche Verbesserung. Hier ist der neue EXPLAIN: cloud.coneybeare.net/6y7c
coneybeare
Die Erklärung zur Abfrage aus Empfehlung 1: cloud.coneybeare.net/6xZ2 Es dauerte ungefähr 30 Sekunden, um diese Abfrage
auszuführen
Ich musste Ihre Syntax aus irgendeinem Grund ein wenig bearbeiten (ich habe vor der ersten Abfrage ein FROM hinzugefügt und musste den AAA-Alias ​​entfernen). Hier ist die Erklärung: cloud.coneybeare.net/6xlq Die eigentliche Abfrage dauerte etwa 30 Sekunden
coneybeare
@ RolandoMySQLDBA: Erklären Sie auf Ihrem 23:55 Update: cloud.coneybeare.net/6wrN Die eigentliche Abfrage lief über eine Minute, so dass ich den Prozess beendet habe
coneybeare
Die zweite innere Auswahl kann nicht auf die A-Auswahltabelle zugreifen, daher löst A.id einen Fehler aus.
Coneybeare
3

Danke für die EXPLAIN-Ausgabe. Wie Sie dieser Aussage entnehmen können, dauert der vollständige Tabellenscan in der Bewertungstabelle so lange. Nichts in der WHERE-Anweisung filtert die 2 Millionen Zeilen nach unten.

Sie könnten einen Index für reviews.type hinzufügen, aber ich vermute, dass die KARDINALITÄT sehr niedrig sein wird und Sie immer noch einige Zeilen weiter scannen werden ratings.

Alternativ können Sie versuchen , verwenden Index Hinweise auf Kraft mysql die Klänge Indizes zu verwenden.

Aktualisiert:

Wenn ich es wäre, würde ich einen Index hinzufügen, sounds.createdda dies die beste Chance hat, die Zeilen zu filtern, und wahrscheinlich den MySQL-Abfrageoptimierer zwingen wird, die Sounds-Tabellenindizes zu verwenden. Achten Sie nur auf Abfragen, die lange erstellte Zeitrahmen verwenden (1 Jahr, 3 Monate, hängt nur von der Größe der Soundtabelle ab).

Derek Downey
quelle
Anscheinend war Ihr Vorschlag für @coneybeare bemerkenswert. +1 auch von mir.
RolandoMySQLDBA
Der erstellte Index wurde zu keinem Zeitpunkt rasiert. Hier ist die aktualisierte EXPLAIN. cloud.coneybeare.net/6xvc
coneybeare
2

Wenn dies eine "on-the-fly" verfügbare Abfrage sein muss, schränkt dies Ihre Optionen ein wenig ein.

Ich werde vorschlagen, dieses Problem zu teilen und zu erobern.

--
-- 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
quelle
@coneybeare scheint etwas in Ihrem Vorschlag gesehen zu haben. +1 von mir !!!
RolandoMySQLDBA
Ich konnte das eigentlich nicht zum Laufen bringen. Ich bekam SQL-Fehler, bei denen ich mir nicht sicher war, wie ich vorgehen sollte. Ich habe nie wirklich mit temporären Tischen gearbeitet
coneybeare
Ich habe es schließlich bekommen (ich hatte FROM hinzuzufügen sounds, ratingsin der Mitte Abfrage), aber es gesperrt meine SQL - Box und ich hatte den Prozess zu töten.
Coneybeare
0

Verwenden Sie JOINs, keine Unterabfragen. Hat einer Ihrer Unterabfrageversuche geholfen?

SHOW CREATE TABLE klingt \ G.

SHOW CREATE TABLE Bewertungen \ G.

Oft ist es vorteilhaft, "zusammengesetzte" Indizes zu haben, keine einspaltigen. Vielleicht INDEX (Typ, created_at)

Sie filtern nach beiden Tabellen in einem JOIN. Das ist wahrscheinlich ein Leistungsproblem.

Es gibt ungefähr 1500 Sounds und 2 Millionen Bewertungen.

Empfehlen Sie, dass Sie eine auto_increment-ID aktiviert haben ratings, erstellen Sie eine Übersichtstabelle und verwenden Sie die AI-ID, um zu verfolgen, wo Sie " aufgehört " haben. Speichern Sie jedoch keine Durchschnittswerte in einer Übersichtstabelle:

avg (reviews.rating) AS avg_rating,

Behalten Sie stattdessen die SUMME (reviews.rating) bei. Der Durchschnitt der Durchschnittswerte ist für die Berechnung eines Durchschnitts mathematisch falsch. (Summe der Summen) / (Summe der Zählungen) ist korrekt.

Rick James
quelle