Mysql
 sql >> Datenbank >  >> RDS >> Mysql

Wie kann ich eine abgeleitete Tabellenabfrage weiter optimieren, die besser abschneidet als das JOINed-Äquivalent?

Nun, ich habe eine Lösung gefunden. Es brauchte viel Experimentieren und ich denke, ein gutes bisschen blindes Glück, aber hier ist es:

CREATE TABLE magic ENGINE=MEMORY
SELECT
  s.shop_id AS shop_id,
  s.id AS shift_id,
  st.dow AS dow,
  st.start AS start,
  st.end AS end,
  su.user_id AS manager_id
FROM shifts s
JOIN shift_times st ON s.id = st.shift_id
JOIN shifts_users su ON s.id = su.shift_id
JOIN shift_positions sp ON su.shift_position_id = sp.id AND sp.level = 1

ALTER TABLE magic ADD INDEX (shop_id, dow);

CREATE TABLE tickets_extra ENGINE=MyISAM
SELECT 
  t.id AS ticket_id,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.created) = m.dow
    AND TIME(t.created) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_created,
  (
    SELECT m.manager_id
    FROM magic m
    WHERE DAYOFWEEK(t.resolved) = m.dow
    AND TIME(t.resolved) BETWEEN m.start AND m.end
    AND m.shop_id = t.shop_id
  ) AS manager_resolved
FROM tickets t;
DROP TABLE magic;

Ausführliche Erklärung

Jetzt werde ich erklären, warum dies funktioniert, und meinen relativen Prozess und die Schritte, um hierher zu gelangen.

Erstens wusste ich, dass die Abfrage, die ich versuchte, unter der riesigen abgeleiteten Tabelle und den darauf folgenden JOINs litt. Ich nahm meine gut indizierte tickets-Tabelle und verknüpfte alle shift_times-Daten damit, ließ dann MySQL darauf herumkauen, während es versuchte, die shifts- und shift_positions-Tabellen zu verknüpfen. Dieser abgeleitete Gigant wäre ein nicht indiziertes Durcheinander von bis zu 2 Millionen Zeilen.

Jetzt wusste ich, dass dies geschah. Der Grund, warum ich diesen Weg eingeschlagen habe, war, dass der "richtige" Weg, dies zu tun, die ausschließliche Verwendung von JOINs, noch länger dauerte. Dies liegt an dem hässlichen Chaos, das erforderlich ist, um festzustellen, wer der Manager einer bestimmten Schicht ist. Ich muss mich mit shift_times verbinden, um herauszufinden, was die richtige Schicht überhaupt ist, während ich mich gleichzeitig mit shift_positions verbinden muss, um das Level des Benutzers herauszufinden. Ich glaube nicht, dass der MySQL-Optimierer damit sehr gut umgeht und am Ende eine RIESIGE Monstrosität einer temporären Tabelle der Joins erstellt und dann herausfiltert, was nicht zutrifft.

Da die abgeleitete Tabelle der "Weg zu gehen" zu sein schien, beharrte ich eine Weile hartnäckig darauf. Ich habe versucht, es in eine JOIN-Klausel zu stecken, keine Verbesserung. Ich habe versucht, eine temporäre Tabelle mit der abgeleiteten Tabelle darin zu erstellen, aber es war wieder zu langsam, da die temporäre Tabelle nicht indiziert war.

Mir wurde klar, dass ich mit dieser Berechnung von Schicht, Zeiten und Positionen vernünftig umgehen musste. Ich dachte, vielleicht wäre ein VIEW der richtige Weg. Was wäre, wenn ich eine ANSICHT erstellt hätte, die diese Informationen enthält:(shop_id, shift_id, dow, start, end, manager_id). Dann müsste ich einfach nach shop_id und der gesamten DAYOFWEEK/TIME-Berechnung in die tickets-Tabelle eintreten, und ich wäre im Geschäft. Natürlich habe ich vergessen, dass MySQL mit VIEWs ziemlich ungeschickt umgeht. Es materialisiert sie überhaupt nicht, es führt einfach die Abfrage aus, die Sie verwendet hätten, um die Ansicht für Sie zu erhalten. Indem ich also Tickets damit verknüpfte, führte ich im Wesentlichen meine ursprüngliche Abfrage aus – keine Verbesserung.

