Blogs / Postgres EXPLAIN ANALYZE: Abfragen verstehen und optimieren

Postgres EXPLAIN ANALYZE: Abfragen verstehen und optimieren

Veröffentlicht
1. September 2026
Autor
Faizan Nadeem
Tags
PostgreSQL Database Performance Backend Development
Eine Nahaufnahme eines Computermonitors mit einem Liniendiagramm
Foto von Nicholas Cappello auf Unsplash

Die meisten Entwickler fĂŒhren EXPLAIN ANALYZE so aus, wie sie einen Stacktrace bei einem Fehler verwenden, den sie nicht verstehen — in die Abfrage einfĂŒgen, eine Wand aus Text zurĂŒckbekommen, nach einer großen Zahl suchen und raten. Dieses Raten lautet meist „fĂŒge einen Index hinzu“ oder „es ist der JOIN“, und manchmal stimmt das rein zufĂ€llig. Der Plan selbst sagt dir bereits genau, was langsam ist und warum; das Problem ist, dass den meisten von uns nie gezeigt wurde, wie man ihn als strukturiertes Dokument liest statt als furchteinflĂ¶ĂŸenden Monospace-Block.

Diese LĂŒcke kostet echte Zeit. Ein Entwickler, der keinen Plan lesen kann, probiert drei unzusammenhĂ€ngende Korrekturen aus, bevor er die richtige findet — erst SELECT * reduzieren, dann eine Cache-Schicht, dann schließlich einen Index, obwohl der Plan in den ersten zehn Sekunden schon „fehlender Index“ gesagt hĂ€tte, wenn man wĂŒsste, wo man hinschauen muss. Dieser Beitrag fĂŒhrt durch die tatsĂ€chliche Grammatik eines Postgres-Abfrageplans: was jede Zeile codiert, welche Zahlen SchĂ€tzungen sind und welche die RealitĂ€t abbilden, wie Schleifenanzahlen versteckte Kosten vervielfachen, was die Buffer-ZĂ€hler tatsĂ€chlich messen und welche drei wiederkehrenden Planformen die Art von Postgres sind zu sagen, dass ein Index fehlt.

Du lernst:

  • Wie man die Baumstruktur eines Plans liest und erkennt, welcher Knoten die Kosten wirklich treibt
  • Den Unterschied zwischen den geschĂ€tzten Zeilen des Planers und den tatsĂ€chlichen Zeilen von PostgreSQL, und warum eine große LĂŒcke dazwischen das nĂŒtzlichste Signal im gesamten Plan ist
  • Warum ein billig aussehender Knoten innerhalb einer Schleife die gesamte Abfragezeit dominieren kann
  • Wie man die Ausgabe von BUFFERS liest — shared hit vs. read, und was das ĂŒber Cache-Druck aussagt
  • Die drei Planmuster — Sequential Scan auf einer großen gefilterten Tabelle, Nested Loop mit hoher Schleifenanzahl und Sort, der auf Disk auslagert — die fast immer einen fehlenden oder falschen Index bedeuten
  • Ein vollstĂ€ndiges Beispiel zur Diagnose einer tatsĂ€chlich langsamen Abfrage allein anhand ihres Plans

Inhaltsverzeichnis

  1. Was EXPLAIN ANALYZE tatsÀchlich macht
  2. Anatomie einer Planzeile
  3. GeschÀtzte vs. tatsÀchliche Zeilen
  4. Loops: Warum ein billiger Knoten dominieren kann
  5. Buffers lesen: Cache-Treffer vs. Disk-LesevorgÀnge
  6. Die drei Muster, die auf einen fehlenden Index hinweisen
  7. Eine vollstĂ€ndige Schritt-fĂŒr-Schritt-Analyse
  8. HĂ€ufige Fallstricke
  9. Wie es von hier aus weitergeht

Was EXPLAIN ANALYZE tatsÀchlich macht

