De les quatre lletres d'ACID, tres són innegociables i una té un comandament de volum: l'aïllament. Pots demanar la il·lusió perfecta que ets l'únic usuari —i pagar-la amb menys concurrència i transaccions que avorten— o relaxar-la a canvi de rendiment, acceptant que passin certes coses estranyes. Aquestes coses estranyes tenen nom des del 1992, són quatre, i aquesta lliçó les demostra una a una en dues sessions.

Després vindran els quatre nivells de l'estàndard SQL amb la taula clàssica de quina anomalia permet cadascun; i tot seguit el que gairebé cap curs no explica: aquesta taula no descriu PostgreSQL. PostgreSQL no implementa READ UNCOMMITTED, i el seu REPEATABLE READ ja impedeix els fantasmes, cosa que l'estàndard no exigeix. Acabarem amb l'error 40001, el més important del mòdul, i amb la seva conseqüència pràctica: si puges de nivell, la teva aplicació ha de saber reintentar.

Tots els exemples són reproduïbles amb dues terminals psql sobre la base acabada de recarregar, en el format de dues sessions de 09-01.

Contingut

  1. Lectura bruta
  2. Lectura no repetible
  3. Lectura fantasma
  4. Actualització perduda i anomalia de serialització
  5. Els quatre nivells de l'estàndard SQL
  6. El que fa PostgreSQL de debò
  7. El nivell per omissió de cada motor
  8. Com es fixa i com es consulta el nivell
  9. READ COMMITTED enfront de REPEATABLE READ en dues sessions
  10. SERIALIZABLE i l'error 40001
  11. El patró de reintent amb retrocés exponencial
  12. Quin nivell triar
  13. Errors habituals i consells
  14. Exercicis
  15. Conclusió

  1. Lectura bruta

Lectura bruta (dirty read): una transacció llegeix dades que una altra ha escrit i encara no ha confirmat, i que poden desaparèixer amb un ROLLBACK.

Instant Sessió A Sessió B
t1 BEGIN;
t2 UPDATE productes SET preu = 99.00 WHERE id = 15;UPDATE 1
t3 SELECT preu FROM productes WHERE id = 15;
t4 ROLLBACK;

En un motor que permetés la lectura bruta, a t3 la sessió B veuria 99.00 — un preu que no ha existit mai per a ningú, perquè a t4 es desfà. Si B fos el procés que genera el fitxer del comparador de preus, BotigaVerda hauria publicat un te matcha a 99 €.

A PostgreSQL, en canvi, B veu 22.00. Sempre. En qualsevol nivell d'aïllament, inclòs el que s'anomena READ UNCOMMITTED:

-- Sessió B, a t3
BEGIN ISOLATION LEVEL READ UNCOMMITTED;
SELECT id, nom, preu FROM productes WHERE id = 15;
COMMIT;
id nom preu
15 Te verd matcha cerimonial 30 g 22.00

I no és que PostgreSQL "s'esforci" a evitar-ho: amb MVCC la lectura bruta és impossible per construcció. La versió nova de la fila porta un xmin d'una transacció no confirmada, i la regla de visibilitat de 09-02 la descarta sense més. No hi ha codi per treure ni nivell per baixar.

Nota de dialecte: la lectura bruta sí que existeix, i s'utilitza. SQL Server implementa READ UNCOMMITTED de debò, i la seva famosa pista WITH (NOLOCK) és exactament això: llegir sense respectar bloquejos ni confirmacions. Es posa en informes per no bloquejar ningú, i a canvi es poden veure files no confirmades, files duplicades i files absents en el mateix recorregut. MySQL/InnoDB també l'implementa. PostgreSQL, Oracle i SQLite, no.

  1. Lectura no repetible

Lectura no repetible (non-repeatable read): la mateixa consulta, sobre la mateixa fila, retorna valors diferents dins d'una mateixa transacció, perquè una altra la va modificar i confirmar pel mig.

El cas de BotigaVerda: l'analista està calculant l'informe de marges i, a mitges, el responsable de compres apuja el preu del matcha.

Instant Sessió A — analista (READ COMMITTED) Sessió B — compres
t1 BEGIN;
t2 SELECT preu FROM productes WHERE id = 15;22.00
t3 UPDATE productes SET preu = 24.00 WHERE id = 15; COMMIT;
t4 SELECT preu FROM productes WHERE id = 15;24.00
t5 COMMIT;

Dues lectures de la mateixa fila dins de la mateixa transacció, dos valors diferents. Si l'informe suma imports a t2 i calcula percentatges a t4, els percentatges no quadraran amb els imports i ningú no sabrà per què.

