systematische Fehlersuche statt Rätselraten
Der Index ist angelegt, die Spalte ist ausgewählt, trotzdem zeigt EXPLAIN einen vollständigen Table Scan. Dieses Muster hat fast immer eine von wenigen konkreten Ursachen: Funktionen auf der indizierten Spalte, implizite Typkonvertierung, führende Wildcards oder veraltete Statistiken. Diese Anleitung zeigt eine feste Reihenfolge, mit der sich die Ursache in wenigen Minuten eingrenzen lässt.
Inhaltsverzeichnis
- 1. Symptome: Full Table Scan trotz vorhandenem Index
- 2. EXPLAIN-Ausgaben richtig lesen für die Index-Diagnose
- 3. Funktionen auf indizierten Spalten verhindern die Nutzung
- 4. Implizite Typkonvertierung als stiller Index-Killer
- 5. Führende Wildcards und Index-Nutzung bei LIKE
- 6. Veraltete Statistiken und Cardinality-Schätzung
- 7. Zusammengesetzte Indizes und Spaltenreihenfolge
- 8. Die systematische Checkliste zur Index-Diagnose
- 9. Ursachen und Lösungen im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Symptome: Full Table Scan trotz vorhandenem Index
Das Symptom ist immer dasselbe: Ein Index existiert nachweislich, \d tabelle oder SHOW INDEX zeigen ihn korrekt an, trotzdem wählt der Optimizer einen Sequential Scan über die gesamte Tabelle statt eines Index Scans. Bei einer Tabelle mit wenigen tausend Zeilen ist das kaum spürbar, bei Millionen Zeilen wird aus einer Abfrage, die Millisekunden dauern sollte, ein mehrsekündiges Warten. Wer die Ursache nicht kennt, neigt dazu, den Index einfach neu anzulegen oder REINDEX auszuführen, was das eigentliche Problem in den meisten Fällen nicht löst.
Bevor man in die Detailanalyse einsteigt, lohnt sich ein kurzer Sanity-Check: Existiert der Index wirklich auf genau der Spalte, die in der WHERE-Klausel verwendet wird, und nicht nur auf einer ähnlich benannten Spalte einer verwandten Tabelle. Ist der Index als gültig markiert, in PostgreSQL sichtbar über indisvalid in pg_index, ein nach einem abgebrochenen CREATE INDEX CONCURRENTLY ungültig gebliebener Index wird vom Optimizer schlicht ignoriert. Erst wenn diese Grundlagen bestätigt sind, führt der Weg über EXPLAIN zur eigentlichen Ursache.
2. EXPLAIN-Ausgaben richtig lesen für die Index-Diagnose
Der erste Analyseschritt ist immer EXPLAIN (ANALYZE, BUFFERS) in PostgreSQL beziehungsweise EXPLAIN FORMAT=JSON in MySQL. Entscheidend ist nicht nur, ob ein Seq Scan oder Index Scan gewählt wird, sondern auch die geschätzte gegenüber der tatsächlichen Zeilenzahl. Weichen diese Werte um mehr als eine Größenordnung voneinander ab, deutet das auf veraltete Statistiken hin, siehe Abschnitt sechs. Zeigt der Plan hingegen einen Filter-Knoten statt eines Index-Condition-Knotens, ist das ein starkes Indiz für eine Funktion oder Typkonvertierung, siehe Abschnitte drei und vier.
Ein häufig übersehenes Detail: Der Optimizer entscheidet sich bewusst gegen einen Index Scan, wenn die geschätzten Kosten höher liegen als bei einem Sequential Scan, selbst wenn der Index technisch nutzbar wäre. Das passiert typischerweise, wenn die WHERE-Klausel einen großen Anteil der Tabelle zurückliefert, etwa mehr als zwanzig Prozent der Zeilen. In diesem Fall ist der Sequential Scan tatsächlich schneller, weil er sequenziell von der Platte liest, während ein Index Scan bei hoher Trefferquote viele zufällige Zugriffe erzeugt. Das ist kein Bug, sondern eine korrekte Kostenentscheidung des Optimizers.
-- Step 1: confirm the index exists and is on the right column
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders';
-- Step 2: run EXPLAIN ANALYZE and compare estimated vs actual rows
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE customer_email = 'jane@example.com';
-- Watch for this pattern in the output:
-- Seq Scan on orders (cost=0.00..48123.00 rows=1 width=120)
-- (actual time=312.442..312.443 rows=1 loops=1)
-- Filter: (customer_email = 'jane@example.com'::text)
-- Rows Removed by Filter: 4999999
-- -> A Filter node instead of an Index Cond means the index was NOT used
3. Funktionen auf indizierten Spalten verhindern die Nutzung
Der häufigste Grund, warum eine Datenbank einen vorhandenen Index nicht nutzt, ist eine Funktion, die auf die indizierte Spalte selbst angewendet wird. Ein klassischer Standard-B-Tree-Index auf email kann nicht genutzt werden, wenn die Abfrage LOWER(email) = 'jane@example.com' lautet, weil der Index die Rohwerte der Spalte speichert, nicht die transformierten Werte. Der Optimizer müsste für jede Zeile LOWER() neu berechnen, um einen Vergleich mit dem Index zu ermöglichen, was ein regulärer B-Tree-Index nicht unterstützt.
Die Lösung ist entweder, die Funktion aus der WHERE-Klausel zu entfernen, etwa indem man die Daten bereits normalisiert speichert, oder einen funktionsbasierten Index anzulegen, der genau diese Transformation vorab indiziert. In PostgreSQL und Oracle ist das direkt als Expression Index möglich, in MySQL seit Version 8.0.13 über Functional Indexes auf generierten virtuellen Spalten. Dieser Fix ist gezielt und minimalinvasiv, weil er keine Änderung an bestehenden Abfragen erfordert, sobald der Index korrekt definiert ist.
-- WRONG: function on the indexed column defeats a plain B-tree index
CREATE INDEX idx_orders_email ON orders (customer_email);
SELECT * FROM orders WHERE LOWER(customer_email) = 'jane@example.com';
-- -> index on customer_email cannot be used, planner falls back to Seq Scan
-- RIGHT (option A): normalize data at write time, index the raw column
UPDATE orders SET customer_email = LOWER(customer_email);
SELECT * FROM orders WHERE customer_email = 'jane@example.com';
-- RIGHT (option B): expression index matching the exact function call
CREATE INDEX idx_orders_email_lower ON orders (LOWER(customer_email));
SELECT * FROM orders WHERE LOWER(customer_email) = 'jane@example.com';
-- -> planner can now use idx_orders_email_lower directly
4. Implizite Typkonvertierung als stiller Index-Killer
Ein subtilerer, aber genauso häufiger Grund ist implizite Typkonvertierung. Wird eine als VARCHAR gespeicherte Spalte mit einem numerischen Literal verglichen, konvertiert die Datenbank je nach Regelwerk entweder den Literalwert oder, schlimmer, jeden gespeicherten Wert der Spalte zur Vergleichszeit. Im zweiten Fall wird der Index unbrauchbar, weil die Konvertierung pro Zeile ausgeführt werden müsste, bevor der Vergleich mit dem indizierten Rohwert überhaupt möglich wäre. MySQL ist von diesem Verhalten besonders betroffen, wenn eine als CHAR gespeicherte ID mit einem ungequoteten Integer verglichen wird.
Das Tückische an diesem Fehler: Die Abfrage liefert weiterhin korrekte Ergebnisse, nur eben langsam, weshalb er in Tests mit kleinen Datenmengen oft unbemerkt bleibt und erst in Produktion mit Millionen Zeilen auffällt. Die Diagnose erfolgt über den Datentypvergleich in information_schema.columns gegen den Literaltyp in der Abfrage. Die Lösung ist immer, explizit im richtigen Datentyp zu vergleichen, entweder durch Anpassung der Anwendung oder durch einen expliziten CAST auf der Literalseite, niemals auf der Spaltenseite.
-- WRONG: comparing a VARCHAR column to an unquoted numeric literal
-- MySQL implicitly casts the COLUMN, not the literal, defeating the index
SELECT * FROM customers WHERE customer_code = 12345; -- customer_code is VARCHAR
-- RIGHT: match the literal to the column's actual type
SELECT * FROM customers WHERE customer_code = '12345';
-- WRONG: comparing a DATE column against a string with wrong format
SELECT * FROM orders WHERE order_date = '2026-07-31 00:00:00'; -- order_date is DATE
-- RIGHT: cast explicitly, on the literal side, never on the indexed column
SELECT * FROM orders WHERE order_date = DATE '2026-07-31';
5. Führende Wildcards und Index-Nutzung bei LIKE
Ein Standard-B-Tree-Index unterstützt Präfixsuchen effizient, weil die Werte sortiert gespeichert werden und ein Bereichsscan von einem gegebenen Präfix aus möglich ist. LIKE 'Jane%' kann so einen Index nutzen, weil die Datenbank direkt zum Bereich der mit "Jane" beginnenden Werte springen kann. Sobald jedoch ein führender Wildcard verwendet wird, LIKE '%Jane%', ist diese Sortierordnung nutzlos, weil der gesuchte String an beliebiger Position im Wert stehen kann. Der Optimizer muss dann zwangsläufig jeden Wert komplett durchsuchen, ein Standard-B-Tree-Index bringt hier keinerlei Vorteil.
Für echte Volltextsuche mit führenden Wildcards sind spezialisierte Indexstrukturen die richtige Lösung, nicht der klassische B-Tree. PostgreSQL bietet dafür GIN-Indizes mit pg_trgm für Trigram-basierte Ähnlichkeitssuche, MySQL und die meisten anderen Systeme bieten dedizierte Volltextindizes. Diese Strukturen indizieren Teilstrings oder Wortfragmente statt vollständiger sortierter Werte und können dadurch auch bei beliebiger Wildcard-Position performant bleiben, während ein regulärer B-Tree-Index bei dieser Abfrageform grundsätzlich ungeeignet ist.
-- Trailing wildcard: standard B-tree index CAN be used (range scan on prefix)
SELECT * FROM customers WHERE last_name LIKE 'Schmid%';
-- Leading wildcard: standard B-tree index CANNOT help, forces full scan
SELECT * FROM customers WHERE last_name LIKE '%schmid%';
-- Fix: trigram index for substring search (PostgreSQL)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_customers_lastname_trgm ON customers USING gin (last_name gin_trgm_ops);
-- Now '%schmid%' can use idx_customers_lastname_trgm efficiently
6. Veraltete Statistiken und Cardinality-Schätzung
Der Query-Planer entscheidet nicht anhand der tatsächlichen Datenverteilung, sondern anhand gespeicherter Statistiken, die periodisch aktualisiert werden. Nach einem großen Bulk-Insert oder Bulk-Delete können diese Statistiken erheblich von der Realität abweichen, sodass der Optimizer die Anzahl zurückgegebener Zeilen falsch schätzt und deshalb fälschlicherweise einen Sequential Scan statt eines Index Scans wählt, oder umgekehrt. Der Vergleich zwischen geschätzter und tatsächlicher Zeilenzahl aus EXPLAIN ANALYZE, siehe Abschnitt zwei, ist der direkte Hinweis auf dieses Problem.
Die Lösung ist in PostgreSQL ein manuelles ANALYZE tabellenname, das die Statistiken sofort neu berechnet, statt auf den nächsten automatischen Autovacuum-Lauf zu warten. In MySQL erfüllt ANALYZE TABLE denselben Zweck. Nach großen Datenänderungen, insbesondere nach initialen Datenimporten oder Massenlöschungen, sollte dieser Befehl explizit Teil des Deployment- oder Migrationsprozesses sein, statt sich auf automatische Trigger zu verlassen, die je nach Konfiguration erst nach einem definierten Schwellenwert an Änderungen greifen.
7. Zusammengesetzte Indizes und Spaltenreihenfolge
Ein zusammengesetzter Index über mehrere Spalten wird nur dann effizient genutzt, wenn die Abfrage die Spalten in der Reihenfolge nutzt, in der sie im Index definiert sind, beginnend mit der ersten Spalte. Ein Index auf (status, created_at) unterstützt effizient eine Abfrage, die nach status filtert, mit oder ohne zusätzlichen Filter auf created_at. Dieselbe Abfrage, die nur nach created_at filtert, ohne status einzubeziehen, kann diesen Index hingegen nicht nutzen, weil die erste Spalte des Index in der WHERE-Klausel fehlt.
Diese sogenannte Left-Prefix-Regel wird in der Praxis regelmäßig missverstanden. Wer einen Index auf (a, b, c) anlegt, hat effektiv drei mögliche Präfixe: (a), (a, b) und (a, b, c), aber keine Kombination, die b oder c ohne a nutzt. Die praktische Konsequenz: Die Spaltenreihenfolge im Index sollte sich an der Selektivität und den tatsächlichen Abfragemustern orientieren, mit der am häufigsten isoliert gefilterten Spalte an erster Stelle.
8. Die systematische Checkliste zur Index-Diagnose
Wenn ein Index nicht genutzt wird, lohnt sich eine feste Prüfreihenfolge statt wahlloser Versuche. Erstens: Existiert der Index tatsächlich, ist er gültig, und deckt er die richtige Spalte ab. Zweitens: EXPLAIN ANALYZE ausführen und geschätzte gegen tatsächliche Zeilenzahl vergleichen. Drittens: Prüfen, ob eine Funktion auf der indizierten Spalte angewendet wird. Viertens: Datentypen der Spalte und des Vergleichswerts abgleichen, um implizite Konvertierung auszuschließen.
Fünftens: Bei LIKE-Abfragen prüfen, ob ein führender Wildcard verwendet wird. Sechstens: Bei zusammengesetzten Indizes prüfen, ob die Abfrage die erste Spalte des Index in der WHERE-Klausel nutzt. Siebtens: ANALYZE manuell ausführen, um veraltete Statistiken auszuschließen. Diese sieben Schritte in dieser Reihenfolge decken in der Praxis die überwiegende Mehrheit aller Fälle ab, in denen ein vorhandener Index unerwartet ignoriert wird.
9. Ursachen und Lösungen im Vergleich
Die folgende Tabelle fasst die häufigsten Ursachen, ihre Erkennungsmerkmale im EXPLAIN-Plan und die passenden Lösungen zusammen.
| Ursache | Erkennungsmerkmal | Lösung |
|---|---|---|
| Funktion auf Spalte | Filter-Knoten statt Index Cond | Expression Index anlegen |
| Implizite Typkonvertierung | Datentyp Spalte ungleich Literal | Explizit im richtigen Typ vergleichen |
| Führender Wildcard | LIKE '%wert%' im Query-Text | GIN oder Volltextindex |
| Veraltete Statistiken | Geschätzt weit von tatsächlich entfernt | ANALYZE tabellenname |
| Falsche Spaltenreihenfolge | Erste Indexspalte fehlt in WHERE | Index neu anlegen mit passender Reihenfolge |
Alle fünf Ursachen lassen sich systematisch über EXPLAIN diagnostizieren, ohne dass man den Index blind neu anlegen oder Rateversuche mit Query-Hints unternehmen muss. Die Kombination aus EXPLAIN-Analyse und dieser Checkliste ersetzt zufälliges Ausprobieren durch einen reproduzierbaren Diagnoseprozess.
Mironsoft
Index-Diagnose, Query-Planer-Analyse und Datenbank-Performance
Ein Index existiert, wird aber trotzdem ignoriert?
Wir analysieren EXPLAIN-Pläne, finden die konkrete Ursache zwischen Funktion, Typkonvertierung und veralteten Statistiken und liefern eine gezielte, minimalinvasive Lösung statt pauschaler Indexänderungen.
EXPLAIN-Analyse
Query-Pläne systematisch auf Filter-Knoten und Kostenwahl prüfen
Index-Redesign
Expression Indizes, Composite Indizes und Volltextindizes gezielt anlegen
Statistik-Pflege
ANALYZE-Läufe in Deployment- und Migrationsprozesse integrieren
10. Zusammenfassung
Wenn eine Datenbank einen vorhandenen Index nicht nutzt, liegt die Ursache fast immer in einer von fünf konkreten Kategorien: einer Funktion auf der indizierten Spalte, impliziter Typkonvertierung, einem führenden Wildcard in einer LIKE-Abfrage, veralteten Statistiken oder einer falschen Spaltenreihenfolge bei zusammengesetzten Indizes. EXPLAIN ANALYZE liefert in jedem dieser Fälle das entscheidende Diagnosemerkmal, sei es ein Filter-Knoten statt eines Index-Condition-Knotens oder eine Abweichung zwischen geschätzter und tatsächlicher Zeilenzahl.
Die systematische Checkliste aus Abschnitt acht ersetzt zufälliges Neuanlegen von Indizes durch einen reproduzierbaren Prozess. Wer diese sieben Schritte konsequent durchgeht, findet die Ursache in den allermeisten Fällen innerhalb weniger Minuten und kann gezielt reagieren, statt pauschal REINDEX auszuführen oder immer neue Indizes anzulegen, die das eigentliche Problem nicht lösen.
Warum die Datenbank den Index nicht nutzt, das Wichtigste auf einen Blick
Diagnose-Werkzeug
EXPLAIN ANALYZE mit Vergleich von geschätzter und tatsächlicher Zeilenzahl ist der erste Schritt in jedem Fall.
Häufigste Ursache
Eine Funktion auf der indizierten Spalte oder implizite Typkonvertierung verhindern die Index-Nutzung am häufigsten.
Wildcards
Führende Wildcards in LIKE-Abfragen erfordern GIN- oder Volltextindizes statt eines regulären B-Tree.
Statistiken
Nach großen Bulk-Operationen manuell ANALYZE ausführen, statt auf den automatischen Lauf zu warten.