EXPLAIN allein fragt den Planer, was er tun wĂŒrde — er schĂ€tzt anhand von Tabellenstatistiken einen Plan und gibt ihn aus, ohne etwas auszufĂŒhren. EXPLAIN ANALYZE fĂŒhrt die Abfrage tatsĂ€chlich aus, misst jeden Schritt zeitlich und gibt dann denselben Baum mit Anmerkungen dazu aus, was wirklich passiert ist. Dieser Unterschied ist wichtiger, als es klingt: EXPLAIN kann man gefahrlos gegen alles ausfĂŒhren, auch gegen ein DROP, das in einer Transaktion steckt, die man zurĂŒckrollen will, aber EXPLAIN ANALYZE fĂŒhrt ein INSERT, UPDATE oder DELETE wirklich aus, sofern man es nicht in BEGIN; ... ROLLBACK; einbettet.

Die Ausgabe, die du fĂŒr echtes Debugging tatsĂ€chlich willst, hat diese Form:

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT o.id, o.total, c.name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at > now() - interval '7 days'
  AND o.status = 'pending';

ANALYZE liefert dir reale Laufzeiten und reale Zeilenanzahlen. BUFFERS liefert dir Informationen zu Cache-Treffern, was aus historischen GrĂŒnden standardmĂ€ĂŸig deaktiviert ist, aber in jeder echten Untersuchung wirklich nĂŒtzlich ist — schalte es jedes Mal ein. Lass COSTS OFF weg, außer du fĂŒgst den Plan irgendwo ein, wo er ĂŒber mehrere LĂ€ufe hinweg diffbar sein muss; du willst die KostenschĂ€tzungen fĂŒr den Vergleich mit den tatsĂ€chlichen Werten.

Ein Plan ist ein Baum, gelesen von unten nach oben und von innen nach außen. Die innersten, am stĂ€rksten eingerĂŒckten Knoten laufen zuerst; ihre Ausgabe fließt in den direkt darĂŒberliegenden Knoten. Die oberste Zeile ist das Letzte, was passiert, und dort werden Gesamtzeit und Gesamtkosten gemeldet. Es ist verlockend, ihn wie Fließtext von oben nach unten zu lesen, aber der eigentliche Datenfluss — und meist auch der eigentliche Flaschenhals — steckt in den BlĂ€ttern ganz unten.

Anatomie einer Planzeile

Hier ist ein einzelner Knoten aus einem echten Plan, StĂŒck fĂŒr StĂŒck erlĂ€utert:

Seq Scan on orders o  (cost=0.00..18734.00 rows=812 width=24)
                       (actual time=0.021..142.558 rows=790 loops=1)
  Filter: (status = 'pending'::text)
  Rows Removed by Filter: 199210
  Buffers: shared hit=210 read=8312

AufgeschlĂŒsselt bedeutet das:

  • Seq Scan on orders o — die Operation und die Tabelle (oder ihr Alias), auf der sie ausgefĂŒhrt wird. Ein Sequential Scan liest jede Zeile der Tabelle oder des Index in physischer Reihenfolge; das ist nicht automatisch schlecht, aber bei einer großen Tabelle genau das, worauf man achten sollte.
  • cost=0.00..18734.00 — der geschĂ€tzte Kostenbereich des Planers, in beliebigen planer-internen Kosteneinheiten (nicht Millisekunden). Die erste Zahl ist die geschĂ€tzte Kostenmenge, um die erste Zeile zurĂŒckzugeben; die zweite ist die geschĂ€tzte Kostenmenge, um alle Zeilen zurĂŒckzugeben. Diese Zahlen sind nur mit anderen Kostenwerten im selben Plan vergleichbar, niemals zwischen Abfragen oder Servern.
  • rows=812 — die SchĂ€tzung des Planers, wie viele Zeilen dieser Knoten erzeugen wird, basierend auf Tabellenstatistiken.
  • width=24 — die geschĂ€tzte durchschnittliche Zeilenbreite in Bytes.
  • actual time=0.021..142.558 — echte, gemessene Millisekunden: Zeit bis zur ersten Zeile und dann Zeit bis zum Abschluss, gemittelt ĂŒber alle SchleifendurchlĂ€ufe hinweg (mehr dazu gleich).
  • rows=790 loops=1 — die reale Anzahl an Zeilen, die dieser Knoten tatsĂ€chlich zurĂŒckgegeben hat, und wie oft der Knoten ausgefĂŒhrt wurde.
  • Filter / Rows Removed by Filter — ein Filter nach dem Scan, der angewendet wird, nachdem die Zeilen geholt wurden. Eine große Zahl bei „rows removed“ neben einer kleinen finalen Zeilenanzahl ist ein starkes Signal, dass der Scan viel mehr Arbeit macht als nötig.
  • Buffers — wie viele 8KB-Seiten berĂŒhrt wurden, aufgeteilt in Cache-Treffer und Disk-LesevorgĂ€nge.