La causa és exactament la de 09-02: a READ COMMITTED, cada sentència pren una instantània nova. A REPEATABLE READ la instantània és una de sola per a tota la transacció, i t4 continuaria retornant 22.00 — ho veuràs a l'apartat 9.

  1. Lectura fantasma

Lectura fantasma (phantom read): la mateixa consulta retorna un conjunt de files diferent, perquè una altra transacció va inserir o esborrar files que compleixen la condició.

La diferència amb l'anterior és subtil però important: allà canviava el valor d'una fila; aquí canvia quines files hi ha.

Instant Sessió A — informe mensual (READ COMMITTED) Sessió B — un client comprant
t1 BEGIN;
t2 SELECT COUNT(*) FROM comandes;20
t3 INSERT INTO comandes (...) VALUES (6, NULL, DATE '2026-03-05', 'pagat', 'targeta', 4.95); COMMIT;
t4 SELECT COUNT(*) FROM comandes;21
t5 SELECT COUNT(*) FROM linies_comanda;47
t6 COMMIT;

L'informe dirà que hi ha 21 comandes i 47 línies, i algú passarà la tarda buscant la comanda sense línies. Aquesta comanda 21 és el fantasma: va aparèixer a mitja elaboració de l'informe.

El detall que gairebé ningú no explica: l'estàndard SQL permet fantasmes a REPEATABLE READ, però PostgreSQL no els permet. El seu REPEATABLE READ està implementat com a snapshot isolation: una única instantània per a tota la transacció, i una instantània no canvia de contingut. A PostgreSQL, t4 retornaria 20 — el mateix número que t2. És una garantia més forta que la de l'estàndard, i hi tornarem a l'apartat 6.

  1. Actualització perduda i anomalia de serialització

Aquesta és la important, la que li pot costar diners a BotigaVerda, i la que va deixar pendent 09-03.

Actualització perduda (lost update): dues transaccions llegeixen el mateix valor, cadascuna calcula un valor nou a partir d'ell i totes dues escriuen. La segona escriptura esborra la feina de la primera.

L'escenari: queden 40 unitats de te matcha i dos clients en compren una cadascun alhora. L'aplicació fa el que és natural —llegir l'estoc, restar, escriure—, que és precisament el que 05-05 ja va assenyalar com a incorrecte:

Instant Sessió A — client 1 Sessió B — client 2
t1 BEGIN;
t2 BEGIN;
t3 SELECT stock FROM productes WHERE id = 15;40
t4 SELECT stock FROM productes WHERE id = 15;40
t5 UPDATE productes SET stock = 39 WHERE id = 15;UPDATE 1
t6 COMMIT;
t7 UPDATE productes SET stock = 39 WHERE id = 15;UPDATE 1
t8 COMMIT;
SELECT id, nom, stock FROM productes WHERE id = 15;
id nom stock
15 Te verd matcha cerimonial 30 g 39

S'han venut dues unitats i només se n'ha descomptat una. Cap sessió no ha rebut un error, cap registre no ha quedat al log, i el descompensament no apareixerà fins a l'inventari físic. Amb dues sessions la pèrdua és d'una unitat; amb un pic de trànsit per Nadal, de desenes.

I aquí la sorpresa: la versió relativa que és correcta

Canvia únicament la forma de l'UPDATE:

Instant Sessió A Sessió B
t5 UPDATE productes SET stock = stock - 1 WHERE id = 15;
t6 COMMIT;
t7 UPDATE productes SET stock = stock - 1 WHERE id = 15;espera fins a t6
t8 COMMIT;
id nom stock
15 Te verd matcha cerimonial 30 g 38

Trenta-vuit: correcte. I no és màgia. Quan B intenta actualitzar a t7 una fila que A té bloquejada, B es queda esperant (és el bloqueig implícit de 09-05). En confirmar A, PostgreSQL no aplica l'UPDATE de B a cegues: torna a llegir la fila actualitzada, reavalua el WHERE i recalcula l'expressió sobre la versió nova. Així que stock - 1 es calcula sobre 39, no sobre 40.

La regla que cal extreure, i val per a tot el mòdul: a READ COMMITTED, una sola sentència d'escriptura és segura; el perill és a llegir amb una sentència i escriure amb una altra. SET stock = stock - 1 és segur; SELECT i després SET stock = 39 no ho és. És la mateixa lliçó de 05-05 amb l'upsert i la de 09-02 amb WHERE stock >= 1.

