Ein systematischer Leitfaden zum Debuggen langsamer PostgreSQL-Abfragen
Debuggen Sie langsame PostgreSQL-Abfragen mit pg_stat_statements, EXPLAIN ANALYZE, Puffer-Lesevorgängen, Zeilenschätzungen, Indizes und Verifizierung.
Ein systematischer Leitfaden zum Debuggen langsamer PostgreSQL-Abfragen
Langsame PostgreSQL-Abfragen sind einfacher zu beheben, wenn Sie aufhören, sie als Rätsel zu behandeln. Die Datenbank kann Ihnen normalerweise den gewählten Pfad anzeigen, wie viele Zeilen sie erwartet hat, wie viele Zeilen sie tatsächlich berührt hat, ob sie aus dem Cache oder von der Festplatte gelesen hat und ob sie auf eine andere Sitzung gewartet hat.
Der Fehler, den ich am häufigsten sehe, ist der direkte Sprung von "dieser Endpunkt ist langsam" zu "einen Index erstellen". Manchmal funktioniert das. Manchmal wird der Index ignoriert, weil das Prädikat so geschrieben ist, dass es nicht verwendet werden kann. Manchmal ist die Abfrage in Ordnung, wird aber von einer Transaktion blockiert, die seit zwanzig Minuten geöffnet ist. Ein systematischer Durchgang spart Zeit, weil er Abfrageform, Planerschätzungen, E/A, Speicher und Sperren trennt.
Grundlegendes zu Engpässen bei der Abfrageleistung
Bevor Sie in die Werkzeuge eintauchen, ist es wichtig, häufige Gründe zu erkennen, warum eine PostgreSQL-Abfrage schlecht performen könnte. Diese Probleme fallen normalerweise in einige Schlüsselkategorien:
- Fehlende oder ineffiziente Indizes: Die Datenbank ist gezwungen, sequentielle Scans auf großen Tabellen durchzuführen, obwohl ein Index schnellen Zugriff bieten könnte.
- Suboptimale Abfragestruktur: Komplexe Joins, unnötige Unterabfragen oder schlechte Verwendung von Funktionen können den Planer verwirren.
- Veraltete Statistiken: PostgreSQL verlässt sich auf Statistiken, um effiziente Ausführungspläne zu erstellen. Wenn Statistiken veraltet sind, könnte der Planer einen ineffizienten Pfad wählen.
- Ressourcenkonflikte: Probleme wie hohe E/A-Wartezeiten, übermäßige Sperren oder unzureichender Arbeitsspeicher, der PostgreSQL zugewiesen ist.
Schritt 1: Identifizieren der langsamen Abfrage
Bevor Sie eine langsame Abfrage beheben können, müssen Sie sie genau identifizieren. Sich auf Benutzerbeschwerden zu verlassen, ist ineffizient; Sie benötigen empirische Daten von der Datenbank selbst.
Verwendung von pg_stat_statements
Die effektivste Methode zur Verfolgung ressourcenintensiver Abfragen in einer Produktionsumgebung ist die Verwendung der Erweiterung pg_stat_statements. Dieses Modul verfolgt Ausführungsstatistiken für alle Abfragen, die gegen die Datenbank ausgeführt werden.
Aktivieren der Erweiterung (erfordert Superuser-Rechte und Neuladen der Konfiguration):
-- 1. Stellen Sie sicher, dass es in postgresql.conf aufgeführt ist
-- shared_preload_libraries = 'pg_stat_statements'
-- 2. Verbinden Sie sich mit der Datenbank und erstellen Sie die Erweiterung
CREATE EXTENSION pg_stat_statements;
Abfragen der Hauptverursacher:
Um die Abfragen zu finden, die die meiste Gesamtzeit verbrauchen, verwenden Sie die folgende Abfrage:
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
rows
FROM
pg_stat_statements
ORDER BY
total_exec_time DESC
LIMIT 10;
In älteren PostgreSQL-Versionen können diese Spalten total_time und mean_time heißen. Verwenden Sie die Namen, die Ihr Server bereitstellt.
Gesamtzeit und Durchschnittszeit beantworten unterschiedliche Fragen. Eine Abfrage, die durchschnittlich 20 Millisekunden dauert, aber eine Million Mal ausgeführt wird, kann Ihre größten Datenbankkosten verursachen. Eine Abfrage, die einmal pro Stunde 40 Sekunden läuft, mag für einen Benutzer schmerzhaft sein, ist aber für das gesamte System weniger wichtig. Betrachten Sie beides.
Wenn Sie die aktuell langsame Abfrage benötigen, nicht die historisch teure, überprüfen Sie aktive Sitzungen:
SELECT pid, now() - query_start AS age, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY query_start;
Schritt 2: Analysieren des Ausführungsplans mit EXPLAIN ANALYZE
Sobald eine langsame Abfrage isoliert ist, besteht der nächste kritische Schritt darin zu verstehen, wie PostgreSQL sie ausführt. Der Befehl EXPLAIN zeigt den beabsichtigten Plan an, aber EXPLAIN ANALYZE führt die Abfrage tatsächlich aus und meldet die tatsächliche Zeit, die für jeden Schritt benötigt wurde.
Syntax und Verwendung
Wickeln Sie Ihre langsame Abfrage immer mit EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) für die detaillierteste Ausgabe ein. Die Option BUFFERS ist entscheidend, da sie die Festplatten-E/A-Aktivität anzeigt.
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM large_table lt
JOIN other_table ot ON lt.id = ot.lt_id
WHERE lt.status = 'active' AND lt.created_at > NOW() - INTERVAL '1 day';
Interpretieren der Ausgabe
Die Ausgabe wird von unten nach oben und von rechts nach links gelesen, da die innersten Knoten zuerst ausgeführt werden. Zu den wichtigsten Metriken, auf die Sie sich konzentrieren sollten, gehören:
cost=: Die geschätzten Kosten des Planers, nicht die Wanduhrzeit. Verwenden Sie sie, um Planentscheidungen zu vergleichen, nicht als Millisekunden.rows=: Die geschätzte Anzahl der von diesem Knoten verarbeiteten Zeilen.actual time=: Die tatsächliche Zeit in Millisekunden, die für diese spezifische Operation aufgewendet wurde.rows=(Tatsächlich): Die tatsächliche Anzahl der von diesem Knoten zurückgegebenen Zeilen.loops=: Wie oft dieser Knoten ausgeführt wurde (oft hoch bei verschachtelten Schleifen).
Erkennen von Ineffizienzen:
- Sequentielle Scans auf großen Tabellen: Wenn ein großer Tabellenzugriff
Seq Scananstelle einesIndex ScanoderBitmap Index Scanverwendet, benötigen Sie wahrscheinlich einen besseren Index. - Große Diskrepanz zwischen geschätzten und tatsächlichen Zeilen: Wenn der Planer 10 Zeilen geschätzt hat, der Knoten aber tatsächlich 1.000.000 Zeilen verarbeitet hat, sind die Statistiken veraltet oder der Planer hat eine schlechte Wahl getroffen.
- Hohe
actual timebei Joins/Sortierungen: Übermäßige Zeit, die fürHash Join,Merge JoinoderSort-Operationen aufgewendet wird, deutet oft auf unzureichenden Arbeitsspeicher (work_mem) oder die Unfähigkeit hin, Indizes effektiv zu nutzen.
Beachten Sie auch die Buffers-Zeilen. shared hit bedeutet, dass PostgreSQL Seiten im Cache gefunden hat. shared read bedeutet, dass es Seiten vom Speicher lesen musste. Eine Abfrage kann langsam sein, weil der Plan schlecht ist, oder weil der Plan vernünftig ist, aber eine große Menge kalter Daten von der Festplatte liest.
Tipp: Verwenden Sie für komplexe Pläne Online-Tools wie explain.depesz.com oder den visuellen Erklärungsplan-Viewer von pgAdmin, um die Ergebnisse grafisch zu interpretieren.
Schritt 3: Beheben häufiger Engpässe
Basierend auf Ihren EXPLAIN ANALYZE-Ergebnissen wenden Sie gezielte Korrekturen an.
Indexoptimierung
Wenn Seq Scan eine große Tabelle dominiert und die Abfrage selektiv ist, ziehen Sie Indizes für Spalten in Betracht, die in WHERE-, JOIN- und ORDER BY-Klauseln verwendet werden. Ein sequentieller Scan ist nicht automatisch schlecht; PostgreSQL kann ihn korrekt wählen, wenn die meisten Zeilen benötigt werden.
Beispiel: Wenn die Abfrage nach status filtert und dann über user_id joint:
-- Erstellen Sie einen zusammengesetzten Index für schnellere Suchvorgänge und Joins
CREATE INDEX idx_large_table_status_user_id ON large_table (status, user_id);
Verwenden Sie für Produktionssysteme CREATE INDEX CONCURRENTLY, wenn Sie vermeiden müssen, Schreibvorgänge zu blockieren, und denken Sie daran, dass es nicht innerhalb eines normalen Transaktionsblocks ausgeführt werden kann:
CREATE INDEX CONCURRENTLY idx_large_table_status_user_id
ON large_table (status, user_id);
Aktualisieren von Statistiken (VACUUM ANALYZE)
Wenn der Planer wild ungenaue Schätzungen vornimmt (Abweichung zwischen geschätzten und tatsächlichen Zeilen), erzwingen Sie eine Aktualisierung der Tabellenstatistiken.
ANALYZE VERBOSE table_name;
-- Für stark aktive Tabellen erwägen Sie niedrigere Autovacuum/Analyse-Schwellenwerte pro Tabelle.
Speicheroptimierung
Wenn Sortier- oder Hash-Operationen auf die Festplatte ausgelagert werden (oft angezeigt durch hohe E/A in der BUFFERS-Ausgabe oder langsame Sortierung), erhöhen Sie den verfügbaren Arbeitsspeicher von PostgreSQL.
-- Erhöhen Sie work_mem auf Sitzungsebene für das spezifische Abfragetesten
SET work_mem = '128MB';
-- Oder global in postgresql.conf für nachhaltige Leistungsverbesserungen
Warnung: Wenn Sie
work_memglobal zu hoch erhöhen, kann der Systemspeicher erschöpft werden, wenn viele komplexe Abfragen gleichzeitig ausgeführt werden. Passen Sie dies sorgfältig basierend auf der Serverkapazität an.
Achten Sie im Plan auf Details zur Sortier- oder Hash-Auslagerung. Neuere PostgreSQL-Ausgaben können Sortiermethoden wie external merge Disk: ... anzeigen, was ein klares Zeichen dafür ist, dass die Operation den verfügbaren Speicher überschritten und temporäre Dateien geschrieben hat.
Umschreiben von Abfragen
Manchmal ist die Struktur selbst das Problem. Vermeiden Sie nicht-SARGable Prädikate (Bedingungen, die die Indexnutzung verhindern), wie das Anwenden von Funktionen auf indizierte Spalten in der WHERE-Klausel:
Ineffizient (verhindert Indexnutzung):
WHERE DATE(created_at) = '2023-10-01'
Effizient (ermöglicht Indexnutzung):
WHERE created_at >= '2023-10-01 00:00:00' AND created_at < '2023-10-02 00:00:00'
Ein weiteres häufiges Muster ist das Auswählen von weit mehr Spalten als die Anwendung benötigt. SELECT * erschwert die Optimierung von Plänen, erhöht die Speichernutzung und kann zusätzliche Heap-Lesevorgänge erzwingen, wenn ein Index-Only-Scan sonst funktionieren würde. Listen Sie für heiße Pfade die Spalten bewusst auf.
Sperrenprüfungen
Wenn EXPLAIN ANALYZE in Ihrer Sitzung schnell ist, die Anwendung aber langsam, wartet die Abfrage möglicherweise, bevor sie überhaupt nützliche Ausführungszeit erhält. Überprüfen Sie Sperrwartezustände:
SELECT pid, wait_event_type, wait_event, now() - query_start AS age, query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
ORDER BY query_start;
Finden Sie dann den Blocker:
SELECT blocked.pid AS blocked_pid,
blocker.pid AS blocker_pid,
blocked.query AS blocked_query,
blocker.query AS blocker_query
FROM pg_stat_activity blocked
JOIN pg_locks blocked_locks
ON blocked_locks.pid = blocked.pid AND NOT blocked_locks.granted
JOIN pg_locks blocker_locks
ON blocker_locks.locktype = blocked_locks.locktype
AND blocker_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocker_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocker_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocker_locks.pid <> blocked_locks.pid
JOIN pg_stat_activity blocker
ON blocker.pid = blocker_locks.pid;
Die Lösung kann auf Anwendungsebene liegen: kürzere Transaktionen, Verschieben langsamer externer API-Aufrufe außerhalb der Transaktion, Vermeiden unnötiger SELECT ... FOR UPDATE oder Ändern der Reihenfolge, in der Tabellen aktualisiert werden, damit konkurrierende Transaktionen keinen Deadlock verursachen.
Ein kleines Beispiel: Die langsame Dashboard-Abfrage
Angenommen, ein Dashboard führt diese Abfrage alle paar Sekunden aus:
SELECT *
FROM orders
WHERE DATE(created_at) = CURRENT_DATE
AND status = 'paid'
ORDER BY created_at DESC
LIMIT 50;
Die Tabelle hat Millionen von Zeilen. EXPLAIN ANALYZE zeigt einen sequentiellen Scan, eine große Anzahl von durch Filter entfernten Zeilen und eine Sortierung. Der erste Instinkt könnte sein, created_at zu indizieren, aber das Prädikat umschließt die Spalte mit DATE(created_at), sodass ein normaler Index auf created_at weniger nützlich ist.
Schreiben Sie den Datumsfilter als Bereich um:
SELECT id, customer_id, total_cents, created_at
FROM orders
WHERE created_at >= CURRENT_DATE
AND created_at < CURRENT_DATE + INTERVAL '1 day'
AND status = 'paid'
ORDER BY created_at DESC
LIMIT 50;
Erwägen Sie dann einen Index, der dem Filter und der Sortierung entspricht:
CREATE INDEX CONCURRENTLY idx_orders_paid_created_at
ON orders (created_at DESC)
WHERE status = 'paid';
Dies ist kein universelles Indexrezept. Es funktioniert, wenn paid ein häufiger Dashboard-Filter ist und wenn die neuesten bezahlten Bestellungen das sind, was die Anwendung normalerweise abfragt. Wenn die Anwendung auch stark nach Konto filtert, wäre der bessere Index möglicherweise einer, der mit account_id beginnt. Der Punkt ist, den Index um das tatsächliche Zugriffsmuster herum zu entwerfen, nicht um eine einzelne in der Abfrage erwähnte Spalte.
Nach der Änderung sollte der Plan weniger gescannte Zeilen zeigen und idealerweise eine explizite Sortierung vermeiden. Wenn der Plan immer noch einen sequentiellen Scan wählt, überprüfen Sie, ob der Datumsbereich zu breit ist, ob die Statistiken veraltet sind oder ob die Abfrageparameter in der Produktion von Ihrem Testfall abweichen.
Schritt 4: Verifizierung und Überwachung
Nach der Implementierung einer Änderung führen Sie EXPLAIN ANALYZE erneut für genau dieselbe Abfrage mit vergleichbaren Parametern aus. Das Ziel ist nicht immer, einen Index-Scan zu sehen. Das Ziel ist, weniger Arbeit zu sehen: weniger durch Filter entfernte Zeilen, weniger gelesene Puffer, keine Festplattenauslagerung, bessere Zeilenschätzungen oder weniger Zeit, die im teuren Knoten verbracht wird.
Überwachen Sie weiterhin pg_stat_statements, um zu bestätigen, dass die geänderte Abfrage nicht mehr in der Liste der Hauptverursacher erscheint, und stellen Sie sicher, dass die Korrektur eine positive globale Auswirkung hat.
Beobachten Sie auch die Schreibkosten nach dem Hinzufügen von Indizes. Jeder neue Index muss während Einfügungen, Aktualisierungen und Löschungen gepflegt werden. Ein perfekter Leseindex für ein Dashboard kann ein schlechter Kompromiss sein, wenn er einen Pfad mit hohem Datenaufkommen verlangsamt. Überprüfen Sie für wichtige Tabellen beide Seiten: Hat sich die langsame Abfrage verbessert, und haben sich die Schreiblatenz oder das Tabellenwachstum danach verschlechtert?
Eine weitere Gewohnheit hilft bei echten Vorfällen: Testen Sie mit realistischen Parameterwerten. PostgreSQL kann unterschiedliche Pläne für einen Kunden mit zehn Zeilen und einen Kunden mit zehn Millionen Zeilen wählen. Wenn die Anwendung vorbereitete Anweisungen verwendet, können sich generische Pläne auch anders verhalten als die einmalige Abfrage, die Sie in psql einfügen. Wenn das Produktionsproblem einen Mandanten, ein Konto oder einen Datumsbereich betrifft, reproduzieren Sie diese Form so genau wie möglich in einer sicheren Umgebung.
Wenn die Abfrage destruktiv oder zu teuer ist, um sie mit EXPLAIN ANALYZE auszuführen, beginnen Sie mit einfachem EXPLAIN, führen Sie sie auf einem Staging-System aus oder wickeln Sie den Test in eine Transaktion ein, die Sie zurückrollen. Für UPDATE und DELETE können Sie dennoch viel aus dem Scan- und Join-Teil des Plans lernen, bevor Sie eine Produktionsänderung vornehmen.
Führen Sie eine kurze Notiz mit dem Vorher-Nachher-Plan, der Zeitmessung und dem Grund für die Änderung. Diese Gewohnheit verhindert spätere versehentliche Regressionen und gibt der nächsten Person eine echte Erklärung anstelle eines mysteriösen Indexnamens im Schema.