Jeder Knoten im Baum hat diese gleiche Form. Sobald du eine Zeile parsen kannst, kannst du den ganzen Plan lesen — die FĂ€higkeit besteht vollstĂ€ndig darin zu wissen, welche Zahlen man miteinander vergleichen muss.

GeschÀtzte vs. tatsÀchliche Zeilen

Das ist der Vergleich mit dem höchsten Nutzen im gesamten Plan. Der Planer schĂ€tzt rows=812, bevor irgendetwas ausgefĂŒhrt wird, basierend auf Statistiken, die durch ANALYZE gesammelt wurden (den Wartungsbefehl, nicht die EXPLAIN-Option — verwirrenderweise haben sie denselben Namen). Nach der AusfĂŒhrung meldet Postgres, was tatsĂ€chlich herauskam. Wenn diese beiden Zahlen nah beieinanderliegen, hatte der Planer gute Informationen und hat mit hoher Wahrscheinlichkeit einen guten Plan gewĂ€hlt. Wenn sie um den Faktor 10, 100 oder mehr danebenliegen, dann wurde alles, was auf dieser SchĂ€tzung aufbaut — Join-Strategie, Speicherzuweisung, AusfĂŒhrungsreihenfolge — mit schlechten Informationen entschieden, und der resultierende Plan ist oft auf eine Weise schlecht, die sich allein aus der SchĂ€tzung kaum vorhersagen lĂ€sst.

-> Index Scan using idx_orders_status on orders
     (cost=0.42..8.44 rows=1 width=24)
     (actual rows=48000 loops=1)

Eine SchĂ€tzung von 1 Zeile gegenĂŒber tatsĂ€chlich 48.000 ist eine massive FehleinschĂ€tzung. Das passiert normalerweise aus einem von wenigen GrĂŒnden:

  • Veraltete Statistiken. Die Tabelle hat sich seit dem letzten ANALYZE deutlich verĂ€ndert, und autovacuum ist noch nicht nachgekommen. FĂŒhre ANALYZE orders; manuell aus und vergleiche.
  • Korrelierte Spalten. Der Planer nimmt standardmĂ€ĂŸig an, dass Spalten unabhĂ€ngig sind. Ein Filter wie WHERE status = 'pending' AND created_at > now() - interval '7 days' kann in Kombination deutlich selektiver (oder weniger selektiv) sein als jede Spalte fĂŒr sich, und die Standardstatistiken erfassen diese Korrelation nicht. Postgres-CREATE STATISTICS fĂŒr erweiterte Statistiken existiert genau, um das zu beheben.
  • UngleichmĂ€ĂŸige Datenverteilung. Wenn 90 % der Zeilen denselben Wert in einer schief verteilten Spalte teilen, kann das Standardziel fĂŒr Statistiken (standardmĂ€ĂŸig 100 Buckets) diese Schiefe möglicherweise nicht fein genug auflösen. Das Erhöhen von default_statistics_target oder das Setzen pro Spalte mit ALTER TABLE ... ALTER COLUMN ... SET STATISTICS gibt dem Planer ein feineres Histogramm.