Quan la lògica no cap en una sentència —perquè cal consultar tres taules, aplicar una regla i decidir— hi ha dues solucions correctes, i són les dues lliçons que queden: pujar el nivell d'aïllament (apartats següents) o bloquejar explícitament la fila amb SELECT ... FOR UPDATE (09-05).

  1. Els quatre nivells de l'estàndard SQL

L'estàndard SQL:1992 va definir els nivells per les anomalies que permeten, no per com s'implementen:

Nivell Lectura bruta Lectura no repetible Lectura fantasma
READ UNCOMMITTED Possible Possible Possible
READ COMMITTED No Possible Possible
REPEATABLE READ No No Possible
SERIALIZABLE No No No

Aquesta és la taula que surt a totes les entrevistes de feina. I és una definició de mínims: diu el que un motor pot permetre, no el que fa. Un motor que no permeti cap anomalia en cap nivell compleix l'estàndard perfectament.

  1. El que fa PostgreSQL de debò

Nivell Lectura bruta No repetible Fantasma Actualització perduda Anomalia de serialització
READ UNCOMMITTED Impossible Possible Possible Possible Possible
READ COMMITTED (per omissió) Impossible Possible Possible Possible Possible
REPEATABLE READ Impossible No No No (avorta amb 40001) Possible
SERIALIZABLE Impossible No No No No (avorta amb 40001)

Tres diferències respecte de l'estàndard, i totes tres importen:

  1. READ UNCOMMITTED no existeix. S'accepta la sintaxi per compatibilitat, però es comporta exactament com READ COMMITTED. La lectura bruta és impossible amb MVCC (apartat 1).
  2. REPEATABLE READ ja impedeix els fantasmes. L'estàndard els permet; PostgreSQL, no. La seva implementació és snapshot isolation: una única instantània per a tota la transacció. Només hi ha tres nivells distingibles a la pràctica.
  3. REPEATABLE READ i SERIALIZABLE no bloquegen per aconseguir-ho: avorten. En lloc de fer-te esperar, et deixen treballar i, si al final detecten un conflicte, maten la teva transacció amb l'error 40001. Aquest canvi de model és el que obliga a reintentar, i és l'apartat 11.

I una quarta que mereix la seva pròpia línia, perquè és la raó de ser del nivell més alt:

L'anomalia de serialització és més general que les tres clàssiques: és qualsevol resultat que cap execució seqüencial de les mateixes transaccions no hauria pogut produir, encara que cadascuna hagi llegit i escrit dades confirmades i diferents. SERIALIZABLE és l'únic nivell que la impedeix, i ho aconsegueix amb SSI (Serializable Snapshot Isolation): vigila les dependències de lectura i escriptura entre transaccions i avorta les que formin un cicle perillós.

  1. El nivell per omissió de cada motor

Motor Per omissió Notes imprescindibles
PostgreSQL READ COMMITTED READ UNCOMMITTED = READ COMMITTED. REPEATABLE READ sense fantasmes. SERIALIZABLE amb SSI
MySQL / InnoDB REPEATABLE READ Evita fantasmes a les lectures normals per instantània, i a les de bloqueig amb gap locks. Però les seves escriptures llegeixen l'última versió confirmada, no la de la instantània: permet actualitzacions perdudes que PostgreSQL avortaria
SQL Server READ COMMITTED Amb bloquejos, no amb instantànies: un lector pot bloquejar un escriptor. Amb READ_COMMITTED_SNAPSHOT ON passa a un model tipus MVCC. Implementa READ UNCOMMITTED (WITH (NOLOCK))
Oracle READ COMMITTED No implementa REPEATABLE READ: només té READ COMMITTED, SERIALIZABLE i READ ONLY. El seu SERIALIZABLE és snapshot isolation, més feble que el de PostgreSQL
SQLite SERIALIZABLE de facto Un únic escriptor alhora a tota la base. Aïllament perfecte i concurrència d'escriptura nul·la

Nota de dialecte — el parany en migrar. El nom del nivell no diu el mateix en dos motors. Una aplicació escrita contra MySQL corre en REPEATABLE READ sense saber-ho i, en portar-la a PostgreSQL, passa a READ COMMITTED: apareixen lectures no repetibles que allà no passaven. I a l'inrevés: codi que a PostgreSQL funciona en REPEATABLE READ perquè el motor avorta els conflictes, a MySQL no avorta i perd actualitzacions en silenci. Verifica sempre el nivell efectiu en migrar; és el primer que cal mirar.

  1. Com es fixa i com es consulta el nivell

