Kapitel 7. Optimierung

Inhaltsverzeichnis

7.1. Überblick über die Optimierung
7.1.1. MySQL: konzeptionelle Einschränkungen
7.1.2. Applikationskonzepte unter Beachtung von Portabilitätsaspekten
7.1.3. Wofür benutzen wir MySQL?
7.1.4. Die MySQL-Benchmark-Reihe
7.1.5. Wie Sie Ihre eigenen Benchmarks benutzen
7.2. SELECT-Anweisungen und andere Anfragen optimieren
7.2.1. EXPLAIN-Syntax (Informationen über ein SELECT erhalten)
7.2.2. Anfragenperformance abschätzen
7.2.3. Geschwindigkeit von SELECT-Anweisungen
7.2.4. Optimierungen der WHERE-Klausel
7.2.5. Bereichsoptimierung
7.2.6. Optimierung durch Indexverschmelzung
7.2.7. IS NULL-Optimierung
7.2.8. Optimierung von DISTINCT
7.2.9. Optimierung von LEFT JOIN und RIGHT JOIN
7.2.10. Optimierung verschachtelter Joins
7.2.11. Vereinfachungsmöglichkeit für äußere Joins
7.2.12. ORDER BY-Optimierung
7.2.13. GROUP BY-Optimierung
7.2.14. LIMIT-Optimierung
7.2.15. Vermeidung von Tabellenscans
7.2.16. Geschwindigkeit von INSERT-Anweisungen
7.2.17. Geschwindigkeit von UPDATE-Anweisungen
7.2.18. Geschwindigkeit von DELETE-Anfragen
7.2.19. Weitere Optimierungstipps
7.3. Probleme mit Sperren
7.3.1. Wie MySQL Tabellen sperrt
7.3.2. Themen, die Tabellensperren betreffen
7.3.3. Gleichzeitige Einfügevorgänge
7.4. Optimierung der Datenbankstruktur
7.4.1. Überlegungen zum Datenbankdesign
7.4.2. Wie Sie Ihre Daten so klein wie möglich bekommen
7.4.3. Spaltenindizes
7.4.4. Mehrspaltige Indizes
7.4.5. Wie MySQL Indizes benutzt
7.4.6. Der MyISAM-Schlüssel-Cache
7.4.7. Sammlung von MyISAM-Indexstatistiken
7.4.8. Nachteile der Erzeugung großer Mengen von Tabellen in derselben Datenbank
7.4.9. Warum gibt es so viele offene Tabellen?
7.5. Optimierung des MySQL Servers
7.5.1. System/Kompilierzeitpunkt und Tuning der Startparameter
7.5.2. Serverparameter feineinstellen
7.5.3. Leistung des Abfragenoptimierers steuern
7.5.4. Wie Kompilieren und Linken die Geschwindigkeit von MySQL beeinflusst
7.5.5. Wie MySQL Speicher benutzt
7.5.6. Wie MySQL DNS benutzt
7.6. Festplatte, Anmerkungen
7.6.1. Symbolische Verknüpfungen

Die Optimierung ist eine komplexe Aufgabe, denn sie setzt letztendlich ein umfassendes Verständnis des gesamten zu optimierenden Systems voraus. Zwar ist es möglich, in eingeschränktem Maße lokale Optimierungen ohne umfangreiche Kenntnisse Ihres Systems oder Ihrer Anwendung vorzunehmen, aber je optimaler Sie Ihr System gestalten wollen, desto mehr müssen Sie darüber wissen.

In diesem Kapitel werden wir versuchen, die verschiedenen Möglichkeiten zur Optimierung von MySQL zu erläutern und entsprechende Beispiele zu geben. Bedenken Sie aber, dass es stets zusätzliche Möglichkeiten gibt, das System zu beschleunigen, wobei dabei jedoch immer auch der entsprechende Aufwand zu berücksichtigen ist.

7.1. Überblick über die Optimierung

Der wesentlichste Faktor bei der Beschleunigung eines Systems ist sein grundsätzlicher Aufbau. Ferner müssen Sie wissen, welche Bearbeitungen Ihr System durchführt und wo Engpässe vorliegen. In den meisten Fällen treten Engpässe aufgrund folgender Ursachen auf:

  • Suchvorgänge auf der Festplatte. Die Festplatte braucht Zeit, um Daten zu finden. Bei modernen Festplatten beträgt die durchschnittliche Dauer eines Suchvorgangs weniger als 10 Millisekunden, d. h., wir können von einem theoretischen Wert von 100 Suchvorgängen pro Sekunden ausgehen. Dieser Wert lässt sich durch neue Festplatten nur geringfügig verbessern und ist für nur eine einzige Tabelle sehr schwierig zu optimieren. Die Verteilung von Daten auf mehrere Festplatten ist jedoch geeignet, die Suchdauer zu verbessern.

  • Lese- und Schreiboperationen auf Festplatte. Wenn die Festplatte die korrekte Position erreicht hat, müssen die Daten gelesen werden. Moderne Festplatten liefern einen Datendurchsatz von mindestens 10 bis 20 Mbyte/s. Dieser Wert ist einfacher zu optimieren als der Suchwert, da parallel von mehreren Festplatten gelesen werden kann.

  • Prozessorzyklen. Wenn die Daten im Hauptspeicher gelandet sind, müssen wir sie verarbeiten, um das gewünschte Ergebnis zu erhalten. Der am meisten einengende Faktor ist das Vorhandensein von im Vergleich zum verfügbaren Speicher kleinen Tabellen. Bei kleinen Tabellen wiederum ist Geschwindigkeit jedoch nicht das Problem.

  • Speicherbandbreite. Wenn der Prozessor mehr Daten benötigt, als in den Prozessor-Cache passen, dann wird die Bandbreite des Hauptspeichers zum Engpass. Dies kommt zwar bei den meisten Systemen höchst selten vor, sollte jedoch immer in Betracht gezogen werden.

7.1.1. MySQL: konzeptionelle Einschränkungen

Wenn Sie die MyISAM-Speicher-Engine verwenden, benutzt MySQL eine extrem schnelle Tabellensperrung, die mehrere lesende Benutzer oder einen schreibenden Benutzer gestattet. Das größte Problem bei dieser Speicher-Engine tritt auf, wenn ein konstanter Strom von gemischten Änderungs- und langsamen Auswahlanweisungen für eine einzelne Tabelle vorhanden ist. Wenn dies bei bestimmten Tabellen ein Problem darstellt, können Sie für diese eine andere Speicher-Engine verwenden. Siehe auch Kapitel 14, Speicher-Engines und Tabellentypen.

MySQL kann sowohl mit transaktionssicheren als auch mit nichttransaktionssicheren Tabellen arbeiten. Um die reibungslose Arbeit mit nichttransaktionssicheren Tabellen (also Tabellen, für die im Fehlerfall kein Rollback durchgeführt werden kann) zu erleichtern, unterstützt MySQL die folgenden Regeln. Beachten Sie, dass diese Regeln nur dann gültig sind, wenn MySQL nicht im strikten SQL-Modus ausgeführt wird oder Sie IGNORE für INSERT- oder UPDATE-Anweisungen angegeben haben.

  • Für alle Spalten gibt es Vorgabewerte.

  • Wenn Sie einen unpassenden oder ungültigen Wert in eine Spalte eingeben, setzt MySQL diesen automatisch auf den „bestmöglichen“ Wert, statt einen Fehler zu melden. Bei numerischen Werten ist dies entweder 0 oder der kleinst- oder größtmögliche Wert. Bei Strings wird entweder der Leer-String oder der Bestandteil des Strings benutzt, der noch in der Spalte gespeichert werden kann.

  • Alle berechneten Ausdrücke geben einen Wert zurück, der verwendet werden kann, statt eine Fehlerbedingung auszugeben. So gibt etwa 1 ÷ 0 NULL zurück.

Um obiges Verhalten zu ändern, können Sie eine striktere Datenverarbeitung aktivieren, indem Sie den SQL-Modus auf dem Server entsprechend ändern. Weitere Informationen zum Umgang mit Daten finden Sie in Abschnitt 1.9.6, „Wie MySQL mit Constraints umgeht“, Abschnitt 5.2.5, „Der SQL-Modus des Servers“, und Abschnitt 13.2.4, „INSERT.

7.1.2. Applikationskonzepte unter Beachtung von Portabilitätsaspekten

Da alle SQL-Server unterschiedliche Teile des SQL-Standards implementieren, ist die Entwicklung portierbarer Datenbankanwendungen sehr aufwändig. Bei sehr simplen Auswahl- und Einfügeoperationen mag die einfache Portierbarkeit noch gegeben sein, aber je mehr Funktionalitäten Sie benötigen, umso komplexer wird sie. Wenn Sie eine Anwendung brauchen, die auf vielen verschiedenen Datenbanksystemen schnell ist, wird es noch schwieriger.

Alle Datenbanksysteme haben Schwachpunkte: Entwicklungsseitig enthalten sie Kompromisse, die ein unterschiedliches Verhalten verursachen.

Um eine komplexe Anwendung portierbar zu machen, müssen Sie zunächst ermitteln, auf welchen SQL-Servern sie laufen muss, und dann bestimmen, welche Funktionen diese Server unterstützen. Sie können mit dem MySQL-Programm crash-me Funktionen, Typen und Einschränkungen ermitteln, die für eine Auswahl von Datenbankservern gelten. crash-me prüft nicht alle denkbaren Funktionen, ist aber mit etwa 450 Tests vergleichsweise umfassend. Ein Beispiel für die Art von Informationen, die sich mit crash-me ermitteln lassen, ist etwa die Tatsache, dass Sie keine Spaltennamen verwenden sollten, die mehr als 18 Zeichen umfassen, wenn Sie Informix oder DB2 einsetzen wollen.

Das Programm crash-me und die MySQL-Benchmarks hängen stark von der jeweiligen Datenbank ab. Sehen Sie sich einmal an, wie sie geschrieben sind, um ein Gefühl dafür erhalten, was Sie tun müssen, um Ihre eigenen Anweisungen so datenbankunabhängig wie möglich zu machen. Sie finden die Programme im Verzeichnis sql-bench der MySQL-Quelldistributionen. Sie sind in Perl geschrieben und verwenden die DBI-Datenbankschnittstelle. Bereits die Verwendung von DBI löst das Portabilitätsproblem teilweise, denn die Schnittstelle bietet datenbankübergreifende Zugriffsmethoden. Siehe auch Abschnitt 7.1.4, „Die MySQL-Benchmark-Reihe“.

Wenn Sie nach Datenbankunabhängigkeit streben, brauchen Sie ein gutes Gespür für die Engpässe auf den einzelnen SQL-Servern. So ist MySQL beispielsweise sehr schnell, wenn es um das Abrufen und Aktualisieren von Datensätzen in MyISAM-Tabellen geht, hat aber Probleme bei Fällen, in denen gleichzeitig langsame Lese- und Schreibvorgänge verwaltet werden müssen. Oracle andererseits zeigt sich ausgesprochen widerspenstig, wenn Sie auf Datensätze zugreifen wollen, die Sie kürzlich aktualisiert (und noch nicht auf Festplatte synchronisiert) haben. Transaktionssichere Datenbanksysteme sind generell nicht besonders gut zur Erstellung von Zusammenfassungstabellen aus Logtabellen geeignet, weil in diesem Fall die Sperrung von Datensätzen praktisch nutzlos ist.

Um Ihre Anwendung wirklich datenbankunabhängig zu machen, sollten Sie eine leicht zu erweiternde Schnittstelle definieren, über die Sie Ihre Daten manipulieren. C++ beispielsweise ist auf den meisten Systemen verfügbar, weswegen es sinnvoll ist, eine Datenbankschnittstelle auf Basis einer C++-Klasse zu verwenden.

Wenn Sie die eine oder andere Funktion verwenden, die für ein gegebenes Datenbanksystem spezifisch ist (z. B. die MySQL-Anweisung REPLACE), dann sollten Sie dieselbe Funktion für andere SQL-Server implementieren, indem Sie eine alternative Methode einkodieren. Auch wenn diese Alternative langsamer ist, so erlaubt sie es anderen Servern doch, dieselben Aufgaben auszuführen.

Bei MySQL können Sie mit der Syntax /*! */ MySQL-spezifische Schlüsselwörter zu einer Anweisung hinzufügen. Der Code in /* */ wird von den meisten anderen SQL-Servern als Kommentar betrachtet (und insofern ignoriert). Informationen zum Schreiben von Kommentaren finden Sie in Abschnitt 9.4, „Kommentar“.

Wenn die Leistungsfähigkeit wichtiger ist als die Genauigkeit (wie es beispielsweise bei Webanwendungen der Fall ist), dann können Sie eine Anwendungsschicht erstellen, die alle Ergebnisse zwischenspeichert, um die Performance noch besser zu optimieren. Indem Sie ältere Ergebnisse nach einer bestimmten Zeit ungültig werden lassen, können Sie den Cache ausreichend aktuell halten. Auf diese Weise können Sie Belastungsspitzen abfangen, indem Sie die Cache-Größe dynamisch erhöhen und den Ungültigkeitszeitpunkt nach hinten verschieben, bis sich die Belastung wieder normalisiert hat.

In diesem Fall sollten die Angaben zur Tabellenerstellung Informationen zur ursprünglichen Cache-Größe und dazu enthalten, wie oft die Tabelle normalerweise aktualisiert wird.

Eine interessante Alternative zur Implementierung eines Anwendungs-Caches ist die Verwendung des Abfrage-Caches von MySQL. Wenn Sie den Abfrage-Cache aktivieren, bestimmt der Server selbst, ob ein Abfrageergebnis wiederverwendet werden kann. Dies vereinfacht Ihre Anwendung. Siehe auch Abschnitt 5.14, „MySQL-Anfragen-Cache“.

7.1.3. Wofür benutzen wir MySQL?

Dieser Abschnitt beschreibt eine sehr frühe Anwendung für MySQL.

Im Verlauf der ursprünglichen Entwicklung von MySQL wurden die Funktionen an die Anforderungen unseres größten Kunden angepasst, der das Data-Warehousing für einige der großen schwedischen Handelshäuser implementierte.

Von allen Läden erhielten wir wöchentliche Übersichten über alle Kundenkartentransaktionen, und es wurde von uns verlangt, brauchbare Informationen für Geschäftsinhaber bereitzustellen, damit diese feststellen konnten, wie sich ihre Werbekampagnen auf das Kaufverhalten ihrer Kunden auswirkten.

Das Datenvolumen war gewaltig (ca. sieben Millionen Transaktionen pro Monat), und wir hatten Daten für vier bis zehn Jahre, die wir den Benutzern präsentieren sollten. Allwöchentlich erhielten wir Anfragen von unseren Kunden, die sofortigen Zugriff auf neue, auf diesen Daten basierende Berichte benötigten.

Dieses Problem lösten wir, indem wir alle Daten eines Monats in komprimierten „Transaktionstabellen“ speicherten. Wir hatten eine Anzahl einfacher Makros, die aus den Tabellen, in denen die Transaktionen gespeichert waren, nach verschiedenen Kriterien (Produktgruppe, Kundennummer, Filiale usw.) gruppierte Übersichtstabellen erstellten. Diese Berichte waren Webseiten, die mithilfe eines kleinen Perl-Skripts dynamisch erzeugt wurden. Das Skript analysierte eine Webseite, führte die enthaltenen SQL-Anweisungen aus und fügte die Ergebnisse ein. Wir hätten stattdessen sicher PHP oder mod_perl verwendet, aber diese waren seinerzeit noch nicht verfügbar.

Für die grafische Ausgabe schrieben wir ein kleines Tool in C, das SQL-Abfrageergebnisse verarbeiten und auf Basis der Ergebnisse GIF-Grafiken erstellen konnte. Auch dieses Tool wurde von dem Perl-Skript, welches die Webseiten analysierte, dynamisch ausgeführt.

In den meisten Fällen konnte ein neuer Bericht einfach erstellt werden, indem man ein vorhandenes Skript kopierte und die von ihm verwendete SQL-Abfrage einfach anpasste. In manchen Fällen mussten wir neue Spalten zu einer vorhandenen Übersichtstabelle hinzufügen oder eine neue Tabelle erstellen. Auch dies war ganz einfach, weil wir alle Tabellen mit den Transaktionen auf Festplatte speicherten. (In der Summe hatten wir dann 50 Gbyte Transaktionstabellen sowie 200 Gbyte weiterer Kundendaten.)

Außerdem gestatteten wir unseren Kunden via ODBC den direkten Zugriff auf die Übersichtstabellen, sodass Kunden bei Bedarf selbst mit den Daten experimentieren konnten.

Dieses System funktionierte gut, und wir hatten keine Probleme mit der Verwaltung der Daten auf einer eher bescheidenen Hardware (Sun Ultra SPARCstation, 2 × 200 MHz). Am Ende wurde das System dann auf Linux migriert.

7.1.4. Die MySQL-Benchmark-Reihe

Anhand dieser Benchmark-Reihe kann jeder Benutzer ermitteln, welche Operationen eine gegebene SQL-Implementierung gut oder weniger gut ausführt. Sie können sich einen guten Eindruck davon verschaffen, wie die Benchmarks funktionieren, indem Sie sich den Code und die Ergebnisse im Verzeichnis sql-bench einer beliebigen MySQL-Quelldistribution ansehen.

Beachten Sie, dass diese Benchmark nur einen Thread verwendet – sie misst also die Mindestdauer der durchgeführten Operationen. Wir planen, die Benchmark-Reihe in Zukunft durch Multithread-Tests zu ergänzen.

Um die Benchmark-Reihe verwenden zu können, müssen folgende Voraussetzungen erfüllt sein:

  • Die Benchmark-Reihe wird als Bestandteil der MySQL-Quelldistributionen ausgeliefert. Sie können entweder eine Release-Version von http://dev.mysql.com/downloads/ herunterladen oder den aktuellen Entwicklungs-Source-Tree verwenden. (Siehe auch Abschnitt 2.8.3, „Installation vom Entwicklungs-Source-Tree“.)

  • Die Benchmark-Skripten sind in Perl geschrieben und verwenden das Perl-DBI-Modul für den Zugriff auf Datenbankserver; insofern muss DBI installiert sein. Ferner benötigen Sie die serverspezifischen BDB-Treiber für jeden Server, den Sie testen wollen. Um beispielsweise MySQL, PostgreSQL und DB2 prüfen zu können, müssen die Module DBD::mysql, DBD::Pg und DBD::DB2 installiert sein. Siehe auch Abschnitt 2.13, „Anmerkungen zur Perl-Installation“.

Nachdem Sie sich eine MySQL-Quelldistribution besorgt haben, finden Sie die Benchmark-Reihe im dortigen Verzeichnis sql-bench. Um die Benchmark-Tests durchzuführen, erstellen Sie MySQL und wechseln dann in das Verzeichnis sql-bench. Dort führen Sie das Skript run-all-tests aus:

 shell> cd sql-bench shell>
perl run-all-tests
--server=server_name 

server_name sollte der Name eines der unterstützten Server sein. Um eine Liste aller Optionen und unterstützten Server zu erhalten, rufen Sie folgenden Befehl auf:

 shell> perl run-all-tests --help

Das Skript crash-me befindet sich ebenfalls im Verzeichnis sql-bench. crash-me versucht durch Ausführung von Abfragen zu ermitteln, welche Funktionen ein Datenbanksystem unterstützt und welche Fähigkeiten und Einschränkungen es aufweist. Beispielsweise wird ermittelt,

  • welche Datentypen unterstützt werden,

  • wie viele Indizes unterstützt werden,

  • welche Funktionen unterstützt werden,

  • wie groß eine Abfrage sein darf,

  • wie groß eine VARCHAR-Spalte sein darf.

Die Ergebnisse von crash-me finden Sie für viele verschiedene Datenbankserver unter http://dev.mysql.com/tech-resources/crash-me.php. Weitere Informationen zu Benchmark-Ergebnissen finden Sie unter http://dev.mysql.com/tech-resources/benchmarks/.

7.1.5. Wie Sie Ihre eigenen Benchmarks benutzen

Sie sollten Ihre Anwendung und Ihre Datenbank in jedem Fall mit der Benchmark-Reihe prüfen, um festzustellen, wo ggf. Engpässe vorhanden sind. Wenn Sie einen Engpass behoben (oder ihn durch ein „Dummymodul“ ersetzt) haben, können Sie mit der Suche nach weiteren Engpässen fortfahren. Auch wenn die Gesamtleistung für Ihre Anwendung derzeit akzeptabel sein sollte, sollten Sie zumindest einen Plan für jeden Engpass erstellen und entscheiden, wie Sie ihn bei Bedarf beseitigen, sofern dieser Bedarf eines Tages auftreten sollte.

Beispiele für portable Benchmark-Programme finden Sie in der MySQL-Benchmark-Reihe. Siehe auch Abschnitt 7.1.4, „Die MySQL-Benchmark-Reihe“. Sie können ein beliebiges Programm dieser Reihe nehmen und an Ihre eigenen Bedürfnisse anpassen. Dies erlaubt Ihnen, verschiedene Lösungen für Ihr Problem auszuprobieren und zu ermitteln, welche wirklich die schnellste ist.

Eine weitere kostenlose Benchmark-Reihe ist die Open Source Database Benchmark, die Sie unter http://osdb.sourceforge.net/ erhalten.

Häufig treten Probleme nur dann auf, wenn das System sehr stark ausgelastet ist. Viele unserer Kunden wenden sich an uns, wenn sie ein (getestetes) Produktionssystem haben, welches unter Last Ausfallerscheinungen zeigt. In den meisten Fällen werden leistungsbezogene Probleme durch Faktoren des grundlegenden Datenbankdesigns (etwa im Fall von Schwächen bei Tabellenscans unter Last) oder durch Probleme mit dem Betriebssystem oder mit Bibliotheken verursacht. Meistens würden sich diese Probleme wesentlich einfacher beheben lassen, wenn das jeweilige System nicht bereits in der Produktion verwendet würde.

Um derartige Probleme zu vermeiden, sollten Sie den Aufwand auf sich nehmen und Benchmark-Tests Ihrer Anwendung unter maximaler Last durchführen:

Wie die Namen dieser Programme bereits suggerieren, besteht ihr Zweck darin, Ihr System in die Knie zu zwingen. Es sollte also klar sein, dass Sie diese Programme nur auf Entwicklungssystemen einsetzen.

7.2. SELECT-Anweisungen und andere Anfragen optimieren

Zunächst einmal gibt es einen Faktor, der sich auf alle Anweisungen auswirkt: Je komplexer Ihre Berechtigungskonfiguration ist, desto höher ist der Aufwand für MySQL. Die Verwendung einfacherer Berechtigungen beim Absetzen von GRANT-Anweisungen gestattet MySQL eine Verringerung der berechtigungsbedingten Mehrbelastung, wenn Clients Anweisungen ausführen. Wenn Sie beispielsweise keine Berechtigungen auf Tabellen- oder Spaltenebene gewähren, muss der Server keinerlei Überprüfung der Tabellen tables_priv und columns_priv durchführen. Analog ist auch keine Ressourcenzählung durchzuführen, wenn Sie die Ressourcen für kein Konto beschränken. Wenn Sie eine sehr hohe Auslastung durch die Anweisungsverarbeitung haben, kann es lohnenswert sein, eine vereinfachte Gewährungsstruktur zu verwenden, um die Mehrbelastung durch die Überprüfung von Berechtigungen zu verringern.

Hängt Ihr Problem mit einem bestimmten MySQL-Ausdruck oder einer MySQL-Funktion zusammen, dann können Sie eine Timingprüfung durchführen, indem Sie die Funktion BENCHMARK() mithilfe des Clientprogramms mysql aufrufen. Die Syntax lautet BENCHMARK(loop_count,expression). Der Rückgabewert ist immer null, aber mysql gibt eine Zeile aus, die näherungsweise die Ausführungsdauer der Anweisung angibt. Zum Beispiel:

mysql> SELECT BENCHMARK(1000000,1+1);
+------------------------+
| BENCHMARK(1000000,1+1) |
+------------------------+
|                      0 |
+------------------------+
1 row in set (0.32 sec)

Dieses Ergebnis wurde mit einem Pentium II-System (400 MHz) ermittelt. Es zeigt, dass MySQL auf diesem System 1.000.000 einfache Additionsausdrücke in 0,32 Sekunden ausführen kann.

Eigentlich sollten alle MySQL-Funktionen hochgradig optimiert sein, aber es gibt unter Umständen ein paar Ausnahmen. BENCHMARK() ist ein hervorragendes Tool, um zu ermitteln, ob irgendeine Funktion für Ihre Abfragen problematisch sein könnte.

7.2.1. EXPLAIN-Syntax (Informationen über ein SELECT erhalten)

 EXPLAIN tbl_name 

Oder:

 EXPLAIN [EXTENDED | PARTITIONS] SELECT
select_options 

Die EXPLAIN-Anweisung kann entweder als Synonym für DESCRIBE oder als Möglichkeit verwendet werden, um Informationen bezüglich der Frage zu ermitteln, wie MySQL eine SELECT-Anweisung ausführt:

  • EXPLAIN tbl_name ist synonym zu DESCRIBE tbl_name oder SHOW COLUMNS FROM tbl_name.

  • Wenn Sie einer SELECT-Anweisung das Schlüsselwort EXPLAIN voranstellen, zeigt MySQL Informationen des Optimierers zum Ausführungsplan der Abfrage an. MySQL erläutert also, wie es die SELECT-Anweisung verarbeiten würde, und gibt zudem an, wie und in welcher Reihenfolge Tabellen miteinander verknüpft werden.

  • EXPLAIN PARTITIONS ist seit MySQL 5.1.5 verfügbar. Es ist nur praktisch, wenn Sie Abfragen untersuchen, die sich auf partitionierte Tabellen beziehen. Detaillierte Informationen finden Sie in Abschnitt 17.3.4, „Abruf von Informationen über Partitionen“.

Dieser Abschnitt beschreibt die zweite Verwendungsmöglichkeit von EXPLAIN: die Anforderung von Informationen zum Abfrageausführungsplan. Eine Beschreibung der Anweisungen DESCRIBE und SHOW COLUMNS finden Sie in Abschnitt 13.3.1, „DESCRIBE (Informationen über Spalten abrufen)“, und Abschnitt 13.5.4.3, „SHOW COLUMNS.

Mithilfe von EXPLAIN können Sie ermitteln, wo Sie Indizes für Tabellen konfigurieren sollten, um SELECT-Anweisungen zu beschleunigen, die Datensätze mithilfe von Indizes finden. Sie können mit EXPLAIN auch überprüfen, ob der Optimierer die Tabellen in optimaler Reihenfolge verknüpft. Um den Optimierer zur Verwendung einer Verknüpfungsreihenfolge zu zwingen, die der Reihenfolge entspricht, in der die Tabellen in der SELECT-Anweisung aufgeführt sind, beginnen Sie die Anweisung mit SELECT STRAIGHT_JOIN statt mit SELECT.

Wenn das Problem auftritt, dass Indizes nicht benutzt werden, obwohl Sie annehmen, dass dies eigentlich der Fall sein sollte, dann führen Sie ANALYZE TABLE aus, um die Tabellenstatistiken wie etwa die Kardinalität der Schlüssel, die sich auf die durch den Optimierer vorgenommene Auswahl auswirken kann, zu aktualisieren. Siehe auch Abschnitt 13.5.2.1, „ANALYZE TABLE.

EXPLAIN gibt je einen Datensatz für jede in der SELECT-Anweisung verwendete Tabelle zurück. Die Tabellen sind in der Ausgabe in der Reihenfolge aufgeführt, in der MySQL sie bei der Verarbeitung der Abfrage lesen würde. MySQL löst alle Joins mithilfe einer Methode auf, die Single-Sweep-Multi-Join heißt. Hierbei liest MySQL einen Datensatz aus der ersten Tabelle und sucht dann einen passenden Datensatz in der zweiten Tabelle, der dritten Tabelle usw. Sind alle Tabellen verarbeitet, dann gibt MySQL die gewählten Spalten aus und durchsucht die Tabellenliste in umgekehrter Reihenfolge, bis eine Tabelle gefunden wird, bei der mehr passende Datensätze vorhanden sind. Der nächste Datensatz wird aus genau dieser Tabelle gelesen, und dann wird der Prozess mit der nächsten Tabelle fortgesetzt.

Wenn das Schlüsselwort EXTENDED verwendet wird, erzeugt EXPLAIN zusätzliche Informationen, die durch Absetzen einer SHOW WARNINGS-Anweisung nach der EXPLAIN-Anweisung angezeigt werden können. Diese Informationen geben Hinweise zum Optimierungsprozess, z. B. wie der Optimierer Tabellen- und Spaltennamen in der SELECT-Anweisung qualifiziert oder wie die SELECT-Anweisung nach der Anwendung der Umformulierungs- und Optimierungsregeln aussieht.

Hinweis: Sie können die Schlüsselwörter EXTENDED und PARTITIONS nicht gemeinsam in derselben EXPLAIN-Anweisung verwenden.