Wenn du allgemeines Debugging langsamer Postgres-Abfragen machst, ist dieser Vergleich der erste Ort, auf den du schauen solltest, noch vor Buffers, noch vor Kostenzahlen, noch vor allem anderen — eine schlechte SchĂ€tzung ist meist die eigentliche Ursache, und alles danach ist nur ein Symptom.

Loops: Warum ein billiger Knoten dominieren kann

loops=1 bedeutet, dass der Knoten einmal ausgefĂŒhrt wurde. Unter einem Nested-Loop-Join kann ein innerer Knoten einmal pro Zeile ausgefĂŒhrt werden, die von der Ă€ußeren Seite produziert wird — und die angegebene actual time fĂŒr diesen Knoten ist der Durchschnitt pro Schleife, nicht die Gesamtsumme. Das ist die hĂ€ufigste Fehlinterpretation im gesamten Plan.

Nested Loop  (actual time=0.045..891.223 rows=48000 loops=1)
  -> Seq Scan on customers c (actual time=0.010..12.400 rows=4000 loops=1)
  -> Index Scan using idx_orders_customer on orders o
       (actual time=0.008..0.019 rows=12 loops=4000)

Dieser innere Index Scan sieht trivial billig aus: 0.019ms. Aber er lief 4.000 Mal — einmal pro Kundenzeile aus dem Ă€ußeren Scan — also ist sein tatsĂ€chlicher Beitrag grob 0.019ms * 4000 ≈ 76ms, nicht 0.019ms. Multipliziere die tatsĂ€chliche Zeit immer mit der Schleifenanzahl, bevor du die echten Kosten eines Knotens beurteilst. Eine winzige Zeit pro Schleife bei einer fĂŒnfstelligen Schleifenanzahl ist oft der eigentliche Flaschenhals, der sich offen versteckt, wĂ€hrend der Knoten mit der einzelnen grĂ¶ĂŸten actual time vergleichsweise wenig beitrĂ€gt.

Das ist auch der Mechanismus hinter dem klassischen Bug „funktioniert mit 100 Zeilen gut, kippt bei 100.000 um“: Ein Nested Loop ist eine gute Strategie, wenn die Ă€ußere Seite klein ist, und eine sich linear verschlechternde, wenn sie wĂ€chst — genau die Form des N+1-Problems, das auch auf ORM-Ebene auftaucht. Sieh dir dazu den Django N+1-Beitrag an, um dieses Muster aus Sicht der Anwendung zu sehen.

Buffers lesen: Cache-Treffer vs. Disk-LesevorgÀnge

Buffers: shared hit=210 read=8312 meldet 8KB-Seitenzugriffe auf den Shared-Buffer-Cache:

  • shared hit — Seiten, die bereits im Shared-Buffer-Cache von PostgreSQL gefunden wurden. Schnell; praktisch RAM-Geschwindigkeit.
  • shared read — Seiten, die von Disk geholt werden mussten (oder aus dem OS-Page-Cache, was Postgres nicht von echtem Disk-I/O unterscheiden kann), weil sie nicht im Buffer-Cache lagen.
  • shared dirtied — Seiten, die in dieser Operation verĂ€ndert wurden, relevant bei SchreibvorgĂ€ngen.
  • shared written — Seiten, die hinausgeschrieben wurden, um Platz zu schaffen, oft ein Zeichen fĂŒr Druck auf den Buffer-Cache.

