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
BUFFERSliest âshared hitvs.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
- Was EXPLAIN ANALYZE tatsÀchlich macht
- Anatomie einer Planzeile
- GeschÀtzte vs. tatsÀchliche Zeilen
- Loops: Warum ein billiger Knoten dominieren kann
- Buffers lesen: Cache-Treffer vs. Disk-LesevorgÀnge
- Die drei Muster, die auf einen fehlenden Index hinweisen
- Eine vollstĂ€ndige Schritt-fĂŒr-Schritt-Analyse
- HĂ€ufige Fallstricke
- 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
ANALYZEdeutlich verĂ€ndert, und autovacuum ist noch nicht nachgekommen. FĂŒhreANALYZE 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 STATISTICSfĂŒ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_targetoder das Setzen pro Spalte mitALTER TABLE ... ALTER COLUMN ... SET STATISTICSgibt 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:
- Den richtigen Indextyp wĂ€hlen â B-tree, GIN, GiST und BRIN und bei welchen Abfrageformen jeder davon tatsĂ€chlich hilft.
- N+1-Abfragen in Django finden und beseitigen â das Schleifenmuster aus diesem Beitrag, gesehen von der ORM-Seite statt von der Planseite.
- Connection Pooling fĂŒr Python-Apps â denn ein schneller Abfrageplan hilft nicht, wenn Requests auf eine Verbindung wartend in der Schlange stehen.
- pgvector vs. eine dedizierte Vektor-Datenbank â das Lesen von PlĂ€nen fĂŒr Ăhnlichkeitssuche hat seine eigenen Eigenheiten, die man kennen sollte.
- Postgres-Migrationen ohne Downtime â genau die Indizes aus diesem Beitrag bauen, ohne die Tabelle zu sperren, die man gerade beschleunigen will.
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?