Jeder von EXPLAIN ausgegebene Datensatz enthält Angaben zu genau einer Tabelle. Dabei enthält jeder Datensatz die folgenden Spalten:

  • id

    Der SELECT-Bezeichner. Dies ist die Sequenznummer der SELECT-Anweisung innerhalb der Abfrage.

  • select_type

    Der Typ der SELECT-Anweisung. Dies kann jeder der in der folgenden Tabelle aufgeführten Typen sein:

    SIMPLEeinfache SELECT-Anweisung (ohne UNION oder Unterabfragen).
    PRIMARYäußerste SELECT-Anweisung.
    UNIONzweite oder spätere SELECT-Anweisung in einer UNION.
    DEPENDENT UNIONzweite oder spätere SELECT-Anweisung in einer UNION, abhängig von der äußeren Abfrage.
    UNION RESULTErgebnis einer UNION.
    SUBQUERYerste SELECT-Anweisung in einer Unterabfrage.
    DEPENDENT SUBQUERYerste SELECT-Anweisung in einer Unterabfrage, abhängig von der äußeren Abfrage.
    DERIVEDabgeleitete Tabellen-SELECT-Anweisung (Unterabfrage in FROM-Klausel).

    DEPENDENT bezeichnet normalerweise die Verwendung einer korrelierten Unterabfrage. Siehe auch Abschnitt 13.2.8.7, „Korrelierte Unterabfragen“.

  • table

    Die Tabelle, die der Ausgabedatensatz referenziert.

  • type

    Der Join-Typ. Die verschiedenen Join-Typen sind nachfolgend aufgelistet, sortiert vom besten bis zum schlechtesten:

    • system

      Diese Tabelle hat nur einen Datensatz (Systemtabelle). Dies ist ein Sonderfall des Join-Typs const.

    • const

      Die Tabelle hat maximal einen passenden Datensatz, der beim Start der Abfrage gelesen wird. Da nur ein Datensatz vorhanden ist, können die Werte aus den Spalten dieses Datensatzes vom Optimierer im Folgenden als Konstanten behandelt werden. const-Tabellen sind sehr schnell, da sie nur einmal gelesen werden.

      const wird verwendet, wenn Sie alle Teile eines Primärschlüssels oder eines eindeutigen Indexes mit Konstantenwerten vergleichen. In der folgenden Abfrage kann tbl_name als const-Tabelle verwendet werden:

       SELECT * FROM tbl_name WHERE
      primary_key=1;
      
      SELECT * FROM tbl_name WHERE
      primary_key_part1=1 AND
      primary_key_part2=2; 
    • eq_ref

      Für jede Datensatzkombination aus den vorherigen Tabellen wird genau ein Datensatz aus dieser Tabelle gelesen. Anders als die Typen system und const ist dies der beste Join-Typ. Er wird verwendet, wenn alle Teile eines Indexes vom Join verwendet werden und der Index ein Primärschlüssel oder ein eindeutiger Index ist.

      eq_ref kann für indizierte Spalten benutzt werden, die mithilfe des Operators = verglichen werden. Der Vergleichswert kann eine Konstante oder ein Ausdruck sein, der Spalten aus Tabellen verwendet, die vor dieser Tabelle gelesen wurden. In den folgenden Beispielen kann MySQL einen eq_ref-Join zur Verarbeitung von ref_table verwenden:

      SELECT * FROM ref_table,other_table
        WHERE ref_table.key_column=other_table.column;
      
      SELECT * FROM ref_table,other_table
        WHERE ref_table.key_column_part1=other_table.column
        AND ref_table.key_column_part2=1;
      
    • ref

      Alle Datensätze mit passenden Indexwerten werden aus dieser Tabelle für jede Kombination von Datensätzen in den vorherigen Tabellen gelesen. ref wird benutzt, wenn der Join nur ein linkes Präfix des Schlüssels verwendet oder der Schlüssel kein Primärschlüssel und auch kein eindeutiger Index ist (d. h., wenn der Join keinen einzelnen Datensatz basierend auf dem Schlüsselwert auswählen kann). Wenn der verwendete Schlüssel nur einigen wenigen Datensätzen entspricht, ist dies ein guter Join-Typ.

      ref kann für indizierte Spalten benutzt werden, die mithilfe der Operatoren = oder <=> verglichen werden. In den folgenden Beispielen kann MySQL einen ref-Join zur Verarbeitung von ref_table verwenden:

       SELECT * FROM ref_table WHERE
      key_column=expr;
      
      SELECT * FROM
      ref_table,other_table
      WHERE
      ref_table.key_column=other_table.column;
      
      SELECT * FROM
      ref_table,other_table
      WHERE
      ref_table.key_column_part1=other_table.column
      AND
      ref_table.key_column_part2=1;
      
    • ref_or_null

      Dieser Join-Typ ähnelt ref weitgehend, allerdings führt MySQL hierbei eine zusätzliche Suche nach Datensätzen durch, die NULL-Werte enthalten. Diese Join-Typ-Optimierung wird meistens bei der Auflösung von Unterabfragen verwendet. In den folgenden Beispielen kann MySQL einen ref_or_null-Join zur Verarbeitung von ref_table verwenden:

       SELECT * FROM ref_table WHERE
      key_column=expr OR
      key_column IS NULL; 

      Siehe auch Abschnitt 7.2.7, „IS NULL-Optimierung“.

    • index_merge

      Dieser Join-Typ gibt an, dass die Indexverschmelzungsoptimierung verwendet wird. In diesem Fall enthält die Spalte key im ausgegebenen Datensatz eine Liste der verwendeten Indizes, und key_len enthält eine Liste der längsten Schlüsselteile für die verwendeten Indizes. Weitere Informationen finden Sie unter Abschnitt 7.2.6, „Optimierung durch Indexverschmelzung“.

    • unique_subquery

      Dieser Typ ersetzt ref in einigen IN-Unterabfragen der folgenden Form:

       value IN (SELECT
      primary_key FROM
      single_table WHERE
      some_expr) 

      unique_subquery ist eine Nachschlagefunktion für Indizes, die zur Effizienzsteigerung die Unterabfrage vollständig ersetzt.

    • index_subquery

      Dieser Join-Typ ähnelt unique_subquery. Er ersetzt IN-Unterabfragen, funktioniert aber bei nichteindeutigen Indizes in Unterabfragen folgender Form:

       value IN (SELECT
      key_column FROM
      single_table WHERE
      some_expr) 
    • range

      Es werden nur Datensätze abgerufen, die in einem gegebenen Bereich liegen. Sie werden anhand eines Indexes ausgewählt. Die Spalte key im Ausgabedatensatz zeigt an, welcher Index verwendet wird. key_len enthält den längsten verwendeten Schlüsselteil. Die Spalte ref ist für diesen Typ NULL.

      range kann verwendet werden, wenn eine Schlüsselspalte unter Verwendung eines der Operatoren =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN oder IN mit einer Konstante verglichen wird:

      SELECT * FROM tbl_name
        WHERE key_column = 10;
      
      SELECT * FROM tbl_name
        WHERE key_column BETWEEN 10 and 20;
      
      SELECT * FROM tbl_name
        WHERE key_column IN (10,20,30);
      
      SELECT * FROM tbl_name
        WHERE key_part1= 10 AND key_part2 IN (10,20,30);
      
    • index

      Dieser Join-Typ ist mit ALL bis auf die Tatsache identisch, dass nur der Indexbaum gescannt wird. Insofern ist er in der Regel schneller als ALL, weil die Indexdatei gewöhnlich kleiner ist als die Datendatei.

      MySQL kann diesen Join-Typ verwenden, wenn die Abfrage nur Spalten benutzt, die Teil eines einzelnen Indexes sind.

    • ALL

      Ein vollständiger Tabellenscan wird für jede Kombination von Datensätzen aus den vorherigen Tabellen durchgeführt. Dies ist normalerweise nicht von Vorteil, wenn die Tabelle die erste nicht als const gekennzeichnete Tabelle ist, und in allen anderen Fällen sogar ganz schlimm. Sie können ALL normalerweise vermeiden, indem Sie Indizes hinzufügen, die das Abrufen von Datensätzen aus der Tabelle basierend auf Konstanten- oder Spaltenwerten aus früheren Tabellen erlauben.

  • possible_keys

    Die Spalte possible_keys gibt an, unter welchen Indizes MySQL auswählen kann, um Datensätze in dieser Tabelle zu suchen. Beachten Sie, dass diese Spalte vollständig unabhängig von der Reihenfolge der Tabellen ist, wie sie in der Ausgabe von EXPLAIN angezeigt wird. Das bedeutet, dass einige der Schlüssel in possible_keys mit der erzeugten Tabellenreihenfolge unter Umständen praktisch nicht verwendbar sind.

    Wenn diese Spalte NULL ist, gibt es keine relevanten Indizes. In diesem Fall können Sie die Leistungsfähigkeit Ihrer Abfrage möglicherweise verbessern, indem Sie die WHERE-Klausel daraufhin untersuchen, ob sie eine oder mehrere Spalten referenziert, die für die Indexerstellung geeignet wären. Sollte dies der Fall sein, so erstellen Sie einen passenden Index und überprüfen die Abfrage dann mit EXPLAIN erneut. Siehe auch Abschnitt 13.1.2, „ALTER TABLE.

    Um anzuzeigen, welche Indizes eine Tabelle hat, verwenden Sie SHOW INDEX FROM tbl_name.

  • key

    Die Spalte key gibt den Schlüssel(index) an, für dessen Verwendung sich MySQL tatsächlich entschieden hat. Der Schlüssel ist NULL, wenn kein Index ausgewählt wurde. Um das Verwenden oder Ignorieren eines Indexes, der in der Spalte possible_keys aufgeführt ist, durch MySQL zu erzwingen oder zu ignorieren, verwenden Sie FORCE INDEX oder USE INDEX bzw. IGNORE INDEX in Ihrer Abfrage. Siehe auch Abschnitt 13.2.7, „SELECT.

    Bei MyISAM- und BDB-Tabellen erleichtert die Ausführung von ANALYZE TABLE dem Optimierer die Auswahl geeigneter Indizes. Bei MyISAM-Tabellen tut myisamchk --analyze dasselbe. Siehe auch Abschnitt 13.5.2.1, „ANALYZE TABLE, und Abschnitt 5.10.4, „Benutzung von myisamchk für Tabellenwartung und Absturzreparatur“.

  • key_len

    Die Spalte key_len gibt die Länge des Schlüssels an, für dessen Verwendung sich MySQL entschieden hat. Die Länge ist NULL, wenn in der Spalte key NULL steht. Beachten Sie, dass der Wert von key_len Ihnen die Feststellung gestattet, wie viele Teile eines mehrteiligen Schlüssels MySQL tatsächlich verwendet.

  • ref

    Die Spalte ref zeigt an, welche Spalten oder Konstanten mit dem in der Spalte key genannten Index verglichen werden, um Datensätze aus der Tabelle auszuwählen.

  • rows

    Die Spalte rows gibt die Anzahl der Datensätze an, die MySQL glaubt untersuchen zu müssen, um die Abfrage ausführen zu können.

  • Extra

    Diese Spalte enthält zusätzliche Angaben dazu, wie MySQL die Abfrage auflöst. Es folgt eine Erläuterung der Werte, die in dieser Spalte erscheinen können:

    • Distinct

      MySQL sucht nach unterschiedlichen Werten, d. h., die Suche nach weiteren Datensätzen zur aktuellen Datensatzkombination wird beendet, sobald der erste Datensatz gefunden wurde.

    • Not exists

      MySQL konnte eine LEFT JOIN-Optimierung an der Abfrage vornehmen und untersucht für die vorhergehende Datensatzkombination keine weiteren Datensätze in dieser Tabelle, sobald ein Datensatz gefunden wurde, der den LEFT JOIN-Kriterien entspricht. Hier ein Beispiel für einen Abfragetyp, der auf diese Weise optimiert werden kann:

      SELECT * FROM t1 LEFT JOIN t2 ON t1.id=t2.id
        WHERE t2.id IS NULL;
      

      Nehmen wir an, t2.id sei als NOT NULL definiert. In diesem Fall scannt MySQL t1 und sucht die Datensätze in t2 unter Verwendung der Werte aus t1.id heraus. Findet MySQL einen passenden Datensatz in t2, dann weiß es, dass t2.id niemals NULL sein kann, und scannt die weiteren Datensätze in t2, die denselben id-Wert aufweisen, nicht mehr. Für jeden Datensatz in t1 muss MySQL also nur einen einzigen Suchvorgang in t2 durchführen – unabhängig davon, wie viele entsprechende Datensätze in t2 tatsächlich vorhanden sind.

    • range checked for each record (index map: N)

      MySQL hat keinen geeigneten Index gefunden, aber festgestellt, dass einige der Indizes verwendet werden könnten, nachdem Spaltenwerte aus vorhergehenden Tabellen bekannt geworden sind. Für jede Datensatzkombination in den vorherigen Tabellen überprüft MySQL, ob es möglich ist, zum Abrufen von Datensätzen eine der Zugriffsmethoden range oder index_merge zu verwenden. Dies ist nicht sehr schnell, aber immer noch schneller als die Durchführung eines Joins ohne Index. Die Anwendbarkeitskriterien entsprechen den in Abschnitt 7.2.5, „Bereichsoptimierung“, und Abschnitt 7.2.6, „Optimierung durch Indexverschmelzung“, beschriebenen, nur sind hier alle Spaltenwerte für die vorhergehende Tabelle bekannt und werden als Konstanten betrachtet.

    • Using filesort

      MySQL muss einen zusätzlichen Durchlauf vornehmen, um zu ermitteln, wie die Datensätze in sortierter Reihenfolge abgerufen werden können. Diese Sortierung erfolgt, indem alle Datensätze entsprechend dem Join-Typ überprüft und Sortierschlüssel sowie der Zeiger auf den Datensatz für alle Datensätze gespeichert werden, die der WHERE-Klausel entsprechen. Die Schlüssel werden dann sortiert und die Datensätze entsprechend in sortierter Reihenfolge abgerufen. Siehe auch Abschnitt 7.2.12, „ORDER BY-Optimierung“.

    • Using index

      Die Spaltendaten werden ausschließlich unter Verwendung von Angaben im Indexbaum aus der Tabelle abgerufen – es erfolgt kein zusätzlicher Suchvorgang, um jeweils den eigentlichen Datensatz auszulesen. Diese Strategie kann verwendet werden, wenn die Abfrage nur Spalten benutzt, die Teil eines einzelnen Indexes sind.

    • Using temporary

      Um die Abfrage aufzulösen, muss MySQL eine Temporärtabelle zur Aufnahme des Ergebnisses erstellen. Dies geschieht typischerweise, wenn die Abfrage GROUP BY- und ORDER BY-Klauseln enthält, die Spalten unterschiedlich auflisten.

    • Using where

      Mit einer WHERE-Klausel wird festgelegt, welche Datensätze mit der nächsten Tabelle verglichen oder an den Client gesendet werden. Sofern Sie nicht gezielt alle Datensätze aus der Tabelle abrufen oder untersuchen wollen, haben Sie in der Abfrage einen Fehler gemacht, wenn der Wert Extra nicht Using where ist und ALL oder index als Join-Typ für die Tabelle angegeben ist.

      Wollen Sie Ihre Abfragen so schnell wie möglich machen, dann sollten Sie nach den Extra-Werten Using filesort und Using temporary suchen.

    • Using sort_union(…), Using union(…), Using intersect(…)

      Diese Werte geben an, wie Indexscans für den Join-Typ index_merge zusammengefasst werden. Weitere Informationen finden Sie in Abschnitt 7.2.6, „Optimierung durch Indexverschmelzung“.

    • Using index for group-by

      Ähnlich wie die Tabellenzugriffsmethode Using index gibt Using index for group-by an, dass MySQL einen Index gefunden hat, der zum Abrufen aller Datensätze einer GROUP BY- oder DISTINCT-Abfrage ohne zusätzlichen Festplattenzugriff auf die eigentliche Tabelle verwendet werden kann. Außerdem wird der Index auf die effizienteste Art und Weise verwendet, sodass für jede Gruppe nur ein paar wenige Indexeinträge gelesen werden. Detaillierte Informationen finden Sie in Abschnitt 7.2.13, „GROUP BY-Optimierung“.

    • Using where with pushed condition

      Dieses Element ist ausschließlich für NDB Cluster-Tabellen verfügbar. Es bedeutet, dass MySQL Cluster einen Bedingungs-Pushdown verwendet, um die Effizienz eines direkten Vergleichs (=) zwischen einer nichtindizierten Spalte und einer Konstanten zu steigern. In solchen Fällen wird die Bedingung in den Datenknoten des Clusters „eingekellert“, wo sie dann in allen Partitionen gleichzeitig ausgewertet wird. Hierdurch wird der Versand unpassender Datensätze über das Netzwerk vermieden, und die entsprechenden Abfragen werden um den Faktor 5 bis 10 im Vergleich zu Fällen beschleunigt, in denen ein Bedingungs-Pushdown hätte verwendet werden können, aber nicht verwendet wurde.

      Angenommen, eine Cluster-Tabelle sei wie folgt definiert:

      CREATE TABLE t1 (
          a INT, 
          b INT, 
          KEY(a)
      ) ENGINE=NDBCLUSTER;
      

      In diesem Fall kann ein Bedingungs-Pushdown bei einer Abfrage wie der folgenden verwendet werden:

      SELECT a,b FROM t1 WHERE b = 10;
      

      Die Ausgabe von EXPLAIN SELECT sieht dann wie folgt aus:

      mysql> EXPLAIN SELECT a,b FROM t1 WHERE b = 10\G
      *************************** 1. row ***************************
                 id: 1
        select_type: SIMPLE
              table: t1
               type: ALL
      possible_keys: NULL
                key: NULL
            key_len: NULL
                ref: NULL
               rows: 10
              Extra: Using where with pushed condition
      

      Ein Bedingungs-Pushdown kann nicht mit einer der beiden folgenden Abfragen verwendet werden:

      SELECT a,b FROM t1 WHERE a = 10;
      SELECT a,b FROM t1 WHERE b + 1 = 10;
      

      Bezüglich der ersten dieser zwei Abfragen ist der Bedingungs-Pushdown nicht anwendbar, weil in der Spalte a ein Index vorhanden ist. Im Falle der zweiten Abfrage kann ein Bedingungs-Pushdown nicht verwendet werden, weil der Vergleich unter Berücksichtigung der nichtindizierten Spalte b ein indirekter Vergleich ist. (Er wäre allerdings gültig, wenn Sie in der WHERE-Klausel b + 1 = 10 auf b = 9 reduzieren würden.)

      Allerdings kann ein Bedingungs-Pushdown auch verwendet werden, wenn eine indizierte Spalte unter Verwendung eines der Operatoren > oder < mit einer Konstanten verglichen wird:

      mysql> EXPLAIN SELECT a,b FROM t1 WHERE a<2\G
      *************************** 1. row ***************************
                 id: 1
        select_type: SIMPLE
              table: t1
               type: range
      possible_keys: a
                key: a
            key_len: 5
                ref: NULL
               rows: 2
              Extra: Using where with pushed condition
      

      Merken Sie sich in Bezug auf einen Bedingungs-Pushdown Folgendes:

      • Der Bedingungs-Pushdown ist nur für MySQL Cluster relevant und tritt nicht auf, wenn Sie Abfragen unter Verwendung anderer Speicher-Engines an Tabellen absetzen.

      • Bedingungs-Pushdowns werden nicht standardmäßig eingesetzt. Um sie zu aktivieren, können Sie mysqld mit der Option --engine-condition-pushdown oder die folgende Anweisung ausführen:

        SET engine_condition_pushdown=On;
        

Sie erhalten einen geeigneten Anhaltspunkt zu der Frage, wie gut ein Join ist, indem Sie das Produkt der Werte in der Spalte rows der Ausgabe von EXPLAIN erstellen. Hieraus ergibt sich grob, wie viele Datensätze MySQL untersuchen muss, um die Abfrage auszuführen. Wenn Sie die Abfragen mit der Systemvariablen max_join_size beschränken, wird dieses Datensatzprodukt auch benutzt, um zu bestimmen, welche SELECT-Anweisungen für mehrere Tabellen ausgeführt und welche abgebrochen werden. Siehe auch Abschnitt 7.5.2, „Serverparameter feineinstellen“.

Das folgende Beispiel zeigt, wie ein Join mehrerer Tabellen auf der Basis der von EXPLAIN übermittelten Angaben progressiv optimiert werden kann.

Angenommen, Sie hätten die folgende SELECT-Anweisung, die Sie mit EXPLAIN untersuchen wollen:

EXPLAIN SELECT tt.TicketNumber, tt.TimeIn,
               tt.ProjectReference, tt.EstimatedShipDate,
               tt.ActualShipDate, tt.ClientID,
               tt.ServiceCodes, tt.RepetitiveID,
               tt.CurrentProcess, tt.CurrentDPPerson,
               tt.RecordVolume, tt.DPPrinted, et.COUNTRY,
               et_1.COUNTRY, do.CUSTNAME
        FROM tt, et, et AS et_1, do
        WHERE tt.SubmitTime IS NULL
          AND tt.ActualPC = et.EMPLOYID
          AND tt.AssignedPC = et_1.EMPLOYID
          AND tt.ClientID = do.CUSTNMBR;

Für dieses Beispiel gelten die folgenden Annahmen:

  • Die zu vergleichenden Spalten wurden wie folgt deklariert:

    TabelleSpalteDatentyp
    ttActualPCCHAR(10)
    ttAssignedPCCHAR(10)
    ttClientIDCHAR(10)
    etEMPLOYIDCHAR(15)
    doCUSTNMBRCHAR(15)
  • Die Tabellen haben die nachfolgenden Indizes:

    TabelleIndex
    ttActualPC
    ttAssignedPC
    ttClientID
    etEMPLOYID (Primärschlüssel)
    doCUSTNMBR (Primärschlüssel)
  • Die tt.ActualPC-Werte sind nicht gleichmäßig verteilt.

Anfangs – also vor der Durchführung von Optimierungen – erzeugt die EXPLAIN-Anweisung die folgenden Angaben:

table type possible_keys key  key_len ref  rows  Extra
et    ALL  PRIMARY       NULL NULL    NULL 74
do    ALL  PRIMARY       NULL NULL    NULL 2135
et_1  ALL  PRIMARY       NULL NULL    NULL 74
tt    ALL  AssignedPC,   NULL NULL    NULL 3872
           ClientID,
           ActualPC
      range checked for each record (key map: 35)

Da type für jede Tabelle ALL ist, gibt diese Ausgabe an, dass MySQL ein kartesisches Produkt aller Tabellen erzeugt, also jede mögliche Kombination von Datensätzen. Dies dauert recht lang, weil das Produkt der Anzahl der Datensätze in jeder Tabelle untersucht werden muss. Im vorliegenden Fall beträgt dieses Produkt 74 × 2135 × 74 × 3872 = 45.268.558.720 Datensätze. Wenn die Tabellen noch größer wären, kann man sich nur vage vorstellen, wie lange dies dauern würde.

Ein Problem besteht hier darin, dass MySQL Indizes für Spalten effizienter verwenden kann, wenn sie mit demselben Typ und derselben Größe deklariert wurden. In diesem Kontext werden VARCHAR und CHAR als identisch betrachtet, wenn sie mit derselben Größe deklariert werden. tt.ActualPC wird aber als CHAR(10) und et.EMPLOYID als CHAR(15) deklariert – die Längen stimmen also nicht überein.

Um diese Fehlanpassung zwischen den Spaltenlängen aufzuheben, verlängern Sie ActualPC mit ALTER TABLE von 10 auf 15 Zeichen Länge:

mysql> ALTER TABLE tt MODIFY ActualPC VARCHAR(15);

Nun sind tt.ActualPC und et.EMPLOYID beide VARCHAR(15). Die erneute Ausführung der EXPLAIN-Anweisung erzeugt folgende Ausgabe:

table type   possible_keys key     key_len ref         rows    Extra
tt    ALL    AssignedPC,   NULL    NULL    NULL        3872    Using
             ClientID,                                         where
             ActualPC
do    ALL    PRIMARY       NULL    NULL    NULL        2135
      range checked for each record (key map: 1)
et_1  ALL    PRIMARY       NULL    NULL    NULL        74
      range checked for each record (key map: 1)
et    eq_ref PRIMARY       PRIMARY 15      tt.ActualPC 1

Dies ist noch nicht perfekt, aber es ist schon wesentlich besser: Das Produkt der rows-Werte ist bereits um den Faktor 74 geringer – die Ausführung dieser Version dauert nur ein paar Sekunden.

Eine weitere Änderung kann vorgenommen werden, um die Nichtübereinstimmung der Spaltenlängen für die Vergleiche tt.AssignedPC = et_1.EMPLOYID und tt.ClientID = do.CUSTNMBR zu beseitigen:

mysql> ALTER TABLE tt MODIFY AssignedPC VARCHAR(15),
    ->                MODIFY ClientID   VARCHAR(15);

Nach dieser Änderung sieht die Ausgabe von EXPLAIN so aus:

table type   possible_keys key      key_len ref           rows Extra
et    ALL    PRIMARY       NULL     NULL    NULL          74
tt    ref    AssignedPC,   ActualPC 15      et.EMPLOYID   52   Using
             ClientID,                                         where
             ActualPC
et_1  eq_ref PRIMARY       PRIMARY  15      tt.AssignedPC 1
do    eq_ref PRIMARY       PRIMARY  15      tt.ClientID   1

An dieser Stelle ist die Abfrage schon fast perfekt optimiert. Als letztes Problem verbleibt die Tatsache, dass MySQL standardmäßig voraussetzt, dass Werte in der Spalte tt.ActualPC gleichmäßig verteilt sind; dies ist aber bei der Tabelle tt nicht der Fall. Glücklicherweise ist es einfach, MySQL zur Analyse der Schlüsselverteilung zu bewegen:

mysql> ANALYZE TABLE tt;

Mit den zusätzlichen Indexangaben ist der Join perfekt, und EXPLAIN erzeugt folgendes Ergebnis:

table type   possible_keys key     key_len ref           rows Extra
tt    ALL    AssignedPC    NULL    NULL    NULL          3872 Using
             ClientID,                                        where
             ActualPC
et    eq_ref PRIMARY       PRIMARY 15      tt.ActualPC   1
et_1  eq_ref PRIMARY       PRIMARY 15      tt.AssignedPC 1
do    eq_ref PRIMARY       PRIMARY 15      tt.ClientID   1

Beachten Sie, dass die Spalte rows in der Ausgabe von EXPLAIN eine begründete Annahme des MySQL-Join-Optimierers ist. Sie sollten überprüfen, ob die Zahlen annähernd realistisch sind, indem Sie das rows-Produkt mit der tatsächlichen Anzahl der von der Abfrage zurückgegebenen Datensätze vergleichen. Unterscheiden sich diese Werte erheblich, dann erhalten Sie unter Umständen eine bessere Leistung, wenn Sie STRAIGHT_JOIN in Ihrer SELECT-Abfrage verwenden und versuchen, die Tabellen in der FROM-Klausel in einer anderen Reihenfolge aufzulisten.

7.2.2. Anfragenperformance abschätzen

In den meisten Fällen können Sie die Leistungsfähigkeit von Abfragen einschätzen, indem Sie die Anzahl der Suchvorgänge auf der Festplatte ermitteln. Bei kleinen Tabellen finden Sie gewöhnlich pro derartigen Suchvorgang einen Datensatz (weil der Index im Zweifelsfall im Cache liegt). Bei größeren Tabellen können Sie bei der Verwendung von B-Tree-Indizes schätzungsweise die folgende Anzahl von Suchvorgängen annehmen, um einen Datensatz zu finden: log(row_count) / log(index_block_length / 3 × 2 / (index_length + data_pointer_length)) + 1.