BEGIN ISOLATION LEVEL REPEATABLE READ;     -- per a aquesta transacció
-- o, equivalent:
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SHOW transaction_isolation;                -- quin nivell tinc ara
transaction_isolation
repeatable read

I per a tota la sessió o per a tot el servidor, el de 09-03: SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ..., ALTER ROLE ... SET default_transaction_isolation, o aquest mateix paràmetre a postgresql.conf.

  1. READ COMMITTED enfront de REPEATABLE READ en dues sessions

Tota la diferència és en quan es pren la instantània:

flowchart LR
    subgraph RC["<b>READ COMMITTED</b>"]
        direction TB
        R1["sentència 1 → <b>instantània nova</b>"] --> R2["sentència 2 → <b>instantània nova</b>"] --> R3["sentència 3 → <b>instantània nova</b>"]
    end
    subgraph RR["<b>REPEATABLE READ</b> / <b>SERIALIZABLE</b>"]
        direction TB
        S0["<b>una sola instantània</b><br/>presa a la 1a sentència"] --> S1["sentència 1"] & S2["sentència 2"] & S3["sentència 3"]
    end

Mateix guió, dos nivells, resultats diferents. La sessió A és l'informe; la B, l'operativa de la botiga.

A en READ COMMITTED (una instantània nova per sentència):

Instant Sessió A — BEGIN; Sessió B
t1 SELECT preu FROM productes WHERE id = 15;22.00
t2 SELECT COUNT(*) FROM comandes;20
t3 UPDATE productes SET preu = 24.00 WHERE id = 15;
t4 INSERT INTO comandes (...); COMMIT;
t5 SELECT preu FROM productes WHERE id = 15;24.00 ← no repetible
t6 SELECT COUNT(*) FROM comandes;21 ← fantasma

A en REPEATABLE READ (BEGIN ISOLATION LEVEL REPEATABLE READ;), amb el guió idèntic:

Instant Sessió A Sessió B
t5 SELECT preu FROM productes WHERE id = 15;22.00
t6 SELECT COUNT(*) FROM comandes;20

Els mateixos valors que a t1 i t2. Per a A, el món es va congelar en l'instant de la seva primera consulta, i l'UPDATE i l'INSERT de B no existeixen fins que A confirmi i comenci una altra transacció. Ni lectura no repetible ni fantasma: és exactament el que un informe necessita.

El preu d'aquesta foto fixa apareix així que A intenta escriure alguna cosa que B ja ha canviat:

Instant Sessió A (REPEATABLE READ) Sessió B
t1 BEGIN ISOLATION LEVEL REPEATABLE READ; SELECT stock FROM productes WHERE id = 15;40
t2 UPDATE productes SET stock = 39 WHERE id = 15; COMMIT;
t3 UPDATE productes SET stock = 39 WHERE id = 15;
ERROR:  could not serialize access due to concurrent update

L'actualització perduda de l'apartat 4 ja no passa: en lloc de trepitjar el canvi de B en silenci, PostgreSQL mata la transacció d'A. Aquest és el tracte dels nivells alts, i és un tracte honest: prefereixes un error que puguis reintentar abans que un descompensament invisible.

  1. SERIALIZABLE i l'error 40001

REPEATABLE READ protegeix cada fila, però no protegeix relacions entre files. El cas típic a BotigaVerda: la regla "l'estoc total de la categoria Begudes no pot baixar de 250 unitats", comprovada per dos operaris alhora.

Instant Sessió A (SERIALIZABLE) Sessió B (SERIALIZABLE)
t1 BEGIN ISOLATION LEVEL SERIALIZABLE; BEGIN ISOLATION LEVEL SERIALIZABLE;
t2 SELECT SUM(stock) FROM productes WHERE categoria_id = 4;370
t3 SELECT SUM(stock) FROM productes WHERE categoria_id = 4;370
t4 UPDATE productes SET stock = stock - 60 WHERE id = 14; (370 − 60 = 310 ≥ 250 ✓)
t5 UPDATE productes SET stock = stock - 60 WHERE id = 16; (370 − 60 = 310 ≥ 250 ✓)
t6 COMMIT;COMMIT
t7 COMMIT;
ERROR:  could not serialize access due to read/write dependencies among transactions
DETAIL:  Reason code: Canceled on identification as a pivot, during commit attempt.
HINT:  The transaction might succeed if retried.

