PostgreSQL-Performance-Tuning für Einsteiger
Diese Anleitung zeigt dir das wichtigste PostgreSQL-Performance-Tuning für Einsteiger: sinnvolle Werte in der postgresql.conf mit klaren Faustregeln, B-Tree-Indizes richtig einsetzen, EXPLAIN und EXPLAIN ANALYZE lesen, autovacuum verstehen und PgBouncer für viele Verbindungen.

Ab Werk startet PostgreSQL mit sehr konservativen Standardwerten – shared_buffers liegt zum Beispiel bei nur 128 MB. Für einen Produktionsserver lohnt sich deshalb fast immer ein gezieltes PostgreSQL-Performance-Tuning. Diese Anleitung richtet sich an Admins und Einsteiger im Mittelstand, die einen self-hosted PostgreSQL-Server auf Debian 12 oder Ubuntu 24.04 LTS betreiben. Du lernst, welche Parameter in der postgresql.conf wirklich zählen, wie du mit Faustregeln passende Werte setzt, wann ein Index hilft, wie du den Abfrageplan mit EXPLAIN liest und wie autovacuum und PgBouncer dir die Arbeit abnehmen. Anders als reine Backup- oder Slow-Query-Artikel geht es hier um die Grundkonfiguration und das Verstehen des Abfrageplaners.
Voraussetzungen
Bevor du Werte änderst, sollten ein paar Grundlagen stimmen. Ohne den richtigen Zugriff und ein wenig Wissen über deinen Server gehst du sonst ins Blaue.
- Server: Debian 12 oder Ubuntu 24.04 LTS mit installiertem PostgreSQL (Version 16 oder neuer). Die Pfade in dieser Anleitung nutzen
16– passe die Versionsnummer an deine Installation an. - Zugriff:
sudo-Rechte auf dem Server und Zugang zur Datenbank über denpostgres-Systembenutzer bzw.psql. - RAM bekannt: Du solltest wissen, wie viel Arbeitsspeicher der Server hat. Die Faustregeln in dieser Anleitung beziehen sich auf einen dedizierten DB-Server – läuft auf der Maschine noch eine Web-App, plane den Speicher entsprechend zurückhaltender.
- Speichertyp: Wisse, ob die Datenbank auf SSD/NVMe oder klassischer HDD liegt – das entscheidet über
random_page_cost. - Wartungsfenster: Einige Parameter brauchen einen Neustart von PostgreSQL. Plane dafür ein kurzes Fenster ein.
Schritt 1: Konfigurationsdatei finden und Werte prüfen
Zuerst musst du wissen, wo deine postgresql.conf liegt und welche Werte aktuell gesetzt sind. Auf Debian und Ubuntu liegt die Datei normalerweise unter /etc/postgresql/16/main/postgresql.conf. Den genauen Pfad fragst du direkt bei PostgreSQL ab.
sudo -u postgres psql -c "SHOW config_file;"
# Ausgabe i.d.R.: /etc/postgresql/16/main/postgresql.conf (Versionsnummer anpassen)
Die aktuell wirksamen Werte siehst du in einer psql-Sitzung. SHOW zeigt einen einzelnen Parameter, über pg_settings bekommst du mehrere auf einen Blick – inklusive Einheit.
SHOW shared_buffers;
SHOW work_mem;
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('shared_buffers','work_mem','maintenance_work_mem',
'effective_cache_size','max_connections');
So siehst du sofort, ob noch die konservativen Defaults aktiv sind: shared_buffers mit 128 MB, work_mem mit 4 MB, maintenance_work_mem mit 64 MB, effective_cache_size mit 4 GB und max_connections mit 100.
Schritt 2: Speicher-Parameter mit Faustregeln setzen
Die wichtigsten Stellschrauben betreffen den Arbeitsspeicher. Hier sind die Faustregeln für einen dedizierten DB-Server und was jeder Parameter bewirkt.
| Parameter | Default | Faustregel | Wirkung |
|---|---|---|---|
shared_buffers | 128 MB | ca. 25 % RAM (nicht über 40 %) | von PostgreSQL selbst verwalteter Cache |
effective_cache_size | 4 GB | 50–75 % RAM | nur Planner-Hinweis, reserviert keinen Speicher |
work_mem | 4 MB | 16–64 MB (konservativ) | Speicher pro Sortier-/Hash-Operation und Verbindung |
maintenance_work_mem | 64 MB | 256 MB bis 1 GB | für VACUUM, CREATE INDEX, FK-Anlage |
max_connections | 100 | moderat, lieber PgBouncer | jede Verbindung kostet Speicher |
Eine wichtige Warnung vorweg: work_mem gilt pro Sortier- oder Hash-Knoten und pro Verbindung, nicht global. Bei vielen parallelen Abfragen mit mehreren Knoten kann ein zu hoher Wert den RAM sprengen und den OOM-Killer auslösen. Halte den globalen Wert deshalb niedrig und erhöhe ihn nur für einzelne Sitzungen per SET.
Für einen dedizierten Server mit 16 GB RAM und SSD sieht ein guter Startpunkt in der postgresql.conf so aus:
shared_buffers = 4GB # ~25% RAM
effective_cache_size = 12GB # ~75% RAM (nur Planner-Hinweis)
work_mem = 32MB # pro Sort/Hash je Verbindung -> konservativ
maintenance_work_mem = 1GB # schnelleres VACUUM/CREATE INDEX
max_connections = 100
random_page_cost = 1.1 # fuer SSD/NVMe
Der Wert effective_cache_size reserviert keinen Speicher – er ist nur eine Schätzung für den Planner, wie viel Cache (PostgreSQL plus Betriebssystem) verfügbar ist. Ist er zu niedrig, meidet der Planner unnötig Index Scans. random_page_cost steht per Default auf 4.0 (Annahme einer langsamen HDD); auf SSD/NVMe senkst du ihn auf 1.1, damit der Planner Index Scans bevorzugt.
Schritt 3: Checkpoint- und WAL-Settings anpassen
Schreiblastige Systeme profitieren davon, Checkpoints seltener und gleichmäßiger auszuführen. Die Defaults sind checkpoint_timeout = 5min, max_wal_size = 1GB, min_wal_size = 80MB und checkpoint_completion_target = 0.9. Für write-lastige Datenbanken vergrößerst du das Zeitfenster und den WAL-Spielraum.
checkpoint_timeout = 15min
max_wal_size = 4GB
min_wal_size = 1GB
checkpoint_completion_target = 0.9
Ein längerer checkpoint_timeout und ein größerer max_wal_size sorgen dafür, dass Schreibspitzen besser über die Zeit verteilt werden, statt die Platte in kurzen Stößen zu belasten. Übertreibe es nicht: Ein zu großer WAL-Spielraum verlängert die Wiederherstellungszeit nach einem Absturz.
Schritt 4: Änderungen aktivieren – reload oder restart?
Nicht jeder Parameter wird gleich aktiv. Viele Werte greifen mit einem einfachen Reload, bei dem laufende Verbindungen bestehen bleiben. shared_buffers und max_connections brauchen dagegen einen vollständigen Neustart.
sudo systemctl reload postgresql # fuer reload-faehige Parameter
sudo systemctl restart postgresql # fuer shared_buffers, max_connections
Ob ein Parameter Reload oder Restart braucht, verrät dir die Spalte context in pg_settings. Prüfe das im Zweifel vor einer Änderung, damit du nicht auf einen Effekt wartest, der erst nach einem Neustart eintritt.
SELECT name, setting, context
FROM pg_settings
WHERE name IN ('shared_buffers','max_connections','work_mem','random_page_cost');
Schritt 5: Indizes verstehen und anlegen
Der Standard-Indextyp in PostgreSQL ist der B-Tree (USING btree). Er passt für die häufigsten Fälle: Gleichheit (=), Vergleiche (<, >), BETWEEN, ORDER BY und IS NULL. Ein Index lohnt sich vor allem bei hoher Selektivität, wenn eine Abfrage also nur einen kleinen Teil der Zeilen zurückgibt. Liefert eine Abfrage rund 10 % oder mehr aller Zeilen, wählt der Planner oft zu Recht einen Seq Scan.
In Produktion legst du Indizes ohne Schreib-Lock an. Ein normales CREATE INDEX sperrt nämlich Schreibzugriffe auf die Tabelle, bis es fertig ist. Mit CONCURRENTLY bleibt die Tabelle beschreibbar.
CREATE INDEX CONCURRENTLY idx_kunden_email ON kunden (email);
Beachte dabei: CONCURRENTLY läuft nicht innerhalb eines Transaktionsblocks und scannt die Tabelle zweimal, dauert also länger. In einem Wartungsfenster ohne Schreiblast ist das einfache CREATE INDEX schneller. Vermeide zu viele Indizes: Jeder Index verlangsamt INSERT, UPDATE und DELETE und kostet Plattenplatz.
Schritt 6: EXPLAIN und EXPLAIN ANALYZE lesen
Mit EXPLAIN verstehst du, wie PostgreSQL eine Abfrage ausführt. EXPLAIN allein zeigt nur den geschätzten Plan, ohne die Abfrage auszuführen. EXPLAIN ANALYZE führt die Abfrage tatsächlich aus und zeigt zusätzlich die realen Laufzeiten und Zeilenzahlen.
EXPLAIN SELECT * FROM kunden WHERE email = 'a@b.de';
-- nur Schaetzung, fuehrt nichts aus
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM kunden WHERE email = 'a@b.de';
-- fuehrt aus, zeigt reale Zeiten und Buffer-Treffer
So liest du die wichtigsten Angaben im Plan:
- cost=Start..Gesamt – geschätzte Startkosten bis zur ersten Zeile und Gesamtkosten in einer abstrakten Einheit (keine Sekunden).
- rows – die vom Planner geschätzte Zeilenzahl, width die geschätzte Zeilenbreite in Bytes.
- actual time und rows (nur bei ANALYZE) – die tatsächlich gemessene Zeit und reale Zeilenzahl. Weichen Schätzung und Realität stark ab, sind oft veraltete Statistiken die Ursache.
Achte besonders auf den Scan-Typ. Ein Seq Scan liest die ganze Tabelle – bei kleinen Tabellen völlig in Ordnung, bei großen Tabellen mit selektivem Filter ein Warnsignal für einen fehlenden Index. Ein Index Scan nutzt einen Index. Der Bitmap Heap/Index Scan ist der Mittelweg und kommt zum Einsatz, wenn ein Index passt, aber mehr Zeilen betroffen sind.
Vorsicht:
EXPLAIN ANALYZEführt die Abfrage wirklich aus. BeiINSERT,UPDATEoderDELETEverändert es also Daten. Kapsle solche Statements bei Bedarf inBEGIN; ... ROLLBACK;, um die Änderung zurückzurollen.
Schritt 7: Statistiken und autovacuum verstehen
Der Planner trifft seine Entscheidungen anhand von Statistiken über die Daten. Sind diese veraltet, wählt er falsche Pläne – etwa einen Seq Scan, obwohl ein Index Scan besser wäre. Das passiert typischerweise direkt nach einem großen Datenimport. Aktualisiere die Statistiken dann manuell, statt auf autovacuum zu warten.
ANALYZE kunden; -- aktualisiert nur die Statistik
VACUUM (ANALYZE) kunden; -- raeumt tote Zeilen auf und aktualisiert die Statistik
Im Normalbetrieb erledigt das autovacuum automatisch – es ist per Default eingeschaltet. VACUUM und ANALYZE starten, sobald sich genug Zeilen geändert haben: Die Standardschwelle ist autovacuum_vacuum_scale_factor von 0.2 (also 20 % der Tabelle) plus autovacuum_vacuum_threshold von 50 Zeilen. Bei sehr großen, häufig geänderten Tabellen kann es sinnvoll sein, diese Werte pro Tabelle abzusenken, damit autovacuum öfter und in kleineren Schritten läuft.
Schritt 8: Fehlende und ungenutzte Indizes finden
PostgreSQL führt Statistiken darüber, wie oft Tabellen sequenziell gelesen und Indizes genutzt werden (sofern track_counts auf on steht, was Default ist). Damit findest du Kandidaten für neue Indizes und überflüssige Indizes zum Aufräumen.
Tabellen mit vielen sequenziellen Scans deuten auf fehlende Indizes hin:
SELECT relname, seq_scan, seq_tup_read, idx_scan, n_live_tup
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 20;
Ein hoher seq_scan bei einer großen Tabelle (n_live_tup) ist ein klares Signal, dass hier ein passender Index fehlt. Umgekehrt findest du ungenutzte Indizes über idx_scan = 0 – sie kosten nur Schreibleistung und Platz:
SELECT s.relname AS tabelle, s.indexrelname AS index, s.idx_scan,
pg_size_pretty(pg_relation_size(s.indexrelid)) AS groesse
FROM pg_stat_user_indexes s
JOIN pg_index i ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0 AND NOT i.indisprimary AND NOT i.indisunique
ORDER BY pg_relation_size(s.indexrelid) DESC;
Indizes mit idx_scan = 0, die weder Primärschlüssel noch Unique sind, kannst du nach Prüfung in der Regel löschen. Lass die Statistiken aber eine Weile laufen, bevor du voreilig löschst – ein Index könnte nur für seltene, aber wichtige Abfragen da sein.
Langsamste Abfragen mit pg_stat_statements finden
Um die teuersten Abfragen systematisch aufzuspüren, aktivierst du die Erweiterung pg_stat_statements. Trage sie in shared_preload_libraries ein (Neustart nötig) und lege die Erweiterung in der Datenbank an.
-- 1) in postgresql.conf: shared_preload_libraries = 'pg_stat_statements' (Neustart noetig)
-- 2) in der Datenbank:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT query, calls, mean_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
Schritt 9: Connection-Pooling mit PgBouncer
Jede Datenbankverbindung kostet Speicher. max_connections einfach hochzudrehen ist deshalb kein echtes Tuning. Bei vielen kurzlebigen Verbindungen – etwa von einer Web-Anwendung – ist PgBouncer die richtige Lösung: ein schlanker Connection-Pooler, der eine kleine Zahl echter DB-Verbindungen wiederverwendet.
sudo apt update && sudo apt install -y pgbouncer
Die Konfiguration liegt in /etc/pgbouncer/pgbouncer.ini. Für die meisten Fälle ist der transaction-Modus richtig – eine Verbindung wird nur für die Dauer einer Transaktion an einen Client vergeben.
[databases]
meinedb = host=127.0.0.1 port=5432 dbname=meinedb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25
reserve_pool_size = 5
Danach aktivierst und startest du den Dienst. Deine Anwendung verbindet sich anschließend über Port 6432 statt direkt über 5432.
sudo systemctl enable --now pgbouncer
Wichtig: Der transaction-Modus bricht einige Features. Prepared Statements, gesetzte Session-Variablen (SET), Advisory Locks und LISTEN/NOTIFY funktionieren dann nicht mehr zuverlässig. Anwendungen, die darauf bauen, brauchen entweder den session-Modus oder eine Anpassung.
Typische Fehler
- work_mem zu hoch gesetzt: Der Wert gilt pro Sortier-/Hash-Operation und pro Verbindung.
work_memmal viele parallele Queries mal mehrere Knoten kann den RAM sprengen und den OOM-Killer auslösen. Global niedrig halten, einzeln perSETerhöhen. - effective_cache_size missverstanden: Der Parameter reserviert keinen Speicher, er ist nur ein Hinweis an den Planner. Zu niedrig führt dazu, dass Index Scans unnötig gemieden werden.
- Reload statt Restart erwartet:
shared_buffersundmax_connectionsbrauchen einen Neustart. Prüfe vor jeder Änderung die Spaltecontextinpg_settings. - EXPLAIN ANALYZE auf schreibende Statements: Es führt INSERT/UPDATE/DELETE wirklich aus und verändert Daten. In
BEGIN; ... ROLLBACK;kapseln. - Statistiken nach Import vergessen: Nach großen Importen sind die Statistiken veraltet und der Planner wählt falsche Pläne. Manuell
ANALYZEausführen, nicht auf autovacuum warten. - max_connections als Tuning: Einfaches Hochdrehen ist kein Tuning. Bei vielen Verbindungen gehört PgBouncer im transaction-Modus davor.
- PgBouncer transaction-Modus blind eingesetzt: Er bricht Prepared Statements, Session-State, Advisory Locks und LISTEN/NOTIFY. Prüfe, ob deine Anwendung das verträgt.
- Zu viele oder ungenutzte Indizes: Sie verlangsamen Schreibzugriffe und kosten Platz. Ungenutzte Indizes (
idx_scan = 0) regelmäßig aufräumen. - CREATE INDEX in Produktion ohne CONCURRENTLY: Sperrt Schreibzugriffe bis fertig. In Produktion immer
CREATE INDEX CONCURRENTLYnutzen (läuft aber nicht im Transaktionsblock).
Häufige Fragen
Wie viel RAM soll ich shared_buffers geben?
Auf einem dedizierten DB-Server etwa 25 % des Arbeitsspeichers, in der Regel nicht über 40 %. Der Rest steht dem Betriebssystem-Cache und den Abfragen (work_mem) zur Verfügung. Diese Änderung erfordert einen Neustart von PostgreSQL.
Warum nutzt der Planner meinen Index nicht?
Häufige Gründe sind veraltete Statistiken (führe ANALYZE aus), eine zu geringe Selektivität (bei rund 10 % oder mehr betroffener Zeilen ist ein Seq Scan korrekt) oder ein zu hoher random_page_cost auf SSD-Systemen. Senke ihn dort auf 1.1, damit Index Scans attraktiver werden.
Was ist der Unterschied zwischen EXPLAIN und EXPLAIN ANALYZE?
EXPLAIN zeigt nur den geschätzten Plan und führt die Abfrage nicht aus. EXPLAIN ANALYZE führt die Abfrage tatsächlich aus und zeigt reale Laufzeiten und Zeilenzahlen. Bei schreibenden Statements verändert ANALYZE die Daten – deshalb dort mit BEGIN; ... ROLLBACK; arbeiten.
Muss ich autovacuum selbst starten?
Nein, autovacuum läuft per Default automatisch und hält Statistiken und tote Zeilen in Schach. Manuell eingreifen solltest du nur in Sonderfällen, etwa direkt nach einem großen Datenimport mit einem expliziten ANALYZE oder VACUUM (ANALYZE).
Soll ich max_connections einfach erhöhen?
Besser nicht. Jede Verbindung belegt Speicher, und viele Verbindungen bringen kaum Mehrleistung. Bei vielen kurzlebigen Verbindungen setzt du PgBouncer im transaction-Modus davor und lässt max_connections moderat.
Brauche ich für SSD eine andere Konfiguration?
Ja, einen Wert: Senke random_page_cost von 4.0 (HDD-Annahme) auf 1.1. Damit weiß der Planner, dass zufällige Zugriffe auf SSD/NVMe günstig sind, und bevorzugt Index Scans, wo sie sinnvoll sind.
Fazit
PostgreSQL-Performance-Tuning für Einsteiger ist kein Hexenwerk: Schon das Anheben der konservativen Speicher-Defaults nach klaren Faustregeln (shared_buffers ~25 % RAM, effective_cache_size 50–75 % RAM, work_mem bewusst niedrig, maintenance_work_mem großzügig) bringt spürbar mehr Leistung. Dazu kommen passende B-Tree-Indizes für selektive Abfragen, das Lesen von EXPLAIN (ANALYZE, BUFFERS), ein Verständnis für autovacuum und Statistiken sowie PgBouncer für viele Verbindungen. Arbeite die Schritte der Reihe nach ab, miss vorher und nachher mit EXPLAIN ANALYZE und ändere immer nur eine Stellschraube auf einmal – so siehst du, was wirklich wirkt.
Weiterführende Anleitungen und Quellen
- MariaDB/MySQL installieren und absichern – die Alternative im relationalen Datenbankumfeld, mit Fokus auf Installation und Härtung.
- Grafana-Dashboards bauen: Datenquelle und Panel – um Datenbank-Metriken dauerhaft sichtbar zu machen und Engpässe früh zu erkennen.
- Weitere Anleitungen in der Kategorie Datenbanken.
Quellen: offizielle PostgreSQL-Dokumentation – Resource Consumption (shared_buffers, work_mem, maintenance_work_mem), Using EXPLAIN, WAL Configuration sowie die PgBouncer-Konfigurationsreferenz.