In MySQL ist ein Indexblock gewöhnlich 1024 Byte groß, der Datenzeiger umfasst weitere vier Byte. Bei einer Tabelle mit 500.000 Datensätzen und einer Indexlänge von drei Byte (der Größe von MEDIUMINT) gibt die Formel log(500.000/log(1024/3*2/(3+4))+1 = 4 Suchvorgänge aus.

Dieser Index würde eine Speicherkapazität von 500.000 × 7 × 3/2 = 5,2 Mbyte benötigen (ein typisches Indexpuffer-Füllungsverhältnis von 2 : 3 vorausgesetzt), d. h., Sie hätten wahrscheinlich einen Großteil des Indexes im Speicher und benötigen nur ein oder zwei Aufrufe zum Datenlesen, um den Datensatz zu finden.

Bei Schreiboperationen hingegen benötigen Sie vier Suchanforderungen, um zu ermitteln, wo ein neuer Indexwert abgelegt werden muss, und normalerweise zwei Suchvorgänge, um den Index zu aktualisieren und den Datensatz zu schreiben.

Beachten Sie, dass die vorangegangene Beschreibung nicht bedeutet, dass Ihre Anwendungsleistung sich nach und nach um den Logarithmus N verringert. Solange alles vom Betriebssystem oder vom MySQL Server zwischengespeichert wird, verringert sich die Geschwindigkeit nur marginal, wenn die Tabellen größer werden. Sobald die Daten zu umfangreich geworden sind, um zwischengespeichert zu werden, geht alles viel langsamer, bis Ihre Anwendungen nur noch mit Suchvorgängen auf der Festplatte beschäftigt sind (deren Anzahl sich um log N erhöht). Um dies zu vermeiden, müssen Sie die Größe des Schlüssel-Caches erhöhen, wenn der Umfang Ihrer Daten zunimmt. Bei MyISAM-Tabellen wird die Größe des Schlüssel-Caches von der Systemvariablen key_buffer_size gesteuert. Siehe auch Abschnitt 7.5.2, „Serverparameter feineinstellen“.

7.2.3. Geschwindigkeit von SELECT-Anweisungen

Wenn Sie eine langsame SELECT … WHERE-Anweisung beschleunigen wollen, besteht der erste Schritt in aller Regel darin, zu überprüfen, ob Sie einen Index hinzufügen können. Alle Referenzierungen zwischen verschiedenen Tabellen sollten normalerweise über Indizes erfolgen. Sie können mit der EXPLAIN-Anweisung ermitteln, welche Indizes für eine SELECT-Anweisung verwendet werden. Siehe auch Abschnitt 7.2.1, „EXPLAIN-Syntax (Informationen über ein SELECT erhalten)“, und Abschnitt 7.4.5, „Wie MySQL Indizes benutzt“.

Hier ein paar allgemeine Tipps zur Beschleunigung von Abfragen für MyISAM-Tabellen:

  • Um MySQL bei der Optimierung von Abfragen zu unterstützen, verwenden Sie ANALYZE TABLE oder führen myisamchk --analyze für eine Tabelle aus, nachdem Sie die Daten dort eingeladen haben. Hierdurch wird ein Wert für jeden Indexteil geändert, der die durchschnittliche Anzahl von Datensätzen angibt, die denselben Wert haben. (Bei eindeutigen Indizes ist dies immer 1.) MySQL entscheidet anhand dessen, welcher Index ausgewählt wird, wenn Sie zwei Tabellen basierend auf einem nichtkonstanten Ausdruck verknüpfen. Sie können das Ergebnis der Tabellenanalyse mit SHOW INDEX FROM tbl_name und nachfolgender Überprüfung des Werts Cardinality verifizieren. myisamchk --description --verbose zeigt Angaben zur Indexverteilung.

  • Um einen Index und die Daten gemäß diesem Index zu sortieren, verwenden Sie myisamchk --sort-index --sort-records=1 (in diesem Beispiel erfolgt die Sortierung nach Index 1). Dies ist eine gute Möglichkeit, Abfragen zu beschleunigen, wenn Sie einen eindeutigen Index haben, anhand dessen Sie alle Datensätze in der von Index vorgegebenen Reihenfolge auslesen wollen. Beachten Sie, dass, wenn Sie beim ersten Mal eine große Tabelle auf diese Weise sortieren, dies sehr lang dauern kann.

7.2.4. Optimierungen der WHERE-Klausel

Dieser Abschnitt behandelt Optimierungen, die für die Verarbeitung von WHERE-Klauseln durchgeführt werden können. Die Beispiele verwenden SELECT-Anweisungen, dieselben Optimierungen gelten aber auch für WHERE-Klauseln in DELETE- und UPDATE-Anweisungen.

Wir arbeiten fortlaufend am MySQL-Optimierer, weswegen dieser Abschnitt nicht vollständig ist. MySQL führt eine Vielzahl von Optimierungen durch, die nicht alle an dieser Stelle dokumentiert sind.

Es folgen einige von MySQL durchgeführte Optimierungen:

  • Entfernung unnötiger Klammern:

       ((a AND b) AND c OR (((a AND b) AND (c AND d))))
    -> (a AND b AND c) OR (a AND b AND c AND d)
    
  • Umstellen von Konstanten:

       (a<b AND b=c) AND a=5
    -> b>5 AND b=c AND a=5
    
  • Entfernung von Konstantenbedingungen (erforderlich zum Umstellen von Konstanten):

       (B>=5 AND B=5) OR (B=6 AND 5=5) OR (B=7 AND 5=6)
    -> B=5 OR B=6
    
  • Konstantenausdrücke, die von Indizes verwendet werden, werden nur einmal ausgewertet.

  • COUNT(*) für eine einzelne Tabelle ohne WHERE wird direkt aus den Tabellendaten für MyISAM- und MEMORY-Tabellen abgerufen. Dies wird auch für jeden NOT NULL-Ausdruck gemacht, der nur für eine einzige Tabelle verwendet wird.

  • Früherkennung ungültiger Konstantenausdrücke. MySQL erkennt sehr schnell, wenn eine SELECT-Anweisung nicht möglich ist, und gibt keine Datensätze zurück.

  • HAVING wird mit WHERE verschmolzen, wenn Sie GROUP BY nicht verwenden oder Funktionen (wie COUNT(), MIN() usw.) zusammenfassen.

  • Für jede Tabelle in einem Join wird eine einfachere WHERE-Klausel erstellt, um eine schnelle WHERE-Auswertung für die Tabelle zu erhalten und außerdem Datensätze so früh wie möglich zu überspringen.

  • Bevor andere Tabellen in der Abfrage gelesen werden, werden zunächst alle Konstantentabellen ausgelesen. Zu den Konstantentabellen gehören die folgenden:

    • Leere Tabellen oder solche mit nur einem Datensatz.

    • Tabellen, die mit einer WHERE-Klausel für einen Primärschlüssel oder einen eindeutigen Index verwendet werden, wobei alle Indexteile mit Konstantenausdrücken verglichen werden und als NOT NULL definiert sind.

    Alle folgenden Tabellen werden als Konstantentabellen verwendet:

    SELECT * FROM t WHERE primary_key=1;
    SELECT * FROM t1,t2
      WHERE t1.primary_key=1 AND t2.primary_key=t1.id;
    
  • Die beste Join-Kombination zur Verknüpfung der Tabellen wird durch Ausprobieren aller Möglichkeiten ermittelt. Wenn alle Spalten in den ORDER BY- und GROUP BY-Klauseln aus derselben Tabelle stammen, dann wird diese Tabelle beim Verknüpfen bevorzugt.

  • Wenn eine ORDER BY- und eine davon unterschiedliche GROUP BY-Klausel vorhanden sind, oder wenn ORDER BY oder GROUP BY Spalten aus anderen als der ersten Tabelle in der Join-Warteschlange enthalten, dann wird eine Temporärtabelle erstellt.

  • Wenn Sie die Option SQL_SMALL_RESULT verwenden, dann benutzt MySQL eine speicherresidente Temporärtabelle.

  • Alle Tabellenindizes werden abgefragt, und der beste Index wird verwendet, sofern der Optimierer nicht der Ansicht ist, dass die Verwendung eines Tabellenscans effizienter ist. Früher wurde ein Scan basierend darauf eingesetzt, ob der beste Index sich über einen Anteil von mehr als 30 Prozent der Tabelle erstreckte; mittlerweile wird die Frage, ob ein Index oder ein Scan verwendet werden soll, nicht mehr auf der Basis eines festen Prozentwerts entschieden. Der Optimierer ist mittlerweile komplexer, und seine Schätzungen fußen auf weiteren Faktoren wie Tabellengröße, Anzahl der Datensätze und I/O-Blockgröße.

  • In manchen Fällen kann MySQL Datensätze aus dem Index auslesen, ohne überhaupt die Datendatei abfragen zu müssen. Wenn alle aus dem Index verwendeten Spalten numerisch sind, wird zur Auflösung der Abfrage nur der Indexbaum verwendet.

  • Bevor ein Datensatz ausgegeben wird, werden alle Datensätze, die nicht der HAVING-Klausel entsprechen, übersprungen.

Hier einige Beispiele für Abfragen, die sehr schnell sind:

SELECT COUNT(*) FROM tbl_name;

SELECT MIN(key_part1),MAX(key_part1) FROM tbl_name;

SELECT MAX(key_part2) FROM tbl_name
  WHERE key_part1=constant;

SELECT ... FROM tbl_name
  ORDER BY key_part1,key_part2,... LIMIT 10;

SELECT ... FROM tbl_name
  ORDER BY key_part1 DESC, key_part2 DESC, ... LIMIT 10;

MySQL löst die folgenden Abfragen nur mithilfe des Indexbaums auf und setzt dabei voraus, dass die indizierten Spalten numerisch sind:

SELECT key_part1,key_part2 FROM tbl_name WHERE key_part1=val;

SELECT COUNT(*) FROM tbl_name
  WHERE key_part1=val1 AND key_part2=val2;

SELECT key_part2 FROM tbl_name GROUP BY key_part1;

Die folgenden Abfragen verwendet die Indizierung zur Abfrage von Datensätzen in sortierter Reihenfolge ohne separaten Sortierdurchlauf:

SELECT ... FROM tbl_name
  ORDER BY key_part1,key_part2,... ;

SELECT ... FROM tbl_name
  ORDER BY key_part1 DESC, key_part2 DESC, ... ;

7.2.5. Bereichsoptimierung

Die Zugriffsmethode range ruft mithilfe eines einzelnen Indexes eine Teilmenge der Datensätze ab, die in der Tabelle innerhalb eines oder mehrerer Indexwertintervalle liegen. Sie kann für einen ein- oder mehrteiligen Index verwendet werden. Die folgenden Abschnitte erläutern detailliert, wie Intervalle aus der WHERE-Klausel extrahiert werden.

7.2.5.1. Die Bereichszugriffsmethode (Range Access) für Indizes, die aus einzelnen Komponenten bestehen

Bei einem einteiligen Index lassen sich die Indexwertintervalle bequem durch entsprechende Bedingungen in der WHERE-Klausel darstellen. Deswegen sprechen wir in diesem Fall von Bereichsbedingungen statt von „Intervallen“.

Die Definition einer Bereichsbedingung für einen einteiligen Index sieht wie folgt aus:

  • Bei BTREE- und HASH-Indizes ist der Vergleich eines Schlüsselteils mit einem Konstantenwert eine Bereichsbedingung, wenn die Operatoren =, <=>, IN, IS NULL oder IS NOT NULL verwendet werden.

  • Bei BTREE-Indizes ist der Vergleich eines Schlüsselteils mit einem Konstantenwert eine Bereichsbedingung, wenn die Operatoren >, <, >=, <=, BETWEEN, != oder <> verwendet werden, oder aber bei LIKE 'pattern' (wobei 'pattern' nicht mit einem Jokerzeichen beginnt).

  • Bei allen Indextypen bilden mehrere Bereichsbedingungen, die mit OR oder AND kombiniert werden, eine Bereichsbedingung.

In den obigen Erläuterungen bezeichnet „Konstantenwert“ eines der folgenden Elemente:

  • eine Konstante aus dem Abfrage-String

  • eine Spalte aus einer const- oder system-Tabelle aus demselben Join

  • das Ergebnis einer unkorrelierten Unterabfrage

  • jeden Ausdruck, der vollständig aus Unterausdrücken der vorangegangenen Typen zusammengesetzt ist

Es folgen ein paar Beispiele für Abfragen mit Bereichsbedingungen in der WHERE-Klausel:

SELECT * FROM t1
  WHERE key_col > 1 
  AND key_col < 10;

SELECT * FROM t1 
  WHERE key_col = 1 
  OR key_col IN (15,18,20);

SELECT * FROM t1 
  WHERE key_col LIKE 'ab%' 
  OR key_col BETWEEN 'bar' AND 'foo';

Beachten Sie, dass einige nichtkonstante Werte während der Weitergabephase für Konstanten ihrerseits in Konstanten umgewandelt werden könnten.

MySQL versucht, die Bereichsbedingungen für alle möglichen Indizes aus der WHERE-Klausel zu extrahieren. Während des Extraktionsvorgangs werden Bedingungen, die nicht zur Bildung der Bereichsbedingung verwendet werden können, gelöscht; Bedingungen, die überschneidende Bereiche erzeugen, werden kombiniert; schließlich werden Bedingungen, die leere Bereiche erzeugen, entfernt.

Betrachten Sie die folgende Anweisung (hierbei ist key1 eine indizierte Spalte, während nonkey nicht indiziert ist):

SELECT * FROM t1 WHERE
  (key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
  (key1 < 'bar' AND nonkey = 4) OR
  (key1 < 'uux' AND key1 > 'z');

Der Extraktionsprozess für den Schlüssel key1 sieht wie folgt aus:

  1. Am Anfang steht die ursprüngliche WHERE-Klausel:

    (key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
    (key1 < 'bar' AND nonkey = 4) OR
    (key1 < 'uux' AND key1 > 'z')
    
  2. Nun werden nonkey = 4 und key1 LIKE '%b' entfernt, weil sie für einen Bereichsscan nicht verwendet werden können. Die korrekte Vorgehensweise zur Entfernung besteht darin, sie durch TRUE zu ersetzen, damit bei der Durchführung des Bereichsscans keine passenden Datensätze übersehen werden. Nach der Ersetzung mit TRUE erhalten wir Folgendes:

    (key1 < 'abc' AND (key1 LIKE 'abcde%' OR TRUE)) OR
    (key1 < 'bar' AND TRUE) OR
    (key1 < 'uux' AND key1 > 'z')
    
  3. Die folgenden Kollapsbedingungen sind immer wahr oder falsch:

    • (key1 LIKE 'abcde%' OR TRUE) ist immer wahr.

    • (key1 < 'uux' AND key1 > 'z') ist immer falsch.

    Wenn wir diese Bedingungen durch Konstanten ersetzen, erhalten wir Folgendes:

    (key1 < 'abc' AND TRUE) OR (key1 < 'bar' AND TRUE) OR (FALSE)
    

    Durch Entfernen unnötiger TRUE- und FALSE-Konstanten erhalten wir:

    (key1 < 'abc') OR (key1 < 'bar')
    
  4. Die Zusammenfassung überschneidender Intervalle hat die Bedingung zum Ergebnis, die endgültig für den Bereichsscan verwendet werden muss:

    (key1 < 'bar')
    

Im Allgemeinen (und wie auch durch obiges Beispiel veranschaulicht) ist die für einen Bereichsscan verwendete Bedingung weniger restriktiv als die WHERE-Klausel. MySQL führt eine zusätzliche Prüfung durch, um Datensätze auszufiltern, die zwar die Bereichsbedingung erfüllen, nicht aber die vollständige WHERE-Klausel.

Der Extraktionsalgorithmus für die Bereichsbedingung kann verschachtelte AND- und/oder OR-Konstrukte beliebiger Tiefe verarbeiten. Außerdem hängt seine Ausgabe nicht von der Reihenfolge ab, in der die Bedingungen in der WHERE-Klausel erscheinen.

7.2.5.2. Die Bereichszugriffsmethode für mehrteilige Indizes

Die Bereichsbedingungen für einen mehrteiligen Index stellen eine Erweiterung der Bereichsbedingungen für einen einteiligen Index dar. Eine Bereichsbedingung für einen mehrteiligen Index beschränkt die Indexdatensätze auf solche, die innerhalb eines oder mehrerer Schlüsseltupelintervalle liegen. Schlüsseltupelintervalle werden über eine Menge von Schlüsseltupeln definiert, wobei die Sortierung dem Index entnommen wird.

Betrachten Sie beispielsweise einen mehrteiligen Index, der als key1(key_part1, key_part2, key_part3) definiert ist, und die folgende Menge der in der Schlüsselreihenfolge aufgelisteten Schlüsseltupel:

key_part1  key_part2  key_part3
  NULL       1          'abc'
  NULL       1          'xyz'
  NULL       2          'foo'
   1         1          'abc'
   1         1          'xyz'
   1         2          'abc'
   2         1          'aaa'

Die Bedingung key_part1 = 1 definiert das folgende Intervall:

(1,-inf,-inf) <= (key_part1,key_part2,key_part3) < (1,+inf,+inf)

Das Intervall deckt das vierte, das fünfte und das sechste Tupel in der obigen Datenmenge ab und kann von der Bereichszugriffsmethode verwendet werden.

Im Gegensatz dazu definiert die Bedingung key_part3 = 'abc' kein einzelnes Intervall und kann von der Bereichszugriffsmethode nicht verwendet werden.

Die folgenden Beschreibungen erläutern im Detail, wie Bereichsbedingungen bei mehrteiligen Indizes funktionieren.

  • Bei HASH-Indizes kann jedes Intervall verwendet werden, das identische Werte enthält. Das bedeutet, dass das Intervall nur für Bedingungen in der folgenden Form erzeugt werden kann:

        key_part1 cmp const1
    AND key_part2 cmp const2
    AND ...
    AND key_partN cmp constN;
    

    Hierbei sind const1, const2, … Konstanten, cmp ist einer der Vergleichsoperatoren =, <=> oder IS NULL, und die Bedingungen decken alle Indexbestandteile ab. (Das bedeutet, es gibt N Bedingungen: eine für jeden Teil eines N-teiligen Indexes.) Nachfolgend gezeigt ist etwa eine Bereichsbedingung für einen dreiteiligen HASH-Index:

    key_part1 = 1 AND key_part2 IS NULL AND key_part3 = 'foo'
    

    Eine Definition dessen, was als Konstanten betrachtet wird, finden Sie in Abschnitt 7.2.5.1, „Die Bereichszugriffsmethode (Range Access) für Indizes, die aus einzelnen Komponenten bestehen“.

  • Bei einem BTREE-Index kann ein Intervall für Bedingungen verwendbar sein, die mit AND kombiniert wurden, wobei jede Bedingung einen Schlüsselteil mit einem Konstantenwert unter Verwendung von =, <=>, IS NULL, >, <, >=, <=, !=, <>, BETWEEN oder LIKE 'pattern' vergleicht (wobei 'pattern' nicht mit einem Jokerzeichen beginnen darf). Ein Intervall kann verwendet werden, solange es möglich ist, ein einzelnes Schlüsseltupel zu bestimmen, das alle Datensätze enthält, die der Bedingung entsprechen (bzw. zwei Intervalle, wenn <> oder != verwendet werden). Betrachten Sie etwa folgende Bedingung:

    key_part1 = 'foo' AND key_part2 >= 10 AND key_part3 > 10
    

    Das einzelne Intervall ist:

    ('foo',10,10) < (key_part1,key_part2,key_part3) < ('foo',+inf,+inf)
    

    Es ist möglich, dass das erstellte Intervall mehr Datensätze enthält als die Ursprungsbedingung. So umfasst das obige Intervall beispielsweise den Wert ('foo', 11, 0), der die ursprüngliche Bedingung nicht erfüllt.

  • Wenn Bedingungen, die Mengen von Datensätzen innerhalb von Intervallen abdecken, mit OR kombiniert werden, bilden sie eine Bedingung, die eine Menge von Datensätzen abdeckt, die in der Union dieser Intervalle enthalten sind. Werden die Bedingungen mit AND kombiniert, dann bilden sie eine Bedingung, die die Menge von Datensätzen in der Schnittmenge der Intervalle abdeckt. Betrachten Sie etwa die folgende Bedingung für einen zweiteiligen Index:

    (key_part1 = 1 AND key_part2 < 2) OR (key_part1 > 5)
    

    Die Intervalle sehen hier wie folgt aus:

    (1,-inf) < (key_part1,key_part2) < (1,2)
    (5,-inf) < (key_part1,key_part2)
    

    In diesem Beispiel verwendet das Intervall für die erste Zeile einen Schlüsselteil für die linke Grenze und zwei Schlüsselteile für die rechte Grenze. Das Intervall in der zweiten Zeile benutzt dagegen nur einen Schlüsselteil. Die Spalte key_len in der Ausgabe von EXPLAIN gibt die maximale Länge des verwendeten Schlüsselpräfixes an.

    In manchen Fällen kann key_len anzeigen, dass ein Schlüsselteil verwendet wurde, aber dies entspricht unter Umständen nicht dem, was Sie erwarten. Angenommen, key_part1 und key_part2 können NULL sein. In diesem Fall zeigt die Spalte key_len zwei Schlüsselteillängen für die folgende Bedingung an:

    key_part1 >= 1 AND key_part2 < 2
    

    Tatsächlich aber wird die Bedingung wie folgt konvertiert:

    key_part1 >= 1 AND key_part2 IS NOT NULL
    

Abschnitt 7.2.5.1, „Die Bereichszugriffsmethode (Range Access) für Indizes, die aus einzelnen Komponenten bestehen“, beschreibt, wie Optimierungen zur Kombination oder Beseitigung von Intervallen für Bereichsbedingungen für einen einteiligen Index durchgeführt werden. Für Bereichsbedingungen für mehrteilige Indizes werden die Schritte analog durchgeführt.

7.2.6. Optimierung durch Indexverschmelzung

Die Indexverschmelzungsmethode wird verwendet, um Datensätze mit mehreren range-Scans abzurufen und deren Ergebnisse zu einem Ergebnis zu verschmelzen. Die Verschmelzung kann Unions, Schnittmengen oder Schnittmengen-Unions der zugrunde liegenden Scans erzeugen.

In der Ausgabe von EXPLAIN erscheint die Indexverschmelzungsmethode als index_merge in der type-Spalte. In diesem Fall enthält die Spalte key eine Liste der verwendeten Indizes, und key_len enthält eine Liste der längsten Schlüsselteile für diese Indizes.

Ein paar Beispiele:

SELECT * FROM tbl_name WHERE key_part1 = 10 OR key_part2 = 20;

SELECT * FROM tbl_name
  WHERE (key_part1 = 10 OR key_part2 = 20) AND non_key_part=30;

SELECT * FROM t1, t2
  WHERE (t1.key1 IN (1,2) OR t1.key2 LIKE 'value%')
  AND t2.key1=t1.some_col;

SELECT * FROM t1, t2
  WHERE t1.key1=1
  AND (t2.key1=t1.some_col OR t2.key2=t1.some_col2);

Die Indexverschmelzungsmethode umfasst mehrere Zugriffsalgorithmen (diese können dem Feld Extra in der Ausgabe von EXPLAIN entnommen werden):

  • Using intersect(…)

  • Using union(…)

  • Using sort_union(…)

Die folgenden Abschnitte beschreiben diese Methoden im Detail.

Hinweis: Der Optimierungsalgorithmus für die Indexverschmelzung weist die folgenden bekannten Defizite auf:

  • Wenn ein Bereichsscan für einen Schlüssel möglich ist, wird eine Indexverschmelzung nicht in Betracht gezogen. Betrachten Sie etwa folgende Abfrage:

    SELECT * FROM t1 WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;
    

    Hierbei sind zwei Pläne möglich:

    • ein Indexverschmelzungsscan unter Verwendung der Bedingung (goodkey1 < 10 OR goodkey2 < 20)

    • ein Bereichsscan unter Verwendung der Bedingung badkey < 30

    Der Optimierer allerdings zieht nur den zweiten Plan in Betracht. Wenn Sie dies nicht wollen, können Sie mit IGNORE INDEX oder FORCE INDEX dafür sorgen, dass der Optimierer auch die Indexverschmelzung berücksichtigt. Die folgenden beiden Abfragen werden unter Verwendung der Indexverschmelzung ausgeführt:

    SELECT * FROM t1 FORCE INDEX(goodkey1,goodkey2)
      WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;
    
    SELECT * FROM t1 IGNORE INDEX(badkey)
      WHERE (goodkey1 < 10 OR goodkey2 < 20) AND badkey < 30;
    
  • Wenn Ihre Abfrage eine komplexe WHERE-Klausel mit tiefer AND- oder OR-Verschachtelung enthält und MySQL nicht den optimalen Plan auswählt, versuchen Sie die Terme mithilfe der folgenden Identitätsgesetze zu verteilen:

    (x AND y) OR z = (x OR z) AND (y OR z)
    (x OR y) AND z = (x AND z) OR (y AND z)
    
  • Die Indexverschmelzung ist nicht für Volltextindizes einsetzbar. Wir beabsichtigen, dies in einem zukünftigen MySQL-Release zu ändern.

Die Auswahl zwischen verschiedenen möglichen Varianten der Indexverschmelzungsmethode und anderen Zugriffsmethoden basiert auf Kostenschätzungen der verschiedenen verfügbaren Optionen.

7.2.6.1. Der Zugriffsalgorithmus Schnittmenge (Intersection) bei der Indexverschmelzung

Diese Zugriffsmethode kann verwendet werden, wenn eine WHERE-Klausel in mehrere Bereichsbedingungen für verschiedene, mit AND kombinierte Schlüssel konvertiert wurde und jede Bedingung eines der folgenden Elemente ist:

  • In dieser Form, wo der Index genau N Teile hat (d. h. alle Indexteile sind abgedeckt):

    key_part1=const1 AND key_part2=const2 ... AND key_partN=constN
    
  • Jede Bereichsbedingung über einen Primärschlüssel einer InnoDB- oder BDB-Tabelle.

Ein paar Beispiele:

SELECT * FROM innodb_table WHERE primary_key < 10 AND key_col1=20;

SELECT * FROM tbl_name
  WHERE (key1_part1=1 AND key1_part2=2) AND key2=2;

Der Indexverschmelzungs-Schnittmengenalgorithmus führt gleichzeitige Scans aller verwendeten Indizes durch und erzeugt die Schnittmenge der Datensatzsequenzen, die er den verschmolzenen Indexscans entnimmt.

Wenn alle in der Abfrage verwendeten Spalten von den verwendeten Indizes abgedeckt werden, werden keine vollständigen Tabellendatensätze abgerufen. (In diesem Fall enthält die Ausgabe von EXPLAIN Using index im Feld Extra.) Hier ein Beispiel für eine solche Abfrage:

SELECT COUNT(*) FROM t1 WHERE key1=1 AND key2=1;

Wenn die verwendeten Indizes nicht alle in der Abfrage verwendeten Spalten abdecken, werden vollständige Datensätze nur dann abgerufen, wenn die Bereichsbedingungen für alle Schlüssel erfüllt sind.

Wenn eine der verschmolzenen Bedingungen eine Bedingung über einen Primärschlüssel einer InnoDB- oder BDB-Tabelle ist, dann wird sie nicht zum Abrufen von Datensätzen, sondern zum Ausfiltern von Datensätzen verwendet, die mit anderen Bedingungen abgerufen wurden.

7.2.6.2. Der Zugriffsalgorithmus Union bei der Indexverschmelzung

Die Anwendbarkeitskriterien für diesen Algorithmus ähneln denen des Schnittmengenalgorithmus der Indexverschmelzungsmethode. Dieser Algorithmus kann verwendet werden, wenn die WHERE-Klausel der Tabelle in mehrere Bereichsbedingungen für verschiedene, mit OR kombinierte Schlüssel konvertiert wurde und jede Bedingung eines der folgenden Elemente ist:

  • In dieser Form, wo der Index genau N Teile hat (d. h. alle Indexteile sind abgedeckt):

    key_part1=const1 AND key_part2=const2 ... AND key_partN=constN
    
  • Jede Bereichsbedingung über einen Primärschlüssel einer InnoDB- oder BDB-Tabelle.

  • Eine Bedingung, auf die der Schnittmengenalgorithmus der Indexverschmelzungsmethode anwendbar ist.

Ein paar Beispiele:

SELECT * FROM t1 WHERE key1=1 OR key2=2 OR key3=3;

SELECT * FROM innodb_table WHERE (key1=1 AND key2=2) OR
  (key3='foo' AND key4='bar') AND key5=5;

7.2.6.3. Der Zugriffsalgorithmus Sort-Union bei der Indexverschmelzung

Dieser Zugriffsalgorithmus wird verwendet, wenn die WHERE-Klausel in verschiedene Bereichsbedingungen konvertiert wurde, die mit OR verknüpft wurden, aber für die der Union-Algorithmus der Indexverschmelzungsmethode nicht anwendbar ist.

Ein paar Beispiele:

SELECT * FROM tbl_name WHERE key_col1 < 10 OR key_col2 < 20;

SELECT * FROM tbl_name
     WHERE (key_col1 > 10 OR key_col2 = 20) AND nonkey_col=30;

Der Unterschied zwischen dem Sort-Union-Algorithmus und dem Union-Algorithmus besteht darin, dass der Sort-Union-Algorithmus zunächst die Datensatzkennungen aller Datensätze abrufen und diese sortieren muss, bevor er Datensätze zurückgeben kann.

7.2.7. IS NULL-Optimierung

MySQL kann dieselbe Optimierung an col_name IS NULL vornehmen, die es auch für col_name = constant_value durchführen kann. So kann MySQL etwa Indizes und Bereiche zur Suche nach NULL mit IS NULL verwenden.

Ein paar Beispiele:

SELECT * FROM tbl_name WHERE key_col IS NULL;

SELECT * FROM tbl_name WHERE key_col <=> NULL;

SELECT * FROM tbl_name
  WHERE key_col=const1 OR key_col=const2 OR key_col IS NULL;

Wenn eine WHERE-Klausel eine Bedingung col_name IS NULL für eine Spalte enthält, die als NOT NULL deklariert ist, dann wird dieser Ausdruck wegoptimiert. Diese Optimierung findet allerdings nicht in Fällen statt, in denen die Spalte ohnehin NULL erzeugen könnte – z. B. wenn sie aus einer Tabelle auf der rechten Seite eines LEFT JOIN stammt.

MySQL kann auch die Kombination col_name = expr AND col_name IS NULL optimieren; diese Form tritt bei aufgelösten Unterabfragen häufig auf. EXPLAIN zeigt ref_or_null an, wenn diese Optimierung verwendet wird.

Diese Optimierung kann je Schlüsselteil eine IS NULL-Bedingung verarbeiten.

Es folgen einige Beispiele für optimierte Abfragen (hierbei wird davon ausgegangen, dass ein Index für die Spalten a und b der Tabelle t2 vorhanden ist):

SELECT * FROM t1 WHERE t1.a=expr OR t1.a IS NULL;

SELECT * FROM t1, t2 WHERE t1.a=t2.a OR t2.a IS NULL;

SELECT * FROM t1, t2
  WHERE (t1.a=t2.a OR t2.a IS NULL) AND t2.b=t1.b;

SELECT * FROM t1, t2
  WHERE t1.a=t2.a AND (t2.b=t1.b OR t2.b IS NULL);

SELECT * FROM t1, t2
  WHERE (t1.a=t2.a AND t2.a IS NULL AND ...)
  OR (t1.a=t2.a AND t2.a IS NULL AND ...);

Bei ref_or_null wird zunächst der Referenzschlüssel ausgelesen. Danach erfolgt eine separate Suche nach Datensätzen mit einem NULL-Schlüsselwert.

Beachten Sie, dass die Optimierung nur eine IS NULL-Ebene verarbeiten kann. In der folgenden Abfrage verwendet MySQL Schlüsselsuchvorgänge nur für den Ausdruck (t1.a=t2.a AND t2.a IS NULL). Der Schlüsselteil zu b kann von MySQL nicht verwendet werden:

SELECT * FROM t1, t2
     WHERE (t1.a=t2.a AND t2.a IS NULL)
     OR (t1.b=t2.b AND t2.b IS NULL);

7.2.8. Optimierung von DISTINCT

DISTINCT kombiniert mit ORDER BY benötigt in vielen Fällen eine Temporärtabelle.

Weil DISTINCT unter Umständen GROUP BY verwendet, sollten Sie wissen, wie MySQL mit Spalten in ORDER BY- oder HAVING-Klauseln verfährt, die nicht Teil der gewählten Spalten sind. Siehe auch Abschnitt 12.11.3, „GROUP BY mit versteckten Feldern“.

In vielen Fällen kann eine DISTINCT-Klausel als Sonderfall von GROUP BY betrachtet werden. So sind beispielsweise die beiden folgenden Abfragen gleichwertig:

SELECT DISTINCT c1, c2, c3 FROM t1 WHERE c1 > const;

SELECT c1, c2, c3 FROM t1 WHERE c1 > const GROUP BY c1, c2, c3;

Aufgrund dieser Äquivalenz können die Optimierungen, die für GROUP BY-Abfragen verwendbar sind, auch auf Abfragen mit einer DISTINCT-Klausel angewendet werden. Insofern sollten Sie, um weitere Informationen zu Optimierungsmöglichkeiten bei DISTINCT-Abfragen zu erhalten, unter Abschnitt 7.2.13, „GROUP BY-Optimierung“, nachschlagen.

Wenn Sie LIMIT row_count mit DISTINCT kombinieren, beendet MySQL die Abfrage, sobald row_count eindeutige Datensätze gefunden wurden.

Wenn Sie nicht Spalten aus allen in einer Abfrage aufgeführten Tabellen verwenden, beendet MySQL das Scannen unbenutzter Tabellen, sobald die erste Übereinstimmung gefunden wurde. Im folgenden Fall (in dem vorausgesetzt wird, dass t1 vor t2 benutzt wird, was mit EXPLAIN verifiziert werden kann) beendet MySQL das Auslesen von t2 für einen beliebigen Datensatz in t1, sobald der erste Datensatz in t2 gefunden wird:

SELECT DISTINCT t1.a FROM t1, t2 where t1.a=t2.a;

7.2.9. Optimierung von LEFT JOIN und RIGHT JOIN

MySQL implementiert A LEFT JOIN B join_condition wie folgt:

  • Tabelle B ist so konfiguriert, dass sie von Tabelle A und allen Tabellen abhängt, von denen auch Tabelle A abhängt.

  • Tabelle A ist ihrerseits so konfiguriert, dass sie von allen Tabellen (mit Ausnahme von B) abhängt, die in der LEFT JOIN-Bedingung verwendet werden.

  • Anhand der LEFT JOIN-Bedingung wird entschieden, wie Datensätze aus der Tabelle B abgerufen werden. (Das bedeutet, dass keine Bedingung in der WHERE-Klausel verwendet wird.)

  • Alle Standardoptimierungen für Joins werden durchgeführt; einzige Ausnahme ist, dass jede Tabelle stets erst gelesen wird, nachdem alle Tabellen gelesen wurden, von denen sie abhängt. Gibt es eine Zirkelabhängigkeit, dann gibt MySQL einen Fehler aus.

  • Alle Standardoptimierungen für WHERE werden durchgeführt.

  • Wenn ein Datensatz in A vorhanden ist, der der WHERE-Klausel entspricht, aber kein Datensatz in B, der der ON-Bedingung entspricht, dann wird ein zusätzlicher Datensatz in B erzeugt, bei dem alle Spalten auf NULL gesetzt sind.

  • Wenn Sie mit LEFT JOIN Datensätze suchen, die in keiner der Tabellen vorhanden sind, und die Überprüfung col_name IS NULL im WHERE-Teil vorhanden ist (wobei col_name eine als NOT NULL deklarierte Spalte ist), dann beendet MySQL die Suche nach weiteren Datensätzen (für eine bestimmte Schlüsselkombination), wenn es genau einen Datensatz gefunden hat, der der LEFT JOIN-Bedingung entspricht.

Die Implementierung von RIGHT JOIN erfolgt analog zu der von LEFT JOIN, wobei lediglich die Rollen der Tabellen umgekehrt werden.

Der Join-Optimierer berechnet die Reihenfolge, in der die Tabellen verknüpft werden sollten. Die durch LEFT JOIN oder STRAIGHT_JOIN erzwungene Lesereihenfolge für die Tabellen unterstützt den Join-Optimierer bei seiner Arbeit wesentlich effizienter, weil erheblich weniger Tabellenumstellungen zu überprüfen sind. Beachten Sie, dass dies bedeutet, dass, wenn Sie eine Abfrage folgenden Typs ausführen, MySQL einen vollständigen Scan in b durchführt, weil diese Tabelle aufgrund von LEFT JOIN vor d gelesen wird:

SELECT *
  FROM a,b LEFT JOIN c ON (c.key=a.key) LEFT JOIN d ON (d.key=a.key)
  WHERE b.key=d.key;

Die Lösung besteht in diesem Fall darin, die Reihenfolge umzukehren, in der a und b in der FROM-Klausel aufgelistet werden:

SELECT *
  FROM b,a LEFT JOIN c ON (c.key=a.key) LEFT JOIN d ON (d.key=a.key)
  WHERE b.key=d.key;

Wenn bei einem LEFT JOIN die WHERE-Bedingung für den erzeugten NULL-Datensatz immer falsch ist, dann wird der LEFT JOIN in einen normalen Join umgesetzt. So wäre beispielsweise die WHERE-Klausel in der folgenden Abfrage falsch, wenn t2.column1 NULL wäre:

SELECT * FROM t1 LEFT JOIN t2 ON (column1) WHERE t2.column2=5;

Aus diesem Grund kann die Abfrage problemlos in einen normalen Join konvertiert werden:

SELECT * FROM t1, t2 WHERE t2.column2=5 AND t1.column1=t2.column1;

Dies kann schneller erfolgen, weil MySQL die Tabelle t2 vor t1 verwenden kann, sofern dies einen besseren Abfrageplan zum Ergebnis hätte. Mit STRAIGHT_JOIN können Sie eine bestimmte Tabellenreihenfolge erzwingen.

7.2.10. Optimierung verschachtelter Joins

Die Syntax zum Ausdrücken von Joins gestattet auch verschachtelte Joins. Die nachfolgende Beschreibung bezieht sich auf die in Abschnitt 13.2.7.1, „JOIN, beschriebene Join-Syntax.

Die Syntax von table_factor ist im Vergleich zum SQL-Standard erweitert. SQL akzeptiert nur table_reference, nicht aber eine in Klammern gesetzte Liste mit Referenzierungen. Dies ist eine konservative Erweiterung, sofern wir jedes Komma in einer Liste mit table_reference-Elementen als äquivalent zu einem inneren Join betrachten. Zum Beispiel:

SELECT * FROM t1 LEFT JOIN (t2, t3, t4)
                 ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)

ist äquivalent mit

SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4)
                 ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c)

In MySQL ist CROSS JOIN syntaktisch ein Äquivalent zu INNER JOIN (diese lassen sich gegeneinander austauschen). Nach SQL-Standard hingegen sind beide nicht äquivalent. INNER JOIN wird bei einer ON-Klausel und CROSS JOIN andernfalls verwendet.

Klammern können in Join-Ausdrücken, die nur innere Join-Operationen enthalten, ignoriert werden. Betrachten Sie folgenden Ausdruck:

t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
   ON t1.a=t2.a

Nach Entfernen der Klammern und Gruppieren der Operationen auf der linken Seite entsteht folgender Ausdruck:

(t1 LEFT JOIN t2 ON t1.a=t2.a) LEFT JOIN t3
    ON t2.b=t3.b OR t2.b IS NULL

Trotzdem sind die beiden Ausdrücke nicht äquivalent. Um dies nachzuweisen, nehmen wir an, dass die Tabellen t1, t2 und t3 die folgenden Zustände haben:

  • Tabelle t1 enthält die Datensätze {1}, {2}.

  • Tabelle t2 enthält den Datensatz {1,101}.

  • Tabelle t3 enthält den Datensatz {101}.

In diesem Fall gibt der erste Ausdruck eine Ergebnismenge mit den Datensätzen {1,1,101,101}, {2,NULL,NULL,NULL} zurück; der zweite Ausdruck hingegen gibt die Datensätze {1,1,101,101}, {2,NULL,NULL,101} zurück:

mysql> SELECT *
    -> FROM t1
    ->      LEFT JOIN
    ->      (t2 LEFT JOIN t3 ON t2.b=t3.b OR t2.b IS NULL)
    ->      ON t1.a=t2.a;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL | NULL |
+------+------+------+------+

mysql> SELECT *
    -> FROM (t1 LEFT JOIN t2 ON t1.a=t2.a)
    ->      LEFT JOIN t3
    ->      ON t2.b=t3.b OR t2.b IS NULL;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL |  101 |
+------+------+------+------+

Im folgenden Beispiel wird ein äußerer Join gemeinsam mit einem inneren Join verwendet:

t1 LEFT JOIN (t2, t3) ON t1.a=t2.a

Dieser Ausdruck kann nicht in den folgenden Ausdruck transformiert werden:

t1 LEFT JOIN t2 ON t1.a=t2.a, t3.

Für die gegebenen Tabellenzustände geben die beiden Ausdrücke verschiedene Datensatzmengen zurück:

mysql> SELECT *
    -> FROM t1 LEFT JOIN (t2, t3) ON t1.a=t2.a;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL | NULL |
+------+------+------+------+

mysql> SELECT *
    -> FROM t1 LEFT JOIN t2 ON t1.a=t2.a, t3;
+------+------+------+------+
| a    | a    | b    | b    |
+------+------+------+------+
|    1 |    1 |  101 |  101 |
|    2 | NULL | NULL |  101 |
+------+------+------+------+

Aus diesem Grund ändern wir, wenn wir in einem Join-Ausdruck mit äußeren Join-Operatoren die Klammern weglassen, unter Umständen die Ergebnismenge für den ursprünglichen Ausdruck.

Genauer formuliert: Wir dürfen die Klammern im rechten Operanden der linken äußeren Join-Operation und im linken Operanden einer rechten Join-Operation nicht ignorieren. Oder: Die Klammern der inneren Tabellenausdrücke äußerer Join-Operationen dürfen nicht unbeachtet bleiben. Klammern für den anderen Operanden (d. h. den Operanden der äußeren Tabelle) können hingegen ignoriert werden.

Der Ausdruck

(t1,t2) LEFT JOIN t3 ON P(t2.b,t3.b)

ist äquivalent zu folgendem Ausdruck:

t1, t2 LEFT JOIN t3 ON P(t2.b,t3.b)

Dies gilt für alle Tabellen t1,t2,t3 und alle Bedingungen P für Attribute t2.b und t3.b.

Immer dann, wenn die Join-Operationen in einem Join-Ausdruck (join_table) nicht von links nach rechts ausgeführt werden, redet man von verschachtelten Joins. Betrachten Sie einmal die folgenden Abfragen:

SELECT * FROM t1 LEFT JOIN (t2 LEFT JOIN t3 ON t2.b=t3.b) ON t1.a=t2.a
  WHERE t1.a > 1

SELECT * FROM t1 LEFT JOIN (t2, t3) ON t1.a=t2.a
  WHERE (t2.b=t3.b OR t2.b IS NULL) AND t1.a > 1

Diese Abfragen enthalten die folgenden verschachtelten Joins:

t2 LEFT JOIN t3 ON t2.b=t3.b
t2, t3

Der verschachtelte Join wird in der ersten Abfrage mit einem Left-Join gebildet, während er in der zweiten Abfrage aufgrund einer inneren Join-Operation entsteht.

In der ersten Abfrage können die Klammern weggelassen werden: Die grammatische Struktur des Join-Ausdrucks schreibt dieselbe Ausführungsreihenfolge für Join-Operationen vor. Bei der zweiten Abfrage dürfen die Klammern nicht weggelassen werden, obwohl der Join-Ausdruck hier auch ohne sie eindeutig interpretiert werden könnte. (In unserer erweiterten Syntax sind die Klammern im Ausdruck (t2, t3) der zweiten Abfrage erforderlich, obwohl die Abfrage theoretisch auch ohne sie analysiert werden könnte: Wir hätten immer noch eine eindeutige syntaktische Struktur für die Abfrage, da LEFT JOIN und ON die Rolle der linken und rechten Begrenzungen für den Ausdruck (t2,t3) übernähmen.)

Die obigen Beispiele veranschaulichen die folgenden Aspekte:

  • Bei Join-Ausdrücken, die nur innere Joins (und keine äußeren Joins) enthalten, können die Klammern entfernt werden. Sie können die Klammern entfernen und die Auswertung von links nach rechts vornehmen (tatsächlich kann die Auswertung der Tabellen sogar in beliebiger Reihenfolge erfolgen).

  • Dies gilt allerdings in der Regel nicht für äußere Joins oder mit inneren Joins gemischte äußere Joins. Ein Entfernen der Klammern kann das Ergebnis ändern.

Abfragen mit verschachtelten äußeren Joins werden in derselben Weise ausgeführt wie Abfragen mit inneren Joins. Genauer gesagt, wird eine Abwandlung des Join-Algorithmus mit verschachtelten Schleifen benutzt. Vergegenwärtigen Sie sich, mit welchem Algorithmusschema ein Join mit verschachtelten Schleifen eine Abfrage ausführt. Angenommen, wir haben eine Join-Abfrage über drei Tabellen T1,T2,T3 der folgenden Form:

SELECT * FROM T1 INNER JOIN T2 ON P1(T1,T2)
                 INNER JOIN T3 ON P2(T2,T3)
  WHERE P(T1,T2,T3).

Hierbei seien P1(T1,T2) und P2(T3,T3) Join-Bedingungen (für Ausdrücke), während P(t1,t2,t3) eine Bedingung über Spalten der Tabellen T1,T2,T3 ist.

Der Algorithmus für Joins mit verschachtelten Schleifen würde die Abfrage wie folgt ausführen:

FOR each row t1 in T1 {
  FOR each row t2 in T2 such that P1(t1,t2) {
    FOR each row t3 in T3 such that P2(t2,t3) {
      IF P(t1,t2,t3) {
         t:=t1||t2||t3; OUTPUT t;
      }
    }
  }
}

Die Notation t1||t2||t3 bedeutet einen „Datensatz, der durch Verkettung der Spalten der Datensätze t1, t2 und t3 entsteht“. In einigen der folgenden Beispiele bezeichnet, wenn ein Datensatzname erscheint, NULL die Tatsache, dass NULL für jede Spalte dieses Datensatzes benutzt wird. Beispielsweise bedeutet t1||t2||NULLeinen Datensatz, der aus den verketteten Spalten der Datensätze t1 und t2 sowie NULL für jede Spalte von t3 besteht“.

Betrachten wir nun eine Abfrage mit verschachtelten äußeren Joins:

SELECT * FROM T1 LEFT JOIN
              (T2 LEFT JOIN T3 ON P2(T2,T3))
              ON P1(T1,T2)
  WHERE P(T1,T2,T3).

Für diese Abfrage ändern wir das Muster für verschachtelte Schleifen ab und erhalten Folgendes:

FOR each row t1 in T1 {
  BOOL f1:=FALSE;
  FOR each row t2 in T2 such that P1(t1,t2) {
    BOOL f2:=FALSE;
    FOR each row t3 in T3 such that P2(t2,t3) {
      IF P(t1,t2,t3) {
        t:=t1||t2||t3; OUTPUT t;
      }
      f2=TRUE;
      f1=TRUE;
    }
    IF (!f2) {
      IF P(t1,t2,NULL) {
        t:=t1||t2||NULL; OUTPUT t;
      }
      f1=TRUE;
    }
  }
  IF (!f1) {
    IF P(t1,NULL,NULL) {
      t:=t1||NULL||NULL; OUTPUT t;
    }
  }
}

Generell wird für jede verschachtelte Schleife für die erste innere Tabelle einer äußeren Join-Operation ein Flag eingeführt, welches vor der Schleife gelöscht und danach wieder gesetzt wird. Das Flag wird gesetzt, wenn für den aktuellen Datensatz aus der äußeren Tabelle eine Entsprechung in der Tabelle gefunden wird, die den inneren Operanden darstellt. Wenn das Flag am Ende des Schleifenzyklus immer noch nicht gelöscht ist, dann wurde für den aktuellen Datensatz in der äußeren Tabelle keine Entsprechung gefunden. In diesem Fall wird der Datensatz mit NULL-Werten für die Spalten der inneren Tabellen ergänzt. Der Ergebnisdatensatz wird zur letzten Überprüfung an die Ausgabe oder in die nächste verschachtelte Schleife übergeben – aber nur dann, wenn der Datensatz die Join-Bedingung für alle eingebetteten äußeren Joins erfüllt.

In unserem Beispiel ist die mit dem folgenden Ausdruck beschriebene äußere Join-Tabelle eingebettet:

(T2 LEFT JOIN T3 ON P2(T2,T3))

Beachten Sie, dass der Optimierer für die Abfrage mit inneren Joins eine andere Reihenfolge für verschachtelte Schleifen wählen könnte – etwa die folgende:

FOR each row t3 in T3 {
  FOR each row t2 in T2 such that P2(t2,t3) {
    FOR each row t1 in T1 such that P1(t1,t2) {
      IF P(t1,t2,t3) {
         t:=t1||t2||t3; OUTPUT t;
      }
    }
  }
}

Bei Abfragen mit äußeren Joins kann der Optimierer nur eine Reihenfolge wählen, bei der Schleifen für äußere Tabellen den Schleifen für die inneren Tabellen vorangehen. Insofern ist für unsere Abfrage mit äußeren Joins nur eine Verschachtelungsreihenfolge möglich. Bei der folgenden Abfrage wird der Optimierer zwei verschiedene Verschachtelungen auswerten:

SELECT * T1 LEFT JOIN (T2,T3) ON P1(T1,T2) AND P2(T1,T3)
  WHERE P(T1,T2,T3)

Die Verschachtelungen sind

FOR each row t1 in T1 {
  BOOL f1:=FALSE;
  FOR each row t2 in T2 such that P1(t1,t2) {
    FOR each row t3 in T3 such that P2(t1,t3) {
      IF P(t1,t2,t3) {
        t:=t1||t2||t3; OUTPUT t;
      }
      f1:=TRUE
    }
  }
  IF (!f1) {
    IF P(t1,NULL,NULL) {
      t:=t1||NULL||NULL; OUTPUT t;
    }
  }
}

und

FOR each row t1 in T1 {
  BOOL f1:=FALSE;
  FOR each row t3 in T3 such that P2(t1,t3) {
    FOR each row t2 in T2 such that P1(t1,t2) {
      IF P(t1,t2,t3) {
        t:=t1||t2||t3; OUTPUT t;
      }
      f1:=TRUE
    }
  }
  IF (!f1) {
    IF P(t1,NULL,NULL) {
      t:=t1||NULL||NULL; OUTPUT t;
    }
  }
}