Ein hoher read-Wert relativ zu hit bei einer hĂ€ufig laufenden Abfrage signalisiert, dass das Working Set nicht bequem in shared_buffers passt — oder dass es sich einfach um einen kalten Cache bei einer selten ausgefĂŒhrten Abfrage handelt. FĂŒhre dasselbe EXPLAIN (ANALYZE, BUFFERS) zweimal hintereinander aus; wenn der zweite Lauf ĂŒberwiegend Treffer zeigt, wo der erste ĂŒberwiegend LesevorgĂ€nge zeigte, dann hast du Zahlen aus einem kalten Cache betrachtet und nicht die Kosten im eingeschwungenen Zustand.

Buffers sind außerdem das ehrlichste verfĂŒgbare Kostensignal, weil sie nicht durch beliebige planer-interne Kosteneinheiten skaliert sind — es sind buchstĂ€bliche Seitenanzahlen, die sich direkt ĂŒber verschiedene Abfragen und verschiedene PlĂ€ne derselben Abfrage hinweg vergleichen lassen. Wenn zwei mögliche Indizes PlĂ€ne mit Ă€hnlicher actual time erzeugen, dann verrichtet der mit weniger gesamten Buffer-Zugriffen tatsĂ€chlich weniger I/O-Arbeit und hĂ€lt unter paralleler Last besser stand.

Die drei Muster, die auf einen fehlenden Index hinweisen

Wenn man genug PlÀne gesehen hat, tauchen drei Formen stÀndig wieder auf, und alle drei deuten auf dieselbe Grundursache hin.

1. Ein Sequential Scan mit einem selektiven Filter auf einer großen Tabelle.

Seq Scan on orders o  (cost=0.00..18734.00 rows=812 width=24)
                       (actual time=0.021..142.558 rows=790 loops=1)
  Filter: (status = 'pending'::text)
  Rows Removed by Filter: 199210

200.000 Zeilen zu scannen, um 790 davon zu behalten, bedeutet, dass der Plan fast seine gesamte Arbeit damit verbringt, Zeilen wegzuwerfen. Rows Removed by Filter, das um GrĂ¶ĂŸenordnungen ĂŒber der finalen Zeilenanzahl liegt, auf einer Tabelle, die zu groß ist, um bequem in den Cache zu passen, ist die implizite Art von Postgres zu sagen: „Ich habe keinen Index, um direkt zu den gewĂŒnschten Zeilen zu springen.“ Ein Index auf status — oder besser ein partieller Index (WHERE status = 'pending'), wenn dieser Wert selten ist — verwandelt das in einen Index Scan, der nur die passenden Zeilen berĂŒhrt.

2. Ein Nested Loop mit sehr hoher Schleifenanzahl, der einen unindizierten inneren Scan speist.

-> Seq Scan on order_items oi
     (actual time=0.412..3.891 rows=6 loops=4000)
   Filter: (order_id = o.id)

Ein innerer Sequential Scan, der tausende Male lĂ€uft und jedes Mal die gesamte Tabelle order_items auf eine Handvoll passender Zeilen herunterfiltert, ist das Schleifenmuster aus dem vorherigen Abschnitt kombiniert mit einem fehlenden Index auf der Join-Spalte. Ein Index auf order_items(order_id) verwandelt jeden dieser 4.000 Sequential Scans in einen billigen Index-Lookup, und die gesamte Abfragezeit sinkt meist um eine GrĂ¶ĂŸenordnung oder mehr.

3. Ein Sort, der auf Disk auslagert, statt im Speicher abgeschlossen zu werden.

Sort  (cost=41293.55..41808.36 rows=205925 width=32)
      (actual time=387.223..421.009 rows=205925 loops=1)
  Sort Method: external merge  Disk: 7128kB
  -> Seq Scan on orders ...

Sort Method: external merge Disk: ...kB bedeutet, dass der Sort nicht in work_mem gepasst hat und in temporĂ€re Dateien auf Disk ausgelagert wurde — spĂŒrbar langsamer als ein In-Memory-Quicksort oder ein Top-N-Heapsort. Hier greifen zwei unabhĂ€ngige Lösungen, und sie schließen sich nicht gegenseitig aus: Erhöhe work_mem fĂŒr die Session oder die Abfrage, wenn der Server noch Spielraum hat, oder — meist die bessere Lösung — fĂŒge einen Index hinzu, der zur ORDER BY-Klausel passt, sodass der Sort vollstĂ€ndig vermieden wird und die Zeilen bereits vorsortiert aus einem Index Scan statt aus einem Sort-Knoten kommen.

