Le dépannage PostgreSQL progresse d'un symptôme observé vers une résolution vérifiée par une investigation systématique. Ce guide couvre l'identification de version, l'analyse des journaux, la validation de configuration, les diagnostics de performance et les procédures de récupération pour les développeurs, ingénieurs DevOps et équipes techniques opérant PostgreSQL en production. Chaque section fournit des commandes concrètes, sorties attendues, signaux d'échec et chemins de restauration pour agir avec confiance plutôt que par approximation.
Inventaire de version et d'environnement
Avant d'investiguer tout problème, établissez une base précise de ce qui tourne et où.
Identifier la version et les détails de compilation PostgreSQL
Exécutez la commande de version depuis le client psql ou le binaire serveur :
psql --version
# psql (PostgreSQL) 16.2
# Ou depuis le binaire serveur
postgres --version
# postgres (PostgreSQL) 16.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 11.4.0, 64-bit
Dans une session connectée, interrogez la version serveur et les options de compilation :
SELECT version();
-- PostgreSQL 16.2 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 11.4.0, 64-bit
SHOW server_version_num;
-- 160002
Résultat attendu : Chaîne de version majeur.mineur.patch correspondant à votre manifeste de déploiement. Signal d'échec : Incompatibilité de version entre client et serveur, ou version majeure non supportée (ex. PostgreSQL 12 en fin de vie depuis novembre 2024).
Capturer la topologie de déploiement
Déterminez si vous exécutez une instance unique, une réplication en flux, une réplication logique, Patroni, pg_auto_failover, ou un service géré (RDS, Cloud SQL, Azure Database). Exécutez :
SELECT * FROM pg_stat_replication;
-- Résultat vide = aucune réplica en flux connectée
SELECT * FROM pg_stat_subscription;
-- Affiche les abonnements de réplication logique le cas échéant
Vérifiez la présence de pool de connexions (PgBouncer, pgpool-II) et notez le mode de pool (session, transaction, instruction).
Enregistrer les réglages critiques et prérequis
Capturez la configuration active avec ses sources :
SELECT name, setting, unit, source, context
FROM pg_settings
WHERE source NOT IN ('default', 'override')
ORDER BY name;
Exportez vers un fichier horodaté pour comparaison ultérieure :
psql -c "SELECT name, setting, unit, source FROM pg_settings WHERE source != 'default' ORDER BY name;" > pg_settings_$(date +%F_%H%M).txt
Prérequis : Superutilisateur ou rôle pg_read_all_settings. Rayon d'action : Lecture seule. Vérification : Le fichier existe et contient des entrées non-par-défaut.
Chemin de configuration sécurisé
Les modifications de configuration doivent être observables, minimales et réversibles. N'éditez jamais postgresql.conf directement sans sauvegarde versionnée.
Observer l'état de configuration actuel
Utilisez pg_settings pour voir la valeur effective, les redémarrages en attente et la source :
SELECT name, setting, pending_restart, context, source
FROM pg_settings
WHERE name IN ('shared_buffers', 'work_mem', 'maintenance_work_mem', 'effective_cache_size', 'max_connections', 'wal_level', 'max_wal_senders');
Appliquer une modification unique et ciblée
Exemple : Augmenter work_mem de 4 Mo à 16 Mo pour une charge de travail de reporting.
- Sauvegarder la configuration actuelle :
cp /etc/postgresql/16/main/postgresql.conf /etc/postgresql/16/main/postgresql.conf.$(date +%F_%H%M).bak
- Modifier via
ALTER SYSTEM(préféré pour la persistance après redémarrage) :
ALTER SYSTEM SET work_mem = '16MB';
SELECT pg_reload_conf();
- Vérifier que la modification a pris effet :
SHOW work_mem;
-- work_mem: 16MB
Résultat attendu : SHOW retourne la nouvelle valeur ; pg_settings.pending_restart est faux pour ce paramètre. Signal d'échec : pg_reload_conf() retourne faux, ou le paramètre affiche encore l'ancienne valeur (contexte = postmaster nécessite un redémarrage). Restauration : ALTER SYSTEM RESET work_mem; SELECT pg_reload_conf(); puis vérifier.
Paramètres nécessitant un redémarrage
Pour shared_buffers, max_connections, wal_level ou max_wal_senders, un redémarrage est obligatoire. Planifiez une fenêtre de maintenance, informez les parties prenantes, et vérifiez que le cluster redémarre proprement :
systemctl restart postgresql@16-main
# ou
pg_ctlcluster 16 main restart
Vérifiez le journal pour un démarrage réussi :
tail -n 50 /var/log/postgresql/postgresql-16-main.log
-- LOG: database system is ready to accept connections
Vérification et diagnostics
Lorsque des symptômes apparaissent (requêtes lentes, épuisement des connexions, décalage de réplication, problèmes de vacuum), utilisez ces parcours diagnostiques.
Analyse des connexions et sessions
Sessions actives avec durée et état :
SELECT pid, usename, application_name, client_addr, state,
now() - backend_start AS session_duration,
now() - state_change AS state_duration,
wait_event_type, wait_event,
LEFT(query, 120) AS query_preview
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY backend_start;
Nombre de connexions vs limite :
SELECT count(*) AS active_connections,
setting::int AS max_connections,
round(100.0 * count(*) / setting::int, 1) AS pct_used
FROM pg_stat_activity, pg_settings
WHERE name = 'max_connections';
Signal d'échec : Utilisation > 80 % des connexions ou sessions bloquées en active avec wait_event_type = 'Lock' depuis > 30 secondes.
Analyse des blocages et verrous
Identifier les sessions bloquantes et bloquées :
SELECT blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_query,
blocking_activity.query AS blocking_query,
blocked_activity.state AS blocked_state,
blocking_activity.state AS blocking_state
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid != blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted;
Résultat attendu : Zéro ligne (aucun blocage). Signal d'échec : Lignes retournées ; examinez blocking_query — souvent une transaction non validée ou un DDL long.
Santé de la réplication (flux)
Côté primaire :
SELECT client_addr, state, sync_state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_lag_bytes,
pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS replay_lag_human
FROM pg_stat_replication;
Côté réplica :
SELECT status, receiver_state, sender_state,
pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn(),
pg_last_wal_replay_timestamp(),
now() - pg_last_wal_replay_timestamp() AS replay_delay
FROM pg_stat_wal_receiver;
Signal d'échec : replay_lag_bytes > 1 Go ou replay_delay > 5 minutes sur une réplica synchrone.
Surveillance du vacuum et de la fragmentation
Vérifier l'activité autovacuum et estimer la fragmentation des tables :
SELECT schemaname, relname, n_dead_tup, n_live_tup,
round(100.0 * n_dead_tup / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
last_vacuum, last_autovacuum, last_analyze, last_autoanalyze,
vacuum_count, autovacuum_count
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY dead_pct DESC
LIMIT 20;
Signal d'échec : dead_pct > 20 % sur de grosses tables, ou last_autovacuum plus vieux de 24 heures sur tables fréquemment mises à jour.
Déclencher un vacuum manuel si nécessaire :
VACUUM (ANALYZE, VERBOSE) public.grosse_table;
Utilisation des index et index manquants
Index inutilisés (candidats à la suppression) :
SELECT schemaname, relname, indexrelname, idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
WHERE idx_scan = 0
AND pg_relation_size(indexrelid) > 1024 * 1024 -- > 1 Mo
ORDER BY pg_relation_size(indexrelid) DESC;
Balayages séquentiels sur grosses tables (index possibles manquants) :
SELECT schemaname, relname, seq_scan, seq_tup_read,
idx_scan, n_live_tup
FROM pg_stat_user_tables
WHERE seq_scan > 0
AND n_live_tup > 100000
ORDER BY seq_tup_read DESC
LIMIT 20;
Analyse des journaux pour erreurs
Erreurs et fatals récents (dernières 100 lignes) :
grep -E "(ERROR|FATAL|PANIC)" /var/log/postgresql/postgresql-16-main.log | tail -100
Motifs courants :
could not connect to server: Connection refused→ postmaster arrêté ou incompatibilitélisten_addresses/portremaining connection slots are reserved for non-replication superuser connections→max_connectionsépuisécould not write to WAL file: No space left on device→ disque plein surpg_waldeadlock detected→ problème d'ordre de transactions applicatif
Vérifier la destination et le format des journaux :
SHOW log_destination; -- stderr, csvlog, syslog
SHOW logging_collector; -- on/off
SHOW log_directory; -- log/
SHOW log_filename; -- postgresql-%Y-%m-%d_%H%M%S.log
Modes de défaillance et récupération
Scénario 1 : Disque WAL plein
Symptômes : PANIC: could not write to WAL file, la base cesse d'accepter les écritures.
Actions immédiates :
- Vérifier l'espace disque :
df -h /var/lib/postgresql/16/main/pg_wal - Identifier les fichiers supprimables :
ls -lh /var/lib/postgresql/16/main/pg_wal/ | head -20 - Ne supprimez jamais les fichiers WAL manuellement. Libérez plutôt de l'espace ailleurs (vieilles sauvegardes, journaux, tmp) ou agrandissez le volume.
Vérification de la récupération :
systemctl start postgresql@16-main
tail -f /var/log/postgresql/postgresql-16-main.log
-- LOG: database system was interrupted; last known up at ...
-- LOG: entering standby mode (si réplica) ou
-- LOG: database system is ready to accept connections
Scénario 2 : Rétention de slot de réplication causant accumulation WAL
Symptômes : pg_wal grossit sans limite ; pg_replication_slots montre un slot inactif avec restart_lsn très en retard.
Diagnostic :
SELECT slot_name, plugin, slot_type, datoid, database,
active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
Résolution : Supprimer le slot inactif :
SELECT pg_drop_replication_slot('nom_slot_obsolete');
Vérification : La taille de pg_wal se stabilise ; pg_replication_slots n'affiche plus le slot.
Scénario 3 : Index corrompu
Symptômes : ERROR: index "idx_name" contains unexpected data, ou requête retourne de mauvais résultats silencieusement.
Récupération :
REINDEX INDEX CONCURRENTLY idx_name;
-- Ou pour table entière :
REINDEX TABLE CONCURRENTLY grosse_table;
Vérification : Exécuter la requête qui échouait ; vérifier pg_stat_user_indexes pour idx_scan qui s'incrémente.
Scénario 4 : Risque de rebouclage d'identifiant de transaction
Symptômes : WARNING: database "dbname" must be vacuumed within N transactions dans les journaux.
Action immédiate :
VACUUM (FREEZE) DATABASE base_critique;
-- Ou par table pour moins de contention de verrous :
VACUUM (FREEZE, ANALYZE) public.table_critique;
Surveiller la progression :
SELECT pid, phase, heap_blks_scanned, heap_blks_vacuumed,
index_vacuum_count, max_dead_tuples
FROM pg_stat_progress_vacuum;
Vérification : age(datfrozenxid) diminue :
SELECT datname, age(datfrozenxid) AS xid_age
FROM pg_database
WHERE datallowconn
ORDER BY xid_age DESC;
Scénario 5 : DROP TABLE accidentel
Options de récupération (par ordre de préférence) :
- PITR (Point-in-Time Recovery) depuis sauvegarde de base + archive WAL — restaure à un horodatage précis.
- Sauvegarde logique (
pg_dump) si assez récente. - Extension pg_dirtyread (dernier recours, lit les tuples morts) — nécessite superutilisateur et
CREATE EXTENSION pg_dirtyread;.
Aperçu PITR :
# 1. Arrêter le cluster
systemctl stop postgresql@16-main
# 2. Restaurer la sauvegarde de base vers nouveau répertoire de données
pg_basebackup -D /var/lib/postgresql/16/main_recovery -Ft -z -P -h hote_sauvegarde -U replicator
# 3. Créer recovery.signal et configurer restore_command dans postgresql.conf
echo "restore_command = 'cp /mnt/wal_archive/%f %p'" >> /var/lib/postgresql/16/main_recovery/postgresql.conf
echo "recovery_target_time = '2024-01-15 14:30:00'" >> /var/lib/postgresql/16/main_recovery/postgresql.conf
touch /var/lib/postgresql/16/main_recovery/recovery.signal
# 4. Démarrer et vérifier
systemctl start postgresql@16-main_recovery
Liste de contrôle opérationnelle
Utilisez cette liste lors de la maintenance de routine et de la réponse aux incidents.
Quotidien
- [ ] Vérifier
pg_stat_replicationdécalage < 100 Mo sur toutes les réplicas - [ ] Confirmer
pg_stat_activitynombre connexions < 70 % demax_connections - [ ] Scanner les journaux pour
ERROR,FATAL,PANICsur 24 dernières heures - [ ] Confirmer taille répertoire
pg_walstable (pas de croissance non bornée) - [ ] Vérifier achèvement sauvegarde et succès archivage WAL
Hebdomadaire
- [ ] Examiner
pg_stat_user_tablespour tables avecdead_pct> 10 % - [ ] Vérifier
pg_stat_user_indexespour index inutilisés > 100 Mo - [ ] Exécuter
pg_dump --schema-onlydes schémas critiques ; vérifier restauration en staging - [ ] Tester
pg_basebackupdepuis réplica (valide intégrité sauvegarde et réplication) - [ ] Examiner
pg_settingspour dérive par rapport à la configuration versionnée
Mensuel
- [ ]
REINDEX DATABASEcomplet pendant fenêtre maintenance (ouCONCURRENTLYpar table) - [ ] Vérifier restauration PITR vers cluster de staging depuis dernière sauvegarde de base + WAL
- [ ] Mettre à jour version mineure PostgreSQL si correctifs sécurité publiés
- [ ] Examiner et faire tourner les entrées
pg_hba.conf; supprimer accès obsolètes - [ ] Planification capacité : projeter croissance
pg_database_sizesur 90 jours
Réponse incident (Premières 15 minutes)
- [ ] Capturer instantané
pg_stat_activity:SELECT * FROM pg_stat_activity \g activity_$(date +%F_%H%M).txt - [ ] Capturer instantané
pg_lockssi blocage suspecté - [ ] Vérifier espace disque sur volumes
pg_dataetpg_wal - [ ] Suivre journal PostgreSQL pour erreurs actives
- [ ] Identifier fenêtre de changement : déploiement récent, modification config, migration schéma, pic trafic
- [ ] Communiquer statut aux parties prenantes avec faits observés seulement (pas de spéculation)
Conclusion
Un dépannage PostgreSQL efficace repose sur une boucle reproductible : observer l'état actuel avec des commandes adaptées à la version, formuler une hypothèse, appliquer la modification la plus ciblée possible, vérifier le résultat face à un signal attendu, et documenter le chemin de restauration avant d'en avoir besoin. Ce guide vous a fourni des requêtes concrètes pour l'inventaire de version, la sécurité de configuration, les diagnostics de connexions et verrous, la santé de réplication, la surveillance vacuum et fragmentation, l'analyse d'index et l'interprétation des journaux — ainsi que des procédures de récupération pour les modes de défaillance les plus courants. Intégrez la liste de contrôle opérationnelle dans vos runbooks, pratiquez les restaurations PITR chaque trimestre, et traitez chaque modification de configuration comme un artefact déployable avec un plan de test et un bouton de restauration. Au prochain incident, vous passerez du symptôme à la résolution avec des preuves, non des suppositions.