In beiden Verschachtelungen muss T1 in der äußeren Schleife verarbeitet werden, weil sie in einem äußeren Join verwendet wird. T2 und T3 werden hingegen in einem inneren Join benutzt, d. h., der Join muss in der inneren Schleife verarbeitet werden. Allerdings können, weil der Join ein innerer Join ist, T2 und T3 in beliebiger Ordnung verarbeitet werden.

Bei der Beschreibung des Algorithmus mit verschachtelten Schleifen für innere Joins haben wir ein paar Details übergangen, deren Wirkung auf die Leistungsfähigkeit der Abfrageausführung beträchtlich sein kann. Wir haben beispielsweise so genannte „Pushdown-Bedingungen“ nicht erwähnt. Angenommen, unsere WHERE-Bedingung P(T1,T2,T3) könnte mit einer konjunktiven Formel dargestellt werden:

P(T1,T2,T2) = C1(T1) AND C2(T2) AND C3(T3).

In diesem Fall verwendet MySQL in der Tat das folgende Schema mit verschachtelten Schleifen zur Ausführung der Abfrage mit inneren Joins:

FOR each row t1 in T1 such that C1(t1) {
  FOR each row t2 in T2 such that P1(t1,t2) AND C2(t2)  {
    FOR each row t3 in T3 such that P2(t2,t3) AND C3(t3) {
      IF P(t1,t2,t3) {
         t:=t1||t2||t3; OUTPUT t;
      }
    }
  }
}

Sie erkennen, dass die Konjunkte C1(T1), C2(T2) und C3(T3) aus der innersten in die äußerste Schleife verschoben werden, wo sie dann ausgewertet werden können. Wenn C1(T1) eine sehr restriktive Bedingung ist, dann kann dieser Bedingungs-Pushdown die Anzahl der Datensätze aus Tabelle T1, die an die inneren Schleifen weitergegeben werden, erheblich verringern. Hieraus ergibt sich eine beachtliche Verbesserung bei der Ausführungszeit für die Abfrage.

Bei einer Abfrage mit äußeren Joins muss die WHERE-Bedingung erst geprüft werden, wenn festgestellt wurde, dass für den aktuellen Datensatz in der äußeren Tabelle eine Entsprechung in den inneren Tabellen vorhanden ist. Aufgrund dessen kann die Optimierung mit einer Pushdown-Bedingung aus den inneren verschachtelten Schleifen nicht direkt auf Abfragen mit äußeren Joins angewendet werden. Deswegen müssen an dieser Stelle konditionale Pushdown-Prädikate in Verbindung mit Überwachungs-Flags eingeführt werden, die gesetzt werden, sobald eine Übereinstimmung gefunden wurde.

Betrachten Sie noch einmal unser Beispiel mit äußeren Joins:

P(T1,T2,T3)=C1(T1) AND C(T2) AND C3(T3)

Hier sieht das Schema der verschachtelten Schleifen unter Verwendung überwachter Pushdown-Bedingungen wie folgt aus:

FOR each row t1 in T1 such that C1(t1) {
  BOOL f1:=FALSE;
  FOR each row t2 in T2
      such that P1(t1,t2) AND (f1?C2(t2):TRUE) {
    BOOL f2:=FALSE;
    FOR each row t3 in T3
        such that P2(t2,t3) AND (f1&&f2?C3(t3):TRUE) {
      IF (f1&&f2?TRUE:(C2(t2) AND C3(t3))) {
        t:=t1||t2||t3; OUTPUT t;
      }
      f2=TRUE;
      f1=TRUE;
    }
    IF (!f2) {
      IF (f1?TRUE:C2(t2) && P(t1,t2,NULL)) {
        t:=t1||t2||NULL; OUTPUT t;
      }
      f1=TRUE;
    }
  }
  IF (!f1 && P(t1,NULL,NULL)) {
      t:=t1||NULL||NULL; OUTPUT t;
  }
}

Generell können Pushdown-Prädikate aus Join-Bedingungen wie P1(T1,T2) und P(T2,T3) extrahiert werden. In diesem Fall wird ein Pushdown-Prädikat auch von einem Flag überwacht, das eine Überprüfung des Prädikats auf den mit NULL ergänzten Datensatz verhindert, der im Zuge der entsprechenden Join-Operation erzeugt wurde.

Beachten Sie, dass ein Zugriff nach Schlüssel aus einer inneren Tabelle auf eine andere Tabelle im selben verschachtelten Join unzulässig ist, wenn er durch ein Prädikat aus der WHERE-Bedingung herbeigeführt wird. (Wir könnten in diesem Fall einen bedingten Schlüsselzugriff verwenden, aber diese Methode ist in MySQL 5.1 noch nicht implementiert.)

7.2.11. Vereinfachungsmöglichkeit für äußere Joins

Tabellenausdrücke in der FROM-Klausel einer Abfrage werden in vielen Fällen vereinfacht.

Auf der Parserebene werden Abfragen mit rechten äußeren Join-Operationen in gleichwertige Abfragen konvertiert, die nur Left-Join-Operationen enthalten. Im Allgemeinen erfolgt die Konvertierung auf der Basis der folgenden Regel:

(T1, ...) RIGHT JOIN (T2,...) ON P(T1,...,T2,...) =
(T2, ...) LEFT JOIN (T1,...) ON P(T1,...,T2,...).

Alle inneren Join-Ausdrücke der Form T1 INNER JOIN T2 ON P(T1,T2) werden durch die Liste T1,T2 ersetzt, und P(T1,T2) wird als Konjunkt mit der WHERE-Bedingung (oder mit der Join-Bedingung des einbettenden Joins, sofern vorhanden) verknüpft.

Wenn der Optimierer Pläne für Join-Abfragen mit einer äußeren Join-Operation auswertet, berücksichtigt er nur diejenigen Pläne, bei denen bei einer solchen Operation zunächst auf die äußeren und erst dann auf die inneren Tabellen zugegriffen wird. Die Optimiereroptionen sind eingeschränkt, weil nur solche Pläne uns die Ausführung von Abfragen mit äußeren Join-Operationen über das Schema mit verschachtelten Schleifen gestatten.

Angenommen, wir haben eine Abfrage folgenden Aussehens:

SELECT * T1 LEFT JOIN T2 ON P1(T1,T2)
  WHERE P(T1,T2) AND R(T2)

Hierbei schränkt R(T2) die Anzahl passender Datensätze aus der Tabelle T2 erheblich ein. Wenn wir die Abfrage in ihrer ursprünglichen Form ausgeführt hätten, hätte der Optimierer keine andere Chance gehabt, als zuerst auf die Tabelle T1 und erst dann auf T2 zuzugreifen. Dies hätte einen sehr ineffizienten Ausführungsplan zur Folge gehabt.

Glücklicherweise wandelt MySQL eine solche Abfrage in eine Abfrage ohne äußere Join-Operation um, wenn die WHERE-Bedingung nullabweisend wird. Eine Bedingung heißt nullabweisend für eine äußere Join-Operation, wenn sie für jeden NULL-ergänzten Datensatz, der für die Operation erstellt wurde, stets FALSE oder UNKNOWN ist.

Betrachten Sie folgenden äußeren Join:

T1 LEFT JOIN T2 ON T1.A=T2.A

Für ihn sind Bedingungen wie die folgenden nullabweisend:

T2.B IS NOT NULL,
T2.B > 3,
T2.C <= T1.C,
T2.B < 2 OR T2.C > 1

Bedingungen wie die folgenden sind hingegen nicht nullabweisend:

T2.B IS NULL,
T1.B < 3 OR T2.B IS NOT NULL,
T1.B < 3 OR T2.B > 3

Die allgemeinen Regeln zur Überprüfung, ob eine Bedingung für einen äußeren Join nullabweisend ist oder nicht, sind recht einfach. Eine Bedingung ist nullabweisend, wenn sie

  • die Form A IS NOT NULL hat, wobei A ein Attribut einer der inneren Tabellen ist,

  • ein Prädikat ist, das eine innere Tabelle referenziert, die UNKNOWN ist, wenn eines ihrer Argumente NULL ist,

  • eine Konjunktion mit einer nullabweisenden Bedingung als Konjunkt ist,

  • eine Disjunktion nullabweisender Bedingungen ist.

Eine Bedingung kann für eine äußere Join-Operation nullabweisend sein und gleichzeitig für eine andere nicht. Betrachten Sie folgende Abfrage:

SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
                 LEFT JOIN T3 ON T3.B=T1.B
  WHERE T3.C > 0

Hier ist die WHERE-Bedingung für die zweite äußere Join-Operation nullabweisend, nicht jedoch für die erste.

Wenn die WHERE-Bedingung für eine äußere Join-Operation in einer Abfrage nullabweisend ist, wird die äußere Join-Operation durch eine innere Join-Operation ersetzt.

So wird etwa obige Abfrage durch die folgende ersetzt:

SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
                 INNER JOIN T3 ON T3.B=T1.B
  WHERE T3.C > 0

Für die ursprüngliche Abfrage würde der Optimierer Pläne auswerten, die mit der Zugriffsreihenfolge T1,T2,T3 kompatibel wären. Für die ersetzende Abfrage wird außerdem die Zugriffssequenz T3,T1,T2 in Betracht gezogen.

Eine Konvertierung einer äußeren Join-Operation kann eine Konvertierung einer anderen derartigen Operation auslösen. Betrachten Sie folgende Abfrage:

SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
                 LEFT JOIN T3 ON T3.B=T2.B
  WHERE T3.C > 0

Diese wird in die folgende Abfrage konvertiert:

SELECT * FROM T1 LEFT JOIN T2 ON T2.A=T1.A
                 INNER JOIN T3 ON T3.B=T2.B
  WHERE T3.C > 0

Diese wiederum ist äquivalent mit dieser Abfrage:

SELECT * FROM (T1 LEFT JOIN T2 ON T2.A=T1.A), T3
  WHERE T3.C > 0 AND T3.B=T2.B

Nun kann die verbleibende äußere Join-Operation ebenfalls durch einen inneren Join ersetzt werden, weil die Bedingung T3.B=T2.B nullabweisend ist und wir eine Abfrage erhalten, die überhaupt keine äußeren Joins enthält:

SELECT * FROM (T1 INNER JOIN T2 ON T2.A=T1.A), T3
  WHERE T3.C > 0 AND T3.B=T2.B

Manchmal gelingt es, eine eingebettete äußere Join-Operation zu ersetzen, aber der einbettende äußere Join kann nicht konvertiert werden. Betrachten Sie folgende Abfrage:

SELECT * FROM T1 LEFT JOIN
              (T2 LEFT JOIN T3 ON T3.B=T2.B)
              ON T2.A=T1.A
  WHERE T3.C > 0

Sie wird wie folgt konvertiert:

SELECT * FROM T1 LEFT JOIN
              (T2 INNER JOIN T3 ON T3.B=T2.B)
              ON T2.A=T1.A
  WHERE T3.C > 0,

Dies lässt sich nur in einer Form umschreiben, die nach wie vor die einbettende äußere Join-Operation enthält:

SELECT * FROM T1 LEFT JOIN
              (T2,T3)
              ON (T2.A=T1.A AND T3.B=T2.B)
  WHERE T3.C > 0.

Wenn Sie versuchen, eine eingebettete äußere Join-Operation zu konvertieren, dann müssen Sie die Join-Bedingung für den einbettenden äußeren Join gemeinsam mit der WHERE-Bedingung berücksichtigen. Betrachten Sie folgende Abfrage:

SELECT * FROM T1 LEFT JOIN
              (T2 LEFT JOIN T3 ON T3.B=T2.B)
              ON T2.A=T1.A AND T3.C=T1.C
  WHERE T3.D > 0 OR T1.D > 0

Hier ist die WHERE-Bedingung für den eingebetteten äußeren Join nicht nullabweisend, wohl aber die Join-Bedingung des einbettenden äußeren Joins T2.A=T1.A AND T3.C=T1.C. Insofern kann die Abfrage wie folgt konvertiert werden:

SELECT * FROM T1 LEFT JOIN
              (T2, T3)
              ON T2.A=T1.A AND T3.C=T1.C AND T3.B=T2.B
  WHERE T3.D > 0 OR T1.D > 0

7.2.12. ORDER BY-Optimierung

In manchen Fällen kann MySQL mit einem Index eine ORDER BY-Klausel erfüllen, ohne dass eine zusätzliche Sortierung erforderlich wäre.

Der Index kann auch verwendet werden, wenn die ORDER BY-Klausel dem Index nicht exakt entspricht, solange alle nicht verwendeten Bestandteile des Indexes und alle zusätzlichen ORDER BY-Spalten Konstanten in der WHERE-Klausel sind. Die folgenden Abfragen lösen den ORDER BY-Teil mithilfe des Indexes auf:

SELECT * FROM t1 
  ORDER BY key_part1,key_part2,... ;
    
SELECT * FROM t1 
  WHERE key_part1=constant 
  ORDER BY key_part2;
    
SELECT * FROM t1 
  ORDER BY key_part1 DESC, key_part2 DESC;
    
SELECT * FROM t1
  WHERE key_part1=1 
  ORDER BY key_part1 DESC, key_part2 DESC;

In manchen Fällen kann MySQL Indizes nicht zur Auflösung von ORDER BY verwenden, obwohl es mithilfe der Indizes Datensätze findet, die der WHERE-Klausel entsprechen. Dies betrifft u. a. die folgenden Fälle:

  • Sie verwenden ORDER BY für verschiedene Schlüssel:

    SELECT * FROM t1 ORDER BY key1, key2;
    
  • Sie verwenden ORDER BY für Teile eines Schlüssels, die nicht aufeinander folgen:

    SELECT * FROM t1 WHERE key2=constant ORDER BY key_part2;
    
  • Sie verwenden ASC und DESC gemischt:

    SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;
    
  • Der Schlüssel, der zum Holen der Datensätze verwendet wird, ist nicht derselbe wie derjenige, der in der ORDER BY-Klausel verwendet wird:

    SELECT * FROM t1 WHERE key2=constant ORDER BY key1;
    
  • Sie verknüpfen zahlreiche Tabellen, und die Spalten in der ORDER BY-Klausel stammen nicht alle aus der ersten nichtkonstanten Tabelle, die zum Abrufen von Datensätzen verwendet wird. (Dies ist die erste Tabelle in der Ausgabe von EXPLAIN, die nicht den Join-Typ const aufweist.)

  • Sie haben verschiedene ORDER BY- und GROUP BY-Ausdrücke.

  • Der Typ des verwendeten Tabellenindexes speichert die Datensätze nicht in ihrer Reihenfolge. Dies gilt beispielsweise für einen HASH-Index in einer MEMORY-Tabelle.

Mit EXPLAIN SELECT … ORDER BY können Sie überprüfen, ob MySQL Indizes zur Auflösung der Abfrage verwenden kann. Wenn Sie Using filesort in der Spalte Extra sehen, ist dies nicht möglich. Siehe auch Abschnitt 7.2.1, „EXPLAIN-Syntax (Informationen über ein SELECT erhalten)“.

Die verwendete filesort-Optimierung vermerkt nicht nur den Sortierschlüsselwert und die Datensatzposition, sondern auch die Spalten, die für die Abfrage erforderlich sind. Hiermit wird verhindert, dass Datensätze zweimal ausgelesen werden. Der filesort-Algorithmus funktioniert wie folgt:

  1. Die Datensätze, die der WHERE-Klausel entsprechen, werden gelesen.

  2. Für jeden Datensatz wird ein Tupel mit Werten aufgezeichnet, die den Sortierschlüsselwert und die Datensatzposition sowie die Spalten umfassen, die für die Abfrage erforderlich sind.

  3. Die Tupel werden nach dem Sortierschlüsselwert sortiert.

  4. Die Datensätze werden in der sortierten Reihenfolge abgerufen, aber die erforderlichen Spalten werden direkt aus den sortierten Tupeln ausgelesen, statt ein zweites Mal auf die Tabelle zuzugreifen.

Dieser Algorithmus stellt gegenüber dem in älteren MySQL-Versionen verwendeten eine erhebliche Verbesserung dar.

Um eine Verlangsamung zu vermeiden, wird diese Optimierung nur verwendet, wenn die Gesamtgröße der zusätzlichen Spalten im Sortiertupel den Wert der Systemvariablen max_length_for_sort_data nicht überschreitet. (Wenn Sie dieser Variablen einen zu hohen Wert zuweisen, treten typische Symptome wie eine hohe Festplatten- und eine niedrige Prozessoraktivität auf.)

Wenn Sie die Geschwindigkeit von ORDER BY erhöhen wollen, überprüfen Sie, ob Sie MySQL dazu bewegen können, Indizes statt einer zusätzlichen Sortierphase zu verwenden. Ist dies nicht möglich, dann können Sie die folgenden Strategien ausprobieren:

  • Sie erhöhen den Wert der Variablen sort_buffer_size.

  • Sie erhöhen den Wert der Variablen read_rnd_buffer_size.

  • Sie ändern tmpdir so, dass die Variable auf ein dediziertes Dateisystem mit viel freiem Speicher weist. Die Option akzeptiert mehrere Pfade, die zyklisch abwechselnd verwendet werden. Die Pfade sollten unter Unix mit Doppelpunkten (‘:’) und unter Windows, NetWare und OS/2 mit Semikola (‘;’) voneinander getrennt werden. Mithilfe dieser Funktion können Sie die Last auf mehrere Verzeichnisse verteilen. Hinweis: Die Pfade sollten auf Verzeichnisse in Dateisystemen verweisen, die sich auf verschiedenen physischen Festplatten befinden – nicht auf verschiedenen Partitionen derselben Festplatte.

Standardmäßig sortiert MySQL alle GROUP BY col1, col2, …-Abfragen so, als ob Sie in der Abfrage auch ORDER BY col1, col2, … angegeben hätten. Wenn Sie ausdrücklich eine ORDER BY-Klausel angeben, die dieselbe Spaltenliste enthält, optimiert MySQL diese ohne Geschwindigkeitseinbuße weg, auch wenn die Sortierung tatsächlich stattfindet. Enthält eine Abfrage eine GROUP BY-Klausel, während Sie die Mehrbelastung durch die Sortierung des Ergebnisses vermeiden wollen, dann können Sie diese Sortierung durch Angabe von ORDER BY NULL unterdrücken. Zum Beispiel:

INSERT INTO foo
SELECT a, COUNT(*) FROM bar GROUP BY a ORDER BY NULL;

7.2.13. GROUP BY-Optimierung

Die allgemeinste Art und Weise, eine GROUP BY-Klausel zu erfüllen, besteht darin, die gesamte Tabelle zu scannen und eine neue Temporärtabelle zu erstellen, in der alle Datensätze aus allen Gruppen aufeinander folgend aufgeführt sind, und dann mithilfe dieser Temporärtabelle Gruppen zu erkennen und – sofern möglich – Zusammenfassungsfunktionen anzuwenden. In manchen Fällen kann MySQL jedoch auf andere Weise wesentlich effizienter vorgehen und die Erstellung von Temporärtabellen umgehen, indem ein Indexzugriff verwendet wird.

Die wichtigsten Voraussetzungen zur Verwendung von Indizes für GROUP BY bestehen darin, dass alle GROUP BY-Spalten Attribute aus demselben Index referenzieren und dass der Index seine Schlüssel in der Reihenfolge speichert (beispielsweise ist dies ein BTREE- und kein HASH-Index). Ob die Verwendung von Temporärtabellen durch einen Indexzugriff ersetzt werden kann, hängt auch davon ab, welche Teile eines Indexes in einer Abfrage verwendet werden, welche Bedingungen für diese Teile spezifiziert sind und welche Zusammenfassungsfunktionen ausgewählt wurden.

Es gibt zwei Möglichkeiten, eine GROUP BY-Abfrage über den Indexzugriff auszuführen; sie beide werden in den folgenden Abschnitten beschrieben. Bei der ersten Methode wird die Gruppierungsoperation ggf. gemeinsam mit allen Bereichsprädikaten angewandt. Die zweite Methode führt zunächst einen Bereichsscan durch und gruppiert dann die Ergebnistupel.

7.2.13.1. Lockere Indexscans

Die effizienteste Möglichkeit, GROUP BY zu verarbeiten, besteht in der Verwendung des Indexes zum direkten Abrufen der Gruppenfelder. Bei dieser Zugriffsmethode verwendet MySQL die Eigenschaft einiger Indextypen, dass die Schlüssel sortiert sind (z. B. BTREE). Diese Eigenschaft erlaubt die Verwendung von Suchvorgangsgruppen in einem Index, ohne dass alle Schlüssel im Index berücksichtigt werden müssten, die die WHERE-Bedingungen erfüllen. Diese Zugriffsmethode berücksichtigt also nur einen Bruchteil der Schlüssel in einem Index und heißt deswegen lockerer Indexscan. Wenn keine WHERE-Klausel vorhanden ist, liest ein lockerer Indexscan so viele Schlüssel, wie Gruppen vorhanden sind. Diese Anzahl kann bedeutend kleiner sein als die aller Schlüssel. Wenn die WHERE-Klausel Bereichsprädikate enthält (siehe auch die Beschreibung des Join-Typs range in Abschnitt 7.2.1, „EXPLAIN-Syntax (Informationen über ein SELECT erhalten)“), dann sucht ein lockerer Indexscan nach dem jeweils ersten Schlüssel aller Gruppen, die die Bereichsbedingungen erfüllen, und liest dann erneut die kleinstmögliche Anzahl Schlüssel aus. Dies ist unter den folgenden Bedingungen möglich:

  • Die Abfrage gilt einer einzelnen Tabelle.

  • Die GROUP BY-Klausel enthält die ersten aufeinander folgenden Teile des Indexes. (Wenn die Abfrage statt einer GROUP BY- eine DISTINCT-Klausel enthält, referenzieren alle den Anfang des Indexes.)

  • Wenn überhaupt, so werden als einzige Zusammenfassungsfunktionen MIN() und MAX() verwendet, die zudem alle dieselbe Spalte referenzieren.

  • Alle Teile des Indexes, die nicht aus der in der Abfrage referenzierten GROUP BY-Klausel stammen, müssen Konstanten sein (d. h., sie müssen in Gleichungen mit Konstanten referenziert werden). Ausgenommen ist lediglich das Argument der Funktionen MIN() und MAX().

Die EXPLAIN-Ausgabe für solche Abfragen zeigt Using index for group-by in der Spalte Extra an.

Die folgenden Abfragen gehören in diese Kategorie, vorausgesetzt, ein Index idx(c1,c2,c3) ist für die Tabelle t1(c1,c2,c3,c4) vorhanden:

SELECT c1, c2 FROM t1 GROUP BY c1, c2;
SELECT DISTINCT c1, c2 FROM t1;
SELECT c1, MIN(c2) FROM t1 GROUP BY c1;
SELECT c1, c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT MAX(c3), MIN(c3), c1, c2 FROM t1 WHERE c2 > const GROUP BY c1, c2;
SELECT c2 FROM t1 WHERE c1 < const GROUP BY c1, c2;
SELECT c1, c2 FROM t1 WHERE c3 = const GROUP BY c1, c2;

Die folgenden Abfragen können aus den angegebenen Gründen nicht mit dieser schnellen Auswahlmethode ausgeführt werden:

  • Es sind andere Zusammenfassungsfunktionen als MIN() oder MAX() vorhanden, z. B.:

    SELECT c1, SUM(c2) FROM t1 GROUP BY c1;
    
  • Die Felder in der GROUP BY-Klausel referenzieren nicht den Anfang des Indexes:

    SELECT c1,c2 FROM t1 GROUP BY c2, c3;
    
  • Die Abfrage referenziert einen Teil eines Schlüssels, der nach dem GROUP BY-Teil kommt und für den keine Gleichzeit mit einer Konstante vorliegt. Ein Beispiel:

    SELECT c1,c3 FROM t1 GROUP BY c1, c2;
    

7.2.13.2. Enggefasster Indexscan

Ein enggefasster Indexscan kann je nach Abfragebedingung entweder ein vollständiger Indexscan oder ein Bereichsindexscan sein.

Wenn die Bedingungen für einen lockeren Indexscan nicht erfüllt werden, ist es trotzdem möglich, die Erstellung von Temporärtabellen für GROUP BY-Abfragen zu umgehen. Wenn in der WHERE-Klausel Bereichsbedingungen vorhanden sind, liest diese Methode nur die Schlüssel aus, die diese Bedingungen erfüllen. Andernfalls wird ein Indexscan durchgeführt. Da diese Methode alle Schlüssel in jedem von der WHERE-Klausel definierten Bereich liest oder den gesamten Index scannt, sofern keine Bereichsbedingungen vorhanden sind, sprechen wir hier vom enggefassten Indexscan. Beachten Sie, dass die Gruppierungsoperation beim enggefassten Indexscan erst durchgeführt wird, wenn alle Schlüssel gefunden wurden, die die Bereichsbedingungen erfüllen.

Damit diese Methode funktioniert, ist eine Konstantengleichheitsbedingung für alle Spalten in einer Abfrage ausreichend, wenn diese Abfrage die Schlüssel referenziert, die vor oder zwischen den Teilen des GROUP BY-Schlüssels auftreten. Die Konstanten aus den Gleichheitsbedingungen füllen alle „Lücken“ in den Suchschlüsseln auf, sodass es möglich ist, vollständige Präfixe des Indexes zu bilden. Diese Indexpräfixe können dann für Indexsuchvorgänge verwendet werden. Wenn Sie die Sortierung des GROUP BY-Ergebnisses benötigen und es möglich ist, Suchschlüssel zu bilden, die Präfixe des Indexes sind, dann vermeidet MySQL außerdem zusätzliche Sortieroperationen, weil das Suchen mit Präfixen in einem sortierten Index bereits alle Schlüssel in der korrekten Reihenfolge abruft.

Die folgenden Abfragen funktionieren beim oben beschriebenen lockeren Indexscan nicht, wohl allerdings beim enggefassten Indexscan (sofern ein Index idx(c1,c2,c3) für die Tabelle t1(c1,c2,c3,c4) vorhanden ist).

  • In der GROUP BY-Klausel ist zwar eine Lücke vorhanden, aber diese wird von der Bedingung c2 = 'a' abgedeckt.

    SELECT c1, c2, c3 FROM t1 WHERE c2 = 'a' GROUP BY c1, c3;
    
  • GROUP BY beginnt nicht mit dem ersten Teil des Schlüssels, aber es ist eine Bedingung vorhanden, die eine Konstante für diesen Teil angibt:

    SELECT c1, c2, c3 FROM t1 WHERE c1 = 'a' GROUP BY c2, c3;
    

7.2.14. LIMIT-Optimierung

In manchen Fällen verarbeitet MySQL eine Abfrage anders, wenn Sie LIMIT row_count verwenden und HAVING nicht einsetzen:

  • Wenn Sie nur ein paar Datensätze mit LIMIT auswählen, benutzt MySQL in manchen Fällen Indizes, obwohl eigentlich ein vollständiger Tabellenscan zu bevorzugen wäre.

  • Wenn Sie LIMIT row_count mit ORDER BY benutzen, beendet MySQL die Sortierung, sobald die ersten row_count Datensätze des sortierten Ergebnisses gefunden wurden, statt das gesamte Ergebnis zu sortieren. Erfolgt die Sortierung mithilfe eines Indexes, dann ist sie sehr schnell. Ist eine Dateisortierung erforderlich, dann müssen alle Datensätze ausgewählt werden, die der Abfrage ohne LIMIT-Klausel entsprechen, und die meisten davon (oder alle) müssen sortiert werden, bevor sichergestellt ist, dass die ersten row_count Datensätze gefunden wurden. In jedem Fall ist es, wenn die ersten Datensätze gefunden wurden, nicht mehr notwendig, den Rest der Ergebnismenge zu sortieren, weswegen MySQL dies auch nicht tut.

  • Wenn Sie LIMIT row_count mit DISTINCT kombinieren, beendet MySQL die Abfrage, sobald row_count eindeutige Datensätze gefunden wurden.

  • In manchen Fällen kann eine GROUP BY-Klausel durch Lesen des Schlüssels in der Reihenfolge (oder Durchführung einer Sortierung für den Schlüssel) und nachfolgende Berechnung von Zusammenfassungen aufgelöst werden, bis der Schlüsselwert sich ändert. In diesem Fall berechnet LIMIT row_count keine unnötigen GROUP BY-Werte.

  • Sobald MySQL die erforderliche Anzahl von Datensätzen an den Client gesendet hat, bricht es die Abfrage ab, sofern Sie nicht SQL_CALC_FOUND_ROWS verwenden.

  • LIMIT 0 gibt schnell eine leere Menge zurück. Dies kann praktisch sein, um die Gültigkeit einer Abfrage zu überprüfen. Wenn Sie eines der MySQL-APIs verwenden, können Sie damit auch die Typen der Ergebnisspalten ermitteln. (Dieser Trick funktioniert nicht im MySQL Monitor, der in solchen Fällen lediglich Empty set anzeigt. Stattdessen sollten Sie SHOW COLUMNS oder DESCRIBE für diesen Zweck verwenden.)

  • Wenn der Server Temporärtabellen zur Auflösung der Abfrage verwendet, dann berechnet er auf der Basis der LIMIT row_count-Klausel, wie viel Speicher erforderlich ist.

7.2.15. Vermeidung von Tabellenscans

Die Ausgabe von EXPLAIN zeigt ALL in der type-Spalte, wenn MySQL die Abfrage unter Verwendung eines Tabellenscans auflöst. Dies geschieht in der Regel unter den folgenden Bedingungen:

  • Die Tabelle ist so klein, dass ein Tabellenscan schneller ist als eine Schlüsselsuche. Dies betrifft in erster Linie Tabellen mit weniger als zehn Datensätzen und geringer Datensatzlänge.

  • In der ON- oder WHERE-Klausel für indizierte Spalten sind keine verwendbaren Einschränkungen vorhanden.

  • Sie vergleichen indizierte Spalten mit Konstantenwerten, und MySQL hat (basierend auf dem Indexbaum) berechnet, dass die Konstanten einen zu großen Teil der Tabelle abdecken und ein Tabellenscan schneller ginge. Siehe auch Abschnitt 7.2.4, „Optimierungen der WHERE-Klausel“.

  • Sie verwenden einen Schlüssel mit niedriger Kardinalität (d. h., viele Datensätze entsprechen dem Schlüsselwert) über eine andere Spalte. In diesem Fall geht MySQL davon aus, dass die Verwendung des Schlüssels viele Suchvorgänge nach sich ziehen würde und ein Tabellenscan schneller ginge.

Bei kleinen Tabellen ist ein Tabellenscan oft die passende Lösung. Die Auswirkungen auf die Leistung sind vernachlässigbar. Bei großen Tabellen sollten Sie die folgenden Methoden ausprobieren, damit der Optimierer sich nicht fälschlicherweise für einen Tabellenscan entscheidet:

  • Aktualisieren Sie die Schlüsselverteilung in der gescannten Tabelle mit ANALYZE TABLE tbl_name. Siehe auch Abschnitt 13.5.2.1, „ANALYZE TABLE.

  • Verwenden Sie FORCE INDEX für die gescannte Tabelle, um MySQL mitzuteilen, dass Tabellenscans im Vergleich zur Verwendung des angegebenen Indexes sehr kostspielig sind:

    SELECT * FROM t1, t2 FORCE INDEX (index_for_column)
      WHERE t1.col_name=t2.col_name;
    

    Siehe auch Abschnitt 13.2.7, „SELECT.

  • Starten Sie mysqld mit der Option --max-seeks-for-key=1000, oder weisen Sie den Optimierer mit SET max_seeks_for_key=1000 an, vorauszusetzen, dass kein Schlüsselscan mehr als 1000 Schlüsselsuchvorgänge auslösen wird. Siehe auch Abschnitt 5.2.2, „Server-Systemvariablen“.

7.2.16. Geschwindigkeit von INSERT-Anweisungen

Die Zeit, die für das Einfügen eines Datensatzes erforderlich ist, wird von den folgenden Faktoren bestimmt (hierbei geben die Zahlen in den Klammern näherungsweise den Anteil an):

  • Verbindung herstellen: (3)

  • Abfrage an den Server senden: (2)

  • Abfrage analysieren: (2)

  • Datensatz einfügen: (1 × Datensatzlänge)

  • Indizes einfügen: (1 × Anzahl der Indizes)

  • Schließen: (1)

Hierbei wird der Mehraufwand für das Öffnen von Tabellen nicht berücksichtigt (dies ist einmal je nebenläufiger Abfrageausführung erforderlich).

Die Größe der Tabelle verlangsamt das Einfügen von Indizes um den Faktor LogN (für B-Tree-Indizes).