Totes dues van llegir 370, totes dues van restar 60, i el total real hauria quedat en 250… que encara compleix la regla. Canvia els 60 per 70 i el total baixa a 230: les dues comprovacions eren correctes i el resultat combinat, no. Cap fila no es va trepitjar —A va tocar el producte 14 i B el 16—, així que REPEATABLE READ hauria deixat passar totes dues. Només SERIALIZABLE veu que A va llegir el que B va escriure i a l'inrevés i n'avorta una.

Fixa't en tres coses de l'error, perquè són la signatura de tot el nivell: el codi 40001, comú a tots els errors de serialització; el HINT explícit que reintentar pot funcionar —no és un error de dades, és un conflicte de programació temporal—; i que la fallada arriba al COMMIT, quan ja has fet tota la feina.

REPEATABLE READ SERIALIZABLE
Què vigila Que una mateixa fila no l'escriguin dos A més, les dependències de lectura i escriptura entre transaccions
Quan avorta A l'UPDATE/DELETE conflictiu Normalment al COMMIT
Cost Pràcticament el de READ COMMITTED Seguiment de predicats; més memòria i més avortaments
Quan triar-lo Informes coherents, processos per lots Invariants que abasten diverses files o taules

  1. El patró de reintent amb retrocés exponencial

Si fas servir REPEATABLE READ o SERIALIZABLE, la teva aplicació ha d'estar preparada per reintentar. No és opcional ni és un cas rar: és el mode normal de funcionament d'aquests nivells.

INTENTS_MAX = 5
espera = 0,05 s                      # 50 ms

per a intent en 1..INTENTS_MAX:
    connexio.begin(isolation = SERIALIZABLE)
    try:
        ... la unitat de treball COMPLETA ...
        connexio.commit()
        sortir amb èxit
    except error amb SQLSTATE == '40001' o '40P01':     # serialització o interbloqueig
        connexio.rollback()
        if intent == INTENTS_MAX:
            rellançar                                    # es rendeix i avisa
        dormir(espera * (1 + aleatori(0, 0.5)))          # ← "jitter": desincronitza
        espera = espera * 2                              # 50, 100, 200, 400 ms
    except qualsevol_altre_error:
        connexio.rollback()
        rellançar                                        # NO es reintenta

Sis regles que fan que aquest bucle funcioni de debò:

  1. Es reintenta la transacció sencera, des del BEGIN. No es pot continuar on es va quedar: la seva instantània ja no val.
  2. Només es reintenten 40001 i 40P01 (interbloqueig, 09-05). Una violació de clau forana no s'arregla repetint-la: es repetirà igual cinc vegades i es perdran cinc segons.
  3. El retrocés és exponencial i amb atzar. Sense la component aleatòria (jitter), dues transaccions que xoquen reintenten alhora i tornen a xocar, indefinidament.
  4. La transacció ha de ser idempotent, que és la propietat de 05-03 i 09-03: si el primer intent va arribar a inserir la comanda i va fallar després, el segon no la pot duplicar. Una clau única de negoci ho resol.
  5. Res fora de la base de dades dins del bloc: reintentar cinc vegades una transacció que cobra amb targeta significa cinc càrrecs (09-02). I compta els reintents: una taxa creixent de 40001 és el senyal que hi ha contenció real i que el problema és de disseny, no de configuració.

  1. Quin nivell triar

Cas Nivell Per què
Consulta solta, llistat de catàleg, fitxa de producte READ COMMITTED Cada sentència veu l'últim confirmat. És el que vols i és el que hi ha per omissió
Informe analític de diverses consultes que han de quadrar entre si REPEATABLE READ, millor amb READ ONLY Una foto fixa de tota la base. A PostgreSQL, sense fantasmes. I SERIALIZABLE READ ONLY DEFERRABLE (09-03) si a més no vol avortar mai
Confirmar una comanda descomptant estoc READ COMMITTED + UPDATE ... WHERE stock >= n, o SELECT ... FOR UPDATE (09-05) La solució no és pujar el nivell: és fer l'operació en una sentència o bloquejar la fila. Més simple i més ràpid
Comptador o saldo (SET total = total + n) READ COMMITTED La forma relativa ja és segura (apartat 4). Pujar el nivell només afegeix avortaments
Invariant sobre diverses files o taules ("el total de la categoria no baixa de 250", "no més de N reserves") SERIALIZABLE amb reintent És l'únic cas on el nivell més alt és la resposta correcta, perquè cap sentència sola no pot expressar la regla
Procés per lots nocturn sobre dades que ningú més no toca REPEATABLE READ Coherència gratis: sense concurrència real no hi ha conflictes que avortin

