Datenbankadministration für Sysadmins

Indizes und Ausführungspläne: erst lesen, dann anlegen

EXPLAIN ANALYZE auf ein DELETE, nur um zu sehen, was es kosten würde — und die Zeilen sind weg. Die Option führt die Anweisung laut Dokumentation tatsächlich aus; verworfen wird nur die Ausgabe eines SELECT. Dieses Modul liest Ausführungspläne so, dass am Ende eine belegte Entscheidung steht: wann ein Index wirklich hilft, was er auf dem Schreibpfad dauerhaft kostet, und warum sein Aufbau in der Voreinstellung die gesamte Schreibseite anhält.

Lehrtext · 10 Abschnitte · zuletzt geprüft: 2026-09-12

Der Plan ist die Antwort, nicht die Stoppuhr

„Die Abfrage braucht zwölf Sekunden“ ist ein Messwert ohne Erklärung. Zwölf Sekunden können bedeuten, dass zwanzig Millionen Zeilen gelesen wurden, oder dass zweihundert Zeilen gelesen wurden und elf Sekunden lang eine Sperre im Weg stand. Die beiden Fälle haben nichts miteinander zu tun, und keine Maßnahme hilft bei beiden.

Beide Systeme beantworten die Frage über den Ausführungsplan: die Schrittfolge, mit der die Engine an das Ergebnis kommt. Der Plan zeigt, welche Zugriffswege gewählt wurden, in welcher Reihenfolge verbunden wird und wie viele Zeilen dabei erwartet werden.

Sequentieller ScanDie Tabelle wird vollständig gelesen. Bei kleinen Tabellen oder großen Ergebnismengen ist das der richtige Weg, nicht ein Versagen.

IndexzugriffÜber eine Indexstruktur werden gezielt die passenden Zeilen gesucht. Lohnt sich, wenn nur ein kleiner Teil der Tabelle gebraucht wird.

Die Reihenfolge, die dieses Modul vertrittErst den Plan lesen, dann über einen Index nachdenken. Ein Index, der ohne Plan angelegt wird, ist eine Vermutung mit dauerhaften Schreibkosten — und die häufigste Ursache dafür, dass eine Tabelle am Ende neun Indizes trägt, von denen drei benutzt werden.

`EXPLAIN` und `EXPLAIN ANALYZE`: der Unterschied, der Daten kostet

EXPLAIN zeigt den Plan, den der Planer wählen würde, samt geschätzter Kosten und geschätzter Zeilenzahlen. Die Abfrage läuft dabei nicht.

EXPLAIN (ANALYZE) ergänzt die tatsächlichen Werte: die verstrichene Zeit je Planknoten und die Zahl der wirklich zurückgegebenen Zeilen. Das ist die einzige Art herauszufinden, ob die Schätzungen der Wirklichkeit nahekommen — und genau hier liegt eine Falle mit Datenverlustpotenzial.

Wörtlich aus der DokumentationDie Option ANALYZE bewirkt, dass die Anweisung tatsächlich ausgeführt wird, nicht nur geplant. Zwar verwirft EXPLAIN die Ausgabe, die ein SELECT zurückgeben würde — andere Nebenwirkungen der Anweisung treten aber wie üblich ein.

Auf ein SELECT angewandt ist das harmlos. Auf ein UPDATE, ein DELETE, ein INSERT oder ein MERGE angewandt bedeutet es: Die Änderung findet statt. Wer in der Produktion EXPLAIN ANALYZE DELETE FROM ... eingibt, um „nur mal zu sehen, was das kosten würde“, hat gelöscht.

Einen ändernden Befehl gefahrlos vermessen

Szenario

Ein UPDATE auf eine große Fachtabelle läuft ungewöhnlich lange. Gesucht ist der Plan mit echten Zahlen, ohne die Daten anzufassen.

Anforderungen

  • Zugriff auf die Datenbank mit Schreibrecht — der Befehl läuft wirklich
  • Ein Wartungsfenster oder eine Kopie, denn Sperren werden echt genommen

Schritte

  1. Die von der Dokumentation genannte Klammer verwenden: BEGIN; voranstellen, dann EXPLAIN ANALYZE auf den ändernden Befehl, dann ROLLBACK;.
  2. Damit laufen alle Nebenwirkungen ab, werden am Ende aber verworfen. Die gemessenen Zeiten und Zeilenzahlen bleiben gültig.
  3. Beachten, dass während der Messung echte Sperren gehalten werden — auf der Produktion blockiert das andere Sitzungen genauso wie der echte Befehl.
  4. Und dass ein Rollback Arbeit ist: Bei einem großen UPDATE dauert das Zurücknehmen so lange wie das Ausführen.

