2016-05-27 19 views
2

Wir aktualisieren unsere DB-Systeme auf MySQL 5.7 aus MySQL 5.6 und seit dem Upgrade ein paar Abfragen wurden sehr langsam ausgeführt.MySQL JOIN Filterung auf WHERE-Klausel mit < > Operatoren seit Umzug von MySQL 5.6 -> 5.7

Nach einigen Nachforschungen haben wir es auf ein paar JOIN-Abfragen beschränkt, die plötzlich die 'WHERE' Klausel nicht mehr hören, wenn ein 'größer als'> oder 'kleiner als' < Operator verwendet wird. Wenn Sie einen '=' Operator verwenden, funktioniert es wie erwartet. Bei der Abfrage einer großen Tabelle führte dies zu einer konstanten CPU-Auslastung von 100%.

Die Abfragen wurden vereinfacht, um das Problem zu erklären; bei der Verwendung erklären wir die folgenden Ausgaben erhalten:

explain 
     select * from TableA as A 
       left join 
       (
        select 
         DATE_FORMAT(created_at,'%H:%i:00') as `time` 
        FROM 
         TableB 
        WHERE 
         created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR) 
       ) 
       as V ON V.time = A.time 

Ausgabe

Wie Sie sehen können, ist es die Abfrage/488.389 Reihen passend und nicht mit der where-Klausel, da dies die Gesamt ist Datensätze in dieser Tabelle.

Und jetzt die gleiche Abfrage, aber mit einem LIMIT 99999999 Befehl oder mit dem '=' Operator ausgeführt wird:

explain 
     select * from TableA as A 
       left join 
       (
        select 
         DATE_FORMAT(created_at,'%H:%i:00') as `time` 
        FROM 
         TableB 
        WHERE 
         created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR) LIMIT 999999999 
       ) 
       as V ON V.time = A.time 

Ausgabe

id select_type table partitions type possible_keys key key_len ref rows filtered Extra 
1 PRIMARY A NULL ALL NULL NULL NULL NULL 10080 100.00 NULL 
1 PRIMARY <derived2> NULL ALL NULL NULL NULL NULL 244194 100.00 Using where; Using join buffer (Block Nested Loop) 
2 DERIVED TableB NULL range created_at created_at 4 NULL 244194 100.00 Using where; Using index 

Sie können es sehen, nur plötzlich zusammenbringt '244194' Zeilen, die ein Teil der Tabelle ist, oder mit dem '=' Operator:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra 
1 SIMPLE A NULL ALL NULL NULL NULL NULL 10080 100.00 NULL 
1 SIMPLE TableB NULL ref created_at created_at 4 const 1 100.00 Using where; Using index 

Nur 1 Reihe, wie erwartet.

So ist die Frage jetzt ist, haben wir in einer falschen Weise wurde die Abfrage und jetzt nur noch herauszufinden, während oder haben Dinge da 5.6 MySQL geändert Upgrade? Es scheint seltsam, dass der = Operator funktioniert, aber die < und > sind aus irgendeinem Grunde ignoriert, es sei denn, wenn ein LIMIT? ..

Wir um gesucht haben und könnten die nicht gefunden Ursache dieses Problems, und wir verwenden die Lösung 9999999 Limit aus offensichtlichen Gründen lieber nicht in unserem Code.

Hinweis Wenn nur die Abfrage in der Verknüpfung ausgeführt wird, funktioniert es wie erwartet.

Hinweis Wir haben auch den gleichen Test auf MariaDB 10.1 ausgeführt, dasselbe Problem.

+0

Haben Sie die Indizes aktualisiert? Es behauptet, den Index zu verwenden, – Kickstart

+0

Ja, Indizes sind vorhanden und aktualisiert auf die Felder in Frage – Nick

Antwort

1

