Les niveaux d'isolation des transactions PostgreSQL en pratique

PostgreSQL implémente trois niveaux d'isolation distincts — Read Committed, Repeatable Read et Serializable. (READ UNCOMMITTED existe pour la compatibilité avec le standard SQL, mais se comporte exactement comme Read Committed : PostgreSQL ne vous montre jamais de données non validées.) Les différences ne comptent qu'en situation de concurrence, c'est-à-dire précisément quand elles sont le plus difficiles à déboguer — il vaut donc mieux les connaître avant l'incident.

Read Committed : le niveau par défaut, et son angle mort

Chaque instruction voit un instantané de la base de données au moment où cette instruction a commencé. Deux conséquences :

Les correctifs idiomatiques à ce niveau :

-- make the read-modify-write atomic in one statement:
UPDATE accounts SET balance = balance - 10 WHERE id = 1;

-- or lock the row at read time:
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;

Un détail utile : lorsqu'un UPDATE sous Read Committed trouve une ligne déjà modifiée par une transaction concurrente validée, il ré-évalue sa clause WHERE sur la nouvelle version et poursuit. Vous n'obtenez pas d'erreur — ce qui est pratique, et parfois la source de bogues très subtils.

Repeatable Read : un seul instantané pour toute la transaction

L'instantané est pris à la première requête et conservé jusqu'au commit : chaque instruction voit les mêmes données. Dans PostgreSQL, cela empêche aussi les lectures fantômes (plus strict que ce que le standard SQL exige). Le prix se paie sur les écritures :

ERROR:  could not serialize access due to concurrent update

Si vous tentez de modifier une ligne qu'une transaction concurrente a modifiée et validée après la prise de votre instantané, PostgreSQL ne peut pas faire comme si les deux ordres avaient eu lieu — il vous avorte. Votre application doit être prête à rejouer l'intégralité de la transaction. Repeatable Read est le niveau naturel pour des lectures cohérentes multi-requêtes : rapports, exports, sauvegardes (pg_dump s'exécute à ce niveau).

Serializable : comme si les transactions s'exécutaient une à la fois

Serializable dans PostgreSQL, c'est Repeatable Read plus le SSI (serializable snapshot isolation) : le moteur suit les dépendances lecture/écriture entre transactions concurrentes et en avorte une dès qu'un cycle rendrait le résultat impossible sous n'importe quel ordre sériel. Il détecte des anomalies que Repeatable Read ne peut pas — le cas d'école est le write skew (anomalie d'écriture asymétrique) :

-- Two doctors, rule: at least one must stay on call.
-- T1: SELECT count(*) FROM oncall;  -- sees 2, so it's safe to leave
-- T2: SELECT count(*) FROM oncall;  -- sees 2, so it's safe to leave
-- T1: DELETE FROM oncall WHERE doctor = 'alice'; COMMIT;
-- T2: DELETE FROM oncall WHERE doctor = 'bob';   COMMIT;
-- Repeatable Read: both commit, zero doctors on call.
-- Serializable: one of them gets error 40001 and must retry.

Coûts : un peu de CPU et de mémoire pour le suivi des dépendances, et — surtout — des faux positifs : des transactions peuvent être avortées « par précaution ». Toutes les transactions de la charge de travail doivent s'exécuter en Serializable pour que ses garanties tiennent, et chacune d'elles a besoin d'une logique de retry.

Rejouer correctement

Choisir en pratique

SituationChoix raisonnable
OLTP typique avec mises à jour de lignes cibléesRead Committed + mises à jour atomiques / SELECT FOR UPDATE là où c'est important
Lectures multi-instructions devant être mutuellement cohérentesRepeatable Read
Invariants s'étendant sur plusieurs lignes (« au moins un », « la somme ne doit pas dépasser », double réservation)Serializable avec retries — ou verrouillage explicite si les points chauds sont peu nombreux et connus
Files de tâchesRead Committed + FOR UPDATE SKIP LOCKED

Définissez le niveau par transaction (BEGIN ISOLATION LEVEL SERIALIZABLE) ou par session ; mélanger les niveaux ne pose pas de problème, sauf que, comme indiqué, les garanties de Serializable exigent que toutes les transactions liées l'utilisent.

🔍 À lire aussi : VACUUM & bloat — les transactions longues, quel que soit le niveau d'isolation, retiennent le nettoyage pour toute la base de données.
🧯 Erreurs liées : could not serialize access, deadlock detected, lock timeout, et current transaction is aborted — celle que vous voyez quand la logique de retry manque.