Sie können Einfügeoperationen mit den folgenden Methoden beschleunigen:

  • Wenn Sie gleichzeitig viele Datensätze vom selben Client aus einfügen, verwenden Sie INSERT-Anweisungen mit mehreren VALUES-Listen, um mehrere Datensätze zur selben Zeit einzufügen. Dies ist erheblich (in manchen Fällen sogar um mehrere Größenordnungen) schneller als die Verwendung separater INSERT-Anweisungen für je einen Datensatz. Wenn Sie zu einer Tabelle, die nicht leer ist, Daten hinzufügen, dann können Sie die Variable bulk_insert_buffer_size optimieren, um das Einfügen noch mehr zu beschleunigen. Siehe auch Abschnitt 5.2.2, „Server-Systemvariablen“.

  • Wenn Sie eine große Menge Datensätze von verschiedenen Clients einfügen, können Sie den Vorgang durch Verwendung der INSERT DELAYED-Anweisung beschleunigen. Siehe auch Abschnitt 13.2.4.2, „INSERT DELAYED.

  • Bei einer MyISAM-Tabelle können Sie nebenläufige Einfügeoperationen verwenden, um Datensätze einzufügen, während gleichzeitig SELECT-Anweisungen ausgeführt werden, sofern sich in der Mitte der Tabelle keine gelöschten Datensätze befinden. Siehe auch Abschnitt 7.3.3, „Gleichzeitige Einfügevorgänge“.

  • Wenn Sie eine Tabelle aus einer Textdatei laden, verwenden Sie LOAD DATA INFILE. Dies ist normalerweise 20-mal schneller als die Verwendung von INSERT-Anweisungen. Siehe auch Abschnitt 13.2.5, „LOAD DATA INFILE.

  • Durch geringen Mehraufwand können Sie LOAD DATA INFILE für eine MyISAM-Tabelle noch schneller machen, wenn diese Tabelle viele Indizes enthält. Gehen Sie wie folgt vor:

    1. Erstellen Sie die Tabelle mit CREATE TABLE (optional).

    2. Führen Sie eine FLUSH TABLES-Anweisung oder den Befehl mysqladmin flush-tables aus.

    3. Setzen Sie myisamchk --keys-used=0 -rq /path/to/db/tbl_name ab. Hierdurch wird die Verwendung aller Indizes für die Tabelle unterbunden.

    4. Fügen Sie mit LOAD DATA INFILE Daten in die Tabelle ein. Hierbei werden keine Indizes aktualisiert – der Vorgang ist also sehr schnell.

    5. Wenn Sie zukünftig nur aus der Tabelle lesen wollen, komprimieren Sie sie mit myisampack. Siehe auch Abschnitt 14.1.3.3, „Kennzeichen komprimierter Tabellen“.

    6. Erstellen Sie die Indizes mit myisamchk -rq /path/to/db/tbl_name neu. Hierdurch wird der Indexbaum zunächst im Speicher erstellt, bevor er auf die Festplatte geschrieben wird. Dies ist viel schneller als die Aktualisierung des Indexes im Zuge von LOAD DATA INFILE, da zahlreiche Suchvorgänge auf der Festplatte entfallen. Außerdem ist der resultierende Indexbaum einwandfrei verteilt.

    7. Führen Sie eine FLUSH TABLES-Anweisung oder den Befehl mysqladmin flush-tables aus.

    Beachten Sie, dass LOAD DATA INFILE obige Optimierung automatisch durchführt, wenn die MyISAM-Tabelle, in die Sie die Daten einfügen, leer ist. Der wesentliche Unterschied besteht darin, dass Sie myisamchk wesentlich mehr Temporärspeicher für die Indexerstellung zuweisen können, als Sie durch den Server zur Indexneuerstellung reservieren lassen würden, wenn er die LOAD DATA INFILE-Anweisung ausführt.

    Sie können die Indizes für eine MyISAM-Tabelle ferner aktivieren oder deaktivieren, indem Sie statt myisamchk die folgenden Anweisungen verwenden. Bei Verwendung dieser Anweisungen können Sie die FLUSH TABLE-Operationen übergehen:

    ALTER TABLE tbl_name DISABLE KEYS;
    ALTER TABLE tbl_name ENABLE KEYS;
    
  • Um INSERT-Operationen zu beschleunigen, die mit mehreren Anweisungen durchgeführt werden, sperren Sie Ihre Tabellen:

    LOCK TABLES a WRITE;
    INSERT INTO a VALUES (1,23),(2,34),(4,33);
    INSERT INTO a VALUES (8,26),(6,29);
    UNLOCK TABLES;
    

    Hiervon profitiert die Leistungsfähigkeit, weil der Indexpuffer nur einmal auf die Festplatte synchronisiert wird, nachdem alle INSERT-Anweisungen abgeschlossen wurden. Im Normalfall gäbe es so viele Synchronisierungsvorgänge für Indexpuffer, wie INSERT-Anweisungen vorhanden sind. Explizite Sperranweisungen sind nicht erforderlich, wenn Sie alle Datensätze mit einer einzelnen INSERT-Anweisung einfügen können.

    Bei transaktionssicheren Tabellen sollten Sie START TRANSACTION und COMMIT statt LOCK TABLES benutzen, um die Einfügeoperationen zu beschleunigen.

    Eine Sperrung verringert auch die Gesamtdauer für die Überprüfung mehrerer Verbindungen, auch wenn sich die Gesamtwartezeit für einzelne Verbindungen erhöhen kann, weil diese auf das Erwirken der Sperren warten. Zum Beispiel:

    1. Verbindung 1 führt 1.000 Einfügeoperationen durch.

    2. Die Verbindungen 2, 3 und 4 fügen je eine Einfügeoperation durch.

    3. Verbindung 5 führt 1.000 Einfügeoperationen durch.

    Wenn Sie keine Sperrung verwenden, werden die Verbindungen 2, 3 und 4 vor den Verbindungen 1 und 5 beendet; setzen Sie hingegen eine Sperre, dann werden die Verbindungen 2, 3 und 4 zwar (wahrscheinlich) nicht eher als die Verbindungen 1 und 5 enden, aber der gesamte Zeitbedarf verringert sich um ca. 40 Prozent.

    INSERT-, UPDATE- und DELETE-Operationen sind in MySQL sehr schnell, aber Sie können eine noch bessere Gesamtperformance erzielen, indem Sie Sperren für alles setzen, was mehr als fünf Einfüge- oder Änderungsoperationen in einem Datensatz umfasst. Wenn Sie sehr viele Einfügeoperationen in einem Datensatz vornehmen, könnten Sie ab und zu (d. h. etwa alle 1.000 Datensätze) ein LOCK TABLES gefolgt von einem UNLOCK TABLES absetzen, damit auch andere Threads auf die Tabelle zugreifen können. Dies bringt trotz allem einen ansehnlichen Leistungsgewinn.

    Auch wenn Sie die soeben beschriebenen Strategien verwenden, ist INSERT beim Laden von Daten allerdings nach wie vor wesentlich langsamer als LOAD DATA INFILE.

  • Um die Leistung bei MyISAM-Tabellen für LOAD DATA INFILE und INSERT gleichermaßen zu optimieren, vergrößern Sie den Schlüssel-Cache, indem Sie den Wert der Systemvariablen key_buffer_size erhöhen. Siehe auch Abschnitt 7.5.2, „Serverparameter feineinstellen“.

7.2.17. Geschwindigkeit von UPDATE-Anweisungen

Eine Änderungsanweisung wird wie eine SELECT-Abfrage optimiert, es kommt jedoch der zusätzliche Mehraufwand einer Schreiboperation hinzu. Die Geschwindigkeit der Schreiboperation hängt von der Menge der zu ändernden Daten und der Anzahl der zu aktualisierenden Indizes ab. Indizes, die nicht geändert werden, werden nicht aktualisiert.

Eine andere Möglichkeit, Updates zu beschleunigen, besteht darin, sie aufzuschieben und dann viele Updates direkt hintereinander durchzuführen. Wenn Sie die Tabelle sperren, ist die gemeinsame Durchführung vieler Änderungen wesentlich schneller, als wenn Sie immer nur ein Update zur selben Zeit durchführen.

Bei einer MyISAM-Tabelle, die das dynamische Datensatzformat verwendet, kann die Änderung eines Datensatzes auf eine höhere Gesamtlänge dazu führen, dass der Datensatz geteilt wird. Wenn Sie dies oft tun, dürfen Sie keinesfalls vergessen, gelegentlich OPTIMIZE TABLE abzusetzen. Siehe auch Abschnitt 13.5.2.5, „OPTIMIZE TABLE.

7.2.18. Geschwindigkeit von DELETE-Anfragen

Die zum Löschen einzelner Datensätze erforderliche Zeit ist direkt proportional zur Anzahl der Indizes. Um Datensätze schneller zu löschen, können Sie den Wert des Schlüssel-Caches erhöhen, indem Sie die Systemvariable key_buffer_size entsprechend einstellen. Siehe auch Abschnitt 7.5.2, „Serverparameter feineinstellen“.

Wenn Sie alle Datensätze aus einer Tabelle löschen wollen, ist TRUNCATE TABLE tbl_name schneller als DELETE FROM tbl_name. Siehe auch Abschnitt 13.2.9, „TRUNCATE.

7.2.19. Weitere Optimierungstipps

Dieser Abschnitt listet eine Anzahl verschiedener Tipps zur Verbesserung der Abfrageverarbeitungsgeschwindigkeit auf:

  • Verwenden Sie permanente Verbindungen zur Datenbank, um die zusätzliche Belastung durch das Herstellen und Abbauen von Verbindungen zu umgehen. Können Sie keine Permanentverbindungen verwenden und stellen Sie viele neue Verbindungen zur Datenbank her, dann sollten Sie den Wert der Variablen thread_cache_size ändern. Siehe auch Abschnitt 7.5.2, „Serverparameter feineinstellen“.

  • Überprüfen Sie immer, ob alle Ihre Abfragen wirklich die Indizes verwenden, die Sie in den Tabellen erstellt haben. In MySQL können Sie dies mit der EXPLAIN-Anweisung tun. Siehe auch Abschnitt 7.2.1, „EXPLAIN-Syntax (Informationen über ein SELECT erhalten)“.

  • Versuchen Sie komplexe SELECT-Abfragen an häufig aktualisierte MyISAM-Tabellen zu umgehen, um Probleme in Verbindung mit der Tabellensperrung zu vermeiden, die aufgrund des konkurrierenden Zugriffs durch Leser und Schreiber entstehen.

  • Bei MyISAM-Tabellen, bei denen in der Mitte keine gelöschten Datensätze vorhanden sind, können Sie Datensätze am Ende einfügen, während gleichzeitig eine andere Abfrage aus der Tabelle liest. Wenn ein solches Verhalten wichtig ist, sollten Sie die Tabelle so verwenden, dass möglichst keine Datensätze gelöscht werden. Eine andere Möglichkeit besteht darin, die Tabelle mit OPTIMIZE TABLE zu defragmentieren, wenn Sie viele Datensätze gelöscht haben. Siehe auch Abschnitt 14.1, „Die MyISAM-Speicher-Engine“.

  • Um komprimierungsbedingte Probleme zu beheben, die im Zusammenhang mit ARCHIVE-Tabellen auftreten können, können Sie OPTIMIZE TABLE verwenden. Siehe auch Abschnitt 14.8, „Die ARCHIVE-Speicher-Engine“.

  • Verwenden Sie ALTER TABLE … ORDER BY expr1, expr2, …, wenn Sie Datensätze gewöhnlich in der Reihenfolge expr1, expr2, … abrufen. Wenn Sie diese Option nach umfangreicheren Änderungen in der Tabelle verwenden, können Sie die Leistung unter Umständen steigern.

  • In manchen Fällen kann es sinnvoll sein, eine Spalte hinzuzufügen, die auf den Daten in anderen Spalten basierende „Hash-Werte“ enthält. Wenn diese Spalte kurz und ausreichend eindeutig ist, kann dies wesentlich schneller sein als ein „breiter“ Index über viele Spalten. In MySQL ist die Verwendung dieser Zusatzspalte ganz einfach:

    SELECT * FROM tbl_name
      WHERE hash_col=MD5(CONCAT(col1,col2))
      AND col1='constant' AND col2='constant';
    
  • Bei MyISAM-Tabellen, die sich häufig verändern, sollten Sie die Verwendung von Spalten variabler Länge (VARCHAR, BLOB und TEXT) möglichst unterlassen. Die Tabelle verwendet das dynamische Datensatzformat, sobald auch nur eine einzige Spalte variabler Länge vorhanden ist. Siehe auch Kapitel 14, Speicher-Engines und Tabellentypen.

  • Es ist normalerweise nicht sinnvoll, eine Tabelle in mehrere getrennte Tabellen zu unterteilen, weil die Datensätze dann sehr groß werden. Beim Zugriff auf einen Datensatz besteht die größte Anforderung aus leistungstechnischer Sicht im Suchvorgang, der auf der Festplatte ausgeführt wird, um das erste Byte des Datensatzes zu finden. Wurde der Beginn der Daten gefunden, dann können die meisten modernen Festplatten den vollständigen Datensatz für die meisten Anwendungen ausreichend schnell lesen. Die einzigen Fälle, in denen das Aufteilen einer Tabelle einen nennenswerten Unterschied macht, liegen vor, wenn es sich um eine MyISAM-Tabelle handelt, die das dynamische Datensatzformat verwendet, welches Sie in eine feste Datensatzgröße umwandeln können, oder wenn Sie die Tabelle sehr häufig scannen müssen, die meisten Spalten aber nicht benötigen. Siehe auch Kapitel 14, Speicher-Engines und Tabellentypen.

  • Müssen Sie häufig Ergebnisse wie beispielsweise Summenwerte basierend auf Daten aus einer Vielzahl von Datensätzen berechnen, dann kann es praktisch sein, eine neue Tabelle einzurichten und den Zähler in Echtzeit zu aktualisieren. Eine Änderung der folgenden Form ist sehr schnell:

    UPDATE tbl_name SET count_col=count_col+1 WHERE key_col=constant;
    

    Dies ist sehr wichtig, wenn Sie MySQL-Speicher-Engines wie MyISAM verwenden, die nur Sperren auf Tabellenebene bieten (d. h. mehrere Leser bei einem Schreiber). Dies bietet auch bei den meisten anderen Datenbanksystemen eine bessere Performance, da der Datensatzsperrmanager in diesem Fall weniger zu tun hat.

  • Wenn Sie Statistiken aus großen Logtabellen ermitteln müssen, sollten Sie Zusammenfassungstabellen verwenden, statt die gesamte Logtabelle zu scannen. Die Erstellung der Zusammenfassungen sollte wesentlich schneller sein als die Berechnung von „Echtzeitstatistiken“. Die Neuerstellung von Übersichtstabellen aus den Logs ist, wenn sich (abhängig von Geschäftsentscheidungen) Faktoren ändern, schneller als eine Anpassung der laufenden Anwendung.

  • Sofern möglich, sollten Sie Berichte als „Echtzeitberichte“ oder als „statistische Berichte“ klassifizieren, wobei die für die statistischen Berichte erforderlichen Daten nur aus Zusammenfassungstabellen erzeugt werden, die ihrerseits in regelmäßigen Abständen aus den Echtzeitdaten erstellt werden.

  • Nutzen Sie die Tatsache, dass Spalten Standardwerte haben. Fügen Sie Werte nur dann explizit ein, wenn sich der einzufügende Wert von der Vorgabe unterscheidet. Hierdurch verringern Sie die Analysearbeit für MySQL und erhöhen die Einfügegeschwindigkeit.

  • In manchen Fällen ist es sinnvoll, Daten zu packen und in einer BLOB-Spalte zu speichern. In diesem Fall muss Ihre Anwendung zwar Code zum Packen und Entpacken der Daten enthalten, aber Sie sparen sich auf einer gewissen Ebene viele Zugriffe. Dies ist praktisch, wenn Sie Daten haben, die sich nicht gut in die zeilen- und spaltenbasierte Struktur einer Tabelle einfügen lassen.

  • Normalerweise sollten Sie alle Daten nichtredundant halten. (Beachten Sie dabei die in der Datenbanktheorie so genannte Dritte Normalform.) Allerdings gibt es auch Situationen, in denen die Duplizierung von Daten oder die Erstellung zusammenfassender Tabellen zur Geschwindigkeitssteigerung beitragen können.

  • Bei einigen Aufgaben können etwa gespeicherte Routinen oder benutzerdefinierte Funktionen Optionen zur Optimierung der Leistung sein. Weitere Informationen finden Sie in Kapitel 19, Gespeicherte Prozeduren und Funktionen, und Abschnitt 26.3, „Hinzufügen neuer Funktionen zu MySQL“.

  • Sie können immer einen kleinen Geschwindigkeitsvorteil gewinnen, indem Sie Abfragen oder Antworten in Ihrer Anwendung in einem Cache ablegen und dann zahlreiche Einfüge- oder Aktualisierungsvorgänge gemeinsam ausführen. Wenn Ihr Datenbanksystem Tabellensperren unterstützt (wie es beispielsweise bei MySQL und Oracle der Fall ist), dann sollten Sie so sicherstellen können, dass der Index-Cache nur einmal nach Durchführung aller Änderungen synchronisiert wird. Sie können auch vom Abfrage-Cache von MySQL profitieren, um ähnliche Ergebnisse zu erhalten (siehe auch Abschnitt 5.14, „MySQL-Anfragen-Cache“).

  • Verwenden Sie INSERT DELAYED, wenn Sie nicht unbedingt wissen müssen, wann Ihre Daten geschrieben werden. Dies verringert die Gesamtauswirkungen von Einfügeoperationen, weil zahlreiche Datensätze im Zuge eines einzigen Schreibvorgangs auf die Festplatte geschrieben werden.

  • Verwenden Sie INSERT LOW_PRIORITY, wenn Sie SELECT-Anweisungen Vorrang vor den Einfügeoperationen gewähren wollen.

  • Verwenden Sie SELECT HIGH_PRIORITY, um abgerufene Datensätze in der Warteschlange nach vorne zu setzen: In diesem Fall wird die SELECT-Anweisung auch ausgeführt, wenn ein anderer Client darauf wartet, schreiben zu können.

  • Verwenden Sie INSERT-Anweisungen für mehrere Datensätze, um zahlreiche Daten mit einer SQL-Anweisung zu speichern. Dies wird von vielen SQL-Servern (einschließlich MySQL) unterstützt.

  • Laden Sie mit LOAD DATA INFILE große Datenmengen. Dies ist schneller als die Verwendung von INSERT-Anweisungen.

  • Erzeugen Sie mit AUTO_INCREMENT eindeutige Werte.

  • Verwenden Sie hin und wieder OPTIMIZE TABLE, um eine Fragmentierung von MyISAM-Tabellen mit dynamischem Datensatzformat zu verhindern. Siehe auch Abschnitt 14.1.3, „MyISAM-Tabellenformate“.

  • Verwenden Sie, sofern möglich, MEMORY-Tabellen, um die Geschwindigkeit zu erhöhen. Siehe auch Abschnitt 14.4, „Die MEMORY-Speicher-Engine“. MEMORY-Tabellen sind für nichtkritische Daten nützlich, auf die häufig zugegriffen wird, also etwa Informationen zum zuletzt angezeigten Werbebanner für Benutzer, in deren Webbrowser Cookies nicht aktiviert sind. In vielen Webanwendungen stellen Benutzersitzungen eine andere Alternative für den Umgang mit flüchtigen Statusdaten dar.

  • Bei Webservern sollten Bilder und andere Binärressourcen normalerweise als Dateien gespeichert werden: In der Datenbank speichern Sie also statt der Datei selbst nur einen Verweis darauf. Die meisten Webserver können Dateien besser zwischenspeichern als Datenbankinhalte, weswegen die Verwendung von Dateien in der Regel schneller ist.

  • Spalten mit identischen Daten in verschiedenen Tabellen sollten mit identischen Datentypen deklariert werden, damit auf den entsprechenden Spalten basierende Joins beschleunigt werden.

  • Verwenden Sie möglichst einfache Spaltennamen. In einer Tabelle namens customer sollten Sie statt einer Spalte customer_name besser einfach den Spaltennamen name benutzen. Damit die Namen auf andere SQL-Server portierbar sind, sollten Sie sie stets kürzer als 18 Zeichen halten.

  • Wenn Geschwindigkeit für Sie ein absoluter Schlüsselaspekt ist, sollten Sie einen Blick auf maschinennahe Schnittstellen zur Datenspeicherung werfen, die von verschiedenen SQL-Servern unterstützt werden. Indem Sie beispielsweise direkt auf die MyISAM-Speicher-Engine von MySQL zugreifen, können Sie die Geschwindigkeit im Vergleich zur SQL-Schnittstelle um das Zwei- bis Fünffache erhöhen. Zu diesem Zweck müssen die Daten auf demselben Server wie die Anwendung liegen, und der Zugriff sollte möglichst nur von einem Prozess aus erfolgen (da die externe Dateisperrung wirklich langsam ist). Man könnte diese Probleme beseitigen, indem man maschinennahe MyISAM-Befehle am MySQL Server implementiert (dies wäre eine einfache Möglichkeit, bei Bedarf mehr Leistung herauszuholen). Durch sorgfältiges Design der Datenbankschnittstelle könnte es recht einfach sein, diesen Optimierungstyp zu unterstützen.

  • Wenn Sie numerische Daten verwenden, ist es in vielen Fällen schneller, auf Daten in einer Datenbank (über eine laufende Verbindung) statt in einer Textdatei zuzugreifen. Die Daten in der Datenbank sind wahrscheinlich in einem kompakteren Format gespeichert als in der Textdatei, weswegen beim Zugriff weniger Festplattenzugriffe auftreten. Außerdem können Sie Code in Ihrer Anwendung einsparen, denn Sie müssen die Textdateien nicht analysieren, um Zeilen- und Spaltenbegrenzungen zu finden.

  • Bei einigen Operationen kann die Replikation einen Leistungsgewinn bringen. Sie können die Abrufvorgänge durch die Clients auf mehrere Server verteilen, um einen Lastausgleich zu erzielen. Damit der Master bei der Erstellung von Backups nicht verlangsamt wird, können Sie Sicherungen mit einem Slave-Server erstellen. Siehe auch Kapitel 6, Replikation bei MySQL.

  • Die Deklaration einer MyISAM-Tabelle mit der Tabellenoption DELAY_KEY_WRITE=1 beschleunigt Indexaktualisierungen, da diese erst auf Festplatte geschrieben werden, wenn die Tabelle geschlossen wird. Der Nachteil besteht darin, dass Sie, wenn der Server terminiert wird, während eine solche Tabelle geöffnet ist, sicherstellen müssen, dass die Tabelle unbeschädigt ist, indem Sie den Server mit der Option --myisam-recover ausführen oder myisamchk verwenden, bevor Sie den Server neu starten. (Allerdings sollten Sie auch in diesem Fall keine Daten verlieren, wenn Sie DELAY_KEY_WRITE verwenden, weil die Schlüsselinformationen sich immer aus den Datensätzen in der Datendatei ableiten lassen.)

7.3. Probleme mit Sperren

7.3.1. Wie MySQL Tabellen sperrt

MySQL verwendet die Sperrung auf Tabellenebene für MyISAM- und MEMORY-Tabellen, die Sperrung auf Seitenebene für BDB-Tabellen und die Sperrung auf Datensatzebene für InnoDB-Tabellen.

In vielen Fällen können Sie eine begründete Annahme dazu treffen, welcher Sperrungstyp der beste für eine Anwendung ist; generell ist es aber schwierig zu sagen, dass ein bestimmter Sperrungstyp besser ist als ein anderer. Alles hängt von der Anwendung ab, und verschiedene Teile einer Anwendung können unterschiedliche Sperrungstypen erfordern.

Um zu entscheiden, ob Sie eine Speicher-Engine mit Sperrung auf Datensatzebene verwenden sollten, müssen Sie einschätzen, was Ihre Anwendung tut und welche Mischung aus Auswahl- und Änderungsanweisungen sie verwendet. Die meisten Webanwendungen führen beispielsweise sehr viele Auswahloperationen, relativ wenig Löschvorgänge, vorzugsweise auf Schlüsselwerten basierende Änderungen und Einfügungen in nur ein paar bestimmte Tabellen durch. Die MyISAM-Basiskonfiguration von MySQL ist für solche Zwecke hervorragend geeignet.

Die Tabellensperrung in MySQL schließt ein Deadlock bei Speicher-Engines aus, die Sperren auf Tabellenebene verwenden. Die Deadlock-Vermeidung wird verwaltet, indem zu Anfang einer Abfrage immer alle erforderlichen Sperren auf einmal angefordert und die Tabellen immer in derselben Reihenfolge gesperrt werden.

Die Tabellensperrmethode, die MySQL für WRITE-Sperren verwendet, funktioniert wie folgt:

  • Wenn keine Sperre für die Tabelle vorhanden ist, wird eine Schreibsperre verhängt.

  • Andernfalls wird die Sperranforderung in die Warteschlange für Schreibsperranforderungen gesetzt.

Die Tabellensperrmethode, die MySQL für READ-Sperren verwendet, funktioniert wie folgt:

  • Wenn keine Schreibsperre für die Tabelle vorhanden ist, wird eine Lesesperre verhängt.

  • Andernfalls wird die Sperranforderung in die Warteschlange für Lesesperranforderungen gesetzt.

Wenn eine Sperre aufgehoben wird, wird die Sperrmöglichkeit zunächst den Threads in der Warteschleife für Schreibsperranforderungen und dann den Threads in der Warteschleife für Lesesperranforderungen verfügbar gemacht: Wenn Sie also viele Änderungen an einer Tabelle vorgenommen haben, warten SELECT-Anweisungen, bis keine Änderungen mehr anhängig sind.

Sie können die Zugriffssituation bezüglich der Tabellensperren auf Ihrem System analysieren, indem Sie die Statusvariablen Table_locks_waited und Table_locks_immediate überprüfen:

mysql> SHOW STATUS LIKE 'Table%';
+-----------------------+---------+
| Variable_name         | Value   |
+-----------------------+---------+
| Table_locks_immediate | 1151552 |
| Table_locks_waited    | 15324   |
+-----------------------+---------+

Wenn eine MyISAM-Tabelle keine freien Blöcke in der Mitte enthält, werden Datensätze immer am Ende der Datendatei eingefügt. In diesem Fall können Sie INSERT- und SELECT-Anweisungen bei einer MyISAM-Tabelle ohne Sperre gleichzeitig verwenden. Sie können also Datensätze in eine MyISAM-Tabelle einfügen, während andere Clients zur selben Zeit daraus lesen. (Löcher können entstehen, wenn Datensätze in der Mitte der Tabelle gelöscht oder aktualisiert wurden. Wenn Löcher vorhanden sind, sind gleichzeitige Einfügeoperationen nicht möglich; sie werden allerdings sofort wieder verfügbar, sobald alle Löcher mit neuen Daten gefüllt wurden.)

Wenn Sie viele INSERT- und SELECT-Operationen an einer Tabelle durchführen wollen, während gleichzeitige Einfügeoperationen gerade nicht möglich sind, dann können Sie Datensätze in eine Temporärtabelle einfügen und die echte Tabelle dann sporadisch mit den Datensätzen aus dieser Temporärtabelle aktualisieren. Dies ist etwa mit dem folgenden Code möglich:

mysql> LOCK TABLES real_table WRITE, insert_table WRITE;
mysql> INSERT INTO real_table SELECT * FROM insert_table;
mysql> TRUNCATE TABLE insert_table;
mysql> UNLOCK TABLES;

InnoDB verwendet Datensatzsperren, BDB hingegen Seitensperren. Bei diesen beiden Speicher-Engines sind Deadlocks möglich, weil sie während der Verarbeitung von SQL-Anweisungen automatisch Sperren erwirken – und nicht gleich beim Start der Transaktion.

Vorteile der Sperrung auf Datensatzebene:

  • weniger Sperrkonflikte beim Zugriff auf verschiedene Datensätze in vielen Threads

  • weniger Änderungen bei Rollbacks

  • Möglichkeit einer sehr langen Sperrdauer für einen Datensatz

Nachteile der Sperrung auf Datensatzebene:

  • erfordert mehr Speicher als die Sperrung auf Seiten- oder Tabellenebene

  • langsamer als die Sperrung auf Seiten- oder Tabellenebene, wenn sie für einen großen Teil der Tabelle verwendet wird, weil Sie wesentlich mehr Sperren erwirken müssen

  • definitiv wesentlich langsamer als die anderen Sperrungsoptionen, wenn Sie häufig GROUP BY-Operationen für einen großen Teil der Daten durchführen oder oft die gesamte Tabelle scannen müssen

Tabellensperren sind Sperren auf Seiten- oder Datensatzebene in den folgenden Fällen überlegen:

  • Die meisten Anweisungen für die Tabelle sind Leseoperationen.

  • Eine Mischung von Lese- und Schreiboperationen, wobei die Schreiboperationen Updates oder Löschvorgänge für einen einzelnen Datensatz sind, der mit einem Schlüssellesevorgang geholt werden kann:

    UPDATE tbl_name SET column=value WHERE unique_key_col=key_value;
    DELETE FROM tbl_name WHERE unique_key_col=key_value;
    
  • SELECT in Kombination mit gleichzeitigen INSERT-Anweisungen und sehr wenigen UPDATE- oder DELETE-Anweisungen.

  • Viele Scans oder GROUP BY-Operationen über die gesamte Tabelle ohne Schreiber.

Bei Sperren höherer Ebene können Sie Anwendungen einfacher optimieren, indem Sie Sperren verschiedener Typen unterstützen, weil der Overhead wesentlich geringer ist als bei Sperren auf Datensatzebene.

Andere Optionen als Sperrungen auf Datensatz- und Seitenebene:

  • Versionierung (wie sie beispielsweise in MySQL für nebenläufige Einfügeoperationen verwendet wird). Hierbei können ein Schreiber und mehrere Leser nebeneinander arbeiten. Dies bedeutet, dass die Datenbank oder Tabelle verschiedene Views der Daten abhängig vom Zeitpunkt des Zugriffsbeginns unterstützt. Andere gängige Bezeichnungen hierfür sind „Zeitreise“, „Copy on Write“ (Kopieren beim Schreiben) und „Copy on Demand“ (Kopieren bei Bedarf).

  • Copy on Demand ist der Sperrung auf Seiten- oder Datensatzebene in vielen Fällen überlegen. Schlimmstenfalls kann sie jedoch wesentlich mehr Speicher beanspruchen als normale Sperren.

  • Anstelle von Sperren auf Datensatzebene können Sie auch Sperren auf Anwendungsebene verwenden – in MySQL etwa GET_LOCK() und RELEASE_LOCK(). Dies sind beratende Sperren, d. h., sie funktionieren nur in „wohlerzogenen“ Anwendungen.

7.3.2. Themen, die Tabellensperren betreffen

Um eine sehr hohe Sperrgeschwindigkeit zu erzielen, verwendet MySQL die Tabellensperrung (statt der Sperrung auf Seiten-, Datensatz- oder Spaltenebene) für alle Speicher-Engines mit Ausnahme von InnoDB und BDB.

Bei InnoDB- und BDB-Tabellen setzt MySQL die Tabellensperrung nur dann ein, wenn Sie die Tabelle mit LOCK TABLES explizit sperren. Bei diesen Speicher-Engines raten wir von der Verwendung von LOCK TABLES vollständig ab, weil InnoDB eine automatische Sperrung auf Datensatzebene und BDB die Sperrung auf Seitenebene verwendet, um die Transaktionsisolierung sicherzustellen.

Bei großen Tabellen ist die Tabellensperrung für die meisten Anwendungen wesentlich besser als die Datensatzsperrung. Es gibt allerdings auch ein paar Fallstricke:

  • Die Tabellensperrung gestattet mehreren Threads das Lesen aus der Tabelle zur selben Zeit; will ein Thread hingegen in eine Tabelle schreiben, so muss er zunächst exklusiven Zugriff erhalten. Alle anderen Threads, die auf die betreffende Tabelle zugreifen wollen, müssen warten, bis das Update abgeschlossen ist.

  • Tabellenupdates erhalten eine höhere Priorität als Abrufvorgänge, weil sie normalerweise als wichtiger betrachtet werden. Auf diese Weise soll sichergestellt werden, dass Updates für eine Tabelle auch dann nicht „verhungern“, wenn sehr viele SELECT-Anweisungen für die Tabelle abgesetzt werden.

  • Die Tabellensperre kann beispielsweise Probleme verursachen, wenn ein Thread wartet, weil die Festplatte voll ist und erst freier Speicher benötigt wird, damit er fortgesetzt werden kann. In diesem Fall werden alle Threads, die auf die problematische Tabelle zugreifen wollen, ebenfalls in einen Wartezustand versetzt, bis wieder genug Festplattenkapazität verfügbar ist.

Die Tabellensperre ist auch in folgendem Szenario nachteilig:

  • Ein Client setzt eine SELECT-Anweisung ab, deren Ausführung sehr lange dauert.

  • Ein anderer Client setzt dann eine UPDATE-Anweisung für dieselbe Tabelle ab. Dieser Client wartet, bis die SELECT-Anweisung abgeschlossen ist.

  • Wieder ein anderer Client setzt eine weitere SELECT-Anweisung für dieselbe Tabelle ab. Weil UPDATE aber eine höhere Priorität hat als SELECT, wartet diese SELECT-Anweisung darauf, dass die UPDATE-Anweisung wie auch die erste SELECT-Anweisung fertig gestellt werden.

Die folgenden Punkte beschreiben einige Möglichkeiten, um den konkurrierenden Zugriff, der durch Tabellensperren verursacht werden kann, zu vermeiden oder zumindest zu verringern:

  • Versuchen Sie die Ausführung der SELECT-Anweisungen zu beschleunigen, sodass die Tabellensperren verkürzt werden. Zu diesem Zweck müssen Sie unter Umständen einige Zusammenfassungstabellen erstellen.

  • Starten Sie mysqld mit der Option --low-priority-updates. So erhalten alle Anweisungen, die eine Tabelle aktualisieren (also ändern), eine niedrigere Priorität gegenüber SELECT-Anweisungen. In diesem Fall würde die zweite SELECT-Anweisung in obigem Beispiel vor der UPDATE-Anweisung ausgeführt werden, und Sie müssten nicht warten, bis die erste SELECT-Anweisung abgeschlossen ist.

  • Sie können mit der Anweisung SET LOW_PRIORITY_UPDATES=1 festlegen, dass alle Aktualisierungen, die über eine bestimmte Verbindung erfolgen, mit niedrigerer Priorität verarbeitet werden sollen. Siehe auch Abschnitt 13.5.3, „SET.

  • Sie können einer bestimmten INSERT-, UPDATE- oder DELETE-Anweisung mit dem Attribut LOW_PRIORITY eine niedrigere Priorität zuweisen.

  • Sie können einer bestimmten SELECT-Anweisung mit dem Attribut HIGH_PRIORITY eine höhere Priorität zuweisen. Siehe auch Abschnitt 13.2.7, „SELECT.

  • Sie können mysqld mit einem niedrigen Wert für die Systemvariable max_write_lock_count starten: In diesem Fall wird MySQL gezwungen, die Priorität aller SELECT-Anweisungen, die auf eine Tabelle warten, zu senken, nachdem eine bestimmte Anzahl von Einfügeoperationen in die Tabelle stattgefunden hat. Auf diese Weise können nach einer bestimmten Anzahl von WRITE-Sperren auch READ-Sperren gesetzt werden.

  • Wenn Sie Probleme mit INSERT in Verbindung mit SELECT haben, sollten Sie in Betracht ziehen, auf MyISAM-Tabellen umzustellen, denn diese unterstützen gleichzeitige SELECT- und INSERT-Anweisungen.

  • Mischen Sie Einfüge- und Löschoperationen in derselben Tabelle, dann kann INSERT DELAYED sehr hilfreich sein. Siehe auch Abschnitt 13.2.4.2, „INSERT DELAYED.

  • Wenn Sie Probleme mit gemischten SELECT- und DELETE-Anweisungen haben, dann kann die Option LIMIT für DELETE nützlich sein. Siehe auch Abschnitt 13.2.1, „DELETE.

  • Die Verwendung von SQL_BUFFER_RESULT bei SELECT-Anweisungen kann dabei helfen, die Dauer der Tabellensperren zu verkürzen. Siehe auch Abschnitt 13.2.7, „SELECT.

  • Sie können den Sperrcode in mysys/thr_lock.c so abändern, dass nur eine einzelne Warteschlange verwendet wird. In diesem Fall hätten Schreib- und Lesesperren dieselbe Priorität, was bei einigen Anwendungen von Nutzen sein kann.

Hier noch ein paar Tipps zu Tabellensperren in MySQL:

  • Gleichzeitige Benutzer stellen kein Problem dar, wenn Sie Update- nicht mit Auswahloperationen mischen, die viele Datensätze in derselben Tabelle untersuchen müssen.

  • Sie können mit LOCK TABLES die Geschwindigkeit erhöhen, weil viele Updates innerhalb einer einzelnen Sperre wesentlich schneller erfolgen als die Durchführung von Updates ohne Sperre. Auch das Aufteilen des Tabelleninhalts auf mehrere Tabellen kann hilfreich sein.

  • Wenn Sie auf geschwindigkeitsspezifische Probleme in Verbindung mit Tabellensperren bei MySQL treffen, können Sie die Leistung möglicherweise durch Konvertierung einiger Ihrer Tabellen in InnoDB- oder BDB-Tabellen verbessern. Siehe auch Abschnitt 14.2, „InnoDB-Tabellen“, und Abschnitt 14.5, „Die BDB-Speicher-Engine“.

7.3.3. Gleichzeitige Einfügevorgänge

Bei einer MyISAM-Tabelle können Sie nebenläufige Einfügeoperationen verwenden, um Datensätze einzufügen, während gleichzeitig SELECT-Anweisungen ausgeführt werden, sofern sich in der Mitte der Tabelle keine gelöschten Datensätze befinden.

Unter Umständen, bei denen gleichzeitige Einfügeoperationen verwendet werden können, ist es nur sehr selten notwendig, den Modifizierer DELAYED für INSERT-Anweisungen zu verwenden. Siehe Abschnitt 13.2.4.2, „INSERT DELAYED.

Wenn Sie das Binärlog verwenden, werden nebenläufige in normale Einfügeoperationen für die Anweisungen CREATE ... SELECT oder INSERT ... SELECT umgewandelt. Hierdurch wird gewährleistet, dass Sie eine exakte Kopie Ihrer Tabellen neu erstellen können, indem Sie das Log während eines Sicherungsvorgangs anwenden.

Bei LOAD DATA INFILE können, wenn Sie CONCURRENT bei einer MyISAM-Tabelle angeben, die die Bedingungen für gleichzeitige Einfügeoperationen erfüllt (d. h. keine freien Blöcke in der Mitte enthält), andere Threads Daten aus der Tabelle abrufen, während LOAD DATA ausgeführt wird. Die Verwendung dieser Option beeinflusst die Leistungsfähigkeit von LOAD DATA auch dann geringfügig, wenn gleichzeitig kein anderer Thread die Tabelle verwendet.

7.4. Optimierung der Datenbankstruktur