In MySQL 5.7, abgeleitete Tabellen (Unterabfragen in der FROM-Klausel) wird in die äußere Abfrage, wenn möglich zusammengeführt werden. Dies ist normalerweise ein Vorteil, da vermieden wird, dass das Ergebnis der Unterabfrage in einer temporären Tabelle gespeichert wird. Für Ihre Anfrage, MySQL 5.6 erstellt einen Index für diese temporäre Tabelle, der für die Join-Ausführung verwendet werden kann.

Das Problem mit der zusammengeführten Abfrage besteht darin, dass der Index für TableB.created_at nicht verwendet werden kann, wenn die Spalte ein Parameter für eine Funktion ist. Wenn Sie die Abfrage so ändern können, dass die Umwandlung in die Spalte auf der linken Seite des Joins vorgenommen wird, kann über einen Index auf die Tabelle auf der rechten Seite zugegriffen werden. Etwas wie:

select * from TableA as A 
      left join 
      (
       select created_at as time 
       FROM TableB 
       WHERE created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR) 
      ) 
      as V ON V.time = func(A.time) 

Alternativ, wenn Sie links innere Verknüpfung statt beitreten verwenden können, kann MySQL umgekehrte Reihenfolge verbinden, so dass der Index auf tableA.time kann für die Verbindung verwendet werden.

Wenn die Unterabfrage LIMIT verwendet, kann sie nicht zusammengeführt werden. Daher erhalten Sie mit LIMIT den gleichen Abfrageplan wie in MySQL 5.6.

+0

Danke! Dies führte schließlich zur Lösung unseres Problems :-) – Nick

1

Die explainrow -Ausgabe ist nur eine Schätzung, wie viele Zeilen es trifft. Es basiert auf statistischen Daten, die mit Ihrem Update zurückgesetzt wurden. Und wenn ich raten müsste, wie viele Reihen all Ihrer vorhandenen Reihen älter sind als gestern, 21 Uhr, würde ich auch erraten, dass es näher an "allen Reihen" ist als an "nur ein paar Reihen".Der Grund, warum "Limit 99999999" eine andere Anzahl von Zeilen anzeigt, ist die gleiche: Es wird nur vermutet, dass das Limit eine Wirkung hat; In diesem Fall, mysql rät es wird genau die Hälfte der Zeilen (was wäre, wenn wahr, ein seltsamer Zufall), und natürlich, es sieht nicht wirklich auf den Grenzwert, da 999999999 wird nicht begrenzen alles, wenn Sie nur 500k Zeilen haben; und sogar die "1" im Fall von "=" ist nur eine Vermutung (und könnte öfter 0 sein als 1 und vielleicht manchmal mehr).

Diese Schätzung hilft Ihnen bei der Auswahl des richtigen Ausführungsplans, und wenn Sie bei dieser Vermutung falsch liegen, ist das nur ein Problem, wenn es den falschen auswählen würde. Ihr Ausführungsplan sieht jedoch gut aus und es gibt nicht viele Möglichkeiten, dies anders zu machen. Es funktioniert genau wie erwartet: Scannen Sie den Index für alle Daten mit dem Index für created_at. Da Sie einen linken Join ausführen, können Sie keine Werte von überspringen, auch wenn Sie mit der inneren Abfrage beginnen würden, so dass wirklich kein alternativer Ausführungsplan verfügbar ist. (Der Optimierer wurde tatsächlich in 5.7. Geändert, aber hier ist kein Effekt.)

Wenn das Ihre eigentliche Abfrage ist, gibt es keinen wirklichen Grund, warum es langsamer als vorher sein sollte (nur in Bezug auf diese Abfrage Es gibt natürlich eine Menge allgemeiner Leistungsoptionen, die eine indirekte Wirkung haben können, wie Caching-Strategien, Buffergrößen, ..., aber mit Standardoptionen sollte es hier keine Wirkung haben).

Wenn nicht, und Sie z.B. tatsächlich verwenden zusätzliche Spalten von TableB in der Unterabfrage (es ist oft schwer zu raten, welche vielleicht wichtige Dinge in Fragen "vereinfacht" worden sind), und benötigen daher Zugriff auf die tatsächliche Tabelle, es hängt davon ab, wie Ihre Daten strukturiert sind (oder besser: in welcher Reihenfolge hast du es hinzugefügt). Und Sie könnten versuchen Optimize table TableB, um Ihre Tabelle und Indizes frisch und neu zu machen, kann es nicht schaden (aber wird Ihren Tisch für eine Weile sperren).