La regla de decisió, en una frase: comença sempre a READ COMMITTED i puja només quan puguis anomenar l'anomalia concreta que t'està fent mal. Pujar de nivell no és "més segur" sense més: és canviar un problema silenciós per un error explícit que cal gestionar amb codi.

Errors habituals i consells

  • Estudiar-se la taula de l'estàndard i creure que descriu el teu motor. No en descriu cap amb exactitud: PostgreSQL no té READ UNCOMMITTED i el seu REPEATABLE READ no permet fantasmes. I READ COMMITTED no protegeix de l'actualització perduda: protegeix de la lectura bruta i de res més.
  • Confondre SELECT + UPDATE amb un UPDATE relatiu. SET stock = stock - 1 és segur fins i tot a READ COMMITTED; llegir 40 i escriure 39 no ho és. És la distinció més rendible de la lliçó.
  • Pujar a SERIALIZABLE sense escriure el reintent. Canvies corrupció silenciosa per caigudes visibles en producció. El nivell alt sense reintent és pitjor que el nivell baix.
  • Reintentar qualsevol error, o reintentar sense jitter. Només 40001 i 40P01 mereixen reintent; repetir una violació de restricció és perdre el temps cinc vegades seguides. I sense la component aleatòria, dues transaccions sincronitzades tornen a xocar a cada volta.
  • Suposar que el REPEATABLE READ de MySQL és el de PostgreSQL. El de MySQL permet actualitzacions perdudes que PostgreSQL avorta; el de PostgreSQL impedeix fantasmes que el de MySQL només evita amb gap locks. I WITH (NOLOCK) a SQL Server no és "anar més ràpid": és lectura bruta, amb files no confirmades, duplicades o absents en el mateix recorregut.
  • Consell: posa SHOW transaction_isolation; a la teva llista de comprovacions en depurar concurrència; és la primera pregunta. I per a un informe, BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;: coherència total i ni un sol bloqueig a ningú.
  • Consell: mesura els 40001. La seva taxa et diu si el teu disseny té contenció real, i aquesta informació no és a cap altre lloc.

Exercicis

Amb dues terminals psql i la base acabada de recarregar.

Exercici 1

Demostra l'actualització perduda i les seves dues solucions.

  1. Reprodueix l'apartat 4 amb el patró SELECTUPDATE ... SET stock = 39 a READ COMMITTED. Comprova que l'estoc final és 39 i explica quina unitat s'ha perdut.
  2. Repeteix-ho amb UPDATE ... SET stock = stock - 1. Comprova que el resultat és 38 i descriu què fa exactament la sessió B a t7.
  3. Repeteix el cas 1 posant les dues sessions a REPEATABLE READ. Què passa i quan exactament? Copia l'error.
  4. Quin nivell d'aïllament faries servir a l'aplicació real de BotigaVerda per confirmar una comanda, i per què no és la resposta pujar a SERIALIZABLE?

Exercici 2

L'analista Daniel Vercher (empleat 8) llança l'informe mensual i es queixa que "els números no quadren entre les taules del PDF".

  1. Reprodueix el problema: sessió A a READ COMMITTED compta comandes, una altra sessió insereix una comanda amb dues línies i confirma, i A torna a comptar comandes i línies. Mostra els números incoherents.
  2. Identifica quina anomalia és i per què l'estàndard l'anomena així.
  3. Arregla-ho canviant una sola línia del guió d'A, i demostra que ara quadra.
  4. Si l'informe triga 40 minuts, quin efecte col·lateral té mantenir aquesta transacció oberta tot aquest temps? Relaciona-ho amb el que vas veure a 09-02 i proposa la variant de 09-03 que ho mitiga.

Exercici 3

BotigaVerda imposa una regla nova: la categoria Begudes (4) no pot baixar mai de 250 unitats d'estoc total. Avui en té 370.

  1. Escriu la transacció que descompta 70 unitats del producte 14 comprovant abans la regla, a SERIALIZABLE.
  2. Executa-la alhora en dues sessions —una sobre el producte 14 (stock 180) i una altra sobre el 17 (stock 90)— i mostra què passa a cada COMMIT.
  3. Què hauria passat a REPEATABLE READ? I a READ COMMITTED? Raona per què cap dels dos no detecta el problema.
  4. Escriu el pseudocodi de reintent que caldria perquè aquesta operació funcionés en producció, i digues per què no es pot resoldre amb un CHECK.

Solucions

Solució 1

