EXPLAIN QUERY PLAN zeigt dir, wie eine Abfrage ausgeführt wird
Bevor du eine langsame Abfrage optimierst, musst du erst mal wissen, was SQLite überhaupt tut. EXPLAIN QUERY PLAN liefert dir eine kompakte Zusammenfassung der Strategie, die der Planner gewählt hat – welche Tabellen er anfasst, in welcher Reihenfolge und welche Indizes (falls überhaupt) dabei zum Einsatz kommen. Die Abfrage selbst wird nicht ausgeführt; du bekommst nur den Ausführungsplan.
Stell die Schlüsselwörter einfach vor ein beliebiges Statement:
Das Ergebnis sieht ungefähr so aus:
QUERY PLAN
`--SEARCH users USING INDEX sqlite_autoindex_users_1 (email=?)
Diese eine Zeile verrät dir schon eine Menge: SQLite macht hier einen SEARCH (also keinen Scan) auf der Tabelle users, nutzt den automatisch angelegten Unique-Index für email und verwendet email als Suchschlüssel. Genau das, was man sich wünscht.
SCAN vs. SEARCH: Worauf du zuerst schauen solltest
Jede Zeile im Query-Plan beginnt entweder mit SCAN oder SEARCH. Diese Unterscheidung ist das wichtigste Signal in der gesamten Ausgabe.
SCAN <table>— SQLite liest jede Zeile der Tabelle (bzw. jeden Eintrag eines Index). Die Kosten wachsen mit der Tabellengröße.SEARCH <table> USING ...— SQLite springt über einen Index oder Primärschlüssel direkt zu den passenden Zeilen. Die Kosten wachsen mit der Größe des Ergebnisses, nicht der Tabelle.
Schauen wir uns das im direkten Vergleich an. Eine Spalte hat einen Index, die andere nicht:
Der erste Plan zeigt SEARCH orders USING INDEX idx_orders_customer. Der zweite dagegen meldet SCAN orders – ohne Index auf status muss SQLite jede Zeile durchgehen. Bei einer kleinen Tabelle merkt man davon nichts; bei einer Tabelle mit einer Million Zeilen liegen zwischen beiden Varianten Welten – Millisekunden gegen Sekunden.
Ein SCAN ist aber nicht automatisch schlecht. Bei winzigen Lookup-Tabellen oder Abfragen, die ohnehin den Großteil der Zeilen zurückgeben, ist ein Scan genau der richtige Plan. Erst wenn du eine große Tabelle mit einem selektiven Filter hast, ist SCAN das klare Signal, einen Index nachzulegen.
SQLite Index-Nutzung prüfen
Achte im Plan auf die Formulierung USING INDEX <name> (oder USING COVERING INDEX <name> – dazu gleich mehr). Wenn du einen Index angelegt hast und wissen willst, ob der Planner ihn auch wirklich verwendet, prüfst du genau das:
Du solltest jetzt SEARCH events USING INDEX idx_events_user (user_id=?) sehen. Steht dort stattdessen SCAN events, dann hindert irgendetwas den Query Planner daran, den Index zu nutzen. Typische Ursachen: Die Spalte steckt in einer Funktion (WHERE lower(user_id) = ...), du vergleichst unterschiedliche Datentypen, oder du verwendest LIKE '%foo%' mit einem führenden Wildcard.
Ein kurzer Test dazu:
Mit + 0 ist der Index aus dem Spiel – im Plan steht dann wieder SCAN events. Jeder Ausdruck auf der indizierten Spalte hat denselben Effekt.
Covering Indexes erscheinen anders im Query Plan
Wenn ein Index bereits alle Spalten enthält, die die Abfrage braucht, kann SQLite die Anfrage komplett aus dem Index beantworten, ohne die Tabelle anzufassen. Im Plan taucht das als USING COVERING INDEX auf:
Der Plan: SEARCH products USING COVERING INDEX idx_products_sku_price (sku=?). Die Abfrage will price haben, der Index speichert bereits sku und price – SQLite muss die eigentliche Tabelle also gar nicht mehr anfassen. Ein Covering Index ist der schnellste Plan, den du für ein Lookup bekommen kannst. Gut zu wissen, wenn du gerade überlegst, welche Spalten du gemeinsam indizieren willst.
Join Query Plan in SQLite lesen
Bei Joins werden die Pläne erst richtig spannend. Jede Zeile im Plan steht für eine Tabelle aus dem Join, und die Reihenfolge der Zeilen ist genau die Reihenfolge, in der SQLite die Tabellen abarbeitet. Die erste Tabelle ist die äußere Tabelle; alle weiteren werden einmal pro Zeile der äußeren Tabelle nachgeschlagen.
Ein typischer Plan:
QUERY PLAN
|--SEARCH c USING INTEGER PRIMARY KEY (rowid=?)
`--SEARCH o USING INDEX idx_orders_customer (customer_id=?)
Lies den Plan von oben nach unten: SQLite findet zuerst den einen Kunden über den Primärschlüssel und sucht dann für genau diesen Kunden die passenden Bestellungen über den Index auf customer_id. Beide Zeilen zeigen SEARCH – also keine vollständigen Tabellen-Scans, und genau das willst du sehen.
Stünde in der zweiten Zeile stattdessen SCAN o, würde jeder Kundenzugriff einen kompletten Durchlauf über orders auslösen. Bei einer großen Tabelle ist das fatal. Die Lösung ist fast immer ein Index auf der Join-Spalte.
Zusammengesetzte Abfragen und Subqueries
Pläne für UNION, EXCEPT und Subqueries werden verschachtelt dargestellt. Jeder Zweig erscheint eingerückt unter seinem übergeordneten Knoten:
Bei einer COMPOUND QUERY siehst du zwei untergeordnete Zeilen, eine pro Zweig. Subqueries und CTEs funktionieren nach demselben Prinzip – jede bekommt ihren eigenen eingerückten Knoten im Query-Plan, und du liest ihn mit derselben SCAN-vs-SEARCH-Brille.
Die Unterabfrage erscheint als eigener Knoten im Plan (z. B. als "LIST SUBQUERY") und bringt ihre eigene Zugriffsstrategie mit. Prüfe jede Ebene nach denselben Kriterien.
EXPLAIN vs. EXPLAIN QUERY PLAN
Das sind zwei völlig verschiedene Werkzeuge – und genau hier kommen viele durcheinander.
EXPLAIN (ohne QUERY PLAN) gibt den Bytecode aus, den die virtuelle Maschine von SQLite ausführen wird – Dutzende Low-Level-Opcodes wie OpenRead, SeekRowid, Column, ResultRow. Praktisch, wenn du die Engine selbst debuggen willst. Zum Tuning so gut wie nie.
EXPLAIN QUERY PLAN ist die lesbare Zusammenfassung, die du in der Praxis brauchst. Im Zweifel also immer zu EXPLAIN QUERY PLAN greifen.
Workflow für langsame Abfragen in SQLite
Wenn eine Abfrage träge läuft, sieht der Ablauf so aus:
- Lass
EXPLAIN QUERY PLANdarauf laufen. - Schau dir jede Tabellenzeile an und frage dich: Ist das ein
SCANoder einSEARCH? Bei einer großen Tabelle istSCANder Verdächtige. - Filtert ein
SCANauf einer bestimmten Spalte, lohnt sich ein Index auf genau dieser Spalte. - Bei Joins solltest du prüfen, dass die Tabellen der inneren Schleife mit
SEARCH USING INDEXauf der Join-Spalte arbeiten. - Führe
EXPLAIN QUERY PLANnach dem Anlegen des Index erneut aus. Der Plan sollte sich verändern. Tut er das nicht, hat der Planner entschieden, dass dein Index die Mühe nicht wert ist – meist weil die Tabelle zu klein ist oder der Filter nicht selektiv genug.
Ein konkretes Beispiel zu Schritt 5:
Der Plan ist von SCAN auf SEARCH umgesprungen. Genau das ist das Signal dafür, dass der Index tatsächlich genutzt wird. (Bei einer frisch angelegten, fast leeren Tabelle kann der Planner trotzdem scannen, weil sich der Index bei so wenig Daten schlicht nicht lohnt – sobald du die Tabelle füllst oder ANALYZE ausführst, kippt die Entscheidung meist.)
Was der Query Plan dir nicht verrät
EXPLAIN QUERY PLAN zeigt dir die Strategie, nicht die Kosten. Du erfährst also nicht, dass eine Abfrage 800 ms gedauert oder 50.000 Zeilen zurückgegeben hat. Dafür brauchst du Laufzeitmessungen (.timer on in der CLI) und die tatsächliche Zeilenanzahl. Plan und Timing ergänzen sich: Der Plan sagt dir, warum eine Abfrage langsam ist, der Timer sagt dir, ob sie es überhaupt ist.
Zwei weitere Einschränkungen, die du im Hinterkopf haben solltest:
- Der Plan kann sich mit wachsender Datenmenge ändern. Eine Abfrage, die bei 100 Zeilen problemlos einen Full Table Scan macht, braucht bei einer Million Zeilen zwingend einen Index. Prüfe Pläne deshalb mit produktionsnahen Datenmengen – nicht mit deinen Dev-Fixtures.
- Der Planner stützt sich auf Statistiken, die
ANALYZEsammelt. Fehlen sie, fällt er auf Default-Werte zurück, die nicht immer ideal sind. Veraltete oder fehlende Statistiken sind ein häufiger Grund für überraschende Query Pläne.
Als Nächstes: ANALYZE und VACUUM
Der Query Planner trifft seine Entscheidungen anhand von Statistiken über deine Tabellen und Indizes. Sind diese Statistiken veraltet oder gar nicht vorhanden, kann selbst ein sauber indiziertes Schema einen miesen Plan produzieren. Mit ANALYZE hältst du sie aktuell – und VACUUM ist der passende Begleitbefehl, um Speicherplatz freizugeben und die Datenbankdatei zu defragmentieren. Genau darum geht es im nächsten Kapitel.
Häufig gestellte Fragen
Wofür ist EXPLAIN QUERY PLAN in SQLite eigentlich gut?
Damit lässt du dir von SQLite zeigen, wie eine Abfrage ausgeführt würde – ohne sie tatsächlich laufen zu lassen. Du siehst, welche Tabellen gelesen werden, welche Indizes zum Einsatz kommen und in welcher Reihenfolge Joins abgearbeitet werden. Einfach EXPLAIN QUERY PLAN vor ein SELECT, INSERT, UPDATE oder DELETE setzen – fertig.
Was ist der Unterschied zwischen SCAN und SEARCH in der Ausgabe?
SCAN heißt: SQLite liest die komplette Tabelle bzw. den kompletten Index Zeile für Zeile durch. Bei kleinen Tabellen kein Problem, bei großen schnell teuer. SEARCH heißt: SQLite springt über einen Index oder Primary Key direkt zu den passenden Zeilen. Bei großen Tabellen willst du auf den Filterspalten praktisch immer ein SEARCH sehen.
Wie erkenne ich, ob meine Abfrage einen Index verwendet?
Lass EXPLAIN QUERY PLAN über die Query laufen und such in der Ausgabe nach USING INDEX <name> oder USING COVERING INDEX <name>. Steht dort nur SCAN <table> ohne Index-Hinweis, macht SQLite einen Full Table Scan – meistens ein klares Zeichen, dass ein passender Index die Abfrage deutlich beschleunigen würde.
Worin unterscheiden sich EXPLAIN und EXPLAIN QUERY PLAN?
EXPLAIN zeigt dir den Bytecode der virtuellen Maschine, den SQLite intern erzeugt – spannend, wenn man die Engine selbst verstehen will, fürs Query-Tuning aber kaum brauchbar. EXPLAIN QUERY PLAN liefert eine lesbare Zusammenfassung darüber, wie auf Tabellen zugegriffen wird und welche Indizes genutzt werden. Für Performance-Analysen ist das die richtige Wahl.