Merksatz: Die Klammer macht die Messung umkehrbar, nicht folgenlos. Auf einer Produktivinstanz bleibt sie ein Eingriff mit Wartungsfenster.

Was `BUFFERS` dazugibtDie Option zeigt, wie viele Blöcke aus dem Zwischenspeicher bedient und wie viele von der Platte gelesen wurden. Ein Treffer bedeutet laut Dokumentation, dass ein Lesevorgang vermieden wurde, weil der Block bereits im Zwischenspeicher lag. Das trennt die Frage „zu viele Blöcke angefasst“ von „die Blöcke waren kalt“ — zwei Befunde mit zwei verschiedenen Maßnahmen. Bei gesetztem ANALYZE sind die Angaben ohnehin enthalten.

Schätzung gegen Wirklichkeit lesen

Der eigentliche Erkenntnisgewinn eines Plans mit echten Werten liegt nicht in der Gesamtzeit, sondern im Vergleich zwischen geschätzter und tatsächlicher Zeilenzahl je Knoten. Der Planer wählt seinen Weg anhand der Schätzung; liegt sie daneben, ist der Weg falsch, auch wenn die Engine fehlerfrei arbeitet.

Schätzung ≈ Wirklichkeit
Der Planer ist informiert. Ist die Abfrage trotzdem langsam, liegt es an der Datenmenge oder am Zugriffsweg, nicht an den Statistiken.
Schätzung viel kleiner als Wirklichkeit
Der Planer hielt einen Schritt für billig, der teuer war — typischerweise die Ursache für einen ungeeigneten Verbundweg bei vielen Zeilen.
Schätzung viel größer als Wirklichkeit
Der Planer hat einen Indexzugriff verworfen, der sich gelohnt hätte, weil er mit viel mehr Treffern rechnete.

In beiden Fehlerrichtungen ist die erste Maßnahme dieselbe und kostet fast nichts: die Statistiken auffrischen. Die PostgreSQL-Dokumentation hält ausdrücklich fest, dass ANALYZE regelmäßig laufen muss, damit der Planer fundierte Entscheidungen treffen kann. Erst wenn die Schätzungen danach stimmen und die Abfrage weiterhin langsam ist, lohnt das Nachdenken über einen Index.

Zwei Befehle, ein NameANALYZE als eigenständiger SQL-Befehl frischt die Planerstatistiken auf. ANALYZE als Option von EXPLAIN führt die Abfrage aus und misst. Gleiche Schreibweise, völlig verschiedene Wirkung — und der zweite ist der gefährliche.

Wann ein Index hilft — und wann nicht

Ein Index lohnt sich, wenn er die Menge der anzufassenden Zeilen stark verkleinert. Die Dokumentation beschreibt den Fall genau so: Ohne Index müsste das System die gesamte Tabelle Zeile für Zeile durchsuchen; hat die Abfrage viele Zeilen zur Auswahl und liefert nur wenige zurück, ist das offensichtlich unwirtschaftlich.

SelektivitätDer Anteil der Zeilen, den eine Bedingung übrig lässt. Eine Kundennummer ist hochselektiv — eine Zeile von Millionen. Ein Ja-Nein-Kennzeichen ist es nicht — es halbiert bestenfalls.

Daraus folgt die häufigste Fehleinschätzung: Ein Index auf einer Spalte mit wenigen verschiedenen Werten bringt in der Regel nichts. Die Engine müsste den Index lesen und danach noch die halbe Tabelle besuchen; der sequentielle Scan ist dann der billigere Weg, und der Planer wählt ihn zu Recht. Der Index kostet trotzdem Platz und Schreibzeit.

Die Dokumentation nennt auch die Fälle, die gern vergessen werden: Indizes können ebenso UPDATE- und DELETE-Befehle mit Suchbedingung beschleunigen, und sie können in Verbunden verwendet werden — ein Index auf einer Spalte, die Teil einer Verbundbedingung ist, beschleunigt die Abfrage erheblich.

