🔝 Retour au Sommaire
Avant de plonger dans le diagnostic, comprenons d'abord ce qu'est un verrou (ou lock en anglais) dans PostgreSQL.
Imaginez une bibliothèque où plusieurs personnes veulent consulter ou modifier le même livre en même temps. Pour éviter le chaos, il faut établir des règles : si quelqu'un est en train de corriger une page, personne d'autre ne peut modifier cette page en même temps. C'est exactement le rôle des verrous dans une base de données.
Les verrous garantissent l'intégrité des données dans un environnement concurrent. Ils empêchent :
- Les écritures simultanées conflictuelles : Deux transactions ne peuvent pas modifier la même ligne en même temps
- Les lectures incohérentes : Une transaction ne doit pas lire des données en cours de modification
- Les modifications de structure dangereuses : Empêcher la suppression d'une table pendant qu'elle est utilisée
PostgreSQL gère automatiquement la majorité des verrous grâce à son système MVCC (Multiversion Concurrency Control), mais il arrive parfois que des blocages (blocking) se produisent.
Comment savoir si vous avez un problème de verrous ? Voici les signes typiques :
- Des requêtes qui "pendent" : Une requête qui devrait prendre 100ms prend maintenant plusieurs secondes, voire minutes
- L'application devient lente : Les utilisateurs se plaignent de temps de réponse anormalement longs
- Des timeouts : L'application reçoit des erreurs de type "timeout" ou "deadlock detected"
- Une base de données qui semble "figée" : Certaines opérations ne se terminent jamais
PostgreSQL utilise différents types de verrous. Voici les principaux que vous rencontrerez :
- FOR UPDATE : Verrou exclusif sur une ligne, empêche toute modification
- FOR SHARE : Verrou partagé, permet la lecture mais pas la modification
PostgreSQL définit 8 modes de verrous de table, du moins au plus restrictif :
- ACCESS SHARE : Acquis par
SELECT(lecture simple) - ROW SHARE : Acquis par
SELECT FOR UPDATE - ROW EXCLUSIVE : Acquis par
INSERT,UPDATE,DELETE - SHARE UPDATE EXCLUSIVE : Acquis par
VACUUM,CREATE INDEX CONCURRENTLY - SHARE : Acquis par
CREATE INDEX(sans CONCURRENTLY) - SHARE ROW EXCLUSIVE : Protection contre modifications concurrentes
- EXCLUSIVE : Bloque tous sauf ACCESS SHARE
- ACCESS EXCLUSIVE : Le plus restrictif, acquis par
ALTER TABLE,DROP TABLE,TRUNCATE
Important pour les débutants : Vous n'avez pas besoin de mémoriser tous ces modes. Retenez simplement que :
- Les lectures (
SELECT) sont généralement non-bloquantes - Les écritures (
INSERT,UPDATE,DELETE) peuvent se bloquer mutuellement - Les modifications de structure (
ALTER TABLE,DROP) bloquent tout
La vue système pg_locks est la fenêtre qui vous permet de voir tous les verrous actifs dans votre base de données.
SELECT * FROM pg_locks LIMIT 5;Cette vue contient de nombreuses colonnes. Voici les plus importantes pour débuter :
| Colonne | Description |
|---|---|
locktype |
Type de verrou (relation, tuple, transactionid, etc.) |
database |
OID de la base de données concernée |
relation |
OID de la table concernée (NULL si non applicable) |
pid |
ID du processus PostgreSQL qui détient le verrou |
mode |
Mode du verrou (AccessShareLock, RowExclusiveLock, etc.) |
granted |
true si le verrou est acquis, false s'il est en attente |
fastpath |
true si acquisition rapide (optimisation interne) |
C'est la colonne la plus importante pour diagnostiquer un problème :
granted = true: Le processus détient actuellement le verrougranted = false: Le processus attend d'obtenir le verrou (il est bloqué !)
Voici une requête simple pour voir quels processus sont bloqués en attente d'un verrou :
SELECT
pid,
usename,
pg_blocking_pids(pid) AS blocked_by,
query AS current_query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0; Explication ligne par ligne :
pid: L'identifiant du processus bloquéusename: Le nom de l'utilisateur qui a lancé la requêtepg_blocking_pids(pid): Fonction magique qui retourne les PID des processus bloquantsquery: La requête qui est bloquéeWHERE cardinality(pg_blocking_pids(pid)) > 0: Filtre pour ne garder que les processus effectivement bloqués
pid | usename | blocked_by | current_query
------+----------+------------+-----------------------------------
1234 | app_user | {5678} | UPDATE orders SET status = 'paid' WHERE id = 42;
Interprétation : Le processus 1234 est bloqué par le processus 5678.
Introduite dans PostgreSQL 9.6, pg_blocking_pids() est une fonction qui simplifie énormément le diagnostic.
pg_blocking_pids(pid integer) → integer[]Elle retourne un tableau contenant les PID de tous les processus qui bloquent le processus spécifié.
-- Trouver qui bloque le processus 1234
SELECT pg_blocking_pids(1234);
-- Résultat possible : {5678}Pour avoir une vue d'ensemble, vous pouvez créer cette requête qui montre à la fois les bloquants et les bloqués :
SELECT
blocked_activity.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_activity.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_query,
blocking_activity.query AS blocking_query
FROM pg_stat_activity AS blocked_activity
JOIN pg_stat_activity AS blocking_activity
ON blocking_activity.pid = ANY(pg_blocking_pids(blocked_activity.pid))
WHERE cardinality(pg_blocking_pids(blocked_activity.pid)) > 0;Cette requête vous donne :
- Qui est bloqué (
blocked_pid,blocked_user,blocked_query) - Qui bloque (
blocking_pid,blocking_user,blocking_query)
Pour aller plus loin, voici une requête qui combine pg_locks et pg_stat_activity :
SELECT
l.pid,
l.mode,
l.granted,
l.locktype,
l.relation::regclass AS table_name,
a.usename,
a.query,
a.state,
age(now(), a.query_start) AS query_duration
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE NOT l.granted
ORDER BY a.query_start; Que montre cette requête ?
- Les verrous non accordés (
NOT l.granted) - Le nom de la table concernée (
l.relation::regclass) - La durée d'attente (
age(now(), a.query_start)) - L'état du processus (
a.state)
pid | mode | granted | locktype | table_name | usename | query_duration
------+-------------------+---------+----------+----------------+----------+----------------
1234 | RowExclusiveLock | false | tuple | orders | app_user | 00:02:15
Interprétation : Le processus 1234 attend depuis 2 minutes et 15 secondes un verrou de type "RowExclusiveLock" sur la table orders.
Parfois, vous avez une chaîne de blocages : A bloque B, qui bloque C, qui bloque D...
Voici une requête récursive (avec CTE) pour visualiser toute la chaîne :
WITH RECURSIVE lock_chain AS (
-- Ancrage : Les processus qui ne sont pas bloqués mais qui bloquent d'autres
SELECT
pid,
pg_blocking_pids(pid) AS blocking_pids,
array[pid] AS chain,
1 AS depth
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0
UNION ALL
-- Récursion : Remonter la chaîne des bloquants
SELECT
a.pid,
pg_blocking_pids(a.pid),
lc.chain || a.pid,
lc.depth + 1
FROM lock_chain lc
JOIN pg_stat_activity a ON a.pid = ANY(lc.blocking_pids)
WHERE lc.depth < 10 -- Limite de profondeur pour éviter boucles infinies
)
SELECT
chain AS blocking_chain,
depth,
a.pid,
a.usename,
a.query
FROM lock_chain lc
JOIN pg_stat_activity a ON a.pid = lc.chain[1]
ORDER BY depth DESC, chain; Cette requête avancée montre toute la hiérarchie des blocages, ce qui est très utile pour comprendre un problème complexe.
Un deadlock (ou interblocage) se produit quand deux transactions s'attendent mutuellement :
- Transaction A détient le verrou sur la ligne X et attend la ligne Y
- Transaction B détient le verrou sur la ligne Y et attend la ligne X
PostgreSQL détecte automatiquement les deadlocks et en annule un (avec l'erreur deadlock detected).
Les deadlocks sont enregistrés dans les logs PostgreSQL. Configurez votre postgresql.conf :
log_lock_waits = on
deadlock_timeout = 1s
Puis consultez vos logs pour voir les détails des deadlocks détectés.
Une fois que vous avez identifié le processus bloquant, plusieurs options s'offrent à vous :
Si la transaction bloquante devrait se terminer rapidement, attendez simplement. Les verrous sont libérés automatiquement à la fin de la transaction.
Si une transaction est restée ouverte par erreur ou bloque tout le monde, vous pouvez la terminer :
-- Terminer proprement la connexion
SELECT pg_terminate_backend(5678);
-- Si ça ne marche pas, forcer l'arrêt (plus violent)
SELECT pg_cancel_backend(5678);pg_cancel_backend()annule uniquement la requête en courspg_terminate_backend()ferme complètement la connexion- Utilisez ces commandes avec précaution en production !
À plus long terme, les solutions préventives sont meilleures :
- Raccourcir les transactions : Ne gardez pas les transactions ouvertes longtemps
- Indexer correctement : Les requêtes plus rapides retiennent les verrous moins longtemps
- Utiliser FOR UPDATE SKIP LOCKED : Permet de sauter les lignes verrouillées plutôt que d'attendre
- Ordonner les accès : Si plusieurs transactions accèdent aux mêmes ressources, faites-le dans le même ordre
Voici une requête "tout-en-un" à garder sous la main pour surveiller votre base :
SELECT
blocked.pid AS blocked_pid,
blocked.usename AS blocked_user,
blocked.state AS blocked_state,
age(now(), blocked.query_start) AS blocked_duration,
blocked.query AS blocked_query,
blocker.pid AS blocker_pid,
blocker.usename AS blocker_user,
blocker.state AS blocker_state,
age(now(), blocker.query_start) AS blocker_duration,
blocker.query AS blocker_query
FROM pg_stat_activity blocked
JOIN (
SELECT DISTINCT
blocking.pid,
blocked_activity.pid AS blocked_by_pid
FROM pg_stat_activity blocking
JOIN pg_stat_activity blocked_activity
ON blocking.pid = ANY(pg_blocking_pids(blocked_activity.pid))
) blocker_info ON blocker_info.blocked_by_pid = blocked.pid
JOIN pg_stat_activity blocker ON blocker.pid = blocker_info.pid
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0
ORDER BY blocked.query_start; Pour les curieux qui veulent aller plus loin, voici une exploration des principaux locktype :
| Type | Description |
|---|---|
relation |
Verrou sur une table entière |
tuple |
Verrou sur une ligne spécifique |
transactionid |
Verrou sur un ID de transaction (pour attendre sa fin) |
virtualxid |
Verrou virtuel sur l'ID de transaction courante |
page |
Verrou sur une page de données (rare) |
advisory |
Verrou applicatif personnalisé (voir Advisory Locks) |
SELECT mode, count(*)
FROM pg_locks
GROUP BY mode
ORDER BY count(*) DESC; Résultat typique :
mode | count
---------------------+-------
AccessShareLock | 1245
RowExclusiveLock | 87
ExclusiveLock | 12
ShareLock | 3
-- ❌ MAUVAIS : Transaction qui reste ouverte
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- ... longue pause, autre traitement application ...
-- ... requête HTTP externe ...
COMMIT;
-- ✅ BON : Transaction courte et focalisée
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT; Les ALTER TABLE acquièrent un verrou ACCESS EXCLUSIVE qui bloque toutes les autres opérations.
-- Préférez les heures creuses pour :
ALTER TABLE users ADD COLUMN last_login TIMESTAMP;
DROP INDEX idx_old;
TRUNCATE TABLE logs; Des requêtes lentes retiennent les verrous plus longtemps. Une bonne indexation réduit drastiquement les risques de blocage.
Dans votre application ou dans PostgreSQL :
-- Timeout de 5 secondes pour une requête
SET statement_timeout = '5s';
-- Timeout pour l'acquisition d'un verrou
SET lock_timeout = '2s';Pour faciliter le monitoring régulier, vous pouvez créer une vue qui encapsule votre requête de diagnostic :
CREATE OR REPLACE VIEW v_blocked_queries AS
SELECT
blocked.pid AS blocked_pid,
blocked.usename AS blocked_user,
age(now(), blocked.query_start) AS wait_duration,
blocked.query AS blocked_query,
blocker.pid AS blocking_pid,
blocker.usename AS blocking_user,
blocker.query AS blocking_query,
blocker.state AS blocking_state
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocker
ON blocker.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;Ensuite, il suffit de faire :
SELECT * FROM v_blocked_queries;Si vous préférez une interface graphique, plusieurs outils peuvent vous aider :
- Onglet "Dashboard" → Section "Server Activity"
- Menu "Tools" → "Server Activity" (affiche
pg_stat_activityet permet de terminer des processus)
- Outils → "Database Monitor" → Vue "Locks"
Des dashboards Grafana pré-configurés existent pour PostgreSQL, avec des panneaux dédiés aux verrous et blocages.
Imaginons ce scénario : Votre application web est soudainement très lente. Voici la démarche de diagnostic :
SELECT count(*)
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0; Résultat : 45 → Il y a 45 processus bloqués !
SELECT
blocker.pid,
blocker.usename,
blocker.query,
count(*) AS blocked_count
FROM pg_stat_activity blocker
JOIN pg_stat_activity blocked
ON blocker.pid = ANY(pg_blocking_pids(blocked.pid))
GROUP BY blocker.pid, blocker.usename, blocker.query
ORDER BY blocked_count DESC; Résultat : Le processus 8765 bloque 42 autres processus avec la requête :
UPDATE products SET stock = stock - 1 WHERE id = 123;SELECT
state,
age(now(), state_change) AS time_in_state,
age(now(), xact_start) AS transaction_age
FROM pg_stat_activity
WHERE pid = 8765; Résultat :
state | time_in_state | transaction_age
--------+---------------+-----------------
idle in transaction | 00:15:32 | 00:15:32
Diagnostic : La transaction est restée ouverte depuis 15 minutes en état "idle in transaction" ! L'application a probablement oublié de faire un COMMIT ou ROLLBACK.
-- Terminer cette connexion problématique
SELECT pg_terminate_backend(8765);Ajoutez dans postgresql.conf :
idle_in_transaction_session_timeout = 60000 # 60 secondes
Cela fermera automatiquement les transactions inactives après 60 secondes.
Voici un récapitulatif des commandes essentielles pour le diagnostic de verrous :
-- 1. Voir tous les processus bloqués
SELECT pid, usename, pg_blocking_pids(pid), query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
-- 2. Voir tous les verrous actifs
SELECT * FROM pg_locks WHERE NOT granted;
-- 3. Voir qui bloque qui (version simple)
SELECT
blocked.pid AS blocked,
blocker.pid AS blocker,
blocked.query AS blocked_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocker
ON blocker.pid = ANY(pg_blocking_pids(blocked.pid));
-- 4. Terminer un processus bloquant
SELECT pg_terminate_backend(PID);
-- 5. Annuler une requête sans tuer la connexion
SELECT pg_cancel_backend(PID);- Documentation officielle PostgreSQL : Chapitre "Explicit Locking"
- pg_stat_activity : Documentation de la vue
- pg_locks : Documentation de la vue
- Wiki PostgreSQL : "Lock Monitoring" (contient des requêtes avancées communautaires)
✅ Les verrous sont normaux : Ils garantissent l'intégrité des données
✅ pg_locks : Vue système pour voir tous les verrous actifs
✅ pg_blocking_pids() : Fonction pour identifier facilement qui bloque qui
✅ granted = false : Signal d'alarme, un processus attend un verrou
✅ Terminer avec précaution : pg_terminate_backend() est une solution de dernier recours
✅ Prévention : Transactions courtes, bons index, timeouts configurés
✅ Monitoring proactif : Créez des vues et des alertes pour détecter les problèmes tôt
Le diagnostic des verrous est une compétence essentielle pour tout développeur ou DevOps travaillant avec PostgreSQL. Avec les outils présentés (pg_locks, pg_blocking_pids(), et pg_stat_activity), vous êtes maintenant équipé pour :
- Identifier rapidement les situations de blocage
- Comprendre qui bloque qui et pourquoi
- Résoudre les situations d'urgence
- Prévenir les problèmes futurs
N'oubliez pas : la meilleure résolution de problème est la prévention. Des transactions courtes, une bonne indexation et une surveillance proactive vous éviteront la majorité des cauchemars liés aux verrous.
Prochaine étape : 19.4.2 - Saturation des ressources (CPU, RAM, I/O)