7.4.1. Überlegungen zum Datenbankdesign

MySQL speichert Datensätze und Indexdaten in separaten Dateien. Viele andere Datenbanksysteme (sogar fast alle) speichern Datensätze und Indexdaten in derselben Datei. Wir sind allerdings der Ansicht, dass die Vorgehensweise von MySQL für eine Vielzahl moderner Systeme die bessere Wahl ist.

Eine andere Möglichkeit zur Speicherung der Datensätze besteht darin, die Daten für jede Spalte in einem separaten Bereich zu halten (Beispiele für Datenbanksysteme, die diesen Ansatz verwenden, sind SDBM und Focus). Allerdings führt dies zu einem erheblichen Leistungseinbruch, sobald auf mehr als eine Spalte zugegriffen wird. Aufgrund dieses Verhaltens beim Zugriff auf mehrere Spalten glauben wir nicht, dass dieses Modell für universelle Datenbanken gut geeignet ist.

Der häufigere Fall ist jedoch der, dass Indizes und Daten gemeinsam gespeichert werden (wie beispielsweise bei Oracle/Sybase usw.). In diesem Fall finden Sie die Datensatzinformationen auf der Blattseite des Indexes. Der Vorteil dieses Designs besteht darin, dass hierbei in vielen Fällen – abhängig davon, wie gut der Index zwischengespeichert wird – ein Lesevorgang auf der Festplatte eingespart wird. Es gibt aber auch Nachteile:

  • Tabellenscans sind wesentlich langsamer, weil Sie die Indizes durchlesen müssen, um an die Daten zu kommen.

  • Sie können die Daten für eine Abfrage nicht einfach der Indextabelle entnehmen.

  • Sie verbrauchen mehr Speicherplatz, weil Sie Indizes aus den Knoten duplizieren müssen (Sie können den Datensatz nicht in den Knoten speichern).

  • Aufgrund von Löschvorgängen degeneriert die Tabelle im Laufe der Zeit, weil die Indizes in Knoten normalerweise bei Löschvorgängen nicht aktualisiert werden.

  • Es ist schwieriger, nur die Indexdaten zwischenzuspeichern.

7.4.2. Wie Sie Ihre Daten so klein wie möglich bekommen

Eine der einfacheren Optimierungen besteht darin, Ihre Tabellen so zu implementieren, dass sie möglichst wenig Platz auf der Festplatte einnehmen. Dies kann erhebliche Verbesserungen zur Folge haben, weil die Lesevorgänge von der Festplatte schneller erfolgen und kleinere Tabellen normalerweise weniger Hauptspeicher benötigen, während ihre Inhalte bei der Abfrageausführung aktiv verarbeitet werden. Auch die Indizierung ist weniger ressourcenbeanspruchend, wenn sie an schmaleren Spalten vorgenommen wird.

MySQL unterstützt viele verschiedene Speicher-Engines (Tabellentypen) und Datensatzformate. Für jede Tabelle können Sie neu entscheiden, welche Speicher- und Indizierungsmethode verwendet werden soll. Die Auswahl des für Ihre Anwendung geeigneten Tabellenformats kann Ihnen einen erheblichen Performancezuwachs bringen. Siehe auch Kapitel 14, Speicher-Engines und Tabellentypen.

Sie können die Leistungsfähigkeit Ihrer Tabellen optimieren und den Speicherbedarf minimieren, indem Sie die folgenden Methoden benutzen:

  • Verwenden Sie den jeweils effizientesten (d. h. kleinstmöglichen) Datentyp. MySQL bietet viele Spezialtypen, mit denen sich Festplatten- und Arbeitsspeicher einsparen lassen. Setzen Sie beispielsweise möglichst die kleineren Integer-Typen ein, um kleinere Tabellen zu erhalten. MEDIUMINT ist häufig besser geeignet als INT, weil eine MEDIUMINT-Spalte 25 Prozent weniger Platz benötigt.

  • Wenn möglich, deklarieren Sie Spalten als NOT NULL. Hierdurch wird alles schneller, und Sie sparen ein Bit je Spalte. Wenn Sie NULL in Ihrer Anwendung wirklich benötigen, sollten Sie es natürlich benutzen; Sie sollten lediglich verhindern, dass alle Spalten vorgabeseitig so eingestellt sind.

  • Wenn Ihre MyISAM-Tabellen keine Spalten variabler Länge (VARCHAR, TEXT oder BLOB) enthalten, wird ein festes Datensatzformat verwendet. Dieses ist schneller, kann allerdings mehr Platz beanspruchen. Siehe auch Abschnitt 14.1.3, „MyISAM-Tabellenformate“. Sie können mit der Option ROW_FORMAT=FIXED für CREATE TABLE angeben, dass Sie Datensätze fester Länge verwenden wollen, auch wenn VARCHAR-Spalten vorhanden sind.

  • InnoDB-Tabellen verwenden ein kompaktes Speicherformat. In Versionen vor MySQL 5.0.3 enthalten InnoDB-Datensätze einige redundante Informationen, z. B. die Anzahl und die Längen der einzelnen Spalten (und zwar auch bei Spalten fester Größe). Standardmäßig werden Tabellen im kompakten Format erstellt (ROW_FORMAT=COMPACT). Wenn Sie ein Downgrade auf eine ältere MySQL-Version durchführen wollen, können Sie das alte Format mit ROW_FORMAT=REDUNDANT anfordern.

    Das kompakte InnoDB-Format ändert auch die Art und Weise, wie CHAR-Spalten, die Daten im UTF-8-Format enthalten, gespeichert werden. Bei ROW_FORMAT=REDUNDANT belegt eine Spalte CHAR(N) im UTF-8-Format 3 × N Byte (davon ausgehend, dass die maximale Länge eines UTF-8-kodierten Zeichens drei Byte beträgt). Viele Sprachen können vorwiegend mit UTF-8-Zeichen geschrieben werden, die nur ein Byte umfassen, weswegen eine feste Speicherlänge häufig Platzverschwendung ist. Beim ROW_FORMAT=COMPACT-Format reserviert InnoDB eine variable Speichermenge im Bereich zwischen N und 3 × N Byte für diese Spalten, indem Leerzeichen am Ende nach Bedarf abgeschnitten werden. Die Mindestspeichergröße wird als N Byte vermerkt, um in typischen Fällen interne Updates durchführen zu können.

  • Der Primärindex einer Tabelle sollte so kurz wie möglich sein. Dies macht die Identifizierung eines Datensatzes einfach und effizient.

  • Erstellen Sie nur Indizes, die Sie auch wirklich benötigen. Indizes sind praktisch, um Daten abzurufen, aber von Nachteil, wenn Sie Daten schnell speichern müssen. Wenn Sie auf eine Tabelle in erster Linie durch das Durchsuchen einer Kombination von Spalten zugreifen, dann erstellen Sie für diese Kombination einen Index. Der erste Teil des Indexes sollte die meistverwendete Spalte sein. Wenn Sie beim Auswählen aus der Tabelle immer viele Spalten verwenden, sollten Sie die Spalte benutzen, die mehr Dubletten aufweist, damit der Index besser komprimiert werden kann.

  • Wenn es sehr wahrscheinlich ist, dass eine String-Spalte ein eindeutiges Präfix innerhalb der ersten paar Zeichen aufweist, dann ist es besser, nur dieses Präfix zu indizieren. Hierzu verwenden Sie die MySQL-Funktionalität zur Erstellung eines Indexes aus dem linken Teil der Spalte (siehe Abschnitt 13.1.4, „CREATE INDEX). Kürzere Indizes sind nicht nur deswegen schneller, weil sie weniger Festplattenkapazität benötigen, sondern auch, weil sie auf diese Weise mehr Treffer im Index-Cache erhalten (d. h., es sind weniger Suchvorgänge auf der Festplatte erforderlich). Siehe auch Abschnitt 7.5.2, „Serverparameter feineinstellen“.

  • Unter manchen Umständen kann es von Vorteil sein, eine Tabelle, die sehr häufig gescannt wird, in zwei Tabellen aufzutrennen. Dies gilt insbesondere, wenn es sich um eine Tabelle im dynamischen Format handelt und es möglich ist, dass eine kleinere statische Tabelle entsteht, die beim Scannen der Tabelle zum Auffinden der gewünschten Datensätze benutzt werden kann.

7.4.3. Spaltenindizes

Alle MySQL-Datentypen lassen sich indizieren. Die Verwendung von Indizes der relevanten Spalten ist die beste Möglichkeit, die Leistung von SELECT-Operationen zu optimieren.

Die maximale Anzahl von Indizes pro Tabelle und die maximale Indexlänge werden durch die verwendete Speicher-Engine bestimmt. Siehe auch Kapitel 14, Speicher-Engines und Tabellentypen. Alle Speicher-Engines unterstützen mindestens 16 Indizes pro Tabelle und eine Indexgesamtlänge von mindestens 256 Byte. Bei den meisten Speicher-Engines liegen die Grenzen höher.

Mit der Syntax col_name(N) in einer Indexdefinition können Sie einen Index erstellen, der nur die ersten N Zeichen einer String-Spalte verwendet. Wenn Sie auf diese Weise nur ein Präfix der Spaltenwerte indizieren, kann dies die Indexdatei erheblich kleiner machen. Bei der Indizierung einer BLOB- oder TEXT-Spalte müssen Sie eine Präfixlänge für den Index angeben. Zum Beispiel:

CREATE TABLE test (blob_col BLOB, INDEX(blob_col(10)));

Präfixe können bis zu 1.000 Byte lang sein (767 Byte bei InnoDB-Tabellen). Beachten Sie, dass Präfixbeschränkungen in Byte angegeben werden, wohingegen die Präfixlänge in CREATE TABLE-Anweisungen als Anzahl der Zeichen interpretiert wird. Dies muss bei der Angabe einer Präfixlänge für eine Spalte, die einen Multibytezeichensatz verwendet, in jedem Fall berücksichtigt werden.

Sie können außerdem FULLTEXT-Indizes erstellen. Diese werden für die Volltextsuche verwendet. Nur die MyISAM-Speicher-Engine unterstützt FULLTEXT-Indizes, und auch nur für CHAR-, VARCHAR- und TEXT-Spalten. Die Indizierung erfolgt stets über die gesamte Spalte – eine Teilindizierung (Spaltenpräfix) wird nicht unterstützt. Detaillierte Informationen finden Sie in Abschnitt 12.7, „MySQL-Volltextsuche“.

Sie können auch Indizes für raumbezogene Typen erstellen. Derzeit unterstützt nur MyISAM R-Tree-Indizes für raumbezogene Typen. Andere Speicher-Engines verwenden B-Trees zur Indizierung raumbezogener Typen. (Ausnahmen sind lediglich ARCHIVE und NDBCLUSTER, die überhaupt keine raumbezogene Indizierung unterstützen.)

Die MEMORY-Engine verwendet standardmäßig HASH-Indizes, unterstützt aber auch BTREE-Indizes.

7.4.4. Mehrspaltige Indizes

MySQL kann zusammengesetzte Indizes erstellen (d. h. Indizes über mehrere Spalten). Ein Index kann bis zu 15 Spalten umfassen. Bei bestimmten Datentypen können Sie ein Präfix der Spalte indizieren (siehe Abschnitt 7.4.3, „Spaltenindizes“).

Ein mehrspaltiger Index kann als sortiertes Array betrachtet werden, das Werte enthält, die durch Verkettung der Werte der indizierten Spalten entstanden sind.

MySQL verwendet mehrspaltige Indizes derart, dass Abfragen schnell sind, wenn Sie einen bekannten Teil der ersten Spalte des Indexes in einer WHERE-Klausel angeben – und zwar auch dann, wenn Sie keine Werte für andere Spalten angeben.

Angenommen, eine Tabelle habe die folgende Spezifikation:

CREATE TABLE test (
    id INT NOT NULL,
    last_name CHAR(30) NOT NULL,
    first_name CHAR(30) NOT NULL,
    PRIMARY KEY (id),
    INDEX name (last_name,first_name)
);

Der Index name ist ein Index über die Spalten last_name und first_name. Der Index kann für Abfragen, die Werte in einem bekannten Bereich für last_name angeben, oder sowohl für last_name als auch für first_name verwendet werden. Aus diesem Grund wird der Index name in den folgenden Abfragen benutzt:

SELECT * FROM test WHERE last_name='Widenius';

SELECT * FROM test
  WHERE last_name='Widenius' AND first_name='Michael';

SELECT * FROM test
  WHERE last_name='Widenius'
  AND (first_name='Michael' OR first_name='Monty');

SELECT * FROM test
  WHERE last_name='Widenius'
  AND first_name >='M' AND first_name < 'N';

Im Gegensatz dazu wird der Index name in den folgenden Abfragen nicht benutzt:

SELECT * FROM test WHERE first_name='Michael';

SELECT * FROM test
  WHERE last_name='Widenius' OR first_name='Michael';

Die Art und Weise, wie MySQL Indizes zur Verbesserung der Abfrageleistung verwendet, wird in Abschnitt 7.4.5, „Wie MySQL Indizes benutzt“, näher beschrieben.

7.4.5. Wie MySQL Indizes benutzt

Indizes werden verwendet, um schnell Datensätze mit bestimmten Spaltenwerten zu finden. Ohne Index müsste MySQL mit der Suche beim ersten Datensatz beginnen und dann die gesamte Tabelle nach passenden Datensätzen durchforsten. Je größer die Tabelle, desto höher sind die Kosten dafür. Wenn die Tabelle einen Index für die fraglichen Spalten aufweist, kann MySQL die Position in der Mitte der Datendatei, an der die Suche beginnen soll, schnell bestimmen, ohne alle Daten in der Datei überprüfen zu müssen. Wenn eine Tabelle 1.000 Datensätze hat, ist dies mindestens hundertmal schneller als ein sequenzielles Lesen. Wenn Sie hingegen auf die meisten Datensätze zugreifen müssen, ist das sequenzielle Lesen schneller, weil die Anzahl der Festplattenzugriffe geringer ist.

Die meisten MySQL-Indizes (PRIMARY KEY, UNIQUE, INDEX und FULLTEXT) sind in B-Trees gespeichert. Zu erwähnende Ausnahmen sind, dass Indizes für raumbezogene Datentypen R-Typen verwenden und MEMORY-Tabellen auch Hash-Indizes unterstützen.

Strings werden automatisch präfixkomprimiert; Gleiches gilt für Leerzeichen am Ende des Strings. Siehe auch Abschnitt 13.1.4, „CREATE INDEX.

Generell werden Indizes wie nachfolgend beschrieben verwendet. Die spezifischen Eigenschaften von Hash-Indizes, die in Verbindung mit MEMORY-Tabellen verwendet werden, werden am Ende dieses Abschnitts erläutert.

MySQL verwendet Indizes für die folgenden Operationen:

  • Zum schnellen Auffinden von Datensätzen, die der WHERE-Klausel entsprechen.

  • Zum Ignorieren bestimmter Datensätze. Wenn mehrere Indizes vorhanden sind, verwendet MySQL normalerweise denjenigen, der die kleinste Anzahl von Datensätzen findet.

  • Zum Abrufen von Datensätzen aus anderen Tabellen bei der Durchführung von Joins.

  • Zum Auffinden des MIN()- oder MAX()-Werts für eine bestimmte indizierte Spalte key_col. Dies wird mit einem Vorprozessor optimiert, der überprüft, ob Sie WHERE key_part_N = constant für alle Schlüsselteile verwenden, die im Index vor key_col erscheinen. In diesem Fall führt MySQL eine einfache Schlüsselsuche nach jedem MIN()- oder MAX()-Ausdruck durch und ersetzt ihn durch eine Konstante. Wenn alle Ausdrücke durch Konstanten ersetzt werden konnten, wird die Abfrage umgehend abgeschlossen. Zum Beispiel:

    SELECT MIN(key_part2),MAX(key_part2)
      FROM tbl_name WHERE key_part1=10;
    
  • Zum Sortieren oder Gruppieren einer Tabelle, sofern das Sortieren bzw. Gruppieren auf der Basis des linken Präfixes eines verwendbaren Schlüssels erfolgt (z. B. ORDER BY key_part1, key_part2). Wenn auf alle Schlüsselteile DESC folgt, wird der Schlüssel in umgekehrter Reihenfolge gelesen. Siehe auch Abschnitt 7.2.12, „ORDER BY-Optimierung“.

  • In manchen Fällen kann eine Abfrage so optimiert werden, dass sie Werte abruft, ohne die eigentlichen Datensätze zu untersuchen. Wenn eine Abfrage nur Spalten einer Tabelle verwendet, die numerisch sind und bei einem Schlüssel ein linkes Präfix bilden, dann können die ausgewählten Werte auch aus dem Indexbaum abgerufen werden, was deutlich schneller ist:

    SELECT key_part3 FROM tbl_name 
      WHERE key_part1=1
    

Nehmen wir an, dass Sie folgende SELECT-Anweisung absetzen:

mysql> SELECT * FROM tbl_name WHERE col1=val1 AND col2=val2;

Wenn für col1 und col2 ein mehrspaltiger Index vorhanden ist, können die entsprechenden Datensätze direkt abgerufen werden. Sind separate einspaltige Indizes für col1 und col2 vorhanden, dann versucht der Optimierer, den restriktivsten Index zu ermitteln, indem er überprüft, welcher Index weniger Spalten findet; dieser Index wird dann zum Abrufen von Datensätzen benutzt.

Wenn die Tabelle einen mehrspaltigen Index aufweist, kann ein beliebiges linkes Präfix des Indexes vom Optimierer zum Suchen von Datensätzen verwendet werden. Haben Sie beispielsweise einen dreispaltigen Index für (col1, col2, col3), dann bietet dieser Suchfunktionalität für (col1), (col1, col2) und (col1, col2, col3).

MySQL kann keinen Teilindex verwenden, wenn die Spalten kein linkes Präfix für den Index bilden. Betrachten Sie einmal die folgenden SELECT-Anweisungen:

SELECT * FROM tbl_name WHERE col1=val1;
SELECT * FROM tbl_name WHERE col1=val1 AND col2=val2;

SELECT * FROM tbl_name WHERE col2=val2;
SELECT * FROM tbl_name WHERE col2=val2 AND col3=val3;

Wenn ein Index für (col1, col2, col3) vorhanden ist, verwenden diese nur die ersten beiden Abfragen. Die dritte und die vierte Abfrage beziehen sich zwar auf indizierte Spalten, aber (col2) und (col2, col3) sind keine linken Präfixe von (col1, col2, col3).

Ein B-Tree-Index kann für Spaltenvergleiche in Ausdrücken benutzt werden, die die Operatoren =, >, >=, <, <= oder BETWEEN verwenden. Der Index kann auch für LIKE-Vergleiche benutzt werden, wenn das Argument zu LIKE ein Konstanten-String ist, der nicht mit einem Jokerzeichen beginnt. Die folgenden SELECT-Anweisungen verwenden beispielsweise Indizes:

SELECT * FROM tbl_name WHERE key_col LIKE 'Patrick%';
SELECT * FROM tbl_name WHERE key_col LIKE 'Pat%_ck%';

In der ersten Anweisung werden nur Datensätze mit 'Patrick' <= key_col< 'Patricl' berücksichtigt. In der zweiten Anweisung werden nur Datensätze mit 'Pat' <= key_col< 'Pau' berücksichtigt.

Die folgenden SELECT-Anweisungen verwenden keine Indizes:

SELECT * FROM tbl_name WHERE key_col LIKE '%Patrick%';
SELECT * FROM tbl_name WHERE key_col LIKE other_col;

In der ersten Anweisung beginnt der Wert LIKE mit einem Jokerzeichen. In der zweiten Anweisung ist der LIKE-Wert keine Konstante.

Wenn Sie … LIKE '%string%' verwenden und string mehr als drei Zeichen umfasst, verwendet MySQL den Turbo-Boyer-Moore-Algorithmus zur Initialisierung des Musters für den String; mit diesem Muster wird die Suche dann schneller durchgeführt.

Eine Suche unter Verwendung von col_name IS NULL setzt Indizes ein, wenn col_name indiziert ist.

Ein Index, der nicht alle AND-Ebenen in der WHERE-Klausel einbezieht, wird nicht zur Optimierung der Abfrage benutzt. Um also einen Index verwenden zu können, muss in jeder AND-Gruppe ein Präfix des Indexes benutzt werden.

Die folgenden WHERE-Klauseln verwenden Indizes:

... WHERE index_part1=1 AND index_part2=2 AND other_column=3
    /* index = 1 OR index = 2 */
... WHERE index=1 OR A=10 AND index=2
    /* optimized like "index_part1='hello'" */
... WHERE index_part1='hello' AND index_part3=5
    /* Can use index on index1 but not on index2 or index3 */
... WHERE index1=1 AND index2=2 OR index1=3 AND index3=3;

Die folgenden WHERE-Klauseln verwenden Indizes nicht:

    /* index_part1 is not used */
... WHERE index_part2=1 AND index_part3=2

    /*  Index is not used in both parts of the WHERE clause  */
... WHERE index=1 OR A=10

    /* No index spans all rows  */
... WHERE index_part1=1 OR index_part2=10

Manchmal verwendet MySQL auch dann keinen Index, wenn ein solcher vorhanden ist. Ein Umstand, unter dem dies geschieht, liegt vor, wenn der Optimierer der Ansicht ist, dass MySQL bei Verwendung eines Indexes auf einen sehr großen Anteil der Datensätze in der Tabelle zugreifen muss. (In diesem Fall ist ein Tabellenscan mit hoher Wahrscheinlichkeit schneller, weil weniger Suchvorgänge erforderlich sind.) Wenn eine solche Abfrage jedoch dank einer LIMIT-Klausel nur ein paar Datensätze abruft, verwendet MySQL trotzdem einen Index, da sich die Datensätze, die zum Ergebnis gehören, auf diese Weise schneller finden lassen.

Hash-Indizes weisen etwas andere Eigenschaften auf als die soeben beschriebenen:

  • Sie werden nur zu Vergleichen verwendet, die die Operatoren = oder <=> benutzen (aber sie sind extrem schnell). Nicht verwendet werden sie für Vergleiche mit <, bei denen ein Wertebereich gefunden wird.

  • Der Optimierer kann einen Hash-Index nicht zur Beschleunigung von ORDER BY-Operationen verwenden. (Dieser Indextyp kann nicht zur Suche nach dem nächsten Eintrag in der Reihenfolge benutzt werden.)

  • MySQL kann nicht näherungsweise bestimmen, wie viele Datensätze zwischen zwei Werten vorhanden sind (diese Angabe verwendet der Bereichsoptimierer für die Entscheidung, welchen Index er verwenden soll). Dies kann sich auf einige Abfragen auswirken, wenn Sie eine MyISAM-Tabelle in eine MEMORY-Tabelle mit einem Hash-Index konvertieren.

  • Nur vollständige Schlüssel können zur Suche nach einem Datensatz benutzt werden. (Bei einem B-Tree-Index kann dagegen ein beliebiges linkes Präfix des Schlüssels zum Suchen von Datensätzen eingesetzt werden.)

7.4.6. Der MyISAM-Schlüssel-Cache

Um die Festplattenzugriffe zu minimieren, verwendet die MyISAM-Speicher-Engine eine Strategie, auf der zahlreiche Datenbanksysteme basieren. Sie benutzt einen Cache-Mechanismus, um die am häufigsten abgerufenen Tabellenblöcke im Speicher zu halten:

  • Für Indexblöcke wird eine spezielle Struktur namens Schlüssel-Cache (oder Schlüsselpuffer) implementiert. Diese Struktur enthält eine Anzahl von Blockpuffern, in denen die meistverwendeten Indexblöcke abgelegt werden.

  • Für Datenblöcke verwendet MySQL keinen speziellen Cache. Stattdessen wird der Dateisystem-Cache des nativen Betriebssystems benutzt.

Dieser Abschnitt beschreibt zunächst den grundlegenden Betrieb des MyISAM-Schlüssel-Caches. Nachfolgend werden Funktionen erläutert, die die Leistungsfähigkeit des Schlüssel-Caches optimieren und mit denen Sie den Cache-Betrieb besser steuern können:

  • Mehrere Threads können gleichzeitig auf den Cache zugreifen.

  • Sie können mehrere Schlüssel-Caches einrichten und Tabellenindizes speziellen Caches zuweisen.

Die Größe des Schlüssel-Caches stellen Sie mit der Systemvariablen key_buffer_size ein. Wenn diese Variable auf null gesetzt ist, wird kein Cache verwendet. Der Schlüssel-Cache wird ferner nicht benutzt, wenn der Wert key_buffer_size zu klein ist, um die Mindestanzahl von Blockpuffern (8) zu reservieren.

Wenn der Schlüssel-Cache nicht betriebsbereit ist, erfolgt der Zugriff auf Indexdateien nur über die Puffer des nativen Dateisystems, die vom Betriebssystem bereitgestellt werden. (Anders gesagt, wird auf Tabellenindexblöcke unter Verwendung derselben Methode zugegriffen wie auf Tabellendatenblöcke.)

Ein Indexblock ist eine zusammenhängende Zugriffseinheit für MyISAM-Indexdateien. Normalerweise entspricht die Größe eines Indexblocks der Größe der Knoten des B-Trees. (Indizes werden auf der Festplatte mit einer B-Tree-Datenstruktur dargestellt. Knoten am unteren Ende des Trees sind Blattknoten, Knoten oberhalb der Blätter Nichtblattknoten.)

Alle Blockpuffer in einer Schlüssel-Cache-Struktur haben dieselbe Größe. Diese Größe kann der Größe eines Tabellenindexblocks entsprechen, sie kann aber auch höher oder niedriger sein. Normalerweise ist einer dieser beiden Werte ein Vielfaches des anderen.

Wenn ein Zugriff auf Daten aus einem Tabellenindexblock erfolgen muss, dann überprüft der Server zunächst, ob er in einem Blockpuffer des Schlüssel-Caches vorhanden ist. Ist dies der Fall, dann greift der Server auf die Daten im Schlüssel-Cache statt auf der Festplatte zu: Er liest aus und/oder schreibt in den Cache statt auf die Festplatte. Andernfalls wählt der Server einen Cache-Blockpuffer, der einen oder mehrere andere Tabellenindexblöcke enthält, und ersetzt die dortigen Daten durch eine Kopie des erforderlichen Tabellenindexblocks. Sobald der neue Indexblock sich im Cache befindet, kann auf die Indexdaten zugegriffen werden.

Wenn ein für die Ersetzung gewählter Block modifiziert wurde, wird dieser als „schmutzig“ bezeichnet. In diesem Fall wird der Inhalt vor der Ersetzung in den Tabellenindex geschrieben, von dem er kam.

Normalerweise verwendet der Server eine LRU-Strategie (Least Recently Used): Wenn ein Block zur Ersetzung gewählt werden muss, entscheidet er sich für den Indexblock, der am längsten nicht mehr verwendet wurde. Um die Auswahl zu erleichtern, unterhält das Schlüssel-Cache-Modul eine spezielle Warteschlange (die LRU-Kette) mit allen verwendeten Blöcken. Wenn auf einen Block zugegriffen wird, wird er ans Ende der Warteschlange gesetzt. Müssen Blöcke ersetzt werden, dann sind die Blöcke am Anfang der Warteschlange die am längsten nicht verwendeten und werden infolgedessen als Erste geräumt.

7.4.6.1. Zugriff auf den gemeinsam verwendeten Schlüsselspeicher

Threads können gleichzeitig auf Schlüssel-Cache-Puffer zugreifen. Dabei ist Folgendes zu beachten:

  • Ein Puffer kann von mehreren Threads benutzt werden, sofern er derzeit nicht aktualisiert wird.

  • Wenn ein Puffer gerade aktualisiert wird, müssen Threads, die ihn verwenden wollen, warten, bis das Update abgeschlossen ist.

  • Mehrere Threads können Anfragen erzeugen, die zu Ersetzungen im Cache-Block führen können, solange sie sich nicht gegenseitig stören (d. h. solange sie verschiedene Indexblöcke benötigen und insofern auch verschiedene Cache-Blöcke ersetzt werden).

Der gemeinsame Zugriff auf den Schlüssel-Cache ermöglicht dem Server eine erhebliche Verbesserung des Durchsatzes.

7.4.6.2. Mehrfache Index-Caches

Der gemeinsame Zugriff auf den Schlüssel-Cache verbessert die Leistung, beseitigt das Problem des gleichzeitigen Zugriffs durch mehrere Threads aber nicht vollständig. Diese wetteifern weiterhin um Steuerstrukturen, mit denen der Zugriff auf die Schlüssel-Cache-Puffer verwaltet wird. Um also die Rivalität um den Schlüssel-Cache-Zugriff weiter zu verringern, stellt MySQL zusätzlich mehrere Schlüssel-Caches bereit. Diese Funktion erlaubt es Ihnen, verschiedenen Schlüssel-Caches unterschiedliche Tabellenindizes zuzuweisen.

Wenn mehrere Schlüssel-Caches vorhanden sind, muss der Server wissen, welchen Cache er bei der Verarbeitung von Abfragen für eine gegebene MyISAM-Tabelle verwenden soll. Standardmäßig werden alle MyISAM-Tabellenindizes im vorgabeseitigen Schlüssel-Cache zwischengespeichert. Um Tabellenindizes einem bestimmten Schlüssel-Cache zuzuweisen, verwenden Sie die Anweisung CACHE INDEX (siehe Abschnitt 13.5.5.1, „CACHE INDEX). Die folgende Anweisung beispielsweise weist Indizes der Tabellen t1, t2 und t3 dem Schlüssel-Cache namens hot_cache zu:

mysql> CACHE INDEX t1, t2, t3 IN hot_cache;
+---------+--------------------+----------+----------+
| Table   | Op                 | Msg_type | Msg_text |
+---------+--------------------+----------+----------+
| test.t1 | assign_to_keycache | status   | OK       |
| test.t2 | assign_to_keycache | status   | OK       |
| test.t3 | assign_to_keycache | status   | OK       |
+---------+--------------------+----------+----------+

Der in der CACHE INDEX-Anweisung referenzierte Schlüssel-Cache kann erstellt werden, indem seine Größe mit einer SET GLOBAL-Anweisung zur Parametereinstellung oder in den Startoptionen des Servers angegeben wird. Zum Beispiel:

mysql> SET GLOBAL keycache1.key_buffer_size=128*1024;

Um einen Schlüssel-Cache zu beseitigen, setzen Sie seine Größe auf null:

mysql> SET GLOBAL keycache1.key_buffer_size=0;

Beachten Sie, dass Sie den vorgabeseitigen Schlüssel-Cache nicht zerstören können. Alle diesbezüglichen Versuche werden ignoriert:

mysql> SET GLOBAL key_buffer_size = 0;

mysql> show variables like 'key_buffer_size';
+-----------------+---------+
| Variable_name   | Value   |
+-----------------+---------+
| key_buffer_size | 8384512 |
+-----------------+---------+

Schlüssel-Cache-Variablen sind strukturierte Systemvariablen, die einen Namen und Komponenten aufweisen. Bei keycache1.key_buffer_size etwa ist keycache1 der Cache-Variablenname und key_buffer_size die Cache-Komponente. Eine Beschreibung der Syntax zur Referenzierung strukturierter Systemvariablen für den Schlüssel-Cache finden Sie in Abschnitt 5.2.3.1, „Strukturierte Systemvariablen“.

Standardmäßig werden Tabellenindizes dem vorgabeseitigen Schlüssel-Cache (Haupt-Schlüssel-Cache) zugewiesen, der beim Serverstart erstellt wird. Wird ein Schlüssel-Cache zerstört, dann werden alle ihm zugewiesenen Indizes wieder dem Standard-Schlüssel-Cache zugewiesen.

Für einen gut ausgelasteten Server empfehlen wir eine Strategie mit drei Schlüssel-Caches:

  • Ein „heißer“ Schlüssel-Cache, der 20 Prozent des für alle Schlüssel-Caches reservierten Speichers verwendet. Diesen setzen Sie für Tabellen ein, in denen sehr viele Suchvorgänge, aber keine Änderungen erfolgen.

  • Ein „kalter“ Schlüssel-Cache, der weitere 20 Prozent des für alle Schlüssel-Caches reservierten Speichers verwendet. Dieser Cache unterstützt mittelgroße, häufig geänderte Tabellen (z. B. Temporärtabellen).

  • Ein „warmer“ Schlüssel-Cache, der die verbleibenden 60 Prozent des Schlüssel-Cache-Speichers belegt. Diesen verwenden Sie als vorgabeseitigen Schlüssel-Cache, der standardmäßig von allen anderen Tabellen benutzt wird.

Ein Grund dafür, warum die Verwendung dreier Schlüssel-Caches von Vorteil ist, besteht darin, dass der Zugriff auf eine Schlüssel-Cache-Struktur den Zugriff auf die anderen nicht behindert. Anweisungen, die auf Tabellen zugreifen, die dem einen Cache zugewiesen sind, konkurrieren nicht mit Anweisungen, deren referenzierte Tabellen auf einen anderen Cache zugreifen. Ein Leistungsgewinn wird aber auch aus anderen Gründen erzielt:

  • Der heiße Cache wird nur für anfordernde Abfragen verwendet, d. h., sein Inhalt ändert sich nicht. Dies bedeutet, dass, wann immer ein Indexblock von der Festplatte abgerufen werden muss, der Inhalt des für die Ersetzung gewählten Cache-Blocks nicht zuerst synchronisiert werden muss.

  • Ist ein Index dem heißen Cache zugewiesen, dann ist, wenn keine Abfragen kommen, die einen Indexscan erfordern, die Wahrscheinlichkeit hoch, dass die Indexblöcke, die den Nichtblattknoten des B-Trees entsprechen, im Cache verbleiben.

  • Ein Updatevorgang, wie er am häufigsten für Temporärtabellen vorgenommen wird, wird wesentlich schneller durchgeführt, wenn der geänderte Knoten sich im Cache befindet und nicht erst von der Festplatte gelesen werden muss. Wenn die Größe der Indizes der Temporärtabellen mit der Größe des kalten Schlüssel-Caches vergleichbar ist, dann ist die Wahrscheinlichkeit, dass der aktualisierte Knoten sich im Cache befindet, extrem hoch.

CACHE INDEX richtet eine Verknüpfung zwischen einer Tabelle und einem Schlüssel-Cache ein, die jedoch nur bis zum nächsten Serverneustart bestehen bleibt. Wenn diese Verknüpfung jedes Mal, wenn der Server neu startet, erstellt werden soll, können Sie dies etwa mit einer Optionsdatei erreichen: Setzen Sie Variableneinstellungen, mit denen Ihre Schlüssel-Caches konfiguriert werden, und eine Option namens init-file in die Optionsdatei, die eine Datei bezeichnet, welche die auszuführenden CACHE INDEX-Anweisungen enthält. Zum Beispiel:

key_buffer_size = 4G
hot_cache.key_buffer_size = 2G
cold_cache.key_buffer_size = 2G
init_file=/path/to/data-directory/mysqld_init.sql

Die Anweisungen in mysqld_init.sql werden bei jedem Start des Servers ausgeführt. Die Datei sollte eine SQL-Anweisung pro Zeile enthalten. Das folgende Beispiel weist verschiedene Tabellen jeweils hot_cache und cold_cache zu:

CACHE INDEX db1.t1, db1.t2, db2.t3 IN hot_cache
CACHE INDEX db1.t4, db2.t5, db2.t6 IN cold_cache

7.4.6.3. Strategie des Einfügens am Mittelpunkt

Standardmäßig verwendet das System zur Schlüssel-Cache-Verwaltung die LRU-Strategie zur Auswahl der zu räumenden Schlüssel-Cache-Blöcke, unterstützt aber auch eine komplexere Methode namens Strategie des Einfügens am Mittelpunkt.