1. Estoc final 39. S'ha perdut la unitat de la sessió A: el seu UPDATE de t5 es va arribar a escriure i a confirmar, però el de B el va sobreescriure amb un valor absolut calculat a partir d'una lectura ja obsoleta. A va vendre i no va descomptar.

2. Estoc final 38. A t7, la sessió B intenta actualitzar una fila que A té bloquejada, així que es queda esperant —l'UPDATE no retorna res, la terminal es queda penjada— fins que A fa COMMIT a t6. Llavors PostgreSQL rellegeix la fila ja actualitzada, reavalua el WHERE i recalcula stock - 1 sobre 39, no sobre 40. L'atomicitat d'una sola sentència fa la feina.

3. La sessió que arriba segona falla al seu UPDATE, no al COMMIT:

ERROR:  could not serialize access due to concurrent update

L'actualització perduda és ara impossible, però a canvi cal reintentar. I observa el matís: a REPEATABLE READ el conflicte es detecta en escriure, mentre que a SERIALIZABLE (exercici 3) se sol detectar en confirmar.

4. A l'aplicació real, READ COMMITTED més una d'aquestes dues: l'UPDATE ... WHERE id = 15 AND stock >= 1 de 09-02, o el SELECT ... FOR UPDATE de 09-05. Pujar a SERIALIZABLE no és la resposta per tres motius: funcionaria, però obligaria a implementar el bucle de reintent al camí més calent de la botiga; avorta sota càrrega justament quan hi ha més comandes, que és el pitjor possible; i és innecessari, perquè l'invariant afecta una sola fila i una sola sentència el pot expressar. El nivell alt es reserva per a invariants que cap sentència no pot expressar.

Solució 2

1. A obté 20 comandes a la seva primera consulta i, després de l'INSERT de l'altra sessió, 21 comandes i 49 línies. El PDF diria 21 comandes amb 49 línies si totes dues comptes es fessin després, o 20 comandes i 49 línies si es fessin a cavall del COMMIT aliè: en qualsevol cas, dues xifres preses de dos estats diferents del món.

2. És una lectura fantasma: la mateixa consulta retorna un conjunt de files diferent perquè una altra transacció va inserir files que compleixen la condició. S'anomena així perquè les files noves "apareixen" en una consulta que ja s'havia executat, com si es materialitzessin del no-res.

3. La línia que canvia és el BEGIN:

BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;

Amb la instantània fixa, A veu 20 comandes i 47 línies a totes les seves consultes, comenci la sessió B el que comenci. A PostgreSQL això n'hi ha prou perquè el seu REPEATABLE READ no permet fantasmes; en un motor que seguís l'estàndard al peu de la lletra caldria SERIALIZABLE.

4. Mantenir 40 minuts una transacció oberta amb instantània fixa impedeix que VACUUM netegi cap versió de fila posterior a aquell instant, a tota la base de dades: és el mecanisme del bloat que va explicar 09-02 i el dany que va anticipar 09-01. La mitigació de 09-03 és llançar l'informe com a BEGIN ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE;: continua retenint la seva instantània, però com que és de només lectura i diferible no bloqueja ningú ni pot avortar. I la solució de fons és la de sempre: executar els informes llargs sobre una rèplica (09-02).

Solució 3

1. La transacció, per al producte 14:

BEGIN ISOLATION LEVEL SERIALIZABLE;

SELECT SUM(stock) AS total_begudes FROM productes WHERE categoria_id = 4;   -- 370
-- L'aplicació comprova: 370 - 70 = 300 >= 250  ✓
UPDATE productes SET stock = stock - 70 WHERE id = 14;

COMMIT;

2. La primera a confirmar té èxit. La segona falla al COMMIT:

ERROR:  could not serialize access due to read/write dependencies among transactions
HINT:  The transaction might succeed if retried.

Cadascuna va comprovar 370 − 70 = 300 i totes dues tenien raó per separat; juntes deixarien el total en 230, per sota del límit. SERIALIZABLE detecta que A va llegir un conjunt de files que B va modificar i a l'inrevés —una dependència creuada de lectura i escriptura— i en cancel·la una de les dues.

3. Ni REPEATABLE READ ni READ COMMITTED no ho detecten, i pel mateix motiu: no hi ha cap fila en conflicte. A escriu el producte 14 i B el 17; són files diferents, així que no hi ha bloqueig, no hi ha conflicte d'actualització i res per avortar. REPEATABLE READ protegeix files; l'invariant aquí és sobre un conjunt. A READ COMMITTED seria encara pitjor, perquè a més cada sentència veuria dades diferents. Totes dues confirmarien feliçment i el total quedaria en 230.

