MySQL 5: Functions oder Views vs. Indizes
in HTML, MySQL, PHP | Tags: Cache, Indizes, MySQL, Performance, Profiling, Sortierung, Stored Procedure, ViewMein Chef meinte zu unserem neuen Server, der sei “irgendwie lahm” . Ich hatte nach dem ersten Hochspielen unseres neuen Onlineshops eigentlich genau das Gegenteil als Erfahrung gemacht. Tatsächlich, die Seite war lahm. Vielleicht nur in seinem neuen IE7? Naja, im ewigen Streben nach mehr Geschwindigkeit habe ich mich diese Woche also ein wenig mit Indizes und Views sowie Funtionen (UDFs) in MySQL beschäftigt, um den Shop so gut wie möglich auf Trab zu bringen.
Zuerst suchte ich eine schöne handliche Funktion, die mir eine Seitenstruktur aufwärts traversiert, um meine eigene, recht grobgeschnitzte Routine zur Anzeige der Navigation abzulösen.
Also erstmal nach CREATE FUNCTION gegoogelt, denn mein Buch PHP5/MySQL4 umschifft gekonnt dieses technische Terroir. Das Google-Ergebnis war frustrierend: Der wichtigste Link war schließlich die offizielle MySQL-Doku, in den Groups gab es ein paar Beiträge auf polnisch, was ich leider gar nicht verstehe.
Die auf der MySQL-Doku angegebene Syntax-Beschreibung waren abschreckend kompliziert dargestellt. Da muss jemand denken “Eine EBNF sagt mehr als 1000 Worte”. Da hat er wohl recht – wenn man einmal in der Materie drin ist. Wenn mans nicht ist, siehts ganz düster aus.
Eine andere Seite geht dafür den Top-down-Approach, auch mein Traversal-Problem ist hier als Beispiel-Lösung angegeben (juhu! dachte ich erst). Super einfühlsam, hier bleibt man jedoch nach den Überschriften etwas stecken. Denn das Beispiel funktionierte nicht.
Nach etwas Fiddelei stellte sich bei mir heraus: Zunächst war die Syntax “DELIMITER ;” nicht korrekt, dann muss man der Funktion immerhin noch sagen, ob diese READ SQL DATA oder anderen Typs ist. Nach der Syntax dann die Anweisung: Die Abbruchbedingung der WHILE-Schleife haute gar nicht hin. Angeblich soll ja diese Routine in MySQL-Syntax zu beherrschen sein, aber eine WHILE (NOT ISNULL(variable)) scheints nicht zu geben, hier rennt man in eine Endlosschleife. Mit Ctrl-C schießt man dabei leider nur seinen Client ab, der Server läuft mit dem Prozess dann in aller Ruhe weiter. Andere Bedingungen wie “(variable>)” stoppten bei NULL-Werten ebensowenig.
Nach ein paar Versuchen hatte der Server dann einen Load von 9.0 und MySQL eine Prozessorlast von konstant 200 Prozent erreicht. Mein vi hing, die Buchstaben waren eher per Fuß als per ssh auf dem Server. Mühsam schaffte ich es noch per top zu sehen, wer dran schuld war, so dass ich dann MySQL einfach stoppte und neu startete (die anderen in der Firma hatten hoffentlich gerade nix im Intranet zu tun:-)).
Also das war schon mal ein kleiner Reinfall. Meine Routine blieb.
Heute dann mehr Glück, wenn auch mit einem anderen Thema: Views und Indexes. Ich hatte zwischenzeitlich die Shop-Homepage mit einer Menge von Profiling-Anweisungen ausgestattet und konnte so die einzelnen Arbeitsschritte genau analysieren. Erstaunlich schnell konnte ich die “wunden Punkte” erkennen.
Mein erstes Fazit war: Lange Dateien per include in PHP-Code zu laden ist nicht schneller, als wenn es zwei kleinere Dateien sind. Meine Arbeitshypothese lautete bis dato noch: Weniger Dateien zu öffnen dauert nicht so lange wie ein sequentieller Lesevorgang einer Datei. Da lag ich wohl falsch, und bin nun froh, dass sinnvolles Strukturieren von include-Dateien “erlaubt” ist.
Zweites Fazit: Meine Arbeitshypothese, dass ich in Zukunft auf Apache’s mod_mem_cache setzen sollte, damit die ständig geladenen Seitenanteile nicht bei jedem Aufruf von der Platte gelesen werden müssen, ist hinfällig geworden: Nur 10% der gesamten Server-Bearbeitungszeit geht auf das Laden von Seiten zurück:
[PROFILING] => Array
(
[0] => Start
[1] => 0.0000: session.inc.php geladen (0.0000)
[2] => 0.0002: request.inc.php geladen (0.0002)
[3] => 0.0004: ssl.inc.php geladen (0.0002)
[4] => 0.0005: locale.inc.php geladen (0.0001)
[5] => 0.0009: sage.inc.php geladen (0.0004)
[6] => 0.0034: basic.inc.php geladen (0.0026)
[7] => 0.0036: waehrungen.inc.php geladen (0.0002)
[8] => 0.0040: db.inc.php geladen (0.0003)
[9] => 0.0043: db verbunden (0.0004)
[10] => 0.0043: Sortierung berechnet (0.0000)
[11] => 0.0066: konto.inc.php geladen (0.0023)
[12] => 0.0071: aktionen.inc.php geladen (0.0005)
[13] => 0.0074: menue_js.inc.php geladen (0.0003)
[14] => 0.0084: left.php geladen (0.0010)
[15] => 0.0084: goInsite(); (0.0000)
[16] => 0.0091: kategorie.inc.php geladen (0.0008)
[17] => 0.0096: Kopf ausgegeben (0.0005)
[18] => 0.4466: Artikel gefunden (0.4369)
[19] => 0.4471: Sortieren/Blaettern ausgegeben (0.0005)
[20] => 0.4521: Artikel ausgegeben (0.0050)
[21] => 0.4523: Blaettern ausgegeben (0.0001)
[22] => 0.4524: Entering _post.php (0.0001)
[23] => 0.4525: footer.inc.php geladen (0.0001)
[24] => 0.4584: right.inc.php geladen (0.0059)
[25] => 0.4600: menue.inc.php geladen (0.0016)
[26] => 0.4605: Seitenende (0.0005)
)
Hier sieht man schnell: von den 460 Millisekunden gehen 436 für die Artikelsuche in der Datenbank drauf.
Nach der Einführung von Indizes für einige Bereiche der Website und der Einführung eines Views für die Darstellung aller zur Zeit aktiven Artikel sieht das Ergebnis gleich ganz anders aus:
[PROFILING] => Array
(
[0] => Start
[1] => 0.0000: session.inc.php geladen (0.0000)
[2] => 0.0002: request.inc.php geladen (0.0002)
[3] => 0.0004: ssl.inc.php geladen (0.0002)
[4] => 0.0005: locale.inc.php geladen (0.0001)
[5] => 0.0009: sage.inc.php geladen (0.0004)
[6] => 0.0035: basic.inc.php geladen (0.0026)
[7] => 0.0037: waehrungen.inc.php geladen (0.0002)
[8] => 0.0040: db.inc.php geladen (0.0003)
[9] => 0.0044: db verbunden (0.0004)
[10] => 0.0044: Sortierung berechnet (0.0000)
[11] => 0.0067: konto.inc.php geladen (0.0023)
[12] => 0.0072: aktionen.inc.php geladen (0.0005)
[13] => 0.0074: menue_js.inc.php geladen (0.0003)
[14] => 0.0084: left.php geladen (0.0009)
[15] => 0.0088: goInsite(); (0.0004)
[16] => 0.0096: kategorie.inc.php geladen (0.0008)
[17] => 0.0102: Kopf ausgegeben (0.0006)
[18] => 0.0106: Artikel gefunden (0.0004)
[19] => 0.0111: Sortieren/Blaettern ausgegeben (0.0005)
[20] => 0.0142: Artikel ausgegeben (0.0031)
[21] => 0.0144: Blaettern ausgegeben (0.0001)
[22] => 0.0145: Entering _post.php (0.0001)
[23] => 0.0146: footer.inc.php geladen (0.0001)
[24] => 0.0205: right.inc.php geladen (0.0059)
[25] => 0.0236: menue.inc.php geladen (0.0031)
[26] => 0.0236: Seitenende (0.0000)
[max] => Array
(
[0] => 0.00587892532349
[1] => right.inc.php geladen
)
)
Und so wurden aus 461 ms 24 ms Gesamtladezeit. 🙂
Ich muss aber dazu sagen, dass die Einführung eines Views selbst zu keiner nennenswerten Performance-Steigerung führte. Die dabei gemessenen Ergebnisse sahen genauso, wenn nicht noch schlechter als die Abfrage der Originaltabelle aus. Erst die Indizes bringen die Performance, im obigen Beispiel ist dies immerhin eine Reduktion auf 5% der Original-Antwortzeit. Das eigentliche Problem, nämlich 1-2 Sekunden Rendering der Seite im IE7, ist damit (natürlich rein statistisch) auf 0.6-1.4 Sekunden geschrumpft. Dennoch werden wir hier nochmal ran, und diesmal den CSS-Code studieren müssen.
PS: Ich habe das View schließlich doch benutzt, damit mein Code einfacher und leichter wartbar wird. Eventuell werde ich den View noch auf die zur Artikelsuche relevanten Felder begrenzen, und die Messung damit wiederholen. Die großen Unterschiede wird es jedoch nicht mehr machen können. Und die Abweichungen jeder Messung sind augenscheinlich. Zweimal nacheinander eine ähnlich Abfrage bringt allein durch das Caching der Datenbank alle Ergebnisse durcheinander.
well done 🙂