VACUUM, autovacuum e bloat delle tabelle

PostgreSQL non aggiorna mai una riga sul posto. Ogni UPDATE scrive una nuova versione della riga e marca come morta quella vecchia; ogni DELETE si limita a marcare. Lo spazio viene recuperato più tardi, in modo asincrono, da VACUUM. Quando quel ciclo funziona, non ci pensi mai. Quando resta indietro, tabelle e indici crescono silenziosamente — è il bloat — e ogni query ne paga il prezzo.

Perché esistono le righe morte: MVCC

Sotto MVCC (multi-version concurrency control), i lettori non bloccano mai gli scrittori e viceversa, perché ogni transazione vede uno snapshot consistente: le vecchie versioni delle righe vengono conservate finché qualche transazione potrebbe ancora averne bisogno. Nel momento in cui nessuna transazione attiva può vedere una versione morta, essa diventa spazzatura — ma PostgreSQL non la recupera inline. È il lavoro di VACUUM.

Due conseguenze immediate:

Cosa fa davvero VACUUM (e cosa non fa)

Come autovacuum decide di eseguire

Il launcher di autovacuum controlla periodicamente ogni tabella rispetto a una soglia:

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

Lo scale factor di default del 20% va bene per le tabelle piccole ed è terribile per quelle grandi: una tabella da 100 milioni di righe accumula 20 milioni di righe morte prima ancora che autovacuum parta. Per tabelle grandi e calde, imposta un override per tabella:

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

Autovacuum è anche deliberatamente frenato da ritardi basati sul costo (autovacuum_vacuum_cost_delay / autovacuum_vacuum_cost_limit) in modo da non saturare l'I/O. Sull'hardware moderno i default sono conservativi; se autovacuum gira di continuo ma non recupera mai, alzare autovacuum_vacuum_cost_limit è di solito la prima leva.

Rilevare il bloat prima che faccia danni

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;

Aggiornamenti HOT: il pasto gratis per cui vale la pena progettare

Se un UPDATE modifica solo colonne che non sono indicizzate, e la nuova versione entra nella stessa pagina, PostgreSQL esegue un aggiornamento HOT (heap-only tuple): non viene scritta alcuna voce di indice, e la versione morta può essere ripulita a basso costo all'interno della pagina. Ecco perché aggiungere "solo un altro indice" su una colonna aggiornata di frequente può rovinare le performance di scrittura — trasforma ogni aggiornamento HOT in uno completo. Un fillfactor più basso (es. 80–90 per le tabelle con molti update) lascia spazio su ogni pagina e alza il tasso di HOT.

Regole pratiche

Sospetti che il bloat stia rallentando una query? Eseguila con EXPLAIN (ANALYZE, BUFFERS) e butta l'output nel Visualizzatore EXPLAIN per vedere quale nodo sta facendo le letture in eccesso.
🧯 Errori correlati: quando il vacuum resta indietro abbastanza da darti "not accepting commands to avoid wraparound data loss"; la crescita fuori controllo finisce in No space left on device; e sulle repliche, la pulizia causa conflitti con il recovery.