Ein Full Scan
Setzen Sie EXPLAIN QUERY PLAN vor eine beliebige Abfrage, und sie wird nicht ausgeführt; stattdessen kommt der Plan zurück.
1
EXPLAIN QUERY PLAN
SELECT * FROM products WHERE name = 'Quill Helmet';
Ergebnis — 1 Zeile
| id | parent | notused | detail |
|---|---|---|---|
| 2 | 0 | 216 | SCAN products |
Öffnet Planche in einem eigenen Browser-Tab, mit geladener Beispieldatenbank und dieser Abfrage im Editor. Alle Beispiele der Seite verwenden denselben Tab weiter, und ausgeführt wird erst, wenn Sie F9 drücken.
SCAN products
SCAN heißt, die Datenbank hat jede Zeile der Tabelle gelesen und einzeln geprüft. products.name hat keinen Index, es steht also nichts Besseres zur Verfügung. Bei 400 Zeilen merkt das niemand. Der Grund, sich dafür zu interessieren: Diese Kosten wachsen direkt proportional zur Tabelle. Derselbe Plan liest auf zehn Millionen Zeilen zehn Millionen Zeilen – bei jedem Aufruf.
Eine Indexsuche
price hat einen Index.
2
EXPLAIN QUERY PLAN
SELECT * FROM products WHERE price > 2000;
Ergebnis — 1 Zeile
| id | parent | notused | detail |
|---|---|---|---|
| 3 | 0 | 202 | SEARCH products USING INDEX idx_products_price (price>?) |
SEARCH products USING INDEX idx_products_price (price>?)
SEARCH ist das Wort, das Sie sehen wollen. Die Datenbank ist in den Index gegangen, direkt dorthin gesprungen, wo Preise über 2000 beginnen, und hat nur von dort an gelesen – keine Zeile außerhalb der Antwort wurde angefasst. Andere Engines benutzen andere Wörter für dieselbe Unterscheidung: PostgreSQL sagt Seq Scan versus Index Scan, MySQLs EXPLAIN zeigt type: ALL versus type: range, SQL Server zeichnet einen Table Scan versus einen Index Seek.
Ein Join
3
EXPLAIN QUERY PLAN
SELECT o.order_number, c.last_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'delivered';
Ergebnis — 2 Zeilen
| id | parent | notused | detail |
|---|---|---|---|
| 4 | 0 | 61 | SEARCH o USING INDEX idx_orders_status (status=?) |
| 9 | 0 | 45 | SEARCH c USING INTEGER PRIMARY KEY (rowid=?) |
SEARCH o USING INDEX idx_orders_status (status=?)
SEARCH c USING INTEGER PRIMARY KEY (rowid=?)
Lesen Sie es als Schleife: die zugestellten Bestellungen über den Status-Index finden und zu jeder den Kunden über den Primärschlüssel nachschlagen. Beide Schritte sind Suchen, und so sieht ein gesunder Join aus. Misstrauisch macht ein SCAN auf der inneren Seite – das ist eine komplette Tabellenlesung je Zeile der äußeren und der übliche Grund, warum ein Join, der auf der Entwicklungsdatenbank schnell war, in der Produktion Minuten braucht. Fremdschlüsselspalten wollen einen Index; die meisten Datenbanken, SQLite und MySQL eingeschlossen, legen keinen für Sie an.
Wie man einen Index verliert
orders.ordered_at ist indiziert. Diese Abfrage filtert darauf und liest trotzdem die ganze Tabelle.
4
EXPLAIN QUERY PLAN
SELECT * FROM orders WHERE substr(ordered_at, 1, 4) = '2025';
Ergebnis — 1 Zeile
| id | parent | notused | detail |
|---|---|---|---|
| 2 | 0 | 216 | SCAN orders |
SCAN orders
Der Index speichert ordered_at, nicht substr(ordered_at, 1, 4). Sobald die Spalte in eine Funktion eingepackt ist, sagt die gespeicherte Sortierung der Datenbank nichts mehr, sie muss den Ausdruck also für jede Zeile berechnen, um es herauszufinden. Dasselbe gilt für WHERE YEAR(created_at) = 2025, WHERE UPPER(email) = ... und WHERE price * 1.1 > 100.
Schreiben Sie es so um, dass die nackte Spalte dem Vergleich gegenübersteht:
5
EXPLAIN QUERY PLAN
SELECT * FROM orders
WHERE ordered_at >= '2025-01-01'
AND ordered_at < '2026-01-01';
Ergebnis — 1 Zeile
| id | parent | notused | detail |
|---|---|---|---|
| 3 | 0 | 163 | SEARCH orders USING INDEX idx_orders_ordered_at (ordered_at… |
SEARCH orders USING INDEX idx_orders_ordered_at (ordered_at>? AND ordered_at<?)
Dieselben Zeilen, und jetzt macht der Index die Arbeit. Die allgemeine Regel: Lassen Sie die indizierte Spalte allein auf einer Seite des Vergleichs und verschieben Sie die Rechnerei auf die andere. Eine Funktion auf einem Datum in einen halboffenen Bereich zu verwandeln, ist der mit Abstand häufigste Fall davon.
ORDER BY kann gratis sein
6
EXPLAIN QUERY PLAN
SELECT * FROM products ORDER BY price DESC LIMIT 20;
Ergebnis — 1 Zeile
| id | parent | notused | detail |
|---|---|---|---|
| 5 | 0 | 222 | SCAN products USING INDEX idx_products_price |
SCAN products USING INDEX idx_products_price
Eine dritte Form: keine Suche, aber auch kein ungeordneter Scan. Die Abfrage will Zeilen in Preisreihenfolge, und der Index ist bereits nach Preis sortiert, also läuft die Datenbank den Index entlang und hört nach 20 Zeilen auf – ganz ohne Sortierung. Ohne diesen Index würde sie alle 400 Zeilen lesen, sortieren und 380 wegwerfen.
Deshalb hilft ein Index bei ORDER BY ... LIMIT genauso wie bei WHERE, und deshalb lohnt es sich, "Top 20 nach X"-Abfragen zu prüfen: mit dem richtigen Index fassen sie 20 Zeilen an, ohne ihn die ganze Tabelle.
Eine kurze Checkliste
- Indizieren Sie die Spalten, nach denen Sie filtern und joinen – besonders Fremdschlüssel, die selten automatisch einen bekommen.
- Halten Sie indizierte Spalten in
WHEREaus Funktionsaufrufen heraus. - Bevorzugen Sie
LIKE 'abc%'gegenüberLIKE '%abc%', wo es geht; ein führender Platzhalter kann keinen Index nutzen. - Indizes sind nicht gratis – jeder von ihnen muss bei jedem Schreibvorgang aktualisiert werden, indizieren Sie also, was Sie wirklich abfragen, nicht jede Spalte.
- Messen statt raten. Führen Sie
EXPLAINvor und nach der Änderung aus und vergleichen Sie die beiden Pläne.
Wie es weitergeht
Das war der Grundkurs. Die Dialektseiten stellen drei Aufgaben – Paging, Upsert sowie Zeichenketten- und Datumsbehandlung – für MySQL, PostgreSQL, SQL Server, Oracle und SQLite nebeneinander, weil sich bei genau diesen dreien die Syntax so weit unterscheidet, dass eine funktionierende Abfrage vor dem Wechsel auf eine andere Engine umgeschrieben werden muss.