4. El pseudocodi és el de l'apartat 11: bucle de fins a cinc intents, BEGIN ISOLATION LEVEL SERIALIZABLE, captura de 40001, rollback(), espera amb retrocés exponencial i jitter, i rellançar qualsevol altre error sense reintentar.

I no es pot resoldre amb un CHECK pel que va explicar 09-02: un CHECK només pot mirar les columnes de la pròpia fila, i aquesta regla necessita sumar l'estoc de quatre files de la taula. Les alternatives reals són tres: SERIALIZABLE amb reintent (la d'aquest exercici); bloquejar explícitament les quatre files amb SELECT ... FOR UPDATE abans de comprovar (09-05); o mantenir el total agregat en una fila pròpia i protegir-la amb un CHECK i un trigger que la mantingui (10-05). La primera és la més neta; la segona, la més previsible sota càrrega.

Conclusió

Ja saps què et pot passar i quant costa evitar-ho:

  • Les quatre anomalies, demostrades en dues sessions: lectura bruta (llegir el que no s'ha confirmat), lectura no repetible (la mateixa fila canvia de valor), lectura fantasma (apareixen files noves) i actualització perduda / anomalia de serialització (dues transaccions correctes per separat produeixen un resultat que cap execució seqüencial no donaria).
  • Que l'actualització perduda és el cas central: dos clients compren l'últim matcha, tots dos llegeixen 40, tots dos escriuen 39, i es ven una unitat que ningú no descompta, sense error ni traça.
  • I la seva contrapartida més útil de tot el mòdul: a READ COMMITTED, una sola sentència d'escriptura és seguraSET stock = stock - 1 dóna 38 perquè el motor rellegeix i recalcula— i el perill és a llegir amb una sentència i escriure amb una altra.
  • Els quatre nivells de l'estàndard amb la seva taula clàssica… i el que PostgreSQL fa de debò: no implementa READ UNCOMMITTED (la lectura bruta és impossible amb MVCC), el seu REPEATABLE READ ja impedeix els fantasmes i només hi ha tres nivells distingibles.
  • El nivell per omissió de cada motorREAD COMMITTED a PostgreSQL, Oracle i SQL Server; REPEATABLE READ a InnoDB; SERIALIZABLE de facto a SQLite— i el parany en migrar: el mateix nom no significa el mateix.
  • L'error 40001, en les seves dues formes (concurrent update a REPEATABLE READ, read/write dependencies a SERIALIZABLE), amb el seu HINT dient que reintentar pot funcionar. I la conseqüència: pujar de nivell obliga a escriure el bucle de reintent, amb retrocés exponencial, jitter, filtratge per SQLSTATE i una unitat de treball idempotent.
  • I el criteri: READ COMMITTED per omissió; REPEATABLE READ READ ONLY per a informes; SERIALIZABLE només per a invariants que abasten diverses files i cap sentència no pot expressar. Puja de nivell únicament quan puguis anomenar l'anomalia que t'està fent mal.

Queda l'altra resposta a l'actualització perduda, la que no avorta res i la que fan servir de debò els sistemes de comerç electrònic: bloquejar la fila. A Gestió de la concurrència: bloquejos i interbloquejos veuràs per què la sessió B es quedava esperant a t7 i què la retenia; el patró llegir-modificar-escriure segur amb SELECT ... FOR UPDATE i tota la seva família; SKIP LOCKED per repartir una cua de comandes entre diversos processos sense trepitjar-se; el bloqueig optimista amb columna de versió enfront del pessimista; què bloqueja un ALTER TABLE i per què havia d'existir CREATE INDEX CONCURRENTLY; i els interbloquejos: com es produeixen, com els detecta i resol el motor, com es diagnostiquen amb pg_locks i pg_blocking_pids, i les regles per no provocar-los mai.

Curs de SQL

Mòdul 1: Introducció a SQL

Mòdul 2: Consultes bàsiques de SQL

Mòdul 3: Treballar amb múltiples taules

Mòdul 4: Filtratge avançat de dades

Mòdul 5: Manipulació de dades

Mòdul 6: Funcions avançades de SQL

Mòdul 7: Subconsultes i consultes imbricades

Mòdul 8: Índexs i optimització del rendiment

Mòdul 9: Transaccions i concurrència

Mòdul 10: Temes avançats

Mòdul 11: SQL a la pràctica

Mòdul 12: Projecte final

© Copyright 2026. Tots els drets reservats