VACUUM, Autovacuum und Tabellen-Bloat

PostgreSQL aktualisiert eine Zeile nie an Ort und Stelle. Jedes UPDATE schreibt eine neue Zeilenversion und markiert die alte als tot; jedes DELETE markiert nur. Der Platz wird später asynchron von VACUUM zurückgewonnen. Wenn diese Schleife funktioniert, denken Sie nie daran. Wenn sie hinterherhinkt, wachsen Tabellen und Indexe stillschweigend — das ist Bloat — und jede Abfrage bezahlt dafür.

Warum tote Zeilen überhaupt existieren: MVCC

Unter MVCC (Multi-Version Concurrency Control) blockieren Leser nie Schreiber und umgekehrt, weil jede Transaktion einen konsistenten Snapshot sieht: alte Zeilenversionen werden so lange aufbewahrt, wie irgendeine Transaktion sie noch benötigen könnte. In dem Moment, in dem keine aktive Transaktion eine tote Version mehr sehen kann, wird sie zu Müll — aber PostgreSQL gewinnt sie nicht inline zurück. Das ist VACUUMs Aufgabe.

Zwei unmittelbare Konsequenzen:

Was VACUUM tatsächlich tut (und nicht tut)

Wie Autovacuum entscheidet, zu laufen

Der Autovacuum-Launcher prüft jede Tabelle regelmäßig gegen einen Schwellenwert:

vacuum when:  n_dead_tup  >  autovacuum_vacuum_threshold
                             + autovacuum_vacuum_scale_factor × reltuples
-- defaults:  50 + 0.20 × reltuples  (20% of the table)

Der voreingestellte 20-%-Scale-Factor ist für kleine Tabellen in Ordnung und schrecklich für große: eine Tabelle mit 100 Millionen Zeilen sammelt 20 Millionen tote Zeilen an, bevor Autovacuum überhaupt startet. Setzen Sie für große, heiße Tabellen einen Override pro Tabelle:

ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.01,
  autovacuum_vacuum_threshold    = 10000
);

Autovacuum wird außerdem bewusst durch kostenbasierte Verzögerungen gedrosselt (autovacuum_vacuum_cost_delay / autovacuum_vacuum_cost_limit), damit es I/O nicht sättigt. Auf moderner Hardware sind die Voreinstellungen konservativ; wenn Autovacuum ständig läuft, aber nie aufholt, ist das Erhöhen von autovacuum_vacuum_cost_limit meist der erste Hebel.

Bloat erkennen, bevor es wehtut

SELECT relname,
       n_live_tup,
       n_dead_tup,
       last_autovacuum,
       last_autoanalyze
FROM   pg_stat_user_tables
ORDER  BY n_dead_tup DESC
LIMIT  20;

HOT-Updates: das kostenlose Mittagessen, für das sich Engineering lohnt

Wenn ein UPDATE nur Spalten ändert, die nicht indexiert sind, und die neue Version auf dieselbe Seite passt, führt PostgreSQL ein HOT-Update (Heap-Only-Tuple) durch: es werden überhaupt keine Index-Einträge geschrieben, und die tote Version kann günstig innerhalb der Seite bereinigt werden. Deshalb kann das Hinzufügen von „nur einem weiteren Index" auf einer häufig aktualisierten Spalte die Schreib-Performance ruinieren — es macht aus jedem HOT-Update ein vollständiges. Ein niedrigerer fillfactor (z. B. 80–90 für aktualisierungslastige Tabellen) lässt Platz auf jeder Seite und erhöht die HOT-Rate.

Faustregeln

Vermuten Sie, dass Bloat eine Abfrage verlangsamt? Führen Sie sie mit EXPLAIN (ANALYZE, BUFFERS) aus und werfen Sie die Ausgabe in den EXPLAIN Visualizer, um zu sehen, welcher Knoten das übermäßige Lesen verursacht.
🧯 Verwandte Fehler: wenn Vacuum schlimm genug hinterherhinkt, erhalten Sie „not accepting commands to avoid wraparound data loss"; unkontrolliertes Wachstum endet in No space left on device; und auf Replikaten verursacht die Bereinigung Konflikte mit der Recovery.