Alle drei Muster teilen dieselbe zugrunde liegende Geschichte: Der Planer tut mit einer sequentiellen oder Brute-Force-Strategie das Beste, was er kann, weil kein besserer Pfad verfĂŒgbar ist. FĂŒr einen vollstĂ€ndigen Überblick, welcher Indextyp zu welcher dieser Formen passt — B-tree, GIN, GiST oder BRIN — behandelt der Leitfaden zu Postgres-Indextypen die Entscheidung ausfĂŒhrlich.

Eine vollstĂ€ndige Schritt-fĂŒr-Schritt-Analyse

Nehmen wir einen tatsĂ€chlich langsamen Endpunkt: „Liste ausstehende Bestellungen der letzten Woche fĂŒr das Konto eines Kunden auf, die neuesten zuerst.“ Die Abfrage:

EXPLAIN (ANALYZE, BUFFERS)
SELECT id, total, created_at
FROM orders
WHERE customer_id = 4821
  AND status = 'pending'
  AND created_at > now() - interval '7 days'
ORDER BY created_at DESC
LIMIT 20;

Erster Plan, vor irgendwelchen Änderungen:

Limit  (actual time=203.441..203.448 rows=14 loops=1)
  -> Sort  (actual time=203.439..203.443 rows=14 loops=1)
        Sort Key: created_at DESC
        Sort Method: quicksort  Memory: 26kB
        -> Seq Scan on orders  (actual time=0.033..201.887 rows=14 loops=1)
              Filter: ((customer_id = 4821) AND (status = 'pending')
                       AND (created_at > now() - interval '7 days'))
              Rows Removed by Filter: 611982
              Buffers: shared hit=402 read=9812

So liest man ihn: Der Sort ist trivial (14 Zeilen, In-Memory-Quicksort). Der Seq Scan hat 611.982 Zeilen herausgefiltert, um 14 zu behalten, und dabei ĂŒber 10.000 Buffer-Seiten berĂŒhrt — Muster eins, eindeutig. Die Lösung ist ein zusammengesetzter Index, der zu den Filterspalten passt und den SortierschlĂŒssel einschließt, sodass die Datenbank potenziell auch einen separaten Sort-Schritt ĂŒberspringen kann:

CREATE INDEX CONCURRENTLY idx_orders_customer_status_created
ON orders (customer_id, status, created_at DESC);

Wenn man dieselbe Abfrage nach dem Aufbau des Index erneut ausfĂŒhrt:

Limit  (actual time=0.061..0.089 rows=14 loops=1)
  -> Index Scan using idx_orders_customer_status_created on orders
        (actual time=0.060..0.086 rows=14 loops=1)
        Index Cond: ((customer_id = 4821) AND (status = 'pending')
                      AND (created_at > (now() - interval '7 days')))
        Buffers: shared hit=6

203ms runter auf 0.089ms, Buffer-Zugriffe von ~10.200 auf 6 reduziert und ĂŒberhaupt kein Sort-Knoten mehr — die Spaltenreihenfolge des Index passt bereits zu den Anforderungen von LIMIT 20. Das ist der gesamte Diagnosezyklus: den Baum lesen, den Knoten mit dem grĂ¶ĂŸten realen Zeitbeitrag finden, seine Form einem der drei Muster zuordnen, den Index korrigieren und erneut ausfĂŒhren, um zu bestĂ€tigen, dass sich die Planform tatsĂ€chlich geĂ€ndert hat.

HĂ€ufige Fallstricke

