🔝 Retour au Sommaire
- Introduction à l'Administration par Requêtes
- Gestion des Locks (Verrous)
- Détection et Analyse du Bloat
- Surveillance de l'Utilisation des Index
- Mise en Pratique : Scénarios Courants
- Bonnes Pratiques et Recommandations
En tant qu'administrateur ou développeur travaillant avec PostgreSQL, vous devez être capable de diagnostiquer les problèmes de performance et de maintenir la santé de votre base de données. PostgreSQL expose une multitude d'informations sur son état interne via des vues système (system views), et vous pouvez interroger ces vues avec des requêtes SQL standard.
Les trois domaines les plus critiques pour l'administration quotidienne sont :
- Les Locks (Verrous) : Identifier les blocages entre transactions qui ralentissent ou figent votre application
- Le Bloat (Gonflement) : Détecter l'espace disque gaspillé dans vos tables et index
- L'Utilisation des Index : Vérifier que vos index sont effectivement utilisés et identifier ceux qui sont inutiles
PostgreSQL maintient automatiquement des vues système qui contiennent des métadonnées et des statistiques sur :
- Les connexions actives (
pg_stat_activity) - Les verrous en cours (
pg_locks) - Les statistiques de tables (
pg_stat_user_tables) - Les statistiques d'index (
pg_stat_user_indexes) - Le catalogue système (
pg_class,pg_stat_all_tables)
Ces vues sont mises à jour en temps réel (ou quasi temps réel) par PostgreSQL et constituent votre fenêtre d'observation sur le fonctionnement interne du serveur.
Un lock (verrou) est un mécanisme qui empêche deux transactions d'accéder simultanément à une ressource de manière incompatible. Par exemple :
- Si la transaction A est en train de modifier une ligne, la transaction B qui veut aussi modifier cette même ligne devra attendre que A se termine
- C'est ce qui garantit la cohérence des données (propriété ACID)
PostgreSQL utilise différents types de verrous :
| Type de Lock | Description | Exemple |
|---|---|---|
| AccessShareLock | Le plus permissif, posé par SELECT | SELECT * FROM users |
| RowShareLock | Posé par SELECT FOR UPDATE | SELECT * FROM users FOR UPDATE |
| RowExclusiveLock | Posé par INSERT, UPDATE, DELETE | UPDATE users SET ... |
| ShareUpdateExclusiveLock | Posé par VACUUM, CREATE INDEX CONCURRENTLY | VACUUM users |
| ShareLock | Posé par CREATE INDEX (non concurrent) | CREATE INDEX idx_users ... |
| ExclusiveLock | Bloque tout sauf AccessShareLock | LOCK TABLE users |
| AccessExclusiveLock | Le plus restrictif, bloque tout | ALTER TABLE users ... |
Les locks deviennent problématiques quand :
- Deadlock (Interblocage) : Deux transactions s'attendent mutuellement
- Lock Wait (Attente) : Une transaction attend trop longtemps qu'un lock soit libéré
- Blocking Cascade : Une transaction bloque plusieurs autres qui elles-mêmes en bloquent d'autres
Ces situations peuvent ralentir drastiquement votre application ou la figer complètement.
Cette requête vous montre tous les locks actuellement détenus dans votre base de données :
SELECT
pl.locktype,
pl.database,
pl.relation::regclass AS relation_name,
pl.page,
pl.tuple,
pl.virtualxid,
pl.transactionid,
pl.mode,
pl.granted,
pl.pid AS process_id
FROM
pg_locks pl
ORDER BY
pl.pid, pl.granted DESC;Explication des colonnes importantes :
- locktype : Type de ressource verrouillée (
relation,transactionid,tuple, etc.) - relation_name : Nom de la table concernée (si applicable)
- mode : Type de verrou (ex:
AccessShareLock,ExclusiveLock) - granted :
truesi le verrou est obtenu,falsesi en attente - pid : ID du processus qui détient ou attend le verrou
Cette requête plus avancée identifie qui bloque qui :
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_statement,
blocking_activity.query AS blocking_statement,
blocked_activity.application_name AS blocked_application,
blocking_activity.application_name AS blocking_application
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;Ce que cette requête vous montre :
- blocked_pid : Le processus qui est bloqué
- blocking_pid : Le processus qui bloque
- blocked_statement : La requête qui est bloquée
- blocking_statement : La requête qui bloque
PostgreSQL détecte automatiquement les deadlocks et les résout en annulant une des transactions. Pour voir les deadlocks récents, consultez les logs PostgreSQL ou utilisez cette vue :
SELECT
pid,
usename,
application_name,
state,
query,
wait_event_type,
wait_event,
state_change
FROM
pg_stat_activity
WHERE
wait_event_type = 'Lock'
AND state = 'active'
ORDER BY
state_change;Interprétation :
- Les processus listés attendent un verrou
- Si plusieurs processus s'attendent mutuellement, vous avez un deadlock potentiel
Si vous identifiez une transaction qui bloque toutes les autres et qui doit être interrompue :
-- Terminer proprement (recommandé)
SELECT pg_cancel_backend(12345); -- Remplacer 12345 par le PID
-- Tuer de force (en dernier recours)
SELECT pg_terminate_backend(12345);Différence importante :
pg_cancel_backend(): Demande poliment à la transaction de s'arrêter (SIGINT)pg_terminate_backend(): Force l'arrêt immédiat (SIGTERM)
Le bloat (gonflement) est l'accumulation d'espace mort dans vos tables et index. Cela se produit à cause du fonctionnement de PostgreSQL avec MVCC (Multi-Version Concurrency Control).
- Quand vous faites un
UPDATEouDELETE, PostgreSQL ne supprime pas immédiatement les anciennes versions des lignes - Ces anciennes versions (appelées dead tuples) restent dans la table pour permettre aux transactions concurrentes de les voir
- Le processus VACUUM nettoie ces dead tuples... mais si VACUUM ne tourne pas assez souvent, elles s'accumulent
- Résultat : Vos tables et index occupent beaucoup plus d'espace qu'ils ne devraient
- Ralentissement des requêtes : PostgreSQL doit parcourir plus de données
- Gaspillage d'espace disque
- Cache moins efficace : Les buffers sont remplis de données mortes
- Index moins performants
Cette requête estime le pourcentage de bloat dans vos tables :
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,
pg_size_pretty(pg_relation_size(schemaname||'.'||tablename)) AS table_size,
round(100 * pg_relation_size(schemaname||'.'||tablename) /
NULLIF(pg_total_relation_size(schemaname||'.'||tablename), 0), 2) AS table_pct,
n_live_tup,
n_dead_tup,
round(100 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_tuple_pct,
last_vacuum,
last_autovacuum
FROM
pg_stat_user_tables
WHERE
n_live_tup > 0
ORDER BY
n_dead_tup DESC
LIMIT 20;Colonnes importantes :
- total_size : Taille totale (table + index + TOAST)
- table_size : Taille de la table seule
- n_live_tup : Nombre de lignes vivantes
- n_dead_tup : Nombre de lignes mortes (bloat)
- dead_tuple_pct : Pourcentage de bloat
- last_vacuum / last_autovacuum : Dernière exécution de VACUUM
Seuils d'alerte :
dead_tuple_pct > 20%: Bloat modéré, surveillerdead_tuple_pct > 40%: Bloat important, action recommandéedead_tuple_pct > 60%: Bloat critique, action urgente
Pour une estimation plus précise, utilisez l'extension pgstattuple :
-- Installer l'extension (une seule fois)
CREATE EXTENSION IF NOT EXISTS pgstattuple;
-- Analyser une table spécifique
SELECT * FROM pgstattuple('ma_table');Cette fonction retourne :
- table_len : Taille totale de la table en bytes
- tuple_count : Nombre de lignes vivantes
- dead_tuple_count : Nombre de lignes mortes
- free_space : Espace libre récupérable
- dead_tuple_percent : Pourcentage exact de bloat
Attention : pgstattuple() fait un scan complet de la table, donc coûteux sur de grosses tables. À utiliser hors production ou en maintenance.
Les index aussi peuvent souffrir de bloat :
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan,
idx_tup_read,
idx_tup_fetch,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
CASE
WHEN idx_scan = 0 THEN 'NEVER USED'
ELSE 'OK'
END AS usage_status
FROM
pg_stat_user_indexes
WHERE
schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY
pg_relation_size(indexrelid) DESC
LIMIT 20;Indicateurs de bloat d'index :
- Un index qui ne cesse de grossir alors que la table est stable
- Un ratio
idx_tup_read / idx_tup_fetchtrès élevé (index inefficace)
Il existe plusieurs stratégies :
-- VACUUM standard (récupère l'espace mais ne le rend pas à l'OS)
VACUUM VERBOSE ma_table;
-- VACUUM ANALYZE (+ mise à jour des statistiques)
VACUUM ANALYZE ma_table;Avantage : Rapide, n'impose pas de verrou exclusif
Inconvénient : Ne réduit pas physiquement la taille du fichier
-- Réécrit complètement la table et rend l'espace à l'OS
VACUUM FULL ma_table;Avantage : Récupère vraiment tout l'espace
Inconvénient : Pose un verrou exclusif (AccessExclusiveLock), bloque toute opération sur la table
-- Reconstruire un index spécifique
REINDEX INDEX mon_index;
-- Reconstruire tous les index d'une table
REINDEX TABLE ma_table;
-- Version concurrente (sans bloquer les lectures/écritures)
REINDEX INDEX CONCURRENTLY mon_index;PostgreSQL 18 : REINDEX CONCURRENTLY est plus performant grâce aux optimisations I/O.
L'extension pg_repack permet de réorganiser une table sans verrou exclusif :
-- En ligne de commande
pg_repack -d ma_base -t ma_tableAvantage majeur : Pas d'interruption de service
Les index sont essentiels pour la performance, mais :
- Coût de maintenance : Chaque index ralentit les INSERT/UPDATE/DELETE
- Consommation d'espace disque
- Cache pollution : Index inutiles occupent de la mémoire
Il est donc crucial d'identifier :
- Les index inutilisés (à supprimer)
- Les index manquants (à créer)
- Les index redondants (doublons)
Cette requête identifie les index qui ne sont jamais utilisés :
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan AS number_of_scans,
idx_tup_read AS tuples_read,
idx_tup_fetch AS tuples_fetched,
pg_size_pretty(pg_relation_size(tablename::regclass)) AS table_size
FROM
pg_stat_user_indexes
WHERE
schemaname NOT IN ('pg_catalog', 'information_schema')
AND idx_scan = 0
AND indexrelname NOT LIKE '%_pkey' -- Exclure les primary keys
ORDER BY
pg_relation_size(indexrelid) DESC;Interprétation :
- idx_scan = 0 : L'index n'a JAMAIS été utilisé depuis le dernier démarrage ou reset des stats
- Si un index apparaît ici et que votre serveur tourne depuis longtemps : candidat à la suppression
Précautions :
- Ne supprimez pas les primary keys ou les contraintes UNIQUE
- Vérifiez que les statistiques ne viennent pas d'être réinitialisées (
pg_stat_reset())
Pour voir quelle proportion de requêtes utilise les index vs scan séquentiel :
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,
seq_scan AS sequential_scans,
idx_scan AS index_scans,
round(100.0 * idx_scan / NULLIF(seq_scan + idx_scan, 0), 2) AS index_usage_pct,
n_live_tup AS live_rows
FROM
pg_stat_user_tables
WHERE
(seq_scan + idx_scan) > 0
ORDER BY
seq_scan DESC,
pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 20;Analyse :
- index_usage_pct > 90% : Excellent, vos index sont bien utilisés
- index_usage_pct < 50% : Beaucoup de scans séquentiels, envisagez d'ajouter des index
- seq_scan élevé sur grosse table : Problème de performance potentiel
Nuance : Sur de petites tables (<1000 lignes), PostgreSQL préfère souvent le scan séquentiel car c'est plus rapide. C'est normal.
Des index redondants sont des index qui font "doublon". Par exemple :
- Index 1 :
(a, b, c) - Index 2 :
(a, b)← Redondant, car l'index 1 peut le remplacer
SELECT
pg_size_pretty(sum(pg_relation_size(idx))::bigint) AS total_size,
array_agg(indexrelname) AS indexes,
tablename
FROM (
SELECT
indexrelid::regclass AS idx,
indrelid::regclass AS tablename,
indexrelname,
string_to_array(indkey::text, ' ')::int[] AS cols
FROM
pg_index
JOIN
pg_class ON pg_class.oid = pg_index.indexrelid
WHERE
indisvalid
) sub
GROUP BY
tablename, cols
HAVING
count(*) > 1
ORDER BY
sum(pg_relation_size(idx)) DESC;Cette requête détecte :
- Les index ayant exactement les mêmes colonnes dans le même ordre
- Ces index sont des doublons parfaits et l'un peut être supprimé
Mesure l'efficacité du cache pour les index :
SELECT
schemaname,
tablename,
indexrelname,
idx_blks_read AS disk_reads,
idx_blks_hit AS cache_hits,
round(100.0 * idx_blks_hit / NULLIF(idx_blks_hit + idx_blks_read, 0), 2) AS cache_hit_ratio
FROM
pg_statio_user_indexes
WHERE
(idx_blks_hit + idx_blks_read) > 0
ORDER BY
cache_hit_ratio ASC
LIMIT 20;Objectif :
- cache_hit_ratio > 95% : Excellent
- cache_hit_ratio < 80% : L'index est souvent lu depuis le disque, envisagez d'augmenter
shared_buffers
Pour connaître l'empreinte disque de vos index :
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM
pg_stat_user_indexes
WHERE
tablename = 'ma_table' -- Remplacer par votre table
ORDER BY
pg_relation_size(indexrelid) DESC;Analyse :
- Si un gros index a
idx_scan = 0: Supprimez-le - Si un petit index a
idx_scantrès élevé : Gardez-le absolument
Cette approche nécessite l'extension pg_stat_statements (fortement recommandée) :
-- Installer l'extension
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Trouver les requêtes les plus lentes
SELECT
round(total_exec_time::numeric, 2) AS total_time_ms,
calls,
round(mean_exec_time::numeric, 2) AS mean_time_ms,
query
FROM
pg_stat_statements
WHERE
mean_exec_time > 100 -- Plus de 100ms en moyenne
ORDER BY
total_exec_time DESC
LIMIT 20;Processus d'analyse :
- Identifiez les requêtes lentes avec
pg_stat_statements - Pour chaque requête, faites un
EXPLAIN ANALYZEpour voir le plan d'exécution - Si vous voyez des
Seq Scansur de grosses tables : créez un index
Exemple :
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'john@example.com'; Si le plan montre :
Seq Scan on users (cost=0.00..1234.56 rows=1 width=200) (actual time=45.123..45.125 rows=1 loops=1)
Filter: (email = 'john@example.com'::text)
Alors créez :
CREATE INDEX idx_users_email ON users(email);PostgreSQL 18 introduit l'optimisation Skip Scan pour les index multi-colonnes.
Avant PostgreSQL 18 :
- Index sur
(a, b)ne pouvait pas être utilisé efficacement pourWHERE b = 5(sans condition sura) - PostgreSQL faisait un scan séquentiel
Avec PostgreSQL 18 :
- PostgreSQL peut "sauter" les valeurs de
aet scanner uniquement surb - Plus besoin de créer des index redondants dans certains cas
Impact : Moins d'index nécessaires, meilleure performance automatique.
Symptôme : Votre application ne répond plus, timeouts partout.
Diagnostic :
-- Étape 1 : Voir les connexions actives
SELECT pid, usename, state, query, wait_event_type, wait_event
FROM pg_stat_activity
WHERE state != 'idle';
-- Étape 2 : Identifier qui bloque qui
-- (Utiliser la requête de la section 2.4)
-- Étape 3 : Décision
-- Si une transaction bloque tout depuis longtemps :
SELECT pg_terminate_backend(123456); -- PID de la transaction bloquanteSymptôme : Votre table orders fait 500 GB alors qu'elle ne devrait faire que 100 GB.
Diagnostic :
-- Vérifier le bloat
SELECT
tablename,
n_live_tup,
n_dead_tup,
round(100 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
last_autovacuum
FROM pg_stat_user_tables
WHERE tablename = 'orders'; Si dead_pct > 40% :
-- Solution 1 : VACUUM standard (rapide mais ne réduit pas la taille)
VACUUM ANALYZE orders;
-- Solution 2 : VACUUM FULL (hors prod, bloque la table)
VACUUM FULL orders;
-- Solution 3 : pg_repack (recommandé, pas de blocage)
-- En ligne de commande :
-- pg_repack -d ma_base -t ordersSymptôme : Une requête qui prenait 50ms prend maintenant 5 secondes.
Diagnostic :
-- Étape 1 : Vérifier les index inutilisés
-- (Section 4.2)
-- Étape 2 : Analyser le plan d'exécution
EXPLAIN ANALYZE
SELECT * FROM products WHERE category = 'Electronics';
-- Étape 3 : Si vous voyez "Seq Scan" sur une grosse table
-- Créer un index :
CREATE INDEX idx_products_category ON products(category);
-- Étape 4 : Forcer la mise à jour des statistiques
ANALYZE products;Symptôme : Les INSERT sont de plus en plus lents.
Diagnostic :
-- Lister tous les index d'une table avec leur usage
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size,
idx_scan
FROM pg_stat_user_indexes
WHERE tablename = 'ma_table'
ORDER BY idx_scan ASC; Solution : Supprimer les index avec idx_scan = 0 :
DROP INDEX idx_unused_column;Attention : Surveillez les performances après suppression pendant quelques jours.
Mettez en place un monitoring régulier :
- Locks : Alerter si une transaction bloque depuis > 5 minutes
- Bloat : Audit hebdomadaire du bloat, VACUUM si nécessaire
- Index : Audit mensuel de l'utilisation des index
Outils recommandés :
- pg_stat_statements : Extension indispensable
- pgBadger : Analyse de logs
- Prometheus + postgres_exporter : Monitoring temps réel
- Grafana : Dashboards visuels
PostgreSQL 18 : Autovacuum amélioré avec ajustements dynamiques.
Paramètres clés à tuner dans postgresql.conf :
# Activer autovacuum (par défaut activé)
autovacuum = on
# Nombre de workers (augmenter si beaucoup de tables actives)
autovacuum_max_workers = 3 # Défaut, augmenter à 5-10 si nécessaire
# PostgreSQL 18 : Nouveau paramètre
autovacuum_vacuum_max_threshold = 50000000
# Seuil de déclenchement (% de lignes modifiées)
autovacuum_vacuum_scale_factor = 0.1 # 10% de la table
# Coût du vacuum (limiter l'impact I/O)
autovacuum_vacuum_cost_delay = 2ms # Pause entre I/O
autovacuum_vacuum_cost_limit = 200 # Budget I/O
Principes :
- Indexez les colonnes de filtrage (WHERE) : Toujours
- Indexez les colonnes de jointure (JOIN) : FK notamment
- Indexez les colonnes de tri (ORDER BY) : Si tri fréquent
- N'indexez pas les petites tables : < 10 000 lignes, inutile
- Utilisez des index partiels : Pour filtrer sur des valeurs spécifiques
Exemple d'index partiel :
-- Au lieu de indexer toute la colonne status
CREATE INDEX idx_orders_pending ON orders(created_at) WHERE status = 'pending';
-- Plus petit, plus rapide pour les requêtes sur status='pending'Hebdomadaire :
- Audit du bloat (section 3.2)
- Vérification des index inutilisés (section 4.2)
Mensuel :
- VACUUM ANALYZE sur les tables critiques
- REINDEX sur les index gonflés
- Revue des requêtes lentes (
pg_stat_statements)
Annuel :
- Audit complet de l'architecture (normalisation, partitionnement)
- Revue des permissions et sécurité
Tenez un registre :
- Date de création de chaque index et justification
- Historique des VACUUM FULL / REINDEX
- Incidents de locks et leur résolution
Cela vous aidera à identifier les patterns et prévenir les problèmes récurrents.
Avant toute modification majeure (ajout d'index, VACUUM FULL, changement de configuration) :
- Testez en environnement de préproduction
- Mesurez l'impact : Temps de réponse, débit, utilisation CPU/RAM
- Préparez un rollback : Plan B si ça se passe mal
- ✅ Utilisez
pg_stat_activityetpg_lockspour diagnostiquer les blocages - ✅ Identifiez rapidement la transaction bloquante avec la requête de la section 2.4
- ✅ Utilisez
pg_cancel_backend()oupg_terminate_backend()en dernier recours
- ✅ Surveillez le pourcentage de dead tuples avec
pg_stat_user_tables - ✅ Configurez correctement l'autovacuum (PostgreSQL 18 : nouveaux paramètres)
- ✅ Utilisez VACUUM ou pg_repack pour nettoyer le bloat
- ✅ Évitez VACUUM FULL en production (bloque la table)
- ✅ Identifiez et supprimez les index inutilisés (
idx_scan = 0) - ✅ Surveillez le ratio index vs scan séquentiel
- ✅ Créez des index pour les requêtes lentes (vérifiez avec EXPLAIN)
- ✅ PostgreSQL 18 : Skip Scan réduit le besoin d'index redondants
- pg_stat_statements : Tracking des requêtes
- pgBadger : Analyse de logs
- pg_repack : Réorganisation sans verrous
- HypoPG : Tester des index hypothétiques
- Mailing list PostgreSQL : pgsql-general
- Reddit : r/PostgreSQL
- Discord PostgreSQL (communauté francophone et internationale)
- Stack Overflow : Tag
postgresql