Mit mysql 5.7. Können Sie jetzt generierte Spalten hinzufügen, so dass es vielleicht einen Versuch wert wäre, eine bereinigte Spalte time as DATE_FORMAT(created_at,'%H:%i:00') zu generieren, so dass Sie es nicht mehr berechnen müssen. Und vielleicht fügen Sie es Ihrem Index hinzu, also müssen Sie es nicht mehr sortieren, um die block nested join zu verbessern, aber das kann von Ihrer tatsächlichen Frage abhängen und wie oft Sie es verwenden (Spamming-Indizes erhöhen den Overhead und nutzen Speicherplatz).

+0

Danke für die Info, aber wenn ich die oben genannten Abfragen ausführen, sehe ich die gleichen Ergebnisse, 230ms mit der Grenze 99999 Klausel und über 3-4 Minuten ohne es. Während die gleichen Abfragen auch bei MySQL 5.6 um ~ 230ms enden. Ich habe versucht, beide Tabellen zu optimieren, leider ohne Erfolg. Wir könnten tatsächlich in die generierten Spalten schauen, nur ohne Indizes, da diese Abfragen jede Minute ausgeführt werden. – Nick

+1

@Nick Nun, sollte es nicht. Mit einer Schätzung bei ~ 30mb/s HDD-Geschwindigkeit, 4 Byte pro Zeile (im Index, und mysql sagt, dass es den Index verwendet), würde man 500k Werte 2700 mal in 180 Sekunden lesen (nicht mitgerechnet, dass es danach zwischengespeichert wird) das erste gelesen), also ist etwas anderes los. Wenn du glücklich bist, dass es mit 'limit' funktioniert, ist es in Ordnung, andernfalls versuche:' set @@ profiling = 1; Mach deine Anfrage; Zeige Profil; Mach deine zweite Anfrage; Zeige Profil; zeige Profile an; Stellen Sie @@ profiling = 1; 'ein, um zu sehen, wofür die Zeit ausgegeben wird. Und führen Sie Ihre 'limit'-Abfrage zuerst aus/führen Sie sie zweimal aus, damit sie nicht vom Cache begünstigt wird. – Solarflare

0

Verwenden Sie JOIN anstelle von LEFT JOIN, es sei denn, Sie benötigen die 'richtige' Tabelle als Option.

Vermeiden Sie JOIN (SELECT ...). Obwohl 5.6 und 5.7 einige Funktionen hinzugefügt haben, ist es normalerweise besser, die Unterabfrage in eine einfachere JOIN umzuwandeln.

Ihr Zeitausdruck führt gestern zu 9pm; Meinst du "3 Stunden"?

Sehen Sie, wenn diese die gewünschten Ergebnisse liefert und läuft schneller:

select A.*, DATE_FORMAT(B.created_at,'%H:%i:00') as `time` 
    from TableA as A 
    JOIN TableB as B ON B.time = A.time 
    WHERE B.created_at < NOW() - INTERVAL 3 HOUR -- (assuming "3 hours ago") 

Was 5.6 vs 5.7 ... 5.7 verfügt über einen neuen, ‚besseren‘, Optimierer auf Basis eines „Kostenmodell“. Ihre spezielle Abfrage macht es dem Optimierer jedoch nahezu unmöglich, gute Kosten zu erzielen. Ich denke, dass 5.6 auf dem besseren EXPLAIN geschah, und 5.7 geschah auf einem schlechteren. Durch die Vereinfachung der Abfrage werden beide Optimierer eine bessere Chance haben, die Abfrage schneller auszuführen.

Sie tun, um diese Indizes benötigen:

B: INDEX(time, created_at) -- in that order 
A: INDEX(time)