Bei Verwendung dieser Strategie wird die LRU-Kette in zwei Teile aufgeteilt: eine heiße Unterkette und eine warme Unterkette. Der Trennpunkt zwischen diesen beiden Teilen ist nicht fest, sondern das Schlüssel-Cache-Verwaltungssystem achtet darauf, dass der warme Teil nicht „zu klein“ wird und immer mindestens key_cache_division_limit Prozent der Schlüssel-Cache-Blöcke enthält. key_cache_division_limit ist eine Komponente der strukturierten Schlüssel-Cache-Variablen, d. h., ihr Wert ist ein Parameter, der pro Cache eingestellt werden kann.

Wenn ein Indexblock aus einer Tabelle in den Schlüssel-Cache eingelesen wird, wird er an das Ende der warmen Unterkette gesetzt. Nach einer bestimmten Anzahl von Treffern (d. h. von Zugriffen auf den Block) wird er in die heiße Unterkette hochgestuft. Zurzeit ist die Anzahl der für das Hochstufen eines Blocks erforderlichen Treffer für alle Indexblöcke gleich (nämlich 3).

Ein Block, der in die heiße Unterkette hochgestuft wurde, wird an das Ende der Kette gesetzt. Der Block kreist dann innerhalb dieser Unterkette. Bleibt der Block lang genug am Anfang der Unterkette, dann wird er in die warme Unterkette zurückgestuft. Der Zeitpunkt wird vom Wert der Schlüssel-Cache-Komponente key_cache_age_threshold bestimmt.

Der Schwellwert definiert, dass bei einem Schlüssel-Cache mit N Blöcken der am Anfang der heißen Unterkette stehende Block an den Anfang der warmen Unterkette gesetzt wird, sofern er nicht innerhalb der letzten N× key_cache_age_threshold / 100 Treffer aufgerufen wurde. Er wird dann erster Anwärter auf die Räumung, weil für die Ersetzung vorgesehene Blöcke am Anfang der warmen Unterkette entfernt werden.

Die Strategie des Einfügens am Mittelpunkt gestattet Ihnen, Blöcke, die offensichtlich wichtig sind, immer im Cache zu behalten. Wenn Sie die direkte LRU-Strategie bevorzugen, belassen Sie den Wert von key_cache_division_limit auf der Vorgabe von 100.

Die Strategie des Einfügens am Mittelpunkt ist hilfreich zur Leistungssteigerung, wenn die Ausführung einer Abfrage einen Indexscan erfordert und so im Endeffekt all diejenigen Indexblöcke aus dem Cache wirft, die wichtigen B-Tree-Knoten entsprechen. Um dies zu vermeiden, müssen Sie die Strategie mit einem Wert von deutlich weniger als 100 für key_cache_division_limit verwenden, denn so werden „wertvolle“, häufig gefundene Knoten auch bei einem Indexscan in der heißen Unterkette gehalten.

7.4.6.4. Vorladen von Indizes (Preloading)

Sind genug Blöcke in einem Schlüssel-Cache vorhanden, um die Blöcke eines vollständigen Indexes – oder zumindest die Blöcke, die den Nichtblattknoten entsprechen – aufzunehmen, dann ist es sinnvoll, den Schlüssel-Cache mit den Indexblöcken schon vor der eigentlichen Verwendung vorab zu laden. Dieses Vorabladen gestattet es Ihnen, die Tabellenindexblöcke auf effizienteste Art und Weise in einem Schlüssel-Cache-Puffer abzulegen – nämlich durch sequenzielles Lesen der Indexblöcke von der Festplatte.

Ohne Vorabladen werden die Blöcke so im Schlüssel-Cache abgelegt, wie sie von den Abfragen benötigt werden. Zwar bleiben die Blöcke dann im Cache, weil genug Puffer für sie alle vorhanden sind, aber sie werden dann nicht sequenziell, sondern in zufälliger Abfolge von der Festplatte abgerufen.

Um einen Index vorab in einen Cache zu laden, verwenden Sie die Anweisung LOAD INDEX INTO CACHE. Die folgende Anweisung beispielsweise lädt Knoten (Indexblöcke) mit Indizes der Tabellen t1 und t2:

mysql> LOAD INDEX INTO CACHE t1, t2 IGNORE LEAVES;
+---------+--------------+----------+----------+
| Table   | Op           | Msg_type | Msg_text |
+---------+--------------+----------+----------+
| test.t1 | preload_keys | status   | OK       |
| test.t2 | preload_keys | status   | OK       |
+---------+--------------+----------+----------+

Der Modifizierer IGNORE LEAVES bewirkt, dass nur Blöcke für Indexknoten, die nicht Endknoten (Blätter) sind, geladen werden. Auf diese Weise lädt die gezeigte Anweisung alle Indexblöcke von t1, von t2 hingegen nur Blöcke für die Nichtblattknoten.

Wenn ein Index mit einer CACHE INDEX-Anweisung einem Schlüssel-Cache zugewiesen wurde, platziert das Vorabladen die Indexblöcke in diesem Cache. Andernfalls wird der Index in den vorgabeseitigen Schlüssel-Cache geladen.

7.4.6.5. Blockgröße des Index-Caches

Mit der Variablen key_cache_block_size ist es möglich, die Größe von Blockpuffern für einen einzelnen Schlüssel-Cache anzugeben. Auf diese Weise können Sie die Leistung von I/O-Operationen für Indexdateien optimieren.

Die beste Leistung für I/O-Operationen erhalten Sie, wenn die Größe der Lesepuffer der Größe der I/O-Puffer des nativen Betriebssystems entspricht. Allerdings gewährleistet das Einstellen der Schlüsselknotengröße auf die Größe der I/O-Puffer nicht immer die beste Gesamtperformance. Wenn der Server die großen Blattknoten ausliest, ruft er eine Menge unnötiger Daten ab, wodurch im Endeffekt das Lesen anderer Blattknoten verhindert wird.

Zurzeit lässt sich die Größe der Indexblöcke in einer Tabelle nicht steuern. Der Größenwert wird vom Server eingestellt, wenn die .MYI-Indexdatei erstellt wird, und hängt von der Größe der Schlüssel in den in der Tabellendefinition vorhandenen Indizes ab. In den meisten Fällen ist sie identisch mit der I/O-Puffergröße.

7.4.6.6. Restrukturierung eines Index-Caches

Ein Schlüssel-Cache kann jederzeit durch Änderung seiner Parameterwerte umstrukturiert werden. Zum Beispiel:

mysql> SET GLOBAL cold_cache.key_buffer_size=4*1024*1024;

Wenn Sie den Schlüssel-Cache-Komponenten key_buffer_size oder key_cache_block_size einen anderen als den aktuellen Wert zuweisen, zerstört der Server die alte Struktur des Caches und erstellt eine neue, die auf den geänderten Werten basiert. Wenn der Cache schmutzige Blöcke enthält, speichert der Server sie auf der Festplatte, bevor er den Cache zerstört und neu erstellt. Eine Umstrukturierung findet hingegen nicht statt, wenn Sie andere Schlüssel-Cache-Parameter einstellen.

Wenn Sie einen Schlüssel-Cache umstrukturieren, synchronisiert der Server zunächst die Inhalte schmutziger Puffer (soweit vorhanden) auf die Festplatte. Danach ist der Inhalt des Caches nicht mehr verfügbar. Allerdings blockiert die Umstrukturierung keine Abfragen, die dem Cache zugewiesene Indizes verwenden müssen. Stattdessen greift der Server in diesem Fall über das native Dateisystem-Caching direkt auf die Tabellenindizes zu. Das Dateisystem-Caching ist nicht so effizient wie ein Schlüssel-Cache, d. h., die Abfragen werden zwar ausgeführt, aber Sie können von Geschwindigkeitseinbußen ausgehen. Nachdem der Cache umstrukturiert wurde, werden ihm zugewiesene Indizes dort wieder zwischengespeichert, und das Dateisystem-Caching wird nicht mehr für die Indizes verwendet.

7.4.7. Sammlung von MyISAM-Indexstatistiken

Speicher-Engines sammeln tabellenspezifische Statistiken, die der Optimierer verwenden kann. Tabellenstatistiken basieren auf Wertegruppen. Eine Wertegruppe ist eine Menge von Datensätzen mit demselben Schlüsselpräfixwert. Für die Zwecke des Optimierers ist die durchschnittliche Wertegruppengröße eine wichtige Angabe.

MySQL verwendet diese Information,

  • um zu bestimmen, wie viele Datensätze für jeden ref-Zugriff gelesen werden müssen,

  • um einzuschätzen, wie viele Datensätze ein Teil-Join – oder, genauer, eine Operation der folgenden Form – erzeugt:

    (...) JOIN tbl_name ON tbl_name.key = expr
    

Wenn die Durchschnittsgröße der Wertegruppen für einen Index ansteigt, wird der Index für diese beiden Aufgaben zunehmend weniger nützlich, weil auch die durchschnittliche Anzahl von Datensätzen pro Suchvorgang steigt: Damit der Index für Optimierungszwecke gut geeignet ist, wäre es optimal, wenn jeder Indexwert auf eine kleine Anzahl von Datensätzen in der Tabelle abzielt. Hat ein gegebener Indexwert eine hohe Zahl von Datensätzen zum Ergebnis, dann ist sein Nutzen eingeschränkt, und die Wahrscheinlichkeit, dass MySQL ihn verwendet, ist geringer.

Die durchschnittliche Wertegruppengröße bezieht sich auf die Kardinalität der Tabelle, d. h. die Anzahl der Wertegruppen. Die SHOW INDEX-Anweisung zeigt einen Kardinalitätswert basierend auf N ÷ S an, wobei N die Anzahl der Datensätze in der Tabelle und S die durchschnittliche Wertegruppengröße darstellt. Dieses Verhältnis gibt einen Näherungswert für die Anzahl der Wertegruppen in der Tabelle an.

Bei einem Join, der auf dem Vergleichsoperator <=> basiert, wird NULL nicht anders behandelt als andere Werte: NULL <=> NULL (wie N<=> N für jeden anderen Wert N).

Basiert ein Join jedoch auf dem Operator =, dann unterscheidet sich NULL von Nicht-NULL-Werten: expr1 = expr2 ist nicht wahr, wenn expr1 oder expr2 (oder beide) NULL sind. Dies wirkt sich auf ref-Zugriffe für Vergleiche der Form tbl_name.key = expr aus: MySQL greift nicht auf die Tabelle zu, wenn der aktuelle Wert von expr NULL ist, weil der Vergleich nicht wahr sein kann.

Bei =-Vergleichen spielt es keine Rolle, wie viele NULL-Werte in der Tabelle vorhanden sind. Der für Optimierungszwecke relevante Wert ist die Durchschnittsgröße der Gruppe von Nicht-NULL-Werten. Allerdings gestattet MySQL derzeit keine Ermittlung oder Verwendung dieser Durchschnittsgröße.

Bei MyISAM-Tabellen haben Sie mit der Systemvariablen myisam_stats_method in eingeschränktem Maße Kontrolle über die Ermittlung der Tabellenstatistiken. Diese Variable hat zwei mögliche Werte, die sich wie folgt voneinander unterscheiden:

  • Wenn myisam_stats_method nulls_equal ist, dann werden alle NULL-Werte als identisch behandelt (d. h., sie alle bilden eine Wertegruppe).

    Wenn die NULL-Wertegruppe wesentlich größer ist als die Durchschnittsgröße der Gruppe der Nicht-NULL-Werte, dann erhöht diese Methode die durchschnittliche Wertegruppengröße. Aufgrund dessen erscheint der Index dem Optimierer weniger nützlich, als er es für Joins, die nach Nicht-NULL-Werten suchen, tatsächlich ist. Im Ergebnis kann die nulls_equal-Methode also dazu führen, dass der Optimierer den Index für ref nicht verwendet, obwohl er es sollte.

  • Wenn myisam_stats_method nulls_unequal ist, werden NULL-Werte nicht als identisch betrachtet. Stattdessen bildet jeder NULL-Wert eine separate Wertegruppe der Größe 1.

    Wenn Sie viele NULL-Werte haben, verschiebt diese Methode den Durchschnittswert für die Wertegruppengröße nach unten. Ist der Durchschnittswert der Gruppe der Nicht-NULL-Werte hoch, dann kann dieses Verhalten dazu führen, dass der Optimierer den Wert des Indexes für Joins überschätzt, die nach Nicht-NULL-Werten suchen. Aufgrund dessen kann der nulls_unequal-Ansatz dazu führen, dass der Optimierer diesen Index für ref-Suchvorgänge verwendet, obwohl andere Methoden besser geeignet wären.

Wenn Sie häufig viele Joins benutzen, die <=> statt = verwenden, dann werden NULL-Werte in Vergleichen nicht gesondert betrachtet, sondern ein NULL-Wert entspricht dem anderen. In diesem Fall ist nulls_equal die passende Statistikmethode.

Die Systemvariable myisam_stats_method hat globale und sitzungsbezogene Werte. Das Einstellen des globalen Werts wirkt sich auf die MyISAM-Statistikermittlung bei allen MyISAM-Tabellen aus. Im Gegensatz dazu wirkt der Sitzungswert nur auf Statistiken zur aktuellen Clientverbindung. Sie können also, indem Sie den Sitzungswert von myisam_stats_method einstellen, die Neuerstellung der Statistiken zu einer Tabelle mit einer gegebenen Methode erzwingen, ohne dass andere Clients hiervon beeinträchtigt würden.

Zur Neuerstellung der Tabellenstatistiken können Sie eine der folgenden Methoden benutzen:

  • Stellen Sie myisam_stats_method ein und setzen Sie dann eine CHECK TABLE-Anweisung ab.

  • Führen Sie myisamchk --stats_method=method_name --analyze aus.

  • Ändern Sie die Tabelle so ab, dass die Statistiken veralten (indem Sie beispielsweise einen Datensatz einfügen und ihn dann löschen), stellen Sie myisam_stats_method ein und setzen Sie eine ANALYZE TABLE-Anweisung ab.

Die Verwendung von myisam_stats_method hat auch ein paar Nachteile:

Sie können die Ermittlung von Tabellenstatistiken wie gerade beschrieben erzwingen. Allerdings kann MySQL die Statistiken auch automatisch sammeln. Wenn beispielsweise während der Ausführung von Anweisungen für eine Tabelle einige dieser Anweisungen die Tabelle ändern, ermittelt MySQL unter Umständen Statistiken. (Dies kann etwa bei Masseneinfüge- oder -löschoperationen oder bei bestimmten ALTER TABLE-Anweisungen der Fall sein.) Wenn dies geschieht, werden die Statistiken ohne Berücksichtigung des aktuellen Werts von myisam_stats_method gesammelt. Wenn Sie also Statistiken mit einer bestimmten Methode ermitteln, aber myisam_stats_method bei einer späteren automatischen Statistiksammlung auf eine andere Methode gesetzt wird, dann wird diese andere Methode verwendet.

Es gibt keine Möglichkeit, festzustellen, welche Methode zur Erzeugung von Statistiken für eine gegebene MyISAM-Tabelle verwendet wurde.

myisam_stats_method gilt nur für MyISAM-Tabellen. Die übrigen Speicher-Engines haben jeweils eigene Methoden zur Ermittlung von Tabellenstatistiken. Diese ähneln der Methode nulls_equal.

7.4.8. Nachteile der Erzeugung großer Mengen von Tabellen in derselben Datenbank

Wenn Sie einen mysqladmin status-Befehl ausführen, sollten Sie etwa folgendes Ergebnis erhalten:

Uptime: 426 Running threads: 1 Questions: 11082
Reloads: 1 Open tables: 12

Der Open tables-Wert von 12 kann etwas verwirrend sein, wenn Sie nur sechs Tabellen haben.

MySQL arbeitet mit mehreren Threads, d. h., mehrere Clients setzen unter Umständen gleichzeitig Abfragen für eine gegebene Tabelle ab. Um das Problem mit mehreren Client-Threads, die unterschiedliche Zustände bezüglich derselben Tabelle haben, zu minimieren, wird die Tabelle von allen nebenläufigen Threads unabhängig geöffnet. Zwar wird hierdurch mehr Speicher verbraucht, aber in der Regel wird die Leistung verbessert. Bei MyISAM-Tabellen ist pro Client, der eine Tabelle geöffnet hält, ein zusätzlicher Dateideskriptor für die Datendatei erforderlich. (Dies steht im Gegensatz zum Indexdateideskriptor, der von allen Threads gemeinsam verwendet wird.)

Die Systemvariablen table_open_cache, max_connections und max_tmp_tables bestimmen die maximale Anzahl der Dateien, die der Server geöffnet hält. Wenn Sie einen oder mehrere dieser Werte erhöhen, stoßen Sie unter Umständen auf ein Limit, das Ihr Betriebssystem bezüglich der prozessbezogenen Anzahl offener Dateideskriptoren setzt. Viele Betriebssysteme erlauben Ihnen eine Anhebung der Beschränkung für offene Dateien, auch wenn sich die jeweilige Methode von Betriebssystem zu Betriebssystem unterscheidet. Informationen dazu, ob und wie es möglich ist, diesen Wert anzuheben, entnehmen Sie der Dokumentation zu Ihrem Betriebssystem.

table_open_cache bezieht sich auf max_connections. So sollten Sie etwa für 200 gleichzeitig laufende Verbindungen einen Tabellen-Cache mit einer Größe von mindestens 200 × N haben, wobei N die maximale Anzahl von Tabellen pro Join in einer beliebigen von Ihnen ausgeführten Abfrage ist. Sie müssen ferner einige zusätzliche Dateideskriptoren für Temporärtabellen und -dateien reservieren.

Stellen Sie sicher, dass Ihr Betriebssystem die Anzahl der mit der Einstellung von table_open_cache verbundenen Deskriptoren für offene Dateien verarbeiten kann. Wenn für table_open_cache ein zu hoher Wert eingestellt ist, stehen für MySQL irgendwann unter Umständen keine Dateideskriptoren mehr zur Verfügung: MySQL verweigert dann Verbindungen, kann Abfragen nicht ausführen und wird im Ganzen sehr instabil. Sie müssen ferner berücksichtigen, dass die MyISAM-Speicher-Engine zwei Dateideskriptoren für jede offene Tabelle benötigt. Sie können die Anzahl der MySQL zur Verfügung stehenden Dateideskriptoren mit der Startoption --open-files-limit für mysqld_safe erhöhen. Siehe auch Abschnitt A.2.17, „Datei nicht gefunden“.

Der Cache für die offenen Tabellen wird auf einem Niveau von table_open_cache Einträgen gehalten. Der Standardwert beträgt 64, kann aber mit der Option --table_open_cache für mysqld geändert werden. Beachten Sie, dass MySQL zur Ausführung von Abfragen vorübergehend auch mehr Tabellen öffnen kann.

MySQL schließt nicht verwendete Tabellen und entfernt sie aus dem Tabellen-Cache, wenn

  • der Cache voll ist und ein Thread eine Tabelle zu öffnen versucht, die nicht im Cache enthalten ist,

  • der Cache mehr als table_open_cache Einträge enthält und eine Tabelle im Cache von keinem Thread mehr verwendet wird,

  • die Tabelle synchronisiert wird. Dies passiert, wenn jemand eine FLUSH TABLES-Anweisung absetzt oder einen der Befehle mysqladmin flush-tables oder mysqladmin refresh ausführt.

Wenn der Tabellen-Cache Einträge enthält, ermittelt der Server einen zu verwendenden Cache-Eintrag wie folgt:

  • Tabellen, die zurzeit nicht verwendet werden, werden freigegeben. Hierbei wird mit der Tabelle begonnen, die am längsten nicht verwendet wurde.

  • Wenn eine neue Tabelle geöffnet werden muss, der Cache aber voll ist und keine Tabelle freigegeben werden kann, dann wird der Cache vorübergehend nach Bedarf erweitert.

Ist der Cache vorübergehend erweitert und eine Tabelle erhält den Status „nicht verwendet“, dann wird diese Tabelle geschlossen und aus dem Cache entfernt.

Eine Tabelle wird für jeden nebenläufigen Zugriff geöffnet. Das bedeutet, dass die Tabelle zweimal geöffnet werden muss, wenn zwei Threads auf dieselbe Tabelle zugreifen oder ein Thread die Tabelle zweimal in derselben Abfrage referenziert (beispielsweise beim Join einer Tabelle mit sich selbst). Jeder gleichzeitige Öffnungsvorgang erfordert einen Eintrag im Tabellen-Cache. Der erste Öffnungsvorgang einer MyISAM-Tabelle nimmt zwei Dateideskriptoren entgegen, nämlich je einen für die Daten- und die Indexdatei. Jede zusätzliche Verwendung der Tabelle benötigt nur einen Deskriptor für die Datendatei. Der Deskriptor für die Indexdateien wird von allen Threads gemeinsam verwendet.

Wenn Sie eine Tabelle mit der Anweisung HANDLER tbl_name OPEN öffnen, wird für den Thread ein dediziertes Tabellenobjekt reserviert. Dieses Tabellenobjekt wird nicht mit anderen Threads gemeinsam genutzt und wird erst geschlossen, wenn der Thread HANDLER tbl_name CLOSE aufruft oder beendet wird. In diesem Fall wird die Tabelle in den Tabellen-Cache zurückgeführt (sofern dieser nicht voll ist). Siehe auch Abschnitt 13.2.3, „HANDLER.

Sie können ermitteln, ob Ihr Tabellen-Cache zu klein ist, indem Sie die Statusvariable Opened_tables für mysqld überprüfen:

mysql> SHOW STATUS LIKE 'Opened_tables';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Opened_tables | 2741  |
+---------------+-------+

Wenn der Wert sehr groß ist (und zwar auch dann, wenn Sie nicht viele FLUSH TABLES-Anweisungen abgesetzt haben), dann sollten Sie die Größe des Tabellen-Caches erhöhen. Siehe auch Abschnitt 5.2.2, „Server-Systemvariablen“, und Abschnitt 5.2.4, „Server-Statusvariablen“.

7.4.9. Warum gibt es so viele offene Tabellen?

Wenn sich viele MyISAM-Tabellen im selben Datenbankverzeichnis befinden, verlaufen Operationen wie das Öffnen, Schließen und Erstellen von Tabellen sehr langsam. Führen Sie SELECT-Anweisungen für viele verschiedene Tabellen aus, dann tritt eine geringe Mehrbelastung auf, wenn der Tabellen-Cache voll ist, weil für jede Tabelle, die geöffnet wird, eine andere geschlossen werden muss. Sie können diese Mehrbelastung verringern, indem Sie den Tabellen-Cache vergrößern.

7.5. Optimierung des MySQL Servers

7.5.1. System/Kompilierzeitpunkt und Tuning der Startparameter

Wir beginnen mit den Faktoren auf der Systemebene, weil einige dieser Entscheidungen sehr früh getroffen werden müssen, um einen beträchtlichen Leistungszugewinn zu erzielen. In anderen Fällen kann ein kurzer Blick in diesen Abschnitt ausreichen. Allerdings kann es immer praktisch sein, ein Gespür dafür zu entwickeln, welcher Zugewinn sich durch Änderung von Faktoren auf dieser Ebene erzielen lässt.

Das Betriebssystem, welches verwendet werden soll, ist ein wesentlicher Faktor. Um aus Mehrprozessorrechnern die optimale Leistung zu erzielen, sollten Sie Solaris (aufgrund seiner gut funktionierenden Thread-Implementierung) oder Linux verwenden (weil der Kernel der Versionen 2.4 und höher eine gute SMP-Unterstützung bietet). Beachten Sie, dass ältere Linux-Kernels standardmäßig eine Dateigrößenbeschränkung auf 2 Gbyte aufweisen. Wenn Sie einen solchen Kernel verwenden, Ihre Dateien aber größer werden könnten als 2 Gbyte, dann sollten Sie sich den LFS-Patch (Large File Support) für das ext2-Dateisystem besorgen. Andere Dateisysteme wie ReiserFS und XFS weisen diese Beschränkung auf 2 Gbyte nicht auf.

Bevor Sie MySQL in die Produktion nehmen, sollten Sie es auf der vorgesehenen Plattform testen.

Weitere Tipps:

  • Wenn Sie genug RAM haben, können Sie in Betracht ziehen, alle Auslagerungsgeräte zu entfernen. Einige Betriebssysteme verwenden unter bestimmten Umständen ein Auslagerungsgerät auch dann, wenn freier Speicher vorhanden ist.

  • Vermeiden Sie externe Sperren. Seit MySQL 4.0 ist die externe Sperrung standardmäßig auf allen Systemen deaktiviert. Mit den Optionen --external-locking und --skip-external-locking können Sie externe Sperren explizit aktivieren bzw. deaktivieren.

    Beachten Sie, dass die Deaktivierung externer Sperren die Funktionalität von MySQL nicht beeinträchtigt, solange Sie nur einen einzelnen Server ausführen. Denken Sie aber immer daran, den Server abzuschalten (oder die relevanten Tabellen zu sperren und zu synchronisieren), bevor Sie myisamchk ausführen. Auf einigen Systemen ist die Deaktivierung externer Sperren obligatorisch, weil sie dort ohnehin nicht funktionieren.

    Der einzige Fall, in dem Sie die externe Sperrung nicht deaktivieren können, liegt vor, wenn Sie mehrere MySQL Server (nicht Clients) mit demselben Datenbestand betreiben, oder wenn Sie mit myisamchk eine Tabelle überprüfen (nicht reparieren), ohne den Server zuvor angewiesen zu haben, die Tabellen zuerst zu sperren und zu synchronisieren. Beachten Sie, dass von der Verwendung mehrerer MySQL Server für den gleichzeitigen Zugriff auf dieselben Daten generell abzuraten ist, sofern Sie nicht MySQL Cluster verwenden.

    Die Anweisungen LOCK TABLES und UNLOCK TABLES verwenden interne Sperren, d. h., Sie können sie auch bei deaktivierter externer Sperrung verwenden.

7.5.2. Serverparameter feineinstellen

Mit dem folgenden Befehl können Sie die standardmäßigen Puffergrößen festlegen, die der Server mysqld verwendet:

shell> mysqld --verbose --help

Dieser Befehl erzeugt eine Liste aller mysqld-Optionen und konfigurierbarer Systemvariablen. Die Ausgabe enthält die Standardwerte der Variablen und sieht etwa so aus:

help                              TRUE
abort-slave-event-count           0
allow-suspicious-udfs             FALSE
auto-increment-increment          1
auto-increment-offset             1
automatic-sp-privileges           TRUE
basedir                           /home/jon/bin/mysql/
bdb                               FALSE
bind-address                      (No default value)
character-set-client-handshake    TRUE
character-set-server              latin1
character-sets-dir                /home/jon/bin/mysql/share/mysql/charsets/
chroot                            (No default value)
collation-server                  latin1_swedish_ci
completion-type                   0
concurrent-insert                 1
console                           FALSE
datadir                           /home/jon/bin/mysql/var/
default-character-set             latin1
default-collation                 latin1_swedish_ci
default-time-zone                 (No default value)
disconnect-slave-event-count      0
enable-locking                    FALSE
enable-pstack                     FALSE
engine-condition-pushdown         FALSE
external-locking                  FALSE
gdb                               FALSE
large-pages                       FALSE
init-connect                      (No default value)
init-file                         (No default value)
init-slave                        (No default value)
innodb                            TRUE
innodb_checksums                  TRUE
innodb_data_home_dir              (No default value)
innodb_doublewrite                TRUE
innodb_fast_shutdown              1
innodb_file_per_table             FALSE
innodb_flush_log_at_trx_commit    1
innodb_flush_method               (No default value)
innodb_locks_unsafe_for_binlog    FALSE
innodb_log_arch_dir               (No default value)
innodb_log_group_home_dir         (No default value)
innodb_max_dirty_pages_pct        90
innodb_max_purge_lag              0
innodb_status_file                FALSE
innodb_table_locks                TRUE
innodb_support_xa                 TRUE
isam                              FALSE
language                          /home/jon/bin/mysql/share/mysql/english
local-infile                      TRUE
log                               /home/jon/bin/mysql/var/master1.log
log-bin                           /home/jon/bin/mysql/var/master1
log-bin-index                     (No default value)
log-bin-trust-routine-creators    FALSE
log-error                         /home/jon/bin/mysql/var/master1.err
log-isam                          myisam.log
log-queries-not-using-indexes     FALSE
log-short-format                  FALSE
log-slave-updates                 FALSE
log-slow-admin-statements         FALSE
log-slow-queries                  (No default value)
log-tc                            tc.log
log-tc-size                       24576
log-update                        (No default value)
log-warnings                      1
low-priority-updates              FALSE
master-connect-retry              60
master-host                       (No default value)
master-info-file                  master.info
master-password                   (No default value)
master-port                       3306
master-retry-count                86400
master-ssl                        FALSE
master-ssl-ca                     (No default value)
master-ssl-capath                 (No default value)
master-ssl-cert                   (No default value)
master-ssl-cipher                 (No default value)
master-ssl-key                    (No default value)
master-user                       test
max-binlog-dump-events            0
memlock                           FALSE
myisam-recover                    OFF
ndbcluster                        FALSE
ndb-connectstring                 (No default value)
ndb-mgmd-host                     (No default value)
ndb-nodeid                        0
ndb-autoincrement-prefetch-sz     32
ndb-distibution                   KEYHASH
ndb-force-send                    TRUE
ndb_force_send                    TRUE
ndb-use-exact-count               TRUE
ndb_use_exact_count               TRUE
ndb-shm                           FALSE
ndb-optimized-node-selection      TRUE
ndb-cache-check-time              0
ndb-index-stat-enable             TRUE
ndb-index-stat-cache-entries      32
ndb-index-stat-update-freq        20
new                               FALSE
old-alter-table                   FALSE
old-passwords                     FALSE
old-style-user-limits             FALSE
pid-file                          /home/jon/bin/mysql/var/hostname.pid1
port                              3306
relay-log                         (No default value)
relay-log-index                   (No default value)
relay-log-info-file               relay-log.info
replicate-same-server-id          FALSE
report-host                       (No default value)
report-password                   (No default value)
report-port                       3306
report-user                       (No default value)
rpl-recovery-rank                 0
safe-user-create                  FALSE
secure-auth                       FALSE
server-id                         1
show-slave-auth-info              FALSE
skip-grant-tables                 FALSE
skip-slave-start                  FALSE
slave-load-tmpdir                 /tmp/
socket                            /tmp/mysql.sock
sporadic-binlog-dump-fail         FALSE
sql-mode                          OFF
symbolic-links                    TRUE
tc-heuristic-recover              (No default value)
temp-pool                         TRUE
timed_mutexes                     FALSE
tmpdir                            (No default value)
use-symbolic-links                TRUE
verbose                           TRUE
warnings                          1
back_log                          50
binlog_cache_size                 32768
bulk_insert_buffer_size           8388608
connect_timeout                   5
date_format                       (No default value)
datetime_format                   (No default value)
default_week_format               0
delayed_insert_limit              100
delayed_insert_timeout            300
delayed_queue_size                1000
expire_logs_days                  0
flush_time                        0
ft_max_word_len                   84
ft_min_word_len                   4
ft_query_expansion_limit          20
ft_stopword_file                  (No default value)
group_concat_max_len              1024
innodb_additional_mem_pool_size   1048576
innodb_autoextend_increment       8
innodb_buffer_pool_awe_mem_mb     0
innodb_buffer_pool_size           8388608
innodb_concurrency_tickets        500
innodb_file_io_threads            4
innodb_force_recovery             0
innodb_lock_wait_timeout          50
innodb_log_buffer_size            1048576
innodb_log_file_size              5242880
innodb_log_files_in_group         2
innodb_mirrored_log_groups        1
innodb_open_files                 300
innodb_sync_spin_loops            20
innodb_thread_concurrency         20
innodb_commit_concurrency         0
innodb_thread_sleep_delay         10000
interactive_timeout               28800
join_buffer_size                  131072
key_buffer_size                   8388600
key_cache_age_threshold           300
key_cache_block_size              1024
key_cache_division_limit          100
long_query_time                   10
lower_case_table_names            0
max_allowed_packet                1048576
max_binlog_cache_size             4294967295
max_binlog_size                   1073741824
max_connect_errors                10
max_connections                   100
max_delayed_threads               20
max_error_count                   64
max_heap_table_size               16777216
max_join_size                     4294967295
max_length_for_sort_data          1024
max_relay_log_size                0
max_seeks_for_key                 4294967295
max_sort_length                   1024
max_tmp_tables                    32
max_user_connections              0
max_write_lock_count              4294967295
multi_range_count                 256
myisam_block_size                 1024
myisam_data_pointer_size          6
myisam_max_extra_sort_file_size   2147483648
myisam_max_sort_file_size         2147483647
myisam_repair_threads             1
myisam_sort_buffer_size           8388608
myisam_stats_method               nulls_unequal
net_buffer_length                 16384
net_read_timeout                  30
net_retry_count                   10
net_write_timeout                 60
open_files_limit                  0
optimizer_prune_level             1
optimizer_search_depth            62
preload_buffer_size               32768
query_alloc_block_size            8192
query_cache_limit                 1048576
query_cache_min_res_unit          4096
query_cache_size                  0
query_cache_type                  1
query_cache_wlock_invalidate      FALSE
query_prealloc_size               8192
range_alloc_block_size            2048
read_buffer_size                  131072
read_only                         FALSE
read_rnd_buffer_size              262144
div_precision_increment           4
record_buffer                     131072
relay_log_purge                   TRUE
relay_log_space_limit             0
slave_compressed_protocol         FALSE
slave_net_timeout                 3600
slave_transaction_retries         10
slow_launch_time                  2
sort_buffer_size                  2097144
sync-binlog                       0
sync-frm                          TRUE
sync-replication                  0
sync-replication-slave-id         0
sync-replication-timeout          10
table_open_cache                  64
table_lock_wait_timeout           50
thread_cache_size                 0
thread_concurrency                10
thread_stack                      196608
time_format                       (No default value)
tmp_table_size                    33554432
transaction_alloc_block_size      8192
transaction_prealloc_size         4096
updatable_views_with_limit        1
wait_timeout                      28800

Wird gerade ein mysqld-Server ausgeführt, dann können Sie die aktuellen Werte seiner Systemvariablen anzeigen, indem Sie eine Verbindung mit ihm herstellen und die folgende Anweisung absetzen:

mysql> SHOW VARIABLES;

Mit der folgenden Anweisung können Sie statistische Informationen und Statusangaben für einen laufenden Server abrufen:

mysql> SHOW STATUS;

Angaben zu Systemvariablen und Status erhalten Sie auch mit mysqladmin:

shell> mysqladmin variables
shell> mysqladmin extended-status

Eine vollständige Beschreibung aller System- und Statusvariablen finden Sie in Abschnitt 5.2.2, „Server-Systemvariablen“, und Abschnitt 5.2.4, „Server-Statusvariablen“.

MySQL verwendet Algorithmen, die sehr gut skalierbar sind, d. h., Sie können MySQL gewöhnlich mit sehr wenig Speicher ausführen. Andererseits erhalten Sie eine umso bessere Performance, je mehr Speicher Sie MySQL zugestehen.

Wenn Sie einen MySQL Server optimieren, dann sind die beiden wichtigsten zu konfigurierenden Variablen key_buffer_size und table_open_cache. Sie sollten erst ganz sicher sein, dass Sie für diese Variablen geeignete Werte eingestellt haben, bevor Sie andere Variablen zu konfigurieren versuchen.

