PostgreSQL-EXPLAIN-ANALYZE-Ausgabe richtig lesen
EXPLAIN zeigt, was der Planer zu tun beabsichtigt; EXPLAIN ANALYZE führt die Abfrage aus und zeigt, was tatsächlich passiert ist. Die Ausgabe gut zu lesen ist die Fähigkeit mit dem größten Hebel in der PostgreSQL-Performance-Arbeit — und sie hat ein paar Fallen, in die selbst erfahrene Entwickler tappen.
Die Anatomie eines Planknotens
Jede Zeile eines Plans ist ein Knoten in einem Baum. Zeilen fließen von den innersten (am stärksten eingerückten) Knoten nach oben. Ein typischer Knoten sieht so aus:
Index Scan using orders_customer_idx on orders
(cost=0.43..152.80 rows=42 width=98)
(actual time=0.031..0.512 rows=38 loops=1)
Zwei Gruppen von Zahlen, und sie bedeuten Unterschiedliches:
cost=0.43..152.80— die Schätzung des Planers, in willkürlichen Kosteneinheiten (nicht Millisekunden). Die erste Zahl sind die Startkosten, bevor die erste Zeile erzeugt werden kann, die zweite die Gesamtkosten für alle Zeilen.rows=42(in der Kostengruppe) — wie viele Zeilen der Planer erwartete.actual time=0.031..0.512— gemessene Millisekunden, wieder Start..Gesamt, pro Schleife.rows=38 loops=1(in der Ist-Gruppe) — tatsächlich zurückgegebene Zeilen, pro Schleife gemittelt.
Falle #1: alles ist pro Schleife
Wenn ein Knoten auf der inneren Seite eines Nested Loop sitzt, kann er tausendfach ausgeführt werden. PostgreSQL meldet seine actual time und rows als Durchschnitt pro Ausführung, wobei loops Ihnen sagt, wie viele Ausführungen stattfanden:
Index Scan using items_order_idx on items
(actual time=0.005..0.021 rows=3 loops=12000)
Dieser Knoten gab nicht 3 Zeilen in 0,021 ms zurück. Er gab ungefähr 36.000 Zeilen zurück und verbrauchte insgesamt etwa 250 ms (0,021 × 12.000). Multiplizieren Sie immer mit loops, bevor Sie entscheiden, ob ein Knoten günstig ist.
Falle #2: Zeiten sind kumulativ
Die actual time eines Knotens schließt alle seine Kindknoten ein. Wenn ein Sort 900 ms meldet und der Seq Scan darunter 850 ms meldet, kostete die Sortierung selbst nur ~50 ms. Um herauszufinden, wo die Zeit wirklich verbraucht wird, brauchen Sie die exklusive Zeit jedes Knotens: seine Gesamtzeit minus der Gesamtzeiten seiner Kindknoten. Genau das ist die Rechnerei, die in einem 40-Knoten-Plan mühsam wird — und der Hauptgrund, warum Plan-Visualizer existieren.
Falle #3: geschätzte vs. tatsächliche Zeilen
Die wertvollste Diagnose in der gesamten Ausgabe ist die Lücke zwischen geschätzten und tatsächlichen Zeilen. Der Plan wurde auf Basis der Schätzung gewählt — wenn der Planer 40 Zeilen erwartete und 400.000 bekam, wurde jede Entscheidung stromabwärts dieses Knotens (Join-Strategie, Speicherdimensionierung, Index- vs. sequenzieller Scan) auf falschen Annahmen getroffen.
Wenn Sie eine große Abweichung sehen:
- Führen Sie
ANALYZEfür die beteiligten Tabellen aus — die Statistiken sind vielleicht einfach veraltet. - Wenn die Fehlschätzung bei einer bestimmten Spalte fortbesteht, erhöhen Sie deren Stichprobendetail:
ALTER TABLE t ALTER COLUMN c SET STATISTICS 1000; ANALYZE t; - Wenn die Bedingung korrelierte Spalten kombiniert (Stadt + PLZ, Kategorie + Marke), multipliziert der Planer ihre Selektivitäten, als wären sie unabhängig.
CREATE STATISTICSmit der Artdependenciesexistiert genau für diesen Fall.
Nutzen Sie BUFFERS. Immer.
EXPLAIN (ANALYZE, BUFFERS) fügt eine Zeile wie diese hinzu:
Buffers: shared hit=1520 read=8943 dirtied=12
hit— 8 KB große Blöcke, die im Shared-Buffer-Cache von PostgreSQL gefunden wurden.read— Blöcke, die von außerhalb der Shared Buffers kommen mussten (OS-Cache oder Festplatte).dirtied/written— Blöcke, die während der Abfrage geändert oder verdrängt wurden.
Buffer-Zahlen erklären den Unterschied zwischen „schnell in meiner Session, langsam in der Produktion": derselbe Plan ist um Größenordnungen langsamer, wenn seine Blöcke nicht gecacht sind. Sie machen auch Bloat sichtbar — eine Abfrage, die 50.000 Blöcke liest, um 100 schmale Zeilen zurückzugeben, liest überwiegend toten Raum oder scannt weit mehr, als sie sollte.
Weitere Ausgaben, die man kennen sollte
Rows Removed by Filter— geholte und dann verworfene Zeilen. Eine große Zahl bedeutet, dass der Zugriffspfad nicht selektiv ist: ein Index (oder ein besserer Index) könnte die meisten dieser Lesevorgänge vermeiden.Sort Method: external merge Disk: 210400kB— die Sortierung passte nicht inwork_memund lagerte auf die Festplatte aus. Dieselbe Geschichte bei Hash-Joins, dieBatches: 4melden (mehr als ein Batch = Auslagerung).(never executed)— der Knoten lief nie (zum Beispiel erzeugte die andere Seite des Joins null Zeilen). Seine Zahlen sind bedeutungslos; optimieren Sie ihn nicht.loopsmit parallelen Workern — ein paralleler Knoten zeigt eine Schleife pro Worker; die Zahlen pro Schleife sind Durchschnittswerte pro Worker.
Eine Lese-Checkliste
- Finden Sie die gesamte
Execution Timeam Ende — das ist Ihr Budget. - Gehen Sie den Baum nach Knoten durch, deren exklusive Zeit (Gesamt minus Kindknoten, mal Schleifen) einen großen Anteil daran ausmacht. Meist dominieren 1–3 Knoten.
- Vergleichen Sie für jeden heißen Knoten geschätzte vs. tatsächliche Zeilen. Eine 10×-Lücke ist ein Planer-Problem, bevor es ein Hardware-Problem ist.
- Prüfen Sie
BUFFERS: liest er weit mehr Daten, als das Ergebnis rechtfertigt? - Erst dann denken Sie über Abhilfen nach: Statistiken, Indexe, Query-Form,
work_mem— ungefähr in dieser Reihenfolge der Wahrscheinlichkeit.