Der Index, den niemand anlegtGenau hier liegt die Lücke aus dem Grundlagenmodul: Ein Fremdschlüssel erzwingt einen Index auf der referenzierten Seite, weil diese ein Primärschlüssel oder eindeutig sein muss. Auf der referenzierenden Seite legt PostgreSQL laut Dokumentation ausdrücklich keinen an. Ein DELETE in der Elterntabelle muss die Kindtabelle deshalb nach passenden Zeilen durchsuchen — die klassische Ursache dafür, dass ein Löschvorgang mit wachsender Datenmenge unerwartet langsam wird.

Warum ein vorhandener Index nicht genommen wird

Ein Index existiert, die Abfrage filtert auf genau diese Spalte — und der Plan zeigt trotzdem einen vollständigen Scan. Das ist meist kein Fehler des Planers, sondern eine nicht erfüllte Bedingung. Die Dokumentation nennt die Form, in der eine Bedingung überhaupt indexfähig ist: indizierte-Spalte indexfähiger-Operator Vergleichswert, in einer WHERE- oder JOIN-Klausel. Daraus ergeben sich drei Bedingungen, die alle gelten müssen.

  • Die indizierte Seite muss unverändert dastehen. Der Index ist auf eine Spalte oder einen Ausdruck definiert. Wird die Spalte in der Bedingung erst durch eine Funktion geschickt — etwa in Großbuchstaben umgewandelt oder auf einen Datumsteil gekürzt —, steht dort ein anderer Ausdruck als der indizierte, und der Index passt nicht mehr. Der Ausweg ist ein Index auf genau diesem Ausdruck.
  • Der Operator muss zur Operatorklasse des Index passen. Ein Index trägt nicht nur Werte, sondern eine Ordnung samt der Operatoren, die auf ihr arbeiten. Ein Operator, der nicht dazugehört, kann den Index nicht benutzen — klassisch beim Suchen mit einem führenden Platzhalter, das keine Ordnung ausnutzen kann.
  • Der Vergleichswert muss stabil und fremd sein. Er darf laut Dokumentation nicht flüchtig sein und nicht auf die Tabelle des Index selbst verweisen. Ein Wert, der sich bei jedem Aufruf ändern kann, taugt nicht als Einstieg in eine sortierte Struktur.
Was das für die Fehlersuche heißtBevor ein zweiter Index angelegt wird, gehört geprüft, ob die Bedingung überhaupt in dieser Form dasteht. Sehr oft ist die billigere Korrektur, die Abfrage so umzuschreiben, dass die Spalte unverändert links steht — und nicht, eine weitere Struktur mit dauerhaften Schreibkosten anzulegen.

SQL Server: der gruppierte Index **ist** die Tabelle

Bei SQL Server kommt eine Unterscheidung dazu, die es in dieser Form bei PostgreSQL nicht gibt — und die weitreichende Folgen für alle anderen Indizes hat.

Gruppierter Index
Sortiert und speichert die Datenzeilen selbst nach seinen Schlüsselwerten. Es kann laut Dokumentation nur einen je Tabelle geben, weil die Zeilen nur in einer Ordnung liegen können.
Heap
Eine Tabelle ohne gruppierten Index. Die Zeilen liegen dann in einer ungeordneten Struktur.
Nicht gruppierter Index
Eine eigene Struktur neben den Datenzeilen. Jeder Eintrag verweist über einen Zeilenlokalisierer auf die Datenzeile.
Die Folge, die den Speicherverbrauch erklärtDer Zeilenlokalisierer hängt laut Dokumentation davon ab, wie die Daten liegen: Bei einem Heap ist er ein Zeiger auf die Zeile, bei einer gruppierten Tabelle ist er der Schlüssel des gruppierten Index. Ein breiter gruppierter Schlüssel steckt damit in jedem nicht gruppierten Index dieser Tabelle noch einmal drin. Wer den gruppierten Schlüssel um drei Spalten erweitert, vergrößert damit alle anderen Indizes mit.

Die Zuordnung passiert überdies oft, ohne dass jemand einen Index anlegt. Die Dokumentation hält fest: Bei einem PRIMARY KEY erzeugt die Engine automatisch einen gruppierten Index, sofern nicht bereits einer besteht; bei einer UNIQUE-Bedingung einen nicht gruppierten. Existiert schon ein gruppierter Index, wird der Primärschlüssel über einen nicht gruppierten durchgesetzt. Welche Tabelle wie liegt, ist also oft ein Nebenprodukt der Reihenfolge, in der jemand die Bedingungen definiert hat.

