Systematisch vorgehen, bevor die DBA eingeschaltet wird
Eine langsame Abfrage ist selten ein Zufall, sie hat fast immer eine von einer Handvoll wiederkehrender Ursachen. Diese Checkliste fuehrt Entwickler Schritt fuer Schritt durch die Query-Optimierung, von der WHERE-Klausel ueber fehlende Indizes bis zum N+1-Problem, damit die meisten Faelle geloest sind, bevor ueberhaupt eine DBA konsultiert werden muss.
Inhaltsverzeichnis
- 1. Warum eine Checkliste vor der Eskalation an die DBA sinnvoll ist
- 2. Schritt 1: Den EXPLAIN-Plan zuerst pruefen
- 3. Schritt 2: WHERE-Klauseln sargable machen
- 4. Schritt 3: SELECT * vermeiden und Projektionen pruefen
- 5. Schritt 4: Das N+1-Problem identifizieren
- 6. Schritt 5: Statistiken und Datentyp-Kompatibilitaet pruefen
- 7. Schritt 6: Locking und Blocking als Ursache ausschliessen
- 8. Schritt 7: Query-Umschreibung statt Index-Wildwuchs
- 9. Die Checkliste im Ueberblick
- 10. Zusammenfassung
- 11. FAQ
1. Warum eine Checkliste vor der Eskalation an die DBA sinnvoll ist
Wenn eine Abfrage plötzlich oder dauerhaft langsam ist, landet das Problem in vielen Teams zu schnell bei der Datenbankadministration, obwohl die Ursache haeufig direkt im Anwendungscode liegt. Eine strukturierte Query-Optimierung beginnt deshalb nicht mit einer Eskalation, sondern mit einer festen Reihenfolge an Pruefungen, die ein Entwickler selbststaendig durchfuehren kann. Diese Checkliste deckt in der Praxis die grosse Mehrheit aller Performance-Probleme ab, ohne dass tiefes DBA-Wissen noetig waere.
Der Wert einer solchen Checkliste liegt in der Reihenfolge: Man beginnt immer mit der billigsten und aufschlussreichsten Pruefung, dem EXPLAIN-Plan, und arbeitet sich erst bei Bedarf zu aufwendigeren Diagnosen vor, etwa Locking-Analysen. Eine ungeordnete Query-Optimierung, bei der wahllos Indizes hinzugefuegt oder die Abfrage mehrfach umgeschrieben wird, verschwendet Zeit und fuehrt oft zu neuen Problemen, etwa unnoetigen Indizes, die die Schreiblast erhoehen.
Diese Checkliste ist bewusst datenbankunabhaengig gehalten. Die konkreten Befehle unterscheiden sich zwischen MySQL, PostgreSQL und SQL Server, das zugrunde liegende Vorgehen bei der Query-Optimierung jedoch nicht. Jeder Schritt wird mit einem konkreten Beispiel illustriert, sodass die Checkliste direkt im Alltag anwendbar ist.
2. Schritt 1: Den EXPLAIN-Plan zuerst pruefen
Der erste und wichtigste Schritt jeder Query-Optimierung ist, den Ausfuehrungsplan der Abfrage anzusehen, bevor irgendetwas geaendert wird. Ohne diesen Schritt basiert jede Aenderung auf Vermutungen statt auf Daten. Man sucht gezielt nach Full Table Scans auf grossen Tabellen, nach grossen Abweichungen zwischen geschaetzter und tatsaechlicher Zeilenzahl und nach teuren Sortier- oder Join-Operationen.
In der Praxis reicht oft schon ein grober Blick auf den Plan, um die Ursache einzugrenzen. Ein Full Table Scan deutet auf einen fehlenden Index hin, ein grosser Sort-Knoten deutet auf eine fehlende Sortierunterstuetzung im Index hin, und eine grosse Diskrepanz zwischen Schaetzung und Realitaet deutet auf veraltete Statistiken hin. Dieser erste Diagnoseschritt entscheidet oft schon darueber, welcher der folgenden Schritte in der Query-Optimierung-Checkliste als naechstes relevant ist.
-- Step 1 of query optimization: always look at the plan first
EXPLAIN ANALYZE
SELECT o.order_id, c.customer_name, o.total_amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.status = 'pending'
ORDER BY o.order_date DESC
LIMIT 50;
-- Watch for: Seq Scan on large tables, big estimate-vs-actual gaps,
-- unexpected Sort nodes, or nested loops with high loop counts
3. Schritt 2: WHERE-Klauseln sargable machen
Eine WHERE-Klausel ist "sargable" (Search ARGument ABLE), wenn die Datenbank sie ueber einen Index direkt bearbeiten kann, ohne jede Zeile einzeln pruefen zu muessen. Nicht-sargable Bedingungen sind einer der haeufigsten und am einfachsten zu behebenden Gruende fuer eine langsame Abfrage. Typisches Beispiel: eine Funktion um eine indizierte Spalte herum, etwa WHERE YEAR(order_date) = 2026 statt eines Bereichsvergleichs, verhindert die Indexnutzung vollstaendig, weil die Datenbank die Funktion fuer jede Zeile einzeln auswerten muss.
Auch ein fuehrendes Wildcard in einem LIKE-Muster, etwa WHERE name LIKE '%mueller%', ist grundsaetzlich nicht sargable, weil der B-Tree-Index keine sinnvolle Startposition fuer die Suche finden kann. Ein Suffix-Wildcard, WHERE name LIKE 'mueller%', ist dagegen sargable und kann den Index nutzen. Dieser Schritt der Query-Optimierung ist besonders lohnend, weil eine einzige Umformulierung, ohne jeden neuen Index, die Laufzeit oft drastisch senkt.
Implizite Typumwandlungen sind eine weitere haeufige Ursache fuer nicht-sargable Bedingungen. Vergleicht man eine als VARCHAR gespeicherte Spalte mit einer numerischen Konstante ohne Anfuehrungszeichen, konvertiert manche Datenbank stillschweigend jede Zeile, statt den Index zu nutzen. Diese Faelle sind im EXPLAIN-Plan oft an einem "Filter" statt einem "Index Cond" auf der betroffenen Spalte erkennbar.
-- NOT sargable: function wraps the indexed column, index cannot be used
SELECT * FROM orders WHERE YEAR(order_date) = 2026;
-- SARGABLE: range comparison lets the database use the index on order_date
SELECT * FROM orders
WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01';
-- NOT sargable: leading wildcard defeats index navigation
SELECT * FROM customers WHERE last_name LIKE '%mueller%';
-- SARGABLE: trailing wildcard allows index range scan
SELECT * FROM customers WHERE last_name LIKE 'mueller%';
4. Schritt 3: SELECT * vermeiden und Projektionen pruefen
SELECT * laedt jede Spalte einer Tabelle, unabhaengig davon, ob die Anwendung sie tatsaechlich benoetigt. Bei breiten Tabellen mit vielen Spalten, insbesondere mit grossen TEXT- oder BLOB-Feldern, kostet das unnoetig Netzwerk-Bandbreite und I/O. Der zweite Effekt ist subtiler und fuer die Query-Optimierung oft entscheidender: SELECT * verhindert, dass ein Covering Index genutzt werden kann, weil die Datenbank fast zwangslaeufig Spalten anfordert, die nicht im Index enthalten sind, und deshalb zusaetzlich auf die Tabelle zugreifen muss.
Die explizite Auflistung der tatsaechlich benoetigten Spalten ist ein kleiner Aufwand mit spuerbarem Nutzen. Neben der reinen Datenmenge verbessert diese Praxis auch die Wartbarkeit, weil Aenderungen am Tabellenschema, etwa das Hinzufuegen einer neuen Spalte, nicht ungewollt zusaetzliche Daten in bestehende Abfragen einschleusen. Fuer diesen Schritt der Query-Optimierung-Checkliste reicht meist ein kurzer Blick in den Anwendungscode, ohne dass die Datenbank selbst angefasst werden muss.
-- Wasteful: loads every column including large description and image_data
SELECT * FROM products WHERE category_id = 12;
-- Optimized: only the columns the application actually renders
SELECT product_id, product_name, price, stock_quantity
FROM products
WHERE category_id = 12;
-- If (category_id, product_name, price, stock_quantity) exists as a covering
-- index, this second query can be served entirely from the index
5. Schritt 4: Das N+1-Problem identifizieren
Das N+1-Problem gehoert zu den haeufigsten Ursachen fuer schlechte Performance in Anwendungen, die auf ORMs oder wiederholte Einzelabfragen setzen. Statt einer einzigen Abfrage mit JOIN wird eine Liste von Eltern-Objekten geladen (1 Abfrage) und danach fuer jedes einzelne Objekt eine weitere Abfrage fuer die zugehoerigen Detaildaten ausgefuehrt (N Abfragen). Bei hundert Eltern-Objekten sind das 101 Datenbank-Roundtrips statt einem einzigen, jeder mit eigenem Netzwerk- und Parsing-Overhead.
Man erkennt ein N+1-Problem am zuverlaessigsten durch Query-Logging waehrend eines einzelnen Requests. Sieht man in der Log-Ausgabe dieselbe Abfrage-Struktur mit unterschiedlichen Parametern immer wieder hintereinander, ist das ein starkes Indiz. Die Loesung im Rahmen der Query-Optimierung ist fast immer, die N Einzelabfragen durch einen einzigen JOIN oder eine WHERE IN-Klausel mit vorher gesammelten IDs zu ersetzen, was die Anzahl der Roundtrips drastisch reduziert.
Dieses Problem entsteht selten aus purer SQL-Unkenntnis, sondern meist aus der Bequemlichkeit objektorientierter Datenzugriffsmuster, bei denen jedes Objekt seine Beziehungen bei Bedarf laedt, ohne die Gesamtzahl der resultierenden Abfragen im Blick zu behalten. Ein bewusster Blick auf das Abfragemuster im Rahmen der Query-Optimierung deckt dieses Problem meist innerhalb weniger Minuten auf.
-- N+1 pattern: 1 query for parents, then N queries for children
-- SELECT order_id FROM orders WHERE customer_id = 4821;
-- (for each order_id returned) SELECT * FROM order_items WHERE order_id = ?;
-- Fixed with a single JOIN instead of N round trips
SELECT o.order_id, oi.product_id, oi.quantity
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.customer_id = 4821;
-- Or with a batched IN clause when the parent IDs are already known
SELECT * FROM order_items WHERE order_id IN (9001, 9002, 9003, 9004);
6. Schritt 5: Statistiken und Datentyp-Kompatibilitaet pruefen
Wenn die ersten vier Schritte keine Ursache aufgedeckt haben, lohnt sich der Blick auf die Aktualitaet der Tabellenstatistiken. Nach grossen Datenimporten, Bulk-Loeschungen oder Migrationen koennen veraltete Statistiken den Optimizer systematisch in die Irre fuehren, selbst wenn alle Indizes korrekt vorhanden sind. Ein einfaches ANALYZE TABLE in MySQL, ANALYZE in PostgreSQL, oder UPDATE STATISTICS in SQL Server behebt dieses Problem oft ueberraschend schnell, ohne dass eine strukturelle Aenderung noetig waere.
Datentyp-Inkompatibilitaeten zwischen Join-Spalten sind ein weiterer haeufig uebersehener Punkt in der Query-Optimierung. Ein Join zwischen einer INT-Spalte und einer VARCHAR-Spalte, die zwar dieselben logischen Werte enthalten, zwingt die Datenbank zu impliziten Konvertierungen bei jedem Vergleich und verhindert damit oft die effiziente Indexnutzung. Solche Inkonsistenzen entstehen haeufig durch historisch gewachsene Schemas oder durch die Migration von einem System in ein anderes.
-- MySQL: refresh statistics after a large import
ANALYZE TABLE orders;
-- PostgreSQL: refresh statistics for the query planner
ANALYZE orders;
-- SQL Server: refresh statistics with full scan for accuracy
UPDATE STATISTICS orders WITH FULLSCAN;
-- Type mismatch example: orders.customer_id is INT, customers.ext_id is VARCHAR
-- Implicit cast on every row prevents efficient index usage
SELECT * FROM orders o JOIN customers c ON o.customer_id = c.ext_id;
7. Schritt 6: Locking und Blocking als Ursache ausschliessen
Manchmal ist eine Abfrage nicht wegen eines fehlenden Index langsam, sondern weil sie auf eine Sperre wartet, die von einer anderen, laenger laufenden Transaktion gehalten wird. Dieses Phaenomen faellt oft nicht im EXPLAIN-Plan auf, weil der Plan selbst effizient sein kann, die tatsaechliche Ausfuehrung aber durch Warten auf Locks blockiert wird. Ein Blick auf aktive Sperren und wartende Sessions gehoert deshalb in jede vollstaendige Query-Optimierung-Checkliste.
In PostgreSQL zeigt die Systemsicht pg_locks kombiniert mit pg_stat_activity, welche Sessions aufeinander warten. In MySQL liefert die Information-Schema-Tabelle INNODB_LOCK_WAITS aehnliche Informationen, in SQL Server hilft die dynamische Verwaltungssicht sys.dm_tran_locks. Ein typisches Muster ist eine lang laufende Batch-Transaktion, die versehentlich eine ganze Tabelle sperrt, waehrend kurze, eigentlich schnelle Abfragen darauf warten muessen. Diese Ursache wird oft erst spaet in der Query-Optimierung vermutet, obwohl sie in transaktionslastigen Systemen ueberraschend haeufig ist.
8. Schritt 7: Query-Umschreibung statt Index-Wildwuchs
Wenn alle vorherigen Schritte auf eine strukturelle Schwaeche der Abfrage selbst hindeuten, ist der letzte Schritt der Checkliste eine bewusste Umschreibung, statt reflexartig einen weiteren Index hinzuzufuegen. Korrelierte Subqueries lassen sich haeufig als JOIN umformulieren, was dem Optimizer mehr Freiheit bei der Wahl der Join-Strategie gibt. OR-Bedingungen ueber unterschiedliche Spalten lassen sich oft als UNION zweier separat indizierbarer Abfragen umschreiben, weil ein einzelner Index selten fuer beide Seiten einer OR-Bedingung gleichzeitig optimal genutzt werden kann.
Diese Umschreibungen erfordern mehr Verstaendnis als das blosse Hinzufuegen eines Index, liefern aber oft deutlich stabilere Ergebnisse, weil sie die eigentliche Struktur des Problems adressieren, statt Symptome zu behandeln. Der letzte Schritt jeder soliden Query-Optimierung ist deshalb, sich zu fragen: Loest ein Index das eigentliche Problem, oder verdeckt er nur eine ungeeignete Abfragestruktur, die bei wachsender Datenmenge erneut zum Problem wird?
9. Die Checkliste im Ueberblick
Die folgende Tabelle fasst alle Schritte der Query-Optimierung-Checkliste kompakt zusammen, inklusive des ungefaehren Aufwands und der typischen Wirkung jedes Schritts.
| Schritt | Pruefung | Aufwand | Typische Wirkung |
|---|---|---|---|
| 1. EXPLAIN-Plan | Scan-Typ, Kosten, Schaetzung vs. Realitaet | Gering | Grenzt Ursache sofort ein |
| 2. Sargable machen | Funktionen, Wildcards, Typumwandlung | Gering | Oft grosser Sprung ohne neuen Index |
| 3. Projektion | SELECT * vermeiden | Gering | Ermoeglicht Covering Index |
| 4. N+1-Problem | Query-Logging pro Request | Mittel | Reduziert Roundtrips drastisch |
| 5. Statistiken | ANALYZE / UPDATE STATISTICS | Gering | Korrigiert Optimizer-Annahmen |
| 6. Locking | pg_locks, INNODB_LOCK_WAITS | Mittel | Deckt Blocking-Ursachen auf |
| 7. Umschreibung | Subquery zu JOIN, OR zu UNION | Hoch | Loest strukturelle Schwaechen |
Diese Reihenfolge ist bewusst nach Aufwand sortiert: Die ersten Schritte der Query-Optimierung sind schnell durchfuehrbar und loesen die Mehrheit der Faelle, waehrend die letzten Schritte tieferes Verstaendnis erfordern, aber seltener noetig sind.
10. Zusammenfassung
Eine systematische Query-Optimierung folgt einer festen Reihenfolge: zuerst den EXPLAIN-Plan pruefen, dann WHERE-Klauseln sargable machen, unnoetige Projektionen entfernen, das N+1-Problem ausschliessen, Statistiken aktualisieren, Locking pruefen und erst zuletzt die Abfrage strukturell umschreiben. Diese Reihenfolge spiegelt sowohl den Aufwand als auch die Trefferwahrscheinlichkeit jedes Schritts wider und verhindert, dass wertvolle Zeit mit aufwendigen Diagnosen verschwendet wird, waehrend die eigentliche Ursache trivial gewesen waere.
Der groesste Nutzen dieser Checkliste liegt darin, dass ein Entwickler die grosse Mehrheit aller Performance-Probleme selbststaendig loesen kann, ohne eine DBA zu konsultieren. Nur wenn alle sieben Schritte keine klare Ursache liefern, etwa bei komplexen Parallelitaetsproblemen oder Infrastruktur-Engpaessen, ist eine Eskalation an spezialisiertes Datenbank-Personal tatsaechlich noetig. Diese Query-Optimierung-Checkliste spart in der Praxis nicht nur Zeit, sondern schafft auch ein tieferes Verstaendnis dafuer, wie Abfragen tatsaechlich ausgefuehrt werden.
Query-Optimierung Checkliste, das Wichtigste auf einen Blick
Erst messen, dann aendern
EXPLAIN-Plan immer zuerst pruefen, bevor irgendetwas an der Abfrage geaendert wird.
Sargable vor Index
Funktionen und Wildcards um Spalten herum vermeiden, bevor ein neuer Index angelegt wird.
N+1 aktiv suchen
Query-Logging pro Request zeigt wiederholte Abfragemuster sofort auf.
Locking nicht vergessen
Ein effizienter Plan kann trotzdem durch wartende Sperren langsam wirken.