Warum nutzt die Datenbank meinen Index nicht: systematische Fehlersuche
AI generated
SELECT
JOIN
SQL Debugging · EXPLAIN · Index-Diagnose
Warum nutzt die Datenbank meinen Index nicht?
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.

19 Min. Lesezeit EXPLAIN · Statistiken · Typkonvertierung · Composite Index PostgreSQL · MySQL · SQL Server

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.

11. FAQ: Warum nutzt die Datenbank den Index nicht

1Warum ignoriert die Datenbank einen vorhandenen Index?
Meist wegen Funktion auf der Spalte, impliziter Typkonvertierung, führendem Wildcard, veralteten Statistiken oder falscher Spaltenreihenfolge.
2Wie erkenne ich Index-Nutzung in EXPLAIN?
Index Cond Knoten bedeutet Nutzung, Filter Knoten bei Seq Scan bedeutet keine Nutzung. Geschätzte und tatsächliche Zeilenzahl sollten nah beieinander liegen.
3Kann ich LOWER(spalte) trotzdem indizieren?
Ja, über einen Expression Index, direkt in PostgreSQL und Oracle, in MySQL über Functional Indexes ab 8.0.13.
4Warum verhindert VARCHAR gegen Zahl den Index?
MySQL konvertiert dabei oft die ganze Spalte statt des Literals. Immer den Literalwert casten, nicht die Spalte.
5Warum funktioniert 'wert%' mit Index, '%wert%' nicht?
B-Tree-Indizes unterstützen Präfixsuchen effizient. Bei führendem Wildcard ist die Sortierordnung nutzlos, ein voller Scan wird erzwungen.
6Was tue ich gegen veraltete Statistiken?
ANALYZE manuell nach großen Bulk-Operationen ausführen, statt auf den automatischen Lauf zu warten.
7Warum wird der Composite-Index nicht genutzt?
Left-Prefix-Regel: fehlt die erste Indexspalte in der WHERE-Klausel, kann der Index nicht genutzt werden.
8Meidet der Optimizer manchmal absichtlich den Index?
Ja, bei hoher Trefferquote ist ein Sequential Scan wegen sequenziellen Plattenzugriffs oft tatsächlich schneller, eine korrekte Kostenentscheidung.
9Reicht REINDEX zur Behebung?
Meist nicht, REINDEX baut nur die physische Struktur neu, ändert aber nichts an Funktionen oder Wildcards in der Abfrage.
10Was hilft am meisten gegen führende Wildcards?
GIN-Index mit pg_trgm in PostgreSQL oder ein dedizierter Volltextindex in anderen Systemen.