Die folgenden Beispiele zeigen einige typische Variablenwerte für verschiedene Laufzeitkonfigurationen.

  • Wenn Sie mindestens 256 Mbyte Speicher und viele Tabellen haben und für eine moderate Anzahl von Clients die optimale Leistung erzielen wollen, sollten Sie einmal Folgendes probieren:

    shell> mysqld_safe --key_buffer_size=64M --table_open_cache=256 \
               --sort_buffer_size=4M --read_buffer_size=1M &
    
  • Haben Sie nur 128 Mbyte Speicher und nur ein paar Tabellen, die aber häufig sortiert werden müssen, dann können Sie Folgendes ausprobieren:

    shell> mysqld_safe --key_buffer_size=16M --sort_buffer_size=1M
    

    Bei sehr vielen gleichzeitigen Problemen können Auslagerungsprobleme auftreten, sofern mysqld nicht für die Verwendung von sehr wenig Speicher für die jeweilige Verbindung konfiguriert wurde. mysqld arbeitet besser, wenn Sie genug Speicher für alle Verbindungen haben.

  • Bei sehr wenig Speicher und vielen Verbindungen verwenden Sie Folgendes:

    shell> mysqld_safe --key_buffer_size=512K --sort_buffer_size=100K \
               --read_buffer_size=100K &
    

    Oder sogar Folgendes:

    shell> mysqld_safe --key_buffer_size=512K --sort_buffer_size=16K \
               --table_open_cache=32 --read_buffer_size=8K \
               --net_buffer_length=1K &
    

Wenn Sie GROUP BY- oder ORDER BY-Operationen an Tabellen durchführen, die wesentlich größer sind als der verfügbare Speicher, dann sollten Sie den Wert von read_rnd_buffer_size erhöhen, um nach Durchführung von Sortieroperationen das Auslesen von Datensätzen zu beschleunigen.

Wenn Sie MySQL installiert haben, enthält das Verzeichnis support-files eine Anzahl verschiedener my.cnf-Beispieldateien, nämlich my-huge.cnf, my-large.cnf, my-medium.cnf und my-small.cnf. Diese können Sie als Basis zur Optimierung Ihres Systems verwenden. (Unter Windows schauen Sie im MySQL-Installationsverzeichnis nach.)

Wenn Sie für mysqld oder mysqld_safe eine Option auf der Befehlszeile angeben, ist diese nur für den betreffenden Aufruf des Servers gültig. Um diese Option jedes Mal bei Ausführung des Servers zu verwenden, müssen Sie sie in einer Optionsdatei ablegen.

Um die Wirkung einer Parameteränderung zu prüfen, können Sie Folgendes tun:

shell> mysqld --key_buffer_size=32M --verbose --help

Die Variablenwerte sind kurz vor Ende der Ausgabe aufgeführt. Vergewissern Sie sich, dass die Optionen --verbose und --help am Ende stehen. Andernfalls werden die Auswirkungen von Optionen, die nach diesen beiden Optionen auf der Befehlszeile angegeben wurden, in der Ausgabe nicht berücksichtigt.

Informationen zur Optimierung der InnoDB-Speicher-Engine finden Sie in Abschnitt 14.2.11, „Tipps zur Leistungssteigerung“.

7.5.3. Leistung des Abfragenoptimierers steuern

Die Aufgabe des Abfrageoptimierers besteht darin, einen optimalen Plan für die Ausführung einer SQL-Abfrage zu entwickeln. Da der Unterschied zwischen „gut“ und „schlecht“ aus leistungstechnischer Sicht mehrere Größenordnungen betragen kann (d. h. Sekunden im Vergleich zu Stunden oder sogar Tagen), führten die meisten Abfrageoptimierer (einschließlich des MySQL-Optimierers) unter allen möglichen Plänen zur Abfragebewertung eine mehr oder minder umfangreiche Suche nach dem optimalen Plan durch. Bei Join-Abfragen wächst die Anzahl möglicher Pläne, die vom MySQL-Optimierer untersucht werden, exponentiell mit der Anzahl der Tabellen, die in einer Abfrage referenziert werden. Bei einer kleinen Anzahl von Tabellen (etwa 7 bis 10) ist dies unproblematisch. Werden hingegen größere Abfragen abgesetzt, dann kann die für die Abfrageoptimierung erforderliche Zeit schnell zum Engpass für die Leistung des Servers werden.

Eine flexiblere Methode zur Abfrageoptimierung gewährt dem Benutzer Kontrolle darüber, wie erschöpfend der Optimierer bei der Suche nach einem optimalen Abfragebewertungsplan vorgeht. Hintergedanke ist, dass umso weniger Zeit für die Kompilierung einer Abfrage aufgewendet wird, je weniger Pläne vom Optimierer untersucht werden. Andererseits kann es sein, dass der Optimierer, weil er einige Pläne überspringt, den optimalen Plan übersieht.

Das Verhalten des Optimierers in Bezug auf die Anzahl der Pläne, die er bewertet, kann über zwei Systemvariablen gesteuert werden:

  • Die Variable optimizer_prune_level weist den Optimierer an, bestimmte Pläne basierend auf der geschätzten Anzahl der Datensätze, auf die pro Tabelle zugegriffen wird, zu übergehen. Unsere Erfahrung zeigt, dass diese Art der „begründeten Annahme“ optimale Pläne nur in sehr seltenen Fällen verfehlt. Aus diesem Grund ist die Option standardmäßig aktiviert (optimizer_prune_level=1). Wenn Sie allerdings das Gefühl haben, dass der Optimierer einen besseren Abfrageplan hätte finden können, dann können Sie die Option auch abschalten (optimizer_prune_level=0); allerdings besteht dann das Risiko, dass die Abfragekompilierung deutlich länger dauert. Beachten Sie, dass der Optimierer auch bei Verwendung dieser Heuristik eine grob exponentielle Anzahl von Plänen untersucht.

  • Die Variable optimizer_search_depth sagt dem Optimierer, wie weit er bei unvollständigen Plänen „vorausschauen“ soll, um einzuschätzen, ob ein Plan erweitert werden soll. Niedrige Werte für optimizer_search_depth führen zu um mehrere Größenordnungen kürzeren Abfragekompilierungszeiten. So kann die Kompilierung von Abfragen mit 12, 13 oder mehr Tabellen leicht Stunden oder sogar Tage dauern, wenn optimizer_search_depth einen Wert hat, der annähernd der Anzahl der Tabellen in der Abfrage entspricht. Umgekehrt benötigt der Compiler für dieselbe Abfrage noch nicht einmal eine Minute, wenn optimizer_search_depth den Wert 3 oder 4 hat. Wenn Sie nicht sicher sind, welcher Wert für optimizer_search_depth sinnvoll sein könnte, können Sie ihn auf 0 setzen; so weisen Sie den Optimierer an, den Wert automatisch zu ermitteln.

7.5.4. Wie Kompilieren und Linken die Geschwindigkeit von MySQL beeinflusst

Die meisten der folgenden Tests wurden unter Linux mit den MySQL-Benchmarks durchgeführt, sie sollten aber trotzdem ausreichend aussagekräftig auch für andere Betriebssysteme und Belastungsbereiche sein.

Sie erhalten die schnellsten ausführbaren Dateien, indem Sie mit der Option -static verknüpfen.

Unter Linux kompilieren Sie den Server am besten mit pgcc und -O3. Sie benötigen etwa 200 Mbyte Speicher zur Kompilierung von sql_yacc.cc mit diesen Optionen, weil gcc oder pgcc eine Menge Speicher benötigt, um alle Funktionen zu integrieren. Ferner sollten Sie bei der Konfiguration von MySQL CXX=gcc einstellen, um die Miteinbeziehung der Bibliothek libstdc++ zu vermeiden, denn diese wird nicht benötigt. Beachten Sie, dass bei einigen Versionen von pgcc die resultierende Binärdatei nur auf echten Pentium-Prozessoren läuft, auch wenn Sie eine Compileroption verwendet haben, mit der Sie angeben wollten, dass der Ergebniscode auf allen x586-Prozessoren (z. B. AMD) laufen soll.

Sie können eine Geschwindigkeitszunahme von 10 bis 30 Prozent erzielen, indem Sie einen besseren Compiler und geeignetere Kompilierungsoptionen verwenden. Dies ist besonders wichtig, wenn Sie den MySQL Server selbst kompilieren.

Als wir die Cygnus CodeFusion- und Fujitsu-Compiler testeten, stellte sich heraus, dass keiner von diesen ausreichend wenig Bugs aufwies, um MySQL mit aktivierten Optimierungen zu kompilieren.

MySQL-Standarddistributionen werden mit der Unterstützung für alle Zeichensätze kompiliert. Wenn Sie MySQL selbst kompilieren, sollten Sie die Unterstützung nur für diejenigen Zeichensätze integrieren, die Sie tatsächlich verwenden werden. Dies wird mit der Option --with-charset für configure gesteuert.

Es folgt eine Liste einiger Maßnahmen, die wir vorgenommen haben:

  • Wenn Sie pgcc verwenden und alles mit -O6 kompilieren, ist der Server mysqld um ein Prozent schneller als mit gcc 2.95.2.

  • Wenn Sie dynamisch verknüpfen (also ohne -static), ist das Ergebnis unter Linux um 13 Prozent langsamer. Beachten Sie, dass Sie eine dynamisch verknüpfte MySQL-Bibliothek weiterhin für Ihre Clientanwendungen benutzen können. Es ist der Server, der für die Leistungsfähigkeit die kritischste Komponente darstellt.

  • Wenn Sie Ihre mysqld-Binärdatei mit strip mysqld bearbeiten, kann die resultierende Binärdatei um bis zu 4 Prozent schneller sein.

  • Bei einer Verbindung von einem Client zu einem Server, der auf demselben Host läuft, ist die Leistung um 7,5 Prozent geringer, wenn Sie die Verbindung über TCP/IP statt über eine Unix-Socketdatei herstellen. (Unter Unix verwendet MySQL, wenn Sie die Verbindung zum Hostnamen localhost herstellen, standardmäßig eine Socketdatei.)

  • TCP/IP-Verbindungen von einem Client mit einem Server sind um 8 bis 11 Prozent langsamer, wenn der Server auf einem entfernten System liegt, als bei Verbindung mit einem Server auf demselben Host (und zwar auch bei Verbindungen über 100-Mbit/s-Ethernet).

  • Bei Ausführung unserer Benchmark-Tests über sichere Verbindungen (d. h., alle Daten wurden durch die interne SSL-Unterstützung verschlüsselt) war die Leistung um 55 Prozent geringer als bei unverschlüsselten Verbindungen.

  • Wenn Sie mit --with-debug=full kompilieren, sind die meisten Abfragen 20 Prozent langsamer. Einige Abfragen können deutlich länger benötigen; so laufen die MySQL-Benchmarks um 35 Prozent langsamer. Wenn Sie --with-debug (ohne =full) angeben, liegt der Geschwindigkeitsverlust lediglich bei 15 Prozent. Bei einer Version von mysqld, die mit --with-debug=full kompiliert wurde, können Sie die Speicherüberprüfung zur Laufzeit abschalten, indem Sie sie mit der Option --skip-safemalloc starten. Die Ausführungsgeschwindigkeit sollte in diesem Fall nicht allzu weit unter derjenigen liegen, die sich bei einer Konfiguration mit --with-debug erzielen lässt.

  • Auf einem Sun UltraSPARC-IIe ist ein mit Forte 5.0 kompilierter Server 4 Prozent schneller als ein mit gcc 3.2 kompilierter Server.

  • Auf einem Sun UltraSPARC-IIe ist ein mit Forte 5.0 kompilierter Server im 32-Bit-Modus 4 Prozent schneller als im 64-Bit-Modus.

  • Die Kompilierung mit gcc 2.95.2 für UltraSPARC mit den Optionen -mcpu=v8 -Wa und -xarch=v8plusa bietet einen Leistungszuwachs von 4 Prozent.

  • Auf Solaris 2.5.1 ist MIT-pthreads 8 bis 12 Prozent langsamer als native Solaris-Threads auf einem einzelnen Prozessor. Bei hoher Belastung oder mehr Prozessoren sollte der Unterschied noch größer sein.

  • Die Kompilierung auf Linux-x86 mit gcc ohne Frame-Zeiger (-fomit-frame-pointer oder -fomit-frame-pointer -ffixed-ebp) macht mysqld 1 bis 4 Prozent schneller.

Von MySQL AB angebotene MySQL-Binärdistributionen für Linux wurden früher generell mit pgcc kompiliert. Aufgrund eines Bugs in pgcc, der dazu führte, dass die erzeugten Binärdateien nicht auf AMD-Prozessoren liefen, mussten wir jedoch zu gcc zurückkehren. Wir werden gcc weiterhin verwenden, bis dieser Bug behoben ist. In der Zwischenzeit können Sie, wenn Sie kein AMD-System haben, durch Kompilierung mit pgcc eine schnellere Binärdatei erstellen. Die MySQL-Standardbinärdatei für Linux ist statisch verknüpft, um Geschwindigkeit und Portabilität zu optimieren.

7.5.5. Wie MySQL Speicher benutzt

Die folgende Liste gibt einige Möglichkeiten an, wie der mysqld-Server Speicher verwendet. Sofern anwendbar, wird der Name der Systemvariablen angegeben, die für die Speichernutzung relevant ist:

  • Der Schlüsselpuffer (Variable key_buffer_size) wird von allen Threads gemeinsam verwendet. Andere Puffer, die der Server benutzt, werden nach Bedarf zugewiesen. Siehe auch Abschnitt 7.5.2, „Serverparameter feineinstellen“.

  • Jede Verbindung verwendet mehrere Thread-spezifische Bereiche:

    • einen Stapel (standardmäßig 192 Kbyte groß, Variable thread_stack)

    • einen Verbindungspuffer (Variable net_buffer_length)

    • einen Ergebnispuffer (Variable net_buffer_length)

    Verbindungs- und Ergebnispuffer werden nach Bedarf dynamisch bis auf den durch max_allowed_packet angegebenen Wert vergrößert. Während der Ausführung einer Abfrage wird eine Kopie des aktuellen Abfrage-Strings ebenfalls reserviert.

  • Alle Threads verwenden denselben Basisspeicher.

  • Wenn ein Thread nicht mehr benötigt wird, wird der ihm zugewiesene Speicher freigegeben und dem System zurückgegeben, sofern der Thread nicht zurück in den Thread-Cache wandert (in diesem Fall bleibt der Speicher reserviert).

  • Vor MySQL 5.1.4 wurden nur für komprimierte MyISAM-Tabellen Speicher zugeordnet. Seit MySQL 5.1.4 lässt sich die Systemvariable myisam_use_mmap auf 1 setzen, um die Speicherzuordnung für alle MyISAM-Tabellen zu aktivieren. Abschnitt 5.2.2, „Server-Systemvariablen“.

  • Jede Anforderung, die einen sequenziellen Scan einer Tabelle durchführt, reserviert einen Lesepuffer (Variable read_buffer_size).

  • Beim Lesen von Datensätzen in willkürlicher Folge (z. B. nach einer Sortierung) kann ein Zufallslesepuffer reserviert werden, um Festplattenzugriffe zu umgehen (Variable read_rnd_buffer_size).

  • Alle Joins werden in einem einzigen Durchlauf ausgeführt, und die meisten Joins können sogar ohne Temporärtabelle erledigt werden. Die meisten Temporärtabellen sind speicherbasierte Hash-Tabellen. Temporärtabellen mit einer großen Datensatzlänge (berechnet als Summe aller Spaltenlängen) oder solche, die BLOB-Spalten enthalten, werden auf der Festplatte gespeichert.

    Wenn eine interne HEAP-Tabelle die durch tmp_table_size angegebene Größe überschreitet, macht MySQL die HEAP-Tabelle im Speicher nach Bedarf automatisch zu einer festplattenbasierten MyISAM-Tabelle. Sie können auch die Größe der Temporärtabelle erhöhen, indem Sie die mysqld-Option tmp_table_size oder die SQL-Option SQL_BIG_TABLES im Clientprogramm einstellen. Siehe auch Abschnitt 13.5.3, „SET.

  • Die meisten Anforderungen, die eine Sortierung durchführen, reservieren einen Sortierpuffer und abhängig von der Größe der Ergebnismenge null bis zwei Temporärdateien. Siehe auch Abschnitt A.4.4, „Wohin MySQL temporäre Dateien speichert“.

  • Praktisch die gesamte Analyse und Berechnung erfolgt in einem lokalen Speicherbereich. Bei kleinen Elementen ist keine Speichermehrbelastung gegeben, d. h., die normale langsame Speicherreservierung und -freigabe werden umgangen. Speicher wird nur für unerwartet große Strings reserviert. Dies wird mit malloc() und free() bewerkstelligt.

  • Für jede MyISAM-Tabelle, die geöffnet wird, wird die Indexdatei einmal geöffnet. Die Datendatei wird für jeden gleichzeitig laufenden Thread einmal geöffnet. Für jeden nebenläufigen Thread werden eine Tabellenstruktur, Spaltenstrukturen für jede Spalte und ein Puffer der Größe 3 × N reserviert. (Hierbei ist N die maximale Datensatzlänge abzüglich vorhandener BLOB-Spalten.) Eine BLOB-Spalte erfordert fünf bis acht Byte zuzüglich der Länge der BLOB-Daten. Die MyISAM-Speicher-Engine unterhält einen zusätzlichen Puffer zur internen Verwendung.

  • Bei Tabellen mit BLOB-Spalten wird ein Puffer dynamisch vergrößert, um größere BLOB-Werte einlesen zu können. Wenn Sie eine Tabelle scannen, wird ein Puffer reserviert, der so groß ist wie der größte BLOB-Wert.

  • Handler-Strukturen für alle in Verwendung befindlichen Tabellen werden in einem Cache gespeichert und als FIFO verwaltet. Standardmäßig hat der Cache 64 Einträge. Wenn eine Tabelle von zwei laufenden Threads gleichzeitig verwendet wurde, enthält der Cache zwei Einträge für die Tabelle. Siehe auch Abschnitt 7.4.8, „Nachteile der Erzeugung großer Mengen von Tabellen in derselben Datenbank“.

  • Eine FLUSH TABLES-Anweisung oder der Befehl mysqladmin flush-tables schließt alle Tabellen, die gerade nicht verwendet werden, und kennzeichnet alle in Verwendung befindlichen Tabellen, damit diese geschlossen werden, sobald der ausführende Thread endet. Hierdurch wird der größte Teil des in Verwendung befindlichen Speichers freigegeben. FLUSH TABLES wird erst abgeschlossen, wenn alle Tabellen geschlossen wurden.

ps und andere Systemstatusprogramme melden unter Umständen, dass mysqld viel Speicher benötigt. Dies kann von Thread-Stapeln an verschiedenen Speicheradressen verursacht werden. Die Solaris-Version von ps beispielsweise zählt die Menge des nicht verwendeten Speichers zwischen den Stapeln als verwendeten Speicher. Sie können dies überprüfen, indem Sie den verfügbaren Auslagerungsspeicher mit swap -s verifizieren. Wir testen mysqld mit verschiedenen Speicherleckdetektoren (sowohl kommerzieller als auch freier Herkunft), d. h., es sollten keine Speicherlecks vorhanden sein.

7.5.6. Wie MySQL DNS benutzt

Wenn ein neuer Client eine Verbindung mit mysqld herstellt, dann erzeugt mysqld einen neuen Thread, um die Anforderung zu bearbeiten. Dieser Thread überprüft zunächt, ob der Hostname sich im Hostnamens-Cache befindet. Ist dies nicht der Fall, dann versucht der Thread, den Hostnamen aufzulösen:

  • Wenn das Betriebssystem die Thread-sicheren Aufrufe gethostbyaddr_r() und gethostbyname_r() unterstützt, führt der Thread unter ihrer Verwendung die Hostnamensauflösung durch.

  • Unterstützt das Betriebssystem die Thread-sicheren Aufrufe nicht, dann sperrt der Thread ein Mutex und ruft stattdessen gethostbyaddr() und gethostbyname() auf. In diesem Fall kann ein anderer Thread Hostnamen, die nicht im Hostnamens-Cache vorhanden sind, erst auflösen, wenn der erste Thread das Mutex wieder aufgehoben hat.

Sie können DNS-Lookups für Hostnamen deaktivieren, indem Sie mysqld mit der Option --skip-name-resolve starten. Allerdings können Sie in diesem Fall nur IP-Adressen in den MySQL-Grant-Tabellen verwenden.

Wenn Ihr DNS sehr langsam ist und Sie viele Hosts haben, können Sie die Leistung steigern, indem Sie entweder DNS-Lookups mit --skip-name-resolve deaktivieren oder den HOST_CACHE_SIZE-Wert (Standard: 128) erhöhen und mysqld neu kompilieren.

Sie können den Hostnamens-Cache deaktivieren, indem Sie den Server mit der Option --skip-host-cache starten. Um den Cache zu leeren, setzen Sie eine FLUSH HOSTS-Anweisung ab oder führen den Befehl mysqladmin flush-hosts aus.

Wenn Sie TCP/IP-Verbindungen vollständig deaktivieren wollen, starten Sie mysqld mit der Option --skip-networking.

7.6. Festplatte, Anmerkungen

  • Suchvorgänge auf der Festplatte stellen einen für die Leistung beträchtlichen Engpass dar. Dieses Problem wird in dem Moment umso offensichtlicher, wenn die Datenmengen so groß werden, dass ein effizientes Caching unmöglich wird. Bei großen Datenbanken, bei denen der Datenzugriff mehr oder weniger zufällig erfolgt, können Sie sich darauf verlassen, dass Sie mindestens einen Lese- und mehrere Suchvorgänge benötigen, um Daten zu schreiben. Um dieses Problem zu verringern, sollten Sie Festplatten mit schnellen Zugriffszeiten verwenden.

  • Erhöhen Sie die Anzahl der verfügbaren Festplattenspindeln (und verringern Sie dadurch die Suchbelastung), indem Sie entweder Dateien auf anderen Festplatten symbolisch verknüpfen oder das Striping verwenden:

    • Symbolische Verknüpfungen

      Sie verknüpfen die Index- und Datendateien von MyISAM-Tabellen von ihren normalen Positionen im Datenverzeichnis aus auf eine andere Festplatte (für die zudem das Striping angewendet werden kann). Hierdurch werden Such- und Lesezeiten verkürzt (vorausgesetzt, die Festplatte wird nicht noch für andere Zwecke verwendet). Siehe auch Abschnitt 7.6.1, „Symbolische Verknüpfungen“.

    • Striping

      Striping bedeutet, dass Sie viele Festplatten haben und den ersten Block auf der ersten Festplatte, den zweiten Block auf der zweiten und den N-ten Block auf der Festplatte mit der Nummer N MOD number_of_disks ablegen. Wenn Ihre normale Datengröße also geringer ist als die Stripe-Größe (oder perfekt angepasst ist), dann erhalten Sie eine wesentlich bessere Performance. Das Striping hängt sehr stark vom Betriebssystem und der Stripe-Größe ab. Insofern sollten Sie Benchmark-Tests Ihrer Anwendung mit verschiedenen Stripe-Größen durchführen. Siehe auch Abschnitt 7.1.5, „Wie Sie Ihre eigenen Benchmarks benutzen“.

      Der Geschwindigkeitsunterschied für das Striping hängt in erheblichem Maße von den Parametern ab. Je nachdem, wie Sie Ihre Striping-Parameter einstellen und über wie viel Festplatten Sie verfügen, können die Unterschiede mehrere Größenordnungen betragen. Sie müssen sich entscheiden, ob Sie die Optimierung für den wahlfreien oder den sequenziellen Zugriff durchführen wollen.

  • Aus Gründen der Zuverlässigkeit sollten Sie RAID 0+1 (Striping plus Spiegelung) verwenden, allerdings benötigen Sie in diesem Fall 2 × N Laufwerke, um N Laufwerke mit Daten aufzunehmen. Dies ist die wahrscheinlich beste Option, sofern Sie über das nötige Kleingeld verfügen. Allerdings müssen Sie unter Umständen in eine Volumeverwaltungssoftware investieren, um ein solches Setup effizient steuern zu können.

  • Empfehlenswert ist auch der Ansatz, den RAID-Level entsprechend der Wichtigkeit der Daten zu variieren. So können Sie etwa Daten moderater Wichtigkeit, die sich wiederherstellen lassen, auf einer RAID-0-Festplatte speichern, wirklich wichtige Daten (z. B. Hostinformationen und Logdateien) hingegen auf einer RAID 0+1- oder einer RAID-N-Festplatte ablegen. RAID N kann problematisch werden, wenn Sie viele Schreibvorgänge haben, da die Aktualisierung der Paritätsbits zeitaufwändig werden kann.

  • Unter Linux können Sie deutlich mehr Leistung erzielen, indem Sie die Schnittstelle Ihrer Festplatte mit hdparm konfigurieren. (Bis zu 100 Prozent unter Last sind nicht ungewöhnlich.) Die folgenden hdparm-Optionen sollten für MySQL – und auch für viele andere Anwendungen – recht gut geeignet sein:

    hdparm -m 16 -d 1
    

    Beachten Sie, dass, wenn Sie diesen Befehl verwenden, Leistung und Zuverlässigkeit von Ihrer Hardware abhängen; aus diesem Grund empfehlen wir Ihnen dringend, Ihr System nach der Verwendung von hdparm umfassend zu testen. Weitere Informationen entnehmen Sie der hdparm-Manpage. Die unvorsichtige Verwendung von hdparm kann zu Schäden am Dateisystem führen! Erstellen Sie deswegen immer ein Backup, bevor Sie zu experimentieren beginnen.

  • Ferner können Sie die Parameter für das von der Datenbank verwendete Dateisystem einstellen:

    Wenn Sie nicht wissen müssen, wann zuletzt auf die Dateien zugegriffen wurde (was bei einem Datenbankserver eigentlich auch nicht wichtig ist), dann können Sie Ihre Dateisysteme mit der Option -o noatime einbinden. Hierdurch werden Updates bis zum letzten Zugriffszeitpunkt in Inoden auf dem Dateisystem übersprungen, wodurch einige Suchvorgänge auf der Festplatte unnötig werden.

    Bei vielen Betriebssystemen können Sie für ein Dateisystem eine asynchrone Aktualisierung konfigurieren; hierzu binden Sie es mit der Option -o async ein. Wenn Ihr Computer ausreichend stabil ist, sollten Sie auf diese Weise mehr Performance erhalten, ohne zu viel Zuverlässigkeit zu opfern. (Unter Linux ist dieses Flag standardmäßig aktiv.)

7.6.1. Symbolische Verknüpfungen

Sie können Tabellen und Datenbanken aus dem Datenbankverzeichnis an andere Positionen verschieben und sie durch symbolische Verknüpfungen auf diese neuen Positionen ersetzen. Sie könnten dies beispielsweise tun, um eine Datenbank auf ein Dateisystem mit mehr freiem Speicher zu verschieben oder die Geschwindigkeit Ihres Systems durch Verteilen Ihrer Tabellen auf verschiedene Festplatten zu erhöhen.

Die empfohlene Vorgehensweise hierfür besteht schlicht darin, für Datenbanken symbolische Verknüpfungen auf eine andere Festplatte zu erstellen. Symbolische Verknüpfungen für Tabellen sollten hingegen nur als letzter Ausweg verwendet werden.

7.6.1.1. Benutzung symbolischer Links für Datenbanken

Unter Unix erstellen Sie eine symbolische Verknüpfung mit einer Datenbank, indem Sie zuerst ein Verzeichnis auf einer Festplatte erstellen, auf der sich genug freier Speicher befindet, und dann eine symbolische Verknüpfung zu diesem Verzeichnis aus dem MySQL-Datenverzeichnis einrichten.

shell> mkdir /dr1/databases/test
shell> ln -s /dr1/databases/test /path/to/datadir

MySQL unterstützt Verknüpfungen von einem Verzeichnis auf mehrere Datenbanken nicht. Das Ersetzen eines Datenbankverzeichnisses durch eine symbolische Verknüpfung funktioniert, solange Sie keine symbolischen Verknüpfungen zwischen Datenbanken erstellen. Angenommen, Sie haben eine Datenbank db1 im MySQL-Datenverzeichnis und erstellen dann eine symbolische Verknüpfung db2, die auf db1 zeigt:

shell> cd /path/to/datadir
shell> ln -s db1 db2

Im Ergebnis scheint für jede Tabelle tbl_a in db1 offenbar auch eine Tabelle tbl_a in db2 vorhanden zu sein. Wenn ein Client db1.tbl_a und ein anderer Client db2.tbl_a aktualisiert, sind Probleme vorprogrammiert.

Wenn Sie dies aber wirklich tun müssen, ist es möglich, indem Sie die Quelldatei mysys/my_symlink.c ändern. Suchen Sie dort nach folgender Anweisung:

if (!(MyFlags & MY_RESOLVE_LINK) ||
    (!lstat(filename,&stat_buff) && S_ISLNK(stat_buff.st_mode)))

Ändern Sie diese wie folgt ab:

if (1)

7.6.1.2. Benutzung symbolischer Links für Tabellen

Auf Systemen, die keinen vollständig funktionsfähigen realpath()-Aufruf aufweisen, sollten Sie Tabellen nicht symbolisch verknüpfen. (Linux und Solaris unterstützen realpath().) Sie können überprüfen, ob Ihr System symbolische Verknüpfungen unterstützt, indem Sie eine SHOW VARIABLES LIKE 'have_symlink'-Anweisung absetzen.

Symbolische Verknüpfungen werden nur bei MyISAM-Tabellen vollständig unterstützt. Bei Dateien, die von auf anderen Speicher-Engines basierenden Tabellen verwendet werden, werden Sie seltsame Probleme bekommen, wenn Sie versuchen, symbolische Verknüpfungen zu verwenden.

Die Verarbeitung symbolischer Verknüpfungen für MyISAM-Tabellen funktioniert wie folgt:

  • Im Datenverzeichnis befinden sich immer die Tabellenformatdatei (.frm-Datei), die Datendatei (.MYD-Datei) und die Indexdatei (.MYI-Datei). Die Daten- und die Indexdatei können an eine andere Position verschoben und durch symbolische Verknüpfungen im Datenverzeichnis ersetzt werden. Für die Formatdatei ist dies nicht möglich.

  • Sie können die Daten- und die Indexdatei separat in verschiedene Verzeichnisse verschieben und durch Verknüpfungen ersetzen.

  • Sie können einen laufenden MySQL Server anweisen, die symbolischen Verknüpfungen mithilfe der Optionen DATA DIRECTORY und INDEX DIRECTORY für CREATE TABLE zu erstellen. Siehe auch Abschnitt 13.1.5, „CREATE TABLE. Alternativ können die Verknüpfungen mithilfe von ln -s auch manuell über die Befehlszeile eingerichtet werden, wenn mysqld nicht ausgeführt wird.

  • myisamchk ersetzt eine symbolische Verknüpfung nicht durch die Daten- oder Indexdatei, sondern bearbeitet direkt die Datei, auf die die Verknüpfung verweist. Temporärdateien werden in dem Verzeichnis erstellt, in dem sich die Daten- bzw. Indexdatei befindet.

  • Hinweis: Wenn Sie eine Tabelle löschen, die symbolische Verknüpfungen erstellt, dann werden sowohl die Verknüpfung als auch die Datei, auf die die Verknüpfung verweist, gelöscht. Dies ist ein ausgesprochen triftiger Grund, warum Sie mysqld nicht als root des Systems ausführen oder Benutzern Schreibzugriff auf die MySQL-Datenbankverzeichnisse gewähren sollten.

  • Wenn Sie eine Tabelle mit ALTER TABLE … RENAME umbenennen und sie nicht in einer Datenbank verschieben, werden die symbolischen Verknüpfungen im Datenbankverzeichnis auf die neuen Namen umgestellt und die Daten- und Indexdatei entsprechend umbenannt.

  • Verwenden Sie ALTER TABLE … RENAME zur Verschiebung einer Tabelle in eine andere Datenbank, dann wird die Tabelle in das andere Datenbankverzeichnis verschoben. Die alten Verknüpfungen und die Dateien, auf die sie verwiesen, werden gelöscht. Dies bedeutet, dass es für die neue Tabelle keine symbolische Verknüpfung gibt.

  • Wenn Sie keine symbolischen Verknüpfungen verwenden, sollten Sie die Option --skip-symbolic-links für mysqld einsetzen, um zu gewährleisten, dass niemand mit mysqld eine Datei außerhalb des Datenverzeichnisses löschen oder umbenennen kann.

Die folgenden tabellenbezogenen Operationen in Verbindung mit symbolischen Verknüpfungen werden noch nicht unterstützt:

  • ALTER TABLE ignoriert die Tabellenoptionen DATA DIRECTORY und INDEX DIRECTORY.

  • BACKUP TABLE und RESTORE TABLE beachten symbolische Verknüpfungen nicht.

  • Die .frm-Datei darf niemals eine symbolische Verknüpfung sein (wie oben bereits angemerkt; nur Daten- und Indexdateien dürfen symbolische Verknüpfungen sein). Wenn Sie dies trotzdem versuchen (um beispielsweise Synonyme zu erstellen), dann erhalten Sie falsche Ergebnisse. Angenommen, Sie haben eine Datenbank db1 im MySQL-Datenverzeichnis und eine Tabelle tbl1 in dieser Datenbank. Nun erstellen Sie im Verzeichnis db1 eine symbolische Verknüpfung tbl2, die auf tbl1 verweist:

    shell> cd /path/to/datadir/db1
    shell> ln -s tbl1.frm tbl2.frm
    shell> ln -s tbl1.MYD tbl2.MYD
    shell> ln -s tbl1.MYI tbl2.MYI
    

    Probleme treten auf, wenn ein Thread db1.tbl1 liest und ein anderer db1.tbl2 aktualisiert:

    • Der Abfrage-Cache wird „hintergangen“ (denn er kann nicht wissen, dass tbl1 nicht geändert wurde, und gibt aufgrund dessen veraltete Ergebnisse zurück).

    • ALTER-Anweisungen für tbl2 schlagen ebenfalls fehl.

7.6.1.3. Daten unter Windows auf verschiedene Platten aufteilen

Symbolische Verknüpfungen sind standardmäßig auf allen Windows-Servern aktiv. Auf diese Weise können Sie ein Datenbankverzeichnis auf eine andere Festplatte verschieben, indem Sie eine Verknüpfung darauf erstellen. Dies ähnelt der Art und Weise, wie symbolische Verknüpfungen unter Unix funktionieren, auch wenn die Vorgehensweise zur Einrichtung der Verknüpfung eine andere ist. Wenn Sie keine symbolischen Verknüpfungen benötigen, können Sie sie mit der Option --skip-symbolic-links deaktivieren.

Unter Windows erstellen Sie eine Verknüpfung mit einer MySQL-Datenbank, indem Sie eine Datei im Datenverzeichnis anlegen, die den Pfad zum Zielverzeichnis enthält. Diese Datei sollte db_name.sym heißen, wobei db_name der Name der Datenbank ist.

Angenommen, das MySQL-Datenverzeichnis heißt C:\mysql\data. Nun wollen Sie, dass die Datenbank foo auf D:\data\foo abgelegt wird. Gehen Sie wie folgt vor, um eine symbolische Verknüpfung zu erstellen:

  1. Vergewissern Sie sich, dass das Verzeichnis D:\data\foo vorhanden ist (andernfalls müssen Sie es erstellen). Wenn sich in Ihrem Datenverzeichnis bereits ein Datenbankverzeichnis namens foo befindet, sollten Sie es nach D:\data verschieben. Andernfalls wird die Verknüpfung unbrauchbar sein. Um Probleme zu vermeiden, stellen Sie sicher, dass der Server beim Verschieben des Datenbankverzeichnisses nicht ausgeführt wird.

  2. Erstellen Sie eine Textdatei namens C:\mysql\data\foo.sym, die den Pfadnamen D:\data\foo\ enthält.

Danach werden alle Tabellen, die Sie in der Datenbank foo anlegen, in D:\data\foo erstellt. Beachten Sie, dass die symbolische Verknüpfung nicht verwendet wird, wenn ein Verzeichnis mit demselben Namen wie die Datenbank im MySQL-Datenverzeichnis vorhanden ist.


Dies ist eine Übersetzung des MySQL-Referenzhandbuchs, das sich auf dev.mysql.com befindet. Das ursprüngliche Referenzhandbuch ist auf Englisch, und diese Übersetzung ist nicht notwendigerweise so aktuell wie die englische Ausgabe. Das vorliegende deutschsprachige Handbuch behandelt MySQL bis zur Version 5.1.