Eingeschlossene Spalten sind das Gegenmittel gegen zu breite Schlüssel: Nichtschlüsselspalten lassen sich auf der Blattebene eines nicht gruppierten Index ergänzen, um vollständig abgedeckte Abfragen zu ermöglichen — die Engine beantwortet die Abfrage dann allein aus dem Index, ohne die Datenzeile zu besuchen.

Was ein Index kostet

Jeder Index ist eine zweite Struktur, die denselben Inhalt abbildet. Sie muss mitgeführt werden, und das ist kein Nebeneffekt, sondern der Preis.

Aus der DokumentationNach dem Anlegen muss das System den Index mit der Tabelle synchron halten. Das erzeugt zusätzlichen Aufwand bei allen Datenmanipulationen. Indizes können außerdem die Erzeugung von Heap-Only-Tupeln verhindern. Indizes, die selten oder nie in Abfragen verwendet werden, sollten deshalb entfernt werden.

Der letzte Halbsatz ist eine ungewöhnlich klare Empfehlung für eine Referenzdokumentation, und er benennt die Kostenseite doppelt. Neben der reinen Schreibarbeit steht dort ein zweiter, subtilerer Punkt: Wird eine Zeile geändert und liegt die neue Fassung auf derselben Seite, kann PostgreSQL die Indizes unangetastet lassen — aber nur, solange keine indizierte Spalte betroffen ist. Zusätzliche Indizes machen diesen sparsamen Weg seltener möglich.

Messbar wird der Schreibpreis über die WAL-Option von EXPLAIN: Sie zeigt laut Dokumentation die Zahl der erzeugten Protokollsätze, die Menge der erzeugten Protokolldaten in Bytes und wie oft die Protokollpuffer volliefen. Damit lässt sich derselbe UPDATE mit und ohne einen Indexkandidaten vergleichen — und die Antwort ist eine Zahl, keine Meinung.

Auf der Nutzungsseite führen beide Systeme Zähler darüber, wie oft ein Index tatsächlich verwendet wurde. Diese Zähler unterliegen derselben Einschränkung wie alle kumulativen Werte: Ohne Bezugszeitraum sagt „null Verwendungen“ nichts — der Zähler kann seit dem letzten Neustart zählen, und der war gestern.

Den Index anzulegen kostet ein Fenster

Ein Index ist schnell beschlossen und langsam gebaut. Was dabei passiert, unterscheidet sich deutlich von dem, was die meisten erwarten.

Wörtlich aus der DokumentationDas Anlegen eines Index auf einer großen Tabelle kann lange dauern. Voreingestellt erlaubt PostgreSQL, dass Lesevorgänge parallel zum Indexaufbau stattfinden — Schreibvorgänge aber sind blockiert, bis der Aufbau fertig ist. In Produktionsumgebungen ist das oft nicht hinnehmbar.

Ein CREATE INDEX auf einer 200-Gigabyte-Tabelle ist damit kein Einzeiler, sondern ein Wartungsfenster in der Länge des Aufbaus. Wer es zur Hauptlastzeit ausführt, legt die Schreibseite der Anwendung still, ohne dass irgendwo ein Fehler erscheint — die Schreiber warten einfach.

Die Dokumentation nennt auch den Ausweg: Es ist möglich, Schreibvorgänge parallel zum Indexaufbau zuzulassen. Sie verweist dabei ausdrücklich auf mehrere Einschränkungen, die man kennen muss — dieser Weg dauert länger, belastet das System stärker und kann fehlschlagen, wobei ein unbrauchbarer Index zurückbleibt, der von Hand beseitigt werden muss. Er ist die richtige Wahl für die Produktion und trotzdem keine Kleinigkeit.

Kurzcheck

Eine 200-GB-Tabelle soll einen zusätzlichen Index bekommen. Die Anwendung schreibt rund um die Uhr. Was planst du ein?

  • Ein Verfahren, das Schreibvorgänge zulässt — und dessen Einschränkungen
  • Nichts Besonderes — Indexaufbau ist eine reine Lesenoperation
  • Nur genügend Plattenplatz, der Rest läuft nebenher

Treffer. Richtig. Der voreingestellte Aufbau blockiert Schreibvorgänge bis zum Ende. Das nebenläufige Verfahren ist der dokumentierte Ausweg, bringt aber längere Laufzeit und die Möglichkeit eines unbrauchbaren Index mit, der aufgeräumt werden muss.

