09-04 va acabar amb una pregunta oberta: si SERIALIZABLE no és la resposta per confirmar una comanda, quina ho és? I amb una observació sense explicar: quan la sessió B intentava actualitzar una fila que A ja havia tocat, B es quedava esperant. Aquesta lliçó explica què la retenia, i amb això arriba l'eina que els sistemes de comerç electrònic fan servir de debò perquè dos clients no comprin la mateixa unitat: el bloqueig.
Veuràs els bloquejos de fila implícits que ja estaves provocant sense saber-ho; la família completa de SELECT ... FOR UPDATE i el patró llegir-modificar-escriure segur, que és la solució correcta a l'actualització perduda; SKIP LOCKED per repartir una cua de comandes entre diversos operaris sense trepitjar-se; el bloqueig optimista enfront del pessimista; els bloquejos de taula, què bloqueja un ALTER TABLE i per què havia d'existir CREATE INDEX CONCURRENTLY; i els interbloquejos, amb la seva demostració, el seu missatge real, com els resol el motor i les regles per no provocar-los. Tot reproduïble amb dues terminals psql.
Contingut
- Bloquejos de fila implícits
- La família
SELECT ... FOR ... - El patró llegir-modificar-escriure segur
SKIP LOCKED: la cua de treballsNOWAIT: fallar ràpid- Bloqueig optimista enfront de pessimista
- Bloquejos de taula,
ALTER TABLEiCREATE INDEX CONCURRENTLY - Interbloquejos
- Diagnòstic: qui bloqueja qui
- Els quatre temps d'espera
- Seqüències,
ROLLBACKi els números de factura - Errors habituals i consells
- Exercicis
- Conclusió del mòdul
- Bloquejos de fila implícits
No cal demanar un bloqueig: tot UPDATE i tot DELETE bloquegen la fila que toquen fins al final de la transacció. Reprenguem el te matcha (producte 15, stock 40), amb dues terminals:
| Instant | Sessió A | Sessió B |
|---|---|---|
| t1 | BEGIN; |
|
| t2 | UPDATE productes SET stock = stock - 1 WHERE id = 15; → UPDATE 1 |
|
| t3 | BEGIN; |
|
| t4 | SELECT stock FROM productes WHERE id = 15; → 40, a l'instant |
|
| t5 | UPDATE productes SET stock = stock - 1 WHERE id = 15; ← es queda esperant |
|
| t6 | COMMIT; |
↓ |
| t7 | UPDATE 1 — es desbloqueja sola |
|
| t8 | SELECT stock FROM productes WHERE id = 15; → 38 |
|
| t9 | COMMIT; |
Quatre lliçons en nou instants:
- t4: llegir no espera. És MVCC (09-02): els lectors no es bloquegen amb els escriptors. B llegeix la versió antiga i continua treballant.
- t5: escriure sí que espera. La terminal de B es queda penjada, sense missatge ni cursor. No és una penjada: és a la cua.
- t7: el bloqueig dura fins al final de la transacció d'A, no fins al final del seu
UPDATE. Per això una transacció llarga és una transacció perjudicial (09-01). - t8: surt 38, no 39. En desbloquejar-se, PostgreSQL va rellegir la fila ja actualitzada i va recalcular
stock - 1sobre 39. És exactament el que va explicar 09-04 sobre per què la forma relativa és segura.
I si A hagués fet ROLLBACK a t6, B hauria continuat igualment, però calculant sobre 40 i deixant l'estoc a 39. En tots dos casos, el resultat és correcte.
- La família
SELECT ... FOR ...
SELECT ... FOR ...De vegades necessites bloquejar una fila abans d'escriure-la, perquè entre la lectura i l'escriptura hi ha una decisió a prendre. Per a això hi ha els bloquejos explícits, quatre i ordenats de més fort a més feble:
| Clàusula | Què significa | L'adquireix automàticament |
|---|---|---|
FOR UPDATE |
"Modificaré o esborraré aquesta fila" | UPDATE que toca columnes de clau, DELETE |
FOR NO KEY UPDATE |
"La modificaré, però no la seva clau" | UPDATE que no toca columnes de clau |
FOR SHARE |
"La llegiré i necessito que ningú no la canviï" | — |
FOR KEY SHARE |
"Necessito que la seva clau continuï existint" | Comprovació d'una clau forana |
I la taula de compatibilitat: ✗ significa que el segon espera el primer.
| El que arriba ↓ / ja concedit → | KEY SHARE |
SHARE |
NO KEY UPDATE |
UPDATE |
|---|---|---|---|---|
FOR KEY SHARE |
✓ | ✓ | ✓ | ✗ |
FOR SHARE |
✓ | ✓ | ✗ | ✗ |
FOR NO KEY UPDATE |
✓ | ✗ | ✗ | ✗ |
FOR UPDATE |
✗ | ✗ | ✗ | ✗ |
Aquesta taula, que sembla burocràcia, resol un problema real i molt freqüent: FOR KEY SHARE és el que pren PostgreSQL en comprovar una clau forana. Gràcies al fet que és compatible amb FOR NO KEY UPDATE, inserir una línia de la comanda 21 (que necessita comprovar que la comanda 21 existeix) no espera que una altra sessió actualitzi l'estat d'aquesta comanda. Abans de PostgreSQL 9.3 sí que esperava, i era una font clàssica de bloquejos en cascada.
A la feina diària faràs servir FOR UPDATE en el 95 % dels casos i FOR SHARE en algun control d'integritat. Els altres dos els veuràs als diagnòstics, no els demanaràs tu.
- El patró llegir-modificar-escriure segur
Aquesta és la solució correcta a l'actualització perduda de 09-04, i el patró més important de la lliçó:
BEGIN;
SELECT stock FROM productes WHERE id = 15 FOR UPDATE; -- 1. llegir BLOQUEJANT → 40
-- 2. decidir: aquí l'aplicació aplica la seva lògica (descomptes, reserves, límits…)
UPDATE productes SET stock = 39 WHERE id = 15; -- 3. escriure
COMMIT;I ara les dues sessions alhora, el mateix escenari que perdia una venda a 09-04:
| Instant | Sessió A | Sessió B |
|---|---|---|
| t1 | BEGIN; SELECT stock FROM productes WHERE id = 15 FOR UPDATE; → 40 |
|
| t2 | BEGIN; SELECT stock FROM productes WHERE id = 15 FOR UPDATE; ← espera |
|
| t3 | UPDATE productes SET stock = 39 WHERE id = 15; COMMIT; |
|
| t4 | es desbloqueja i llegeix → 39 | |
| t5 | UPDATE productes SET stock = 38 WHERE id = 15; COMMIT; |
| id | nom | stock |
|---|---|---|
| 15 | Te verd matcha cerimonial 30 g | 38 |
Trenta-vuit. La clau és a t2: el FOR UPDATE de B espera que A acabi, i quan passa, B llegeix 39, no 40. La finestra entre llegir i escriure ha desaparegut, perquè durant tota ella la fila era seva.
Compara-ho amb les altres dues solucions que ja coneixes, perquè totes tres són vàlides i es trien per criteris diferents:
Solució Quan Cost UPDATE ... WHERE id = 15 AND stock >= 1(09-02)La lògica cap en una sentència Cap. És la més ràpida i la primera opció SELECT ... FOR UPDATE+UPDATECal decidir entre llegir i escriure Les altres sessions esperen. Predictible SERIALIZABLEamb reintent (09-04)L'invariant abasta diverses files Avortaments i bucle de reintent
Dos avisos sobre FOR UPDATE: no es pot fer servir amb GROUP BY, DISTINCT, UNION ni funcions de finestra, perquè el motor no sabria quines files físiques bloquejar; i bloqueja totes les files que retorna la consulta, així que un SELECT * FROM productes FOR UPDATE sense WHERE bloqueja el catàleg sencer.
Nota de dialecte:
SELECT ... FOR UPDATEexisteix a PostgreSQL, MySQL/InnoDB i Oracle amb la mateixa sintaxi. SQL Server no la té: fa servir pistes de taula,SELECT ... WITH (UPDLOCK, ROWLOCK). I SQLite l'accepta sintàcticament però no fa res, perquè només admet un escriptor alhora i el problema no se li planteja.
SKIP LOCKED: la cua de treballs
SKIP LOCKED: la cua de treballsAfegint-hi SKIP LOCKED, la consulta no espera: se salta les files bloquejades i continua amb les següents. És exactament el que cal per repartir feina entre diversos processos.
El cas de BotigaVerda: diversos operaris de magatzem preparen les comandes pagat —avui són la 18 i la 19— i cap no ha d'agafar la mateixa que un altre.
-- Cada operari executa això, a la seva pròpia transacció
BEGIN;
SELECT id, client_id, data_comanda
FROM comandes
WHERE estat = 'pagat'
ORDER BY data_comanda
FOR UPDATE SKIP LOCKED
LIMIT 1;| Instant | Sessió A — operari 1 | Sessió B — operari 2 |
|---|---|---|
| t1 | BEGIN; + la consulta → agafa la comanda 18 (2026-01-27) |
|
| t2 | BEGIN; + la mateixa consulta → no espera: se salta la 18 i agafa la 19 |
|
| t3 | UPDATE comandes SET estat = 'enviat' WHERE id = 18; COMMIT; |
|
| t4 | UPDATE comandes SET estat = 'enviat' WHERE id = 19; COMMIT; |
|
| t5 | BEGIN; + la consulta → 0 files: no queda feina |
A t1, la consulta d'A retorna la fila 18 | 5 | 2026-01-27; a t2, la de B retorna 19 | 6 | 2026-02-09. Sense SKIP LOCKED, a t2 l'operari 2 s'hauria quedat esperant l'1 per acabar agafant la mateixa comanda que ja estava feta. Amb ell, la consulta es converteix en un repartidor de feina, i és la forma canònica d'implementar una cua de tasques sobre una taula —enviament de correus, generació de factures, sincronització amb el marketplace— sense cap infraestructura addicional.
NOWAIT: fallar ràpid
NOWAIT: fallar ràpidLa tercera opció: ni esperar ni saltar, sinó rendir-se immediatament. SELECT stock FROM productes WHERE id = 15 FOR UPDATE NOWAIT; retorna, si la fila està presa, ERROR: could not obtain lock on row in relation "productes".
| Modificador | Si la fila està bloquejada | Quan fer-lo servir |
|---|---|---|
| (res) | Espera indefinidament | El cas normal: la feina s'ha de fer |
NOWAIT |
Error immediat | Una petició web amb pressupost de temps: val més dir "torna-ho a provar" en 5 ms que penjar l'usuari 30 segons |
SKIP LOCKED |
Ignora aquesta fila i continua | Cues de feina: qualsevol fila lliure serveix |
- Bloqueig optimista enfront de pessimista
Tot l'anterior és bloqueig pessimista: suposes que hi haurà conflicte i reserves la fila per endavant. L'alternativa és suposar que no n'hi haurà i comprovar-ho en escriure.
Pessimista (FOR UPDATE) |
Optimista (columna de versió) | |
|---|---|---|
| Suposa | Que hi haurà conflicte | Que no n'hi haurà |
| Mecanisme | Bloqueja la fila des de la lectura | Comprova en escriure que ningú no l'ha canviada |
| Les altres sessions | Esperen | Continuen treballant; alguna fallarà al final |
| Si hi ha conflicte | No passa res: es va fer cua | Es perd la feina i cal refer-la |
| Requereix | Transacció oberta des de la lectura | Res: funciona sense transacció oberta entre passos |
| Ideal per a | Molta contenció sobre poques files: l'estoc | Poca contenció i espera humana: un formulari d'edició |
El bloqueig optimista s'implementa amb una columna de versió que s'incrementa a cada escriptura:
-- ⚠️ EXEMPLE D'AQUESTA LLIÇÓ: la columna `versio` NO forma part de l'esquema
-- canònic de BotigaVerda (01-06). Afegeix-la per practicar i recarrega després.
ALTER TABLE productes ADD COLUMN versio INTEGER NOT NULL DEFAULT 0;El cicle té tres passos, i entre el primer i el tercer no cal cap transacció oberta — que és tota la gràcia:
SELECT stock, versio FROM productes WHERE id = 15; -- 1. llegir sense bloquejar → 40, 0
-- 2. L'usuari pensa. Pot trigar cinc minuts. Ningú no espera.
UPDATE productes -- 3. escriure NOMÉS si ningú no l'ha tocada
SET stock = 39, versio = versio + 1
WHERE id = 15
AND versio = 0; -- → UPDATE 1 si ningú no s'ha avançatI a la sessió que arribi segona, amb la mateixa versio = 0 llegida, la resposta és UPDATE 0.
Zero files afectades: aquest és el senyal. No hi ha error, no hi ha excepció, no hi ha transacció avortada: hi ha un comptador que val 0 i que el teu codi ha de comprovar. Si val 0, algú se't va avançar i cal rellegir, recalcular i tornar-ho a intentar — o mostrar a l'usuari "aquestes dades han canviat mentre editaves".
El risc de l'optimisme: si no comproves el nombre de files afectades, un
UPDATE 0passa completament inadvertit i el canvi es perd en silenci. És el mateixUPDATE Nque 05-03 demanava mirar sempre, ara convertit en el mecanisme sencer. Els ORM que implementen aquest patró —Hibernate amb@Version, Django ambselect_for_updatecom a alternativa— llancen una excepció precisament perquè no es pugui ignorar.
- Bloquejos de taula,
ALTER TABLE i CREATE INDEX CONCURRENTLY
ALTER TABLE i CREATE INDEX CONCURRENTLYA més dels bloquejos de fila, hi ha vuit modes de bloqueig de taula. No cal memoritzar-los; el que cal saber és quin pren cada operació i quin xoca amb quin:
| Mode (de més feble a més fort) | El pren | Bloqueja |
|---|---|---|
ACCESS SHARE |
SELECT |
Només ACCESS EXCLUSIVE |
ROW SHARE |
SELECT ... FOR UPDATE / FOR SHARE |
EXCLUSIVE i ACCESS EXCLUSIVE |
ROW EXCLUSIVE |
INSERT, UPDATE, DELETE, MERGE |
Des de SHARE cap amunt |
SHARE UPDATE EXCLUSIVE |
VACUUM, ANALYZE, CREATE INDEX CONCURRENTLY, alguns ALTER TABLE |
A si mateix i al més fort. No bloqueja lectures ni escriptures |
SHARE |
CREATE INDEX (sense CONCURRENTLY) |
Les escriptures. Les lectures continuen |
EXCLUSIVE |
REFRESH MATERIALIZED VIEW CONCURRENTLY |
Tot menys SELECT |
ACCESS EXCLUSIVE |
La majoria d'ALTER TABLE, DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL |
Absolutament tot, inclosos els SELECT |
I també es poden demanar a mà, dins d'una transacció — BEGIN; LOCK TABLE productes IN SHARE MODE; … COMMIT; deixa fer un recompte d'inventari coherent sense que ningú escrigui mentrestant.
LOCK TABLErequereix privilegis. Els modes per sobre deROW EXCLUSIVEexigeixen permisos d'escriptura o de manteniment sobre la taula, no n'hi ha prou ambSELECT. Els privilegis i els rols són 11-03.
I aquí es tanquen dues promeses del curs. La primera és de 05-06: la majoria dels ALTER TABLE prenen ACCESS EXCLUSIVE, el mode que bloqueja tot, incloses les consultes. Per això una migració aparentment innòcua pot tombar un lloc: no pel que trigui a executar-se, sinó perquè primer ha d'esperar que acabin totes les transaccions en curs i, mentre espera, encua al darrere tothom qui arribi. Un ALTER TABLE de 10 ms darrere d'un informe de 40 minuts atura la botiga 40 minuts. D'aquí les dues regles de 05-06: lock_timeout sempre abans d'una migració, i variants que no reescriuen la taula (ADD COLUMN amb DEFAULT és instantani des de PostgreSQL 11; ADD CONSTRAINT ... NOT VALID seguit de VALIDATE CONSTRAINT evita el bloqueig llarg).
La segona és de 08-02: CREATE INDEX normal pren SHARE, que bloqueja totes les escriptures de la taula durant tot el temps que trigui a construir-se — minuts o hores en una taula gran. CREATE INDEX CONCURRENTLY pren només SHARE UPDATE EXCLUSIVE, que no bloqueja ni lectures ni escriptures. El preu és el que va explicar 09-03: recorre la taula dues vegades i espera entre passades que acabin les transaccions antigues, així que triga més, no es pot executar dins d'una transacció i, si falla, deixa un índex invàlid que cal esborrar a mà (es detecta amb indisvalid = false a pg_index). En producció, sempre CONCURRENTLY.
- Interbloquejos
Un interbloqueig (deadlock) passa quan dues transaccions s'esperen mútuament: A espera un recurs que té B, i B espera un que té A. Sense intervenció externa, esperarien per sempre.
La demostració de manual, amb dos productes:
| Instant | Sessió A | Sessió B |
|---|---|---|
| t1 | BEGIN; UPDATE productes SET stock = stock - 1 WHERE id = 1; → UPDATE 1 |
|
| t2 | BEGIN; UPDATE productes SET stock = stock - 1 WHERE id = 15; → UPDATE 1 |
|
| t3 | UPDATE productes SET stock = stock - 1 WHERE id = 15; ← espera B |
|
| t4 | UPDATE productes SET stock = stock - 1 WHERE id = 1; ← espera A |
|
| t5 | (al cap d'~1 segon, una de les dues rep l'error) |
flowchart LR
A["<b>Sessió A</b><br/>té el bloqueig<br/>del producte 1"] -->|"espera el<br/>producte 15"| B["<b>Sessió B</b><br/>té el bloqueig<br/>del producte 15"]
B -->|"espera el<br/>producte 1"| A
Aquest cicle al graf d'espera és la definició formal de l'interbloqueig, i és el que PostgreSQL busca. El missatge real:
ERROR: deadlock detected DETAIL: Process 41290 waits for ShareLock on transaction 813; blocked by process 41287. Process 41287 waits for ShareLock on transaction 814; blocked by process 41290. HINT: See server log for query details. CONTEXT: while updating tuple (0,21) in relation "productes"
Com el resol el motor. Cada vegada que una transacció porta esperant més de deadlock_timeout (1 segon per omissió), PostgreSQL construeix el graf d'espera i busca cicles. Si en troba un, tria una víctima i avorta la seva transacció amb el codi 40P01. L'altra continua i acaba amb normalitat. No és una configuració que calgui activar: ve posada i funciona sola.
Les quatre regles per no provocar-los:
- Accedeix sempre als recursos en el mateix ordre. És la regla d'or i resol el 90 % dels casos. Si totes les transaccions que toquen diversos productes els recorren ordenats per
id, el cicle és impossible: qui tingui l'1 demanarà el 15, i qui no tingui l'1 l'estarà esperant, sense haver pres res. A la pràctica:ORDER BY idalSELECT ... FOR UPDATEque precedeix les escriptures. - Transaccions curtes. Com menys temps se sosté un bloqueig, menor la probabilitat de creuar-se.
- Res d'interacció humana ni de crides externes a dins. Un usuari pensant amb dues files bloquejades és una fàbrica d'interbloquejos (09-01, 09-03).
- Toca les files en un sol
UPDATEquan puguis. UnUPDATE ... WHERE id IN (1, 15)bloqueja en ordre determinista i no deixa escletxa.
I com a xarxa final: el 40P01 es reintenta igual que el 40001, amb el mateix bucle de retrocés exponencial de 09-04. Per això aquell patró filtrava els dos codis.
- Diagnòstic: qui bloqueja qui
Quan alguna cosa "s'ha quedat penjada", aquesta és la consulta que cal tenir desada:
SELECT pid, pg_blocking_pids(pid) AS bloquejat_per, state, wait_event_type,
now() - query_start AS esperant_des_de,
left(query, 50) AS consulta
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;| pid | bloquejat_per | state | wait_event_type | esperant_des_de | consulta |
|---|---|---|---|---|---|
| 41290 | {41287} | active | Lock | 00:03:12 | UPDATE productes SET stock = stock - 1 WHERE id |
pg_blocking_pids() és la funció clau: retorna directament els processos que estan bloquejant-ne un de donat. Amb el pid culpable (41287), es mira què està fent a pg_stat_activity —molt sovint, res: idle in transaction— i, si cal, es talla:
| Funció | Efecte |
|---|---|
SELECT pg_cancel_backend(41287); |
Cancel·la la consulta en curs. La transacció continua viva |
SELECT pg_terminate_backend(41287); |
Tanca la connexió sencera. La transacció es desfà |
Per al detall fi existeix pg_locks, que respon a "quin mode exacte està demanant i sobre quin objecte?" — útil sobretot amb els bloquejos de taula de l'apartat 7:
SELECT l.pid, l.locktype, l.mode, l.granted, c.relname
FROM pg_locks AS l LEFT JOIN pg_class AS c ON c.oid = l.relation
WHERE NOT l.granted;
- Els quatre temps d'espera
Quatre paràmetres, quatre funcions diferents, i convé no confondre'ls:
| Paràmetre | Què limita | Valor típic |
|---|---|---|
lock_timeout |
El que una sentència espera per un bloqueig abans de fallar | '2s' abans de qualsevol migració. El més important dels quatre |
statement_timeout |
El que pot durar una sentència en total | '30s' a l'usuari de l'aplicació web |
idle_in_transaction_session_timeout |
El que una transacció pot estar oberta i ociosa | '5min', sempre (09-01) |
deadlock_timeout |
El que s'espera abans de buscar un cicle d'interbloqueig | '1s', el valor per omissió. No és un límit: baixar-lo només fa que es comprovi més sovint |
-- El preàmbul obligatori de qualsevol migració en producció
SET lock_timeout = '2s';
ALTER TABLE productes ADD COLUMN pes_kg NUMERIC(6,3);Si la taula està ocupada, la sentència falla en dos segons amb ERROR: canceling statement due to lock timeout en lloc d'encuar tota la botiga al seu darrere. Es reintenta més tard i no ha passat res. Sense lock_timeout, aquest ALTER TABLE és una caiguda esperant a passar.
- Seqüències,
ROLLBACK i els números de factura
ROLLBACK i els números de facturaJa ho has vist tres vegades —a 05-02, a l'exercici 2 de 09-01 i al lot de 09-03— i ara toca el perquè i la conseqüència:
nextval()no és transaccional. UnROLLBACKno retorna el valor consumit, i per això les seqüències deixen forats.
I és una decisió deliberada, no un descuit. Si nextval respectés les transaccions, hauria de bloquejar la seqüència fins al COMMIT, i totes les sessions que volguessin inserir en aquella taula es posarien en fila índia darrere de la primera: cada INSERT concurrent seria un coll d'ampolla. PostgreSQL canvia unicitat garantida i màxima concurrència per continuïtat, que és el tracte correcte per a una clau subrogada. I d'aquí una conseqüència contundent:
No facis servir mai la clau primària com a número de factura. Una numeració legal de factures ha de ser correlativa i sense forats; una seqüència garanteix que sigui única i creixent, que no és el mateix. Un
ROLLBACK, unINSERTfallit o un upsert que acaba en conflicte (05-05) obren un forat, i aquest forat és un problema amb l'Agència Tributària, no un detall estètic.
Les tres maneres d'obtenir una numeració sense forats, amb el seu preu:
| Enfocament | Com | Preu |
|---|---|---|
| Comptador en una taula, bloquejat | Una fila per sèrie i any; SELECT ... FOR UPDATE sobre ella, sumar 1, escriure |
Serialitza les factures: només una alhora. Acceptable, perquè facturar no és el camí calent |
| Assignar en emetre, no en crear | La comanda té el seu id amb forats; el número de factura s'assigna en un procés posterior, ordenat i per lots |
Requereix separar "comanda" de "factura", que a més és el correcte comptablement |
| Reservar rangs | Cada procés pren un bloc de 100 números | Tornen els forats si un bloc no s'esgota. Només vàlid si la llei ho permet |
La primera és l'habitual, i la seva implementació és exactament el patró de l'apartat 3:
BEGIN;
SELECT ultim_numero FROM comptadors_factura WHERE serie = 'A' AND any_ = 2026 FOR UPDATE;
UPDATE comptadors_factura SET ultim_numero = ultim_numero + 1 WHERE serie = 'A' AND any_ = 2026;
-- ... inserir la factura amb aquest número ...
COMMIT;(La taula comptadors_factura és un exemple d'aquesta lliçó i no forma part de l'esquema canònic de BotigaVerda.) Fixa't en el que estàs fent: renunciar deliberadament a la concurrència en un punt concret perquè un requisit legal ho exigeix. Saber quan fer-ho és, en el fons, de què ha anat tot el mòdul.
Errors habituals i consells
- Creure que un
SELECTnormal bloqueja alguna cosa. No bloqueja res, i per això llegir no espera mai. Si necessites que la fila no canviï, ho has de demanar ambFOR UPDATEoFOR SHARE. I llegir senseFOR UPDATEi escriure després és l'actualització perduda de 09-04, ara sense excusa. - Posar
FOR UPDATEen una consulta senseWHEREselectiu. Bloqueges totes les files que retorna; amb el catàleg sencer, has aturat la botiga. - Oblidar de comprovar el nombre de files afectades amb bloqueig optimista. Un
UPDATE 0és "algú se't va avançar", i si no ho mires el canvi es perd en silenci. - Llançar un
ALTER TABLEen producció senselock_timeout. PrenACCESS EXCLUSIVE, espera la transacció més llarga que hi hagi i encua tothom al darrere. ICREATE INDEXsenseCONCURRENTLYbloqueja les escriptures durant tota la construcció. - Tocar diverses files en ordre diferent a cada part del codi. És la recepta de l'interbloqueig.
ORDER BY idalSELECT ... FOR UPDATE, sempre. - Baixar
deadlock_timeout"perquè ho detecti abans". No és un límit d'espera: només fa que la comprovació de cicles s'executi més sovint i consumeixi més CPU. I el40P01no és un error de programació: és un conflicte temporal que es reintenta, igual que el40001. - Fer servir la PK com a número de factura. Les seqüències deixen forats per disseny i la numeració legal no els admet.
- Consell: desa la consulta de
pg_blocking_pids. És el primer que s'executa quan alguna cosa es penja, i estalvia mitja hora de conjectures. - Consell:
SKIP LOCKEDconverteix una taula en una cua de feina. Abans de muntar una infraestructura de missatgeria, comprova si això no et basta. - Consell: tria pessimista on hi ha contenció real i optimista on hi ha espera humana. Estoc:
FOR UPDATE. Formulari d'edició: columna de versió.
Exercicis
Amb dues terminals psql i la base acabada de recarregar.
Exercici 1
Resol l'actualització perduda amb bloqueig explícit i compara.
- Reprodueix l'apartat 3 en dues sessions i comprova que l'estoc final del producte 15 és 38. Anota en quin instant exacte es queda esperant la sessió B i quin valor llegeix en desbloquejar-se.
- Repeteix-ho canviant el
FOR UPDATEde B perFOR UPDATE NOWAIT. Quin missatge surt i en quant de temps? - Repeteix-ho amb
FOR UPDATE SKIP LOCKED. Quantes files retorna la consulta de B i per què és un resultat perillós en aquest cas concret? - Escriu les tres solucions vàlides a aquest problema que ja coneixes (09-02, 09-04 i aquesta lliçó) i digues quina triaries per al carretó de BotigaVerda i per què.
Exercici 2
Munta la cua de preparació de comandes del magatzem.
- Escriu la consulta que pren la comanda
pagatmés antiga sense trepitjar ningú, i executa-la en dues sessions alhora. Comprova que una agafa la 18 i l'altra la 19. - Afegeix-hi una tercera sessió amb la mateixa consulta. Què retorna i per què?
- Sense
SKIP LOCKED, què hauria fet la segona sessió? I què hauria passat en desbloquejar-se, exactament? - Dissenya la variant que també serveixi per reintentar comandes que es van quedar a mitges (un operari el procés del qual va morir). Què cal afegir a
comandesi per què aquesta columna no forma part de l'esquema canònic?
Exercici 3
Provoca un interbloqueig i diagnostica'l.
- Reprodueix l'apartat 8 amb els productes 1 i 15. Copia el missatge d'error complet i anota quina sessió ha estat la víctima.
- Mentre les dues estan esperant (abans del segon), executa des d'una tercera sessió la consulta de
pg_blocking_pidsi descriu el que veus. - Reescriu les dues transaccions perquè l'interbloqueig sigui impossible, sense fer servir
LOCK TABLEni canviar el nivell d'aïllament. - Si tot i així passés sota càrrega, què hauria de fer l'aplicació? Indica el codi SQLSTATE i el patró exacte.
Solucions
Solució 1
1. L'estoc final és 38. B es queda esperant al seu propi SELECT ... FOR UPDATE, no a l'UPDATE: aquest és el canvi respecte de l'apartat 1. En desbloquejar-se, després del COMMIT d'A, llegeix 39 — el valor ja actualitzat— i calcula 38 sobre ell. La finestra entre llegir i escriure ha desaparegut.
2. L'error és immediat, en mil·lisegons: ERROR: could not obtain lock on row in relation "productes". I és una resposta perfectament vàlida per a una petició web: val més retornar "torna-ho a provar" a l'instant que deixar l'usuari mirant una roda trenta segons.
3. La consulta de B retorna 0 files, i és perillosíssim aquí. SKIP LOCKED està dissenyat per a "dóna'm qualsevol fila lliure"; però B no vol qualsevol producte: vol aquell. Un resultat buit faria que l'aplicació cregués que el producte 15 no existeix, o —pitjor— que continués sense descomptar res. SKIP LOCKED només té sentit quan les files són intercanviables, com en una cua de feina.
4. Les tres solucions:
| Solució | D'on | Quan |
|---|---|---|
UPDATE ... WHERE id = 15 AND stock >= 1 i mirar UPDATE N |
09-02 | La lògica cap en una sentència |
SERIALIZABLE + bucle de reintent |
09-04 | L'invariant abasta diverses files |
SELECT ... FOR UPDATE + UPDATE |
Aquesta lliçó | Cal decidir entre llegir i escriure |
Per al carretó de BotigaVerda: la primera, i si el descompte necessita lògica intermèdia (comprovar reserves, aplicar promocions), la tercera. La segona queda descartada perquè obligaria a implementar reintents al camí més calent de la botiga i avortaria justament als pics de trànsit, que és quan pitjor va.
Solució 2
1. La consulta és la de l'apartat 4: SELECT ... WHERE estat = 'pagat' ORDER BY data_comanda FOR UPDATE SKIP LOCKED LIMIT 1. La sessió A agafa la comanda 18 (2026-01-27) i la B, sense esperar, la 19 (2026-02-09).
2. La tercera retorna 0 files, perquè les dues úniques comandes pagat de BotigaVerda estan bloquejades per A i B. És el comportament correcte d'una cua buida: l'operari 3 no espera, veu que no hi ha feina i torna a preguntar més tard.
3. Sense SKIP LOCKED, la segona sessió es quedaria esperant que A acabés. I en desbloquejar-se passaria el pitjor: PostgreSQL rellegeix la fila, reavalua el WHERE i descobreix que la comanda 18 ja no compleix estat = 'pagat' (A la va passar a enviat), així que la descarta i retorna… 0 files. Ni tan sols agafa la 19, perquè el LIMIT 1 ja s'havia resolt. L'operari 2 hauria esperat per no res.
4. Cal marcar les comandes que algú està preparant i des de quan, per poder recuperar les abandonades:
-- ⚠️ NO canònic: columnes d'exemple d'aquest exercici
ALTER TABLE comandes ADD COLUMN preparant_des_de TIMESTAMPTZ;La consulta passa a ser WHERE estat = 'pagat' AND (preparant_des_de IS NULL OR preparant_des_de < now() - INTERVAL '15 minutes'), i el procés escriu preparant_des_de = now() en prendre-la. Així, si un operari mor a mitges, la seva comanda torna a la cua al cap de quinze minuts. No és canònic perquè l'esquema de 01-06 modela una botiga, no una cua de feina: la columna existeix només per a aquest mecanisme i cap altre mòdul no la fa servir.
Solució 3
1. El missatge és el de l'apartat 8, amb deadlock detected i el codi 40P01. La víctima és la sessió que dispara la detecció, és a dir, la que portava esperant més de deadlock_timeout quan es va trobar el cicle — normalment la segona a quedar-se bloquejada (la B del guió). L'altra continua i confirma amb normalitat.
2. Des de la tercera sessió es veuen dues files, cadascuna bloquejada per l'altra: el pid d'A té a bloquejat_per el pid de B, i el de B té el d'A. Aquesta reciprocitat és el cicle del graf d'espera, visible a la sortida d'una consulta. És el diagnòstic més satisfactori del mòdul, i cal ser ràpid: PostgreSQL el resol en aproximadament un segon.
3. N'hi ha prou amb accedir a les files sempre en el mateix ordre, per exemple per id ascendent: les dues sessions executen BEGIN; → UPDATE ... WHERE id = 1; → UPDATE ... WHERE id = 15; → COMMIT;, en aquest ordre. Ara el cicle és impossible: qui aconsegueixi el producte 1 acabarà aconseguint el 15, i qui no l'aconsegueixi estarà esperant sense tenir res bloquejat, així que no pot bloquejar ningú. És la regla d'or de l'apartat 8. La versió general, quan les files es trien en temps d'execució, és ordenar la llista abans de recórrer-la: SELECT ... WHERE id = ANY($1) ORDER BY id FOR UPDATE.
4. Hauria de reintentar la transacció sencera. El codi és 40P01 (deadlock_detected), i el patró és exactament el bucle de 09-04: rollback(), espera amb retrocés exponencial i jitter, fins a cinc intents, sense reintentar cap altre tipus d'error. Per això aquell pseudocodi filtrava '40001' i '40P01': són els dos errors del mòdul que signifiquen "torna-ho a provar", no "ho has fet malament".
Conclusió del mòdul
Tanques l'última lliçó amb les eines que faltaven:
- Tot
UPDATEiDELETEbloqueja la seva fila fins al final de la transacció, i per això la sessió B esperava. Llegir no espera mai; escriure sí. I en desbloquejar-se, PostgreSQL rellegeix i recalcula, que és per quèSET stock = stock - 1donava 38. - La família
SELECT ... FOR UPDATE / FOR NO KEY UPDATE / FOR SHARE / FOR KEY SHAREamb la seva taula de compatibilitat, i el patró llegir-modificar-escriure segur: la solució correcta a l'actualització perduda de 09-04, i la que fan servir de debò les botigues. SKIP LOCKED, que converteix una consulta en un repartidor de feina —dos operaris, les comandes 18 i 19, sense trepitjar-se— iNOWAITper fallar en mil·lisegons en lloc de penjar un usuari.- Optimista enfront de pessimista: bloquejar per endavant on hi ha contenció real (l'estoc), o comprovar en escriure amb una columna de versió on hi ha espera humana — mirant sempre l'
UPDATE 0, perquè allà no hi ha cap error que t'avisi. - Els bloquejos de taula i els seus vuit modes, amb les dues promeses que es tanquen: la majoria dels
ALTER TABLEprenenACCESS EXCLUSIVEi encuen al darrere tota la base (05-06), iCREATE INDEXbloqueja les escriptures mentre queCREATE INDEX CONCURRENTLYno (08-02). En producció,lock_timeoutiCONCURRENTLY, sempre. - Els interbloquejos: el cicle al graf d'espera, el
deadlock detectedamb el seu40P01, la víctima triada pel motor al cap d'un segon, i la regla d'or que els fa impossibles — accedir sempre als recursos en el mateix ordre. - El diagnòstic:
pg_blocking_pids()com a primera consulta davant d'una penjada,pg_locksper al detall,pg_cancel_backendipg_terminate_backendper tallar, i els quatre temps d'espera amblock_timeoutal capdavant. - I per què una seqüència no es desfà amb un
ROLLBACK: perquè fer-la transaccional convertiria cadaINSERTen un coll d'ampolla. D'aquí els forats, i d'aquí la regla: la clau primària no és un número de factura.
I amb això es tanca el mòdul 9. En cinc lliçons has passat de "hi ha una sentència i l'executo" a entendre el sistema sencer: saps que cada sentència solta ja és una transacció i que confirmar una comanda són quatre operacions que han d'anar juntes; coneixes les quatre garanties ACID una a una, amb el WAL que fa que un COMMIT sobrevisqui a un tall de corrent i amb MVCC, que explica per fi les files mortes, el bloat i el VACUUM que van quedar pendents al mòdul 8; manegues el TCL complet, inclosos els SAVEPOINT que rescaten un lot d'una càrrega fallida; saps anomenar les quatre anomalies de concurrència, has vist perdre's una venda de te matcha en dues sessions i saps que PostgreSQL no implementa READ UNCOMMITTED i el seu REPEATABLE READ no permet fantasmes; i saps triar entre una sentència condicional, un FOR UPDATE i un SERIALIZABLE amb reintent sabent què pagues en cada cas.
Ja saps llegir (mòduls 2 a 4 i 7), escriure (mòdul 5), transformar (mòdul 6), optimitzar (mòdul 8) i coordinar (aquest). El que falta no és una capacitat nova: és l'arsenal que fa mantenible tot l'anterior, i que separa una consulta que funciona d'un sistema amb què es pot conviure. Al mòdul 10, Avançat, veuràs les vistes, per donar nom a una consulta complexa i deixar de copiar-la per tot arreu; les CTE amb WITH, que converteixen una subconsulta imbricada de quaranta línies en passos llegibles i permeten escriure consultes recursives per recórrer la jerarquia d'empleats o la cadena de referits de BotigaVerda; les funcions de finestra, amb què es calculen rànquings, mitjanes mòbils i acumulats sense perdre el detall; els procediments emmagatzemats, on acabarà vivint la confirmació de comanda que has construït a 09-03; els triggers, que apliquen automàticament aquelles regles de negoci que 09-02 va deixar fora de l'abast d'un CHECK; i el tipus JSON, per al que no cap en un esquema fix. És la caixa d'eines de qui ja no està aprenent SQL, sinó fent-lo servir.
Curs de SQL
Mòdul 1: Introducció a SQL
- Què és SQL?
- Configurar el teu entorn SQL
- Sintaxi bàsica de SQL
- Entendre bases de dades i taules
- El model relacional: claus primàries i foranes
- La base de dades del curs: BotigaVerda
Mòdul 2: Consultes bàsiques de SQL
- Instrucció SELECT
- Àlies, expressions i columnes calculades
- Filtrar dades amb WHERE
- DISTINCT i eliminació de duplicats
- Ordenar dades amb ORDER BY
- Limitar resultats amb LIMIT
Mòdul 3: Treballar amb múltiples taules
- Operacions JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN i CROSS JOIN
- Unions de conjunts: UNION, INTERSECT i EXCEPT
Mòdul 4: Filtratge avançat de dades
- Utilitzar LIKE per a coincidència de patrons
- Operadors IN i BETWEEN
- Valors NULL i IS NULL
- Funcions d'agregació: COUNT, SUM, AVG, MIN i MAX
- Agregar dades amb GROUP BY
- Clàusula HAVING
Mòdul 5: Manipulació de dades
- Crear taules i restriccions amb CREATE TABLE
- Instrucció INSERT
- Instrucció UPDATE
- Instrucció DELETE
- Instrucció UPSERT (MERGE)
- Modificar l'esquema: ALTER TABLE i migracions segures
Mòdul 6: Funcions avançades de SQL
- Funcions de cadena
- Funcions numèriques
- Funcions de data i hora
- Conversió de tipus i tractament dels NULL: CAST i COALESCE
- Expressions condicionals
Mòdul 7: Subconsultes i consultes imbricades
- Introducció a les subconsultes
- Subconsultes correlacionades
- EXISTS i NOT EXISTS
- Utilitzar subconsultes en les clàusules SELECT, FROM i WHERE
- Subconsultes o JOIN: quin triar
Mòdul 8: Índexs i optimització del rendiment
- Entendre els índexs
- Creació i gestió d'índexs
- Tipus d'índex i quan no indexar
- Tècniques d'optimització de consultes
- Anàlisi del rendiment de les consultes
Mòdul 9: Transaccions i concurrència
- Introducció a les transaccions
- Propietats ACID
- Instruccions de control de transaccions
- Nivells d'aïllament i anomalies de concurrència
- Gestió de la concurrència: bloquejos i interbloquejos
Mòdul 10: Temes avançats
- Vistes
- Expressions de taula comunes (CTE)
- Funcions de finestra
- Procediments emmagatzemats
- Disparadors (triggers)
- JSON i dades semiestructurades
Mòdul 11: SQL a la pràctica
- Casos d'ús al món real
- Bones pràctiques
- Seguretat: injecció SQL, permisos i rols
- SQL per a l'anàlisi de dades
- SQL en el desenvolupament web
