PostgreSQL 19 : REPACK, la compaction de table sans bloquer vos utilisateurs
Source originale : Understanding PostgreSQL REPACK Through repack.c — Chao Li, Highgo Software Inc. (avril 2026)
Chaque DELETE ou UPDATE laisse derrière lui des dead tuples : des lignes mortes qui gonflent votre table sans jamais vraiment disparaître. VACUUM les marque réutilisables, VACUUM FULL réécrit la table — mais au prix d'un verrou exclusif qui paralyse toute activité. PostgreSQL 19 introduit REPACK, une troisième voie qui réécrit physiquement la table tout en la gardant accessible pendant presque toute l'opération.
Cet article décrypte le fonctionnement interne de REPACK à travers l'analyse de repack.c, et vous donne une checklist pratique avant de l'utiliser en production.
Pourquoi REPACK et pas VACUUM FULL ?
| Critère | VACUUM | VACUUM FULL | REPACK CONCURRENTLY |
|---|---|---|---|
| Libère l'espace OS | ✗ (partiellement) | ✓ | ✓ |
| Table accessible en lecture | ✓ | ✗ | ✓ |
| Table accessible en écriture | ✓ | ✗ | ✓ (sauf phase finale) |
| Durée du verrou exclusif | Aucun | Toute l'opération | Quelques secondes |
REPACK en mode concurrent fonctionne en trois temps :
- Copie d'un snapshot : les tuples vivants sont copiés dans un nouveau fichier heap, basé sur un snapshot de départ.
- Replay des changements concurrents : via le décodage logique (
logical decoding), toutes les modifications intervenues pendant la copie sont rejouées sur le nouveau fichier. - Lock-and-swap : un verrou exclusif très court est pris le temps d'échanger les
relfilenode(le pointeur physique vers le fichier de données) et de mettre à jour les catalogues système.
L'OID de la table, ses privilèges, ses dépendances et ses relations d'héritage restent intacts. Seul le fichier physique sous-jacent est remplacé.
✅ Checklist avant d'exécuter REPACK en production
1. Tester sur un réplica d'abord
Avant tout passage en production, exécutez REPACK sur un réplica de streaming ou une copie de la base :
-- Vérifier le bloat avant
SELECT
relname,
pg_size_pretty(pg_relation_size(oid)) AS taille_actuelle,
n_dead_tup,
n_live_tup
FROM pg_stat_user_tables
WHERE relname = 'ma_table';
-- Lancer REPACK (PostgreSQL 19+)
REPACK TABLE ma_table CONCURRENTLY;
-- Vérifier après
SELECT pg_size_pretty(pg_relation_size('ma_table'::regclass));
Cela vous donne une estimation réaliste du gain d'espace et de la durée de l'opération.
2. Vérifier que le décodage logique est activé
REPACK CONCURRENTLY s'appuie sur le décodage logique pour rejouer les changements concurrents. Votre instance doit avoir :
# postgresql.conf
wal_level = logical -- obligatoire
max_replication_slots = 5 -- doit rester > 0 disponible
max_wal_senders = 5
3. Surveiller les métriques clés pendant l'opération
Ouvrez un œil sur ces indicateurs :
pg_stat_replication: lag de réplication —REPACKgénère du WAL supplémentairepg_replication_slots: un slot temporaire est créé pendantREPACK CONCURRENTLY; vérifiez qu'il est bien supprimé en fin d'opérationpg_locks: surveillez l'acquisition du verrouAccessExclusiveLocklors du swap final- Espace disque : la table est intégralement dupliquée le temps de l'opération — prévoyez
2× taille_tabledisponible pg_stat_progress_cluster: PostgreSQL 19 expose la progression via cette vue
4. Anticiper la fenêtre de verrou
Le swap final (relfilenode swap) requiert un AccessExclusiveLock de quelques secondes. Pour minimiser l'impact :
- Planifiez l'opération hors des pics d'activité
- Utilisez
lock_timeoutpour éviter une attente indéfinie en cas de transaction longue :
SET lock_timeout = '5s';
REPACK TABLE ma_table CONCURRENTLY;
- Si le timeout est atteint,
REPACKabandonne proprement — la table originale est intacte.
5. Préparer un plan de rollback
REPACK est conçu pour être safe, mais un plan de rollback reste indispensable :
- Avant : notez la taille et les stats de la table (
pg_relation_size,pg_stat_user_tables) - Pendant : un
SIGINTou une annulation de la requête stoppe l'opération ; la table originale n'est pas affectée tant que le swap n'a pas eu lieu - Après le swap : il n'y a pas de retour arrière automatique — la nouvelle table devient la référence. Ayez une sauvegarde récente (pg_dump ou snapshot PITR)
⚠️ 3 warnings à connaître avant le swap de relfilenode
1. Les index sont reconstruits, pas juste réutilisés
REPACK reconstruit tous les index de la table, ce qui peut prendre du temps sur des tables volumineuses avec de nombreux index. Vérifiez au préalable leur nombre et leur taille.
2. Le slot de réplication logique temporaire doit être nettoyé
Si REPACK est interrompu brutalement (crash, kill -9), le slot de réplication logique créé temporairement peut rester actif et bloquer la purge des WAL, entraînant une saturation disque. Vérifiez régulièrement :
SELECT slot_name, active, restart_lsn
FROM pg_replication_slots
WHERE slot_name LIKE 'repack%';
Supprimez manuellement tout slot orphelin avec pg_drop_replication_slot().
3. Les triggers et règles sont temporairement désactivés
Pendant la phase de copie initiale, certains triggers peuvent ne pas s'appliquer de la même façon sur le nouveau fichier. Vérifiez que vos contraintes d'intégrité et vos triggers AFTER se comportent comme attendu après l'opération.
Conclusion
REPACK est une avancée majeure de PostgreSQL 19 pour la maintenance des bases volumineuses en production. Il comble le fossé entre VACUUM (trop partiel) et VACUUM FULL (trop bloquant) en combinant réécriture complète et accessibilité continue de la table.
L'analyse de repack.c révèle toute la complexité orchestrée pour rendre cela possible : décodage logique, gestion des snapshots, reconstruction d'index, swap de relfilenode et gestion fine des verrous. Une commande simple en surface, un chef-d'œuvre d'ingénierie en dessous.
Pour les équipes MulerTech gérant des bases PostgreSQL sous Symfony avec un trafic continu, REPACK CONCURRENTLY peut devenir un outil de maintenance de choix — à condition de suivre la checklist ci-dessus et de ne jamais l'expérimenter pour la première fois directement en production.