VACUUM, autovacuum et bloat des tables

PostgreSQL ne met jamais à jour une ligne sur place. Chaque UPDATE écrit une nouvelle version de ligne et marque l'ancienne comme morte ; chaque DELETE se contente de marquer. L'espace est récupéré plus tard, de façon asynchrone, par VACUUM. Quand cette boucle fonctionne, vous n'y pensez jamais. Quand elle prend du retard, les tables et les index grossissent silencieusement — c'est le bloat — et chaque requête en paie le prix.

Pourquoi les lignes mortes existent : le MVCC

Avec le MVCC (contrôle de concurrence multiversion), les lecteurs ne bloquent jamais les écrivains et inversement, car chaque transaction voit un instantané cohérent : les anciennes versions de lignes sont conservées tant qu'une transaction pourrait encore en avoir besoin. Dès qu'aucune transaction active ne peut voir une version morte, celle-ci devient un déchet — mais PostgreSQL ne la récupère pas en ligne. C'est le travail de VACUUM.

Deux conséquences immédiates :

Ce que VACUUM fait réellement (et ne fait pas)

Comment autovacuum décide de s'exécuter

Le lanceur autovacuum vérifie périodiquement chaque table par rapport à un seuil :

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

Le facteur d'échelle par défaut de 20 % convient aux petites tables et est catastrophique pour les grandes : une table de 100 millions de lignes accumule 20 millions de lignes mortes avant même qu'autovacuum ne démarre. Pour les grandes tables très sollicitées, définissez une surcharge par table :

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

Autovacuum est aussi délibérément bridé par des délais fondés sur le coût (autovacuum_vacuum_cost_delay / autovacuum_vacuum_cost_limit) afin de ne pas saturer les I/O. Sur du matériel moderne, les valeurs par défaut sont conservatrices ; si autovacuum s'exécute en permanence sans jamais rattraper son retard, augmenter autovacuum_vacuum_cost_limit est généralement le premier levier.

Détecter le bloat avant qu'il ne fasse mal

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;

Les mises à jour HOT : le repas gratuit qui vaut la peine d'être conçu

Si un UPDATE ne modifie que des colonnes qui ne sont pas indexées, et que la nouvelle version tient sur la même page, PostgreSQL effectue une mise à jour HOT (heap-only tuple) : aucune entrée d'index n'est écrite, et la version morte peut être nettoyée à bon marché au sein de la page. C'est pourquoi ajouter « juste un index de plus » sur une colonne fréquemment mise à jour peut ruiner les performances en écriture — cela transforme chaque mise à jour HOT en une mise à jour complète. Un fillfactor plus bas (par exemple 80 à 90 pour les tables riches en mises à jour) laisse de la place sur chaque page et augmente le taux de HOT.

Règles empiriques

Vous soupçonnez le bloat de ralentir une requête ? Lancez-la avec EXPLAIN (ANALYZE, BUFFERS) et déposez la sortie dans le EXPLAIN Visualizer pour voir quel nœud effectue les lectures excédentaires.
🧯 Erreurs liées : quand vacuum prend suffisamment de retard, vous obtenez « not accepting commands to avoid wraparound data loss » ; une croissance incontrôlée se termine en No space left on device ; et sur les réplicas, le nettoyage provoque des conflits avec la restauration.