Anstelle einer VIEW habe ich mich also für eine TEMPORARY TABLE entschieden. Dies funktionierte gut, wenn ich jeweils nur einen der Manager (erstellt oder gelöst) abgerufen habe, aber es war immer noch ziemlich langsam. Außerdem habe ich herausgefunden, dass Sie mit MySQL in derselben Abfrage nicht zweimal auf dieselbe Tabelle verweisen können (ich müsste meine temporäre Tabelle zweimal verknüpfen, um zwischen manager_created und manager_resolved unterscheiden zu können). Das ist eine große WTF, da ich es tun kann, solange ich nicht "TEMPORARY" spezifiziere - hier kam die CREATE TABLE Magic ENGINE=MEMORY ins Spiel.

Mit dieser pseudotemporären Tabelle in der Hand habe ich meinen JOIN für nur manager_created erneut versucht. Es hat gut funktioniert, aber immer noch ziemlich langsam. Als ich jedoch erneut beitrat, um manager_resolved in derselben Abfrage zu erhalten, stieg die Abfragezeit wieder in die Stratosphäre. Ein Blick auf EXPLAIN zeigte wie erwartet den vollständigen Tabellenscan der Tickets (Zeilen ~2 Millionen) und die JOINs auf dem magischen Tisch bei jeweils ~2.087. Wieder schien ich zu scheitern.

Ich begann jetzt darüber nachzudenken, wie ich die JOINs ganz vermeiden könnte, und da fand ich einen obskuren alten Message-Board-Beitrag, in dem jemand vorschlug, Subselects zu verwenden (ich kann den Link in meinem Verlauf nicht finden). Dies führte zu der zweiten oben gezeigten SELECT-Abfrage (der Erstellung von tickets_extra). Bei der Auswahl nur eines Managerfeldes hat es gut funktioniert, aber bei beiden war es wieder Mist. Ich habe mir EXPLAIN angesehen und Folgendes gesehen:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 173825
        Extra: 
*************************** 2. row ***************************
           id: 3
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
*************************** 3. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: m
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 2037
        Extra: Using where
3 rows in set (0.00 sec)

Ack, die gefürchtete DEPENDENT SUBQUERY. Es wird oft empfohlen, diese zu vermeiden, da MySQL sie normalerweise von außen nach innen ausführt und die innere Abfrage für jede Zeile der äußeren ausführt. Ich ignorierte dies und fragte mich:"Nun ... was wäre, wenn ich diese dumme magische Tabelle einfach indizieren würde?". So wurde der ADD-Index (shop_id, dow) geboren.

Sehen Sie sich das an:

mysql> CREATE TABLE magic ENGINE=MEMORY
<snip>
Query OK, 3220 rows affected (0.40 sec)

mysql> ALTER TABLE magic ADD INDEX (shop_id, dow);
Query OK, 3220 rows affected (0.02 sec)

mysql> CREATE TABLE tickets_extra ENGINE=MyISAM
<snip>
Query OK, 1933769 rows affected (24.18 sec)

mysql> drop table magic;
Query OK, 0 rows affected (0.00 sec)

Jetzt DAS IST wovon ich rede!

Schlussfolgerung

Dies ist definitiv das erste Mal, dass ich spontan eine nicht-TEMPORARY-Tabelle erstellt und spontan INDEXiert habe, einfach um eine einzelne Abfrage effizient durchzuführen. Ich glaube, ich bin immer davon ausgegangen, dass das Hinzufügen eines Indexes im laufenden Betrieb eine unerschwinglich teure Operation ist. (Das Hinzufügen eines Indexes zu meiner Tickets-Tabelle mit 2 Millionen Zeilen kann über eine Stunde dauern). Doch für nur 3.000 Zeilen ist dies ein Kinderspiel.

Haben Sie keine Angst vor DEPENDENT SUBQUERIES, dem Erstellen von TEMPORARY-Tabellen, die es wirklich nicht sind, der schnellen Indizierung oder Aliens. Sie alle können in der richtigen Situation gute Dinge sein.

Danke für all die Hilfe StackOverflow. :-D