Fehler: Kostenwerte zwischen verschiedenen Abfragen vergleichen. Kosten sind eine einheitslose, planer-interne SchĂ€tzung, kalibriert durch random_page_cost und Ă€hnliche Parameter — sie sind nur relativ zu anderen Knoten im selben Plan sinnvoll. Lösung: FĂŒr Vergleiche zwischen Abfragen actual time und Buffer-Anzahlen vergleichen, nicht cost.

Fehler: actual time bei einem Schleifenknoten als Gesamtsumme lesen. Wie oben gezeigt, ist diese Zahl ein Durchschnitt pro Schleife. Lösung: Immer mit loops multiplizieren, bevor du den echten Beitrag eines Knotens beurteilst.

Fehler: EXPLAIN ANALYZE einmal auf einem kalten Cache ausfĂŒhren und daraus schließen, dass die Abfrage in Produktion langsam ist. Der erste Lauf nach einem Neustart oder gegen selten berĂŒhrte Daten zahlt Disk-I/O-Kosten, die der Normalbetrieb meist nicht hat. Lösung: Zweimal ausfĂŒhren und dem VerhĂ€ltnis der Buffer-Treffer vertrauen, nicht nur der ersten Zahl.

Fehler: annehmen, dass „index scan“ immer besser ist als „seq scan“. Auf einer kleinen Tabelle oder wenn eine Abfrage ohnehin die meisten Zeilen der Tabelle braucht, ist ein Sequential Scan tatsĂ€chlich schneller — ohne den Overhead des Index-Durchlaufs. Lösung: Nach tatsĂ€chlicher Zeit und Zeilenanzahl urteilen, nicht allein nach dem Knotennamen.

Fehler: einen Index hinzufĂŒgen und EXPLAIN ANALYZE nicht erneut ausfĂŒhren, um zu bestĂ€tigen, dass sich der Plan tatsĂ€chlich geĂ€ndert hat. Postgres verwendet einen neuen Index nicht zwingend, wenn seine Statistiken weiterhin den alten Plan bevorzugen oder wenn der Index nicht zur fĂŒhrenden Filterspalte der Abfrage passt. Lösung: Immer erneut ausfĂŒhren und prĂŒfen, dass sich die Planform geĂ€ndert hat, nicht nur, dass die Abfrage schneller wurde — eine schnellere Laufzeit allein beweist nicht, dass die Korrektur verallgemeinerbar ist.

Wie es von hier aus weitergeht

Dieser Beitrag behandelt das Lesen eines einzelnen Plans in Isolation. Der Rest dieser Reihe behandelt die Entscheidungen, die ĂŒberhaupt erst bestimmen, welche PlĂ€ne ĂŒberhaupt möglich sind:

Zum Abschluss

Ein Postgres-Abfrageplan ist keine geheimnisvolle Ausgabe, die man nach furchteinflĂ¶ĂŸenden Zahlen ĂŒberfliegt — er ist ein strukturiertes, ehrliches Protokoll darĂŒber, was die Datenbank genau getan hat, in welcher Reihenfolge und wie das im Vergleich zu ihren Erwartungen ausfiel. Lies ihn von unten nach oben, vergleiche zuerst geschĂ€tzte mit tatsĂ€chlichen Zeilen, multipliziere Schleifenknoten vor der Beurteilung ihrer Kosten mit ihrer Schleifenanzahl, prĂŒfe das VerhĂ€ltnis der Buffer-Treffer auf Cache-Druck und gleiche das Gesehene mit den drei Mustern ab, die bedeuten: „Der Planer hat hier keine gute Option.“ Sobald diese Leserichtung automatisch wird, ist EXPLAIN ANALYZE keine Textwand mehr, sondern das schnellste Debugging-Werkzeug im gesamten Stack.

Wenn eine Abfrage das nÀchste Mal langsam ist, kommt dein erster Schritt dann aus dem Plan oder aus einer Vermutung?

Weitere Artikel