Inhaltsverzeichnis
SELECT
-Anweisungen und andere Anfragen optimierenEXPLAIN
-Syntax (Informationen über ein
SELECT
erhalten)SELECT
-AnweisungenWHERE
-KlauselIS NULL
-OptimierungDISTINCT
LEFT JOIN
und RIGHT
JOIN
ORDER BY
-OptimierungGROUP BY
-OptimierungLIMIT
-OptimierungINSERT
-AnweisungenUPDATE
-AnweisungenDELETE
-AnfragenMyISAM
-Schlüssel-CacheMyISAM
-IndexstatistikenDie 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.
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.
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
“.
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“.
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.
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/.
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:
Das Programm mysqlslap kann eine hohe Belastung simulieren, indem die Auswirkungen von durch mehrere Clients gleichzeitig abgesetzte Abfragen nachgestellt werden. Siehe auch Abschnitt 8.14, „mysqlslap — Client zur Lastemulation“.
Sie können auch Super Smack ausprobieren, welches Sie unter http://jeremy.zawodny.com/mysql/super-smack/ finden.
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.
EXPLAIN
-Syntax (Informationen über ein
SELECT
erhalten)SELECT
-AnweisungenWHERE
-KlauselIS NULL
-OptimierungDISTINCT
LEFT JOIN
und RIGHT
JOIN
ORDER BY
-OptimierungGROUP BY
-OptimierungLIMIT
-OptimierungINSERT
-AnweisungenUPDATE
-AnweisungenDELETE
-Anfragen
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(
.
Der Rückgabewert ist immer null, aber mysql
gibt eine Zeile aus, die näherungsweise die Ausführungsdauer der
Anweisung angibt. Zum Beispiel:
loop_count
,expression
)
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.
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
ist synonym zu
tbl_name
DESCRIBE
oder
tbl_name
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:
SIMPLE | einfache SELECT -Anweisung (ohne
UNION oder Unterabfragen). |
PRIMARY | äußerste SELECT -Anweisung. |
UNION | zweite oder spätere SELECT -Anweisung in einer
UNION . |
DEPENDENT UNION | zweite oder spätere SELECT -Anweisung in einer
UNION , abhängig von der
äußeren Abfrage. |
UNION RESULT | Ergebnis einer UNION . |
SUBQUERY | erste SELECT -Anweisung in einer Unterabfrage. |
DEPENDENT SUBQUERY | erste SELECT -Anweisung in einer Unterabfrage,
abhängig von der äußeren Abfrage. |
DERIVED | abgeleitete 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:
Diese Tabelle hat nur einen Datensatz (Systemtabelle).
Dies ist ein Sonderfall des Join-Typs
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 * FROMtbl_name
WHEREprimary_key
=1; SELECT * FROMtbl_name
WHEREprimary_key_part1
=1 ANDprimary_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 * FROMref_table
,other_table
WHEREref_table
.key_column
=other_table
.column
; SELECT * FROMref_table
,other_table
WHEREref_table
.key_column_part1
=other_table
.column
ANDref_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 * FROMref_table
WHEREkey_column
=expr
; SELECT * FROMref_table
,other_table
WHEREref_table
.key_column
=other_table
.column
; SELECT * FROMref_table
,other_table
WHEREref_table
.key_column_part1
=other_table
.column
ANDref_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 * FROMref_table
WHEREkey_column
=expr
ORkey_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 (SELECTprimary_key
FROMsingle_table
WHEREsome_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 (SELECTkey_column
FROMsingle_table
WHEREsome_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 * FROMtbl_name
WHEREkey_column
= 10; SELECT * FROMtbl_name
WHEREkey_column
BETWEEN 10 and 20; SELECT * FROMtbl_name
WHEREkey_column
IN (10,20,30); SELECT * FROMtbl_name
WHEREkey_part1
= 10 ANDkey_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:
Tabelle | Spalte | Datentyp |
tt | ActualPC | CHAR(10) |
tt | AssignedPC | CHAR(10) |
tt | ClientID | CHAR(10) |
et | EMPLOYID | CHAR(15) |
do | CUSTNMBR | CHAR(15) |
Die Tabellen haben die nachfolgenden Indizes:
Tabelle | Index |
tt | ActualPC |
tt | AssignedPC |
tt | ClientID |
et | EMPLOYID (Primärschlüssel) |
do | CUSTNMBR (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.
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“.
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
und
nachfolgender Überprüfung des Werts
tbl_name
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.
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 WHEREprimary_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(*) FROMtbl_name
; SELECT MIN(key_part1
),MAX(key_part1
) FROMtbl_name
; SELECT MAX(key_part2
) FROMtbl_name
WHEREkey_part1
=constant
; SELECT ... FROMtbl_name
ORDER BYkey_part1
,key_part2
,... LIMIT 10; SELECT ... FROMtbl_name
ORDER BYkey_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:
SELECTkey_part1
,key_part2
FROMtbl_name
WHEREkey_part1
=val
; SELECT COUNT(*) FROMtbl_name
WHEREkey_part1
=val1
ANDkey_part2
=val2
; SELECTkey_part2
FROMtbl_name
GROUP BYkey_part1
;
Die folgenden Abfragen verwendet die Indizierung zur Abfrage von Datensätzen in sortierter Reihenfolge ohne separaten Sortierdurchlauf:
SELECT ... FROMtbl_name
ORDER BYkey_part1
,key_part2
,... ; SELECT ... FROMtbl_name
ORDER BYkey_part1
DESC,key_part2
DESC, ... ;
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.
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
'
(wobei
pattern
''
nicht mit einem Jokerzeichen beginnt).
pattern
'
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 WHEREkey_col
> 1 ANDkey_col
< 10; SELECT * FROM t1 WHEREkey_col
= 1 ORkey_col
IN (15,18,20); SELECT * FROM t1 WHEREkey_col
LIKE 'ab%' ORkey_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:
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')
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')
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')
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.
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(
definiert ist,
und die folgende Menge der in der Schlüsselreihenfolge
aufgelisteten Schlüsseltupel:
key_part1
,
key_part2
,
key_part3
)
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
definiert das folgende Intervall:
key_part1
=
1
(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
kein einzelnes Intervall und kann von der
Bereichszugriffsmethode nicht verwendet werden.
key_part3
=
'abc'
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
ANDkey_part2
cmp
const2
AND ... ANDkey_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 ANDkey_part2
IS NULL ANDkey_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
'
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 pattern
'<>
oder
!=
verwendet werden). Betrachten Sie
etwa folgende Bedingung:
key_part1
= 'foo' ANDkey_part2
>= 10 ANDkey_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 ANDkey_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 ANDkey_part2
< 2
Tatsächlich aber wird die Bedingung wie folgt konvertiert:
key_part1
>= 1 ANDkey_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.
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 * FROMtbl_name
WHEREkey_part1
= 10 ORkey_part2
= 20; SELECT * FROMtbl_name
WHERE (key_part1
= 10 ORkey_part2
= 20) ANDnon_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
ANDy
) ORz
= (x
ORz
) AND (y
ORz
) (x
ORy
) ANDz
= (x
ANDz
) OR (y
ANDz
)
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.
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
ANDkey_part2
=const2
... ANDkey_partN
=constN
Jede Bereichsbedingung über einen Primärschlüssel einer
InnoDB
- oder
BDB
-Tabelle.
Ein paar Beispiele:
SELECT * FROMinnodb_table
WHEREprimary_key
< 10 ANDkey_col1
=20; SELECT * FROMtbl_name
WHERE (key1_part1
=1 ANDkey1_part2
=2) ANDkey2
=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.
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
ANDkey_part2
=const2
... ANDkey_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 WHEREkey1
=1 ORkey2
=2 ORkey3
=3; SELECT * FROMinnodb_table
WHERE (key1
=1 ANDkey2
=2) OR (key3
='foo' ANDkey4
='bar') ANDkey5
=5;
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 * FROMtbl_name
WHEREkey_col1
< 10 ORkey_col2
< 20; SELECT * FROMtbl_name
WHERE (key_col1
> 10 ORkey_col2
= 20) ANDnonkey_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.
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 * FROMtbl_name
WHEREkey_col
IS NULL; SELECT * FROMtbl_name
WHEREkey_col
<=> NULL; SELECT * FROMtbl_name
WHEREkey_col
=const1
ORkey_col
=const2
ORkey_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
optimieren; diese Form tritt bei aufgelösten Unterabfragen
häufig auf. col_name
=
expr
AND
col_name
IS NULLEXPLAIN
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);
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
mit
row_count
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;
MySQL implementiert
wie
folgt:
A
LEFT
JOIN B
join_condition
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
im col_name
IS NULLWHERE
-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.
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||NULL
„einen 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.)
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
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 BYkey_part1
,key_part2
,... ; SELECT * FROM t1 WHEREkey_part1
=constant
ORDER BYkey_part2
; SELECT * FROM t1 ORDER BYkey_part1
DESC,key_part2
DESC; SELECT * FROM t1 WHEREkey_part1
=1 ORDER BYkey_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 BYkey1
,key2
;
Sie verwenden ORDER BY
für Teile eines
Schlüssels, die nicht aufeinander folgen:
SELECT * FROM t1 WHEREkey2
=constant
ORDER BYkey_part2
;
Sie verwenden ASC
und
DESC
gemischt:
SELECT * FROM t1 ORDER BYkey_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 WHEREkey2
=constant
ORDER BYkey1
;
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:
Die Datensätze, die der WHERE
-Klausel
entsprechen, werden gelesen.
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.
Die Tupel werden nach dem Sortierschlüsselwert sortiert.
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
-Abfragen so,
als ob Sie in der Abfrage auch col1
,
col2
, …ORDER BY
angegeben
hätten. Wenn Sie ausdrücklich eine col1
,
col2
, …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;
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.
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;
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;
In manchen Fällen verarbeitet MySQL eine Abfrage anders, wenn
Sie LIMIT
verwenden und
row_count
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
mit
row_count
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
mit
row_count
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
keine
unnötigen row_count
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
-Klausel, wie
viel Speicher erforderlich ist.
row_count
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
. Siehe auch
Abschnitt 13.5.2.1, „tbl_name
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“.
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:
Erstellen Sie die Tabelle mit CREATE
TABLE
(optional).
Führen Sie eine FLUSH
TABLES
-Anweisung oder den Befehl
mysqladmin flush-tables aus.
Setzen Sie myisamchk --keys-used=0 -rq
/path/to/db/tbl_name
ab. Hierdurch wird die Verwendung aller Indizes für die
Tabelle unterbunden.
Fügen Sie mit LOAD DATA INFILE
Daten
in die Tabelle ein. Hierbei werden keine Indizes
aktualisiert – der Vorgang ist also sehr schnell.
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“.
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.
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 TABLEtbl_name
DISABLE KEYS; ALTER TABLEtbl_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:
Verbindung 1 führt 1.000 Einfügeoperationen durch.
Die Verbindungen 2, 3 und 4 fügen je eine Einfügeoperation durch.
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“.
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
“.
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
schneller als
tbl_name
DELETE FROM
. Siehe auch
Abschnitt 13.2.9, „tbl_name
TRUNCATE
“.
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
, 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.
expr1
,
expr2
, …
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 * FROMtbl_name
WHEREhash_col
=MD5(CONCAT(col1
,col2
)) ANDcol1
='constant
' ANDcol2
='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:
UPDATEtbl_name
SETcount_col
=count_col
+1 WHEREkey_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.)
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:
UPDATEtbl_name
SETcolumn
=value
WHEREunique_key_col
=key_value
; DELETE FROMtbl_name
WHEREunique_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.
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“.
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.
MyISAM
-Schlüssel-CacheMyISAM
-IndexstatistikenMySQL 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.
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(
im
UTF-8-Format 3 × N
)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.
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
in einer Indexdefinition können Sie einen Index erstellen, der
nur die ersten col_name
(N
)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.
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.
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
für alle
Schlüsselteile verwenden, die im Index vor
key_part_N
=
constant
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
) FROMtbl_name
WHEREkey_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
). Wenn auf
alle Schlüsselteile key_part1
,
key_part2
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:
SELECTkey_part3
FROMtbl_name
WHEREkey_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 * FROMtbl_name
WHERE col1=val1
; SELECT * FROMtbl_name
WHERE col1=val1
AND col2=val2
; SELECT * FROMtbl_name
WHERE col2=val2
; SELECT * FROMtbl_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 * FROMtbl_name
WHEREkey_col
LIKE 'Patrick%'; SELECT * FROMtbl_name
WHEREkey_col
LIKE 'Pat%_ck%';
In der ersten Anweisung werden nur Datensätze mit
'Patrick' <=
berücksichtigt. In der zweiten Anweisung
werden nur Datensätze mit key_col
<
'Patricl''Pat' <=
berücksichtigt.
key_col
< 'Pau'
Die folgenden SELECT
-Anweisungen verwenden
keine Indizes:
SELECT * FROMtbl_name
WHEREkey_col
LIKE '%Patrick%'; SELECT * FROMtbl_name
WHEREkey_col
LIKEother_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
'%
verwenden und
string
%'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
setzt Indizes ein, wenn col_name
IS NULLcol_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:
... WHEREindex_part1
=1 ANDindex_part2
=2 ANDother_column
=3 /*index
= 1 ORindex
= 2 */ ... WHEREindex
=1 OR A=10 ANDindex
=2 /* optimized like "index_part1
='hello'" */ ... WHEREindex_part1
='hello' ANDindex_part3
=5 /* Can use index onindex1
but not onindex2
orindex3
*/ ... WHEREindex1
=1 ANDindex2
=2 ORindex1
=3 ANDindex3
=3;
Die folgenden WHERE
-Klauseln verwenden
Indizes nicht:
/*index_part1
is not used */ ... WHEREindex_part2
=1 ANDindex_part3
=2 /* Index is not used in both parts of the WHERE clause */ ... WHEREindex
=1 OR A=10 /* No index spans all rows */ ... WHEREindex_part1
=1 ORindex_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.)
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.
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.
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
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
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.
N
×
key_cache_age_threshold / 100
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.
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.
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.
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.
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:
(...) JOINtbl_name
ONtbl_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
für jeden anderen Wert
N
<=>
N
N
).
Basiert ein Join jedoch auf dem Operator =
,
dann unterscheidet sich NULL
von
Nicht-NULL
-Werten:
ist nicht wahr, wenn
expr1
=
expr2
expr1
oder
expr2
(oder beide)
NULL
sind. Dies wirkt sich auf
ref
-Zugriffe für Vergleiche der Form
aus: MySQL greift
nicht auf die Tabelle zu, wenn der aktuelle Wert von
tbl_name.key
=
expr
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
.
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 ×
haben, wobei
N
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
ö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
tbl_name
OPENHANDLER
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, „tbl_name
CLOSEHANDLER
“.
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“.
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.
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.
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“.
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.
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.
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 ×
reserviert. (Hierbei
ist N
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.
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
.
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 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
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“.
N
MOD
number_of_disks
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.)
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.
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
shell>/path/to/datadir
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)
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
shell>/path/to/datadir
/db1ln -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.
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
heißen, wobei db_name
.symdb_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:
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.
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.