Der Vorschlag ist kein Befehl

SQL Server sammelt während der Optimierung Hinweise darauf, welche Indizes gefehlt hätten, und stellt sie in eigenen Sichten bereit. Das ist nützlich — und wird regelmäßig als fertige Empfehlung missverstanden. Die Dokumentation nennt die Einschränkungen selbst.

  • Die Angaben überdauern keinen Neustart. Sie werden aktualisiert, wenn eine Abfrage optimiert wird, und nicht dauerhaft gespeichert; sie bleiben nur bis zum Neustart der Engine erhalten. Wer sie behalten will, muss sie regelmäßig wegsichern — und den Startzeitpunkt der Instanz danebenlegen.
  • Die Ergebnismenge ist auf 600 Zeilen begrenzt. Bei mehr fehlenden Indizes sieht man die neueren erst, wenn die vorhandenen abgearbeitet sind.
  • Die Spaltenreihenfolge liefert die Sicht nicht mit. Laut Dokumentation gehören Gleichheitsspalten vor Ungleichheitsspalten, und innerhalb der Gleichheitsspalten ist nach Selektivität zu ordnen — die selektivste zuerst. Eingeschlossene Spalten gehören in die entsprechende Klausel, nicht in den Schlüssel.
  • Vorschläge können einander doppeln. Sie entstehen je Abfrage, ohne Rücksicht darauf, was schon existiert oder was ein anderer Vorschlag bereits abdeckt.
Was daraus folgtDie Sicht beantwortet „welche Zugriffe hätten von einem Index profitiert“. Sie beantwortet nicht „welche Indizes soll ich anlegen“ — dafür fehlen ihr die Schreibkosten, der Bestand und die Zusammenschau. Wer die generierte Anweisung ungeprüft ausführt, legt tendenziell zu viele, zu breite und einander überlappende Indizes an, und bezahlt das auf dem Schreibpfad jeder folgenden Änderung.

Vom langsamen Befund zur belegten Änderung

Plan mit echten Zahlen holen — ändernde Befehle nur in der KlammerSchätzung gegen Wirklichkeit halten, notfalls Statistiken auffrischenSelektivität prüfen, bevor ein Index entworfen wirdSchreibkosten und Aufbaufenster gegenrechnenNach der Änderung denselben Plan erneut holen
Plan vor Index

Der letzte Schritt wird am häufigsten weggelassen und ist der einzige, der die Änderung belegt. Ein Index, der angelegt und nie gegengemessen wurde, ist eine Vermutung mit Schreibkosten — und er bleibt oft jahrelang liegen, weil niemand belegen kann, dass er nötig ist, und niemand sich traut, ihn zu entfernen.

Preis dieses VorgehensEs ist langsamer als der Reflex, den vorgeschlagenen Index einfach anzulegen. Bei einem akut stehenden Dienst ist diese Langsamkeit ein echter Nachteil, und manchmal ist der schnelle Index die richtige Notfallmaßnahme. Dann gehört aber festgehalten, dass die Begründung nachzuholen ist — sonst wird aus der Notfallmaßnahme dauerhafter Bestand.
AbgrenzungWie eine Schemaänderung samt Indexaufbau in eine Auslieferung eingebettet wird, steht im Modul über Schemaänderungen im laufenden Betrieb. Wachstum, Aufräumen und Reorganisation von Indizes stehen im Modul über Speicherwachstum und Routinewartung. Hier ging es um die Frage, ob ein Index überhaupt der richtige Hebel ist — und wie man das belegt statt es zu vermuten.

Jetzt anwenden

Diesen Stoff gibt es als Modul mit bewerteten Entscheidungs-Checks — dieselbe Einführung, danach die Übungen.

Zum Modul →
Quellen & Aktualität6 Primärquellen · zuletzt geprüft:
  1. 01postgresql.org/docs/current/sql-explain.html
  2. 02postgresql.org/docs/current/indexes-intro.html
  3. 03postgresql.org/docs/current/ddl-constraints.html
  4. 04postgresql.org/docs/current/monitoring-stats.html
  5. 05learn.microsoft.com/en-us/sql/relational-datab…ed-indexes-described
  6. 06learn.microsoft.com/en-us/sql/relational-datab…details-transact-sql