El mòdul 8 va acabar confessant una mentida: durant vuitanta i escaig lliçons hem suposat que som l'únic usuari de la base de dades. Aquest mòdul desmunta aquesta suposició, i comença per la peça que la fa manejable: la transacció, una unitat de treball que el motor tracta com a indivisible. O passa sencera, o no passa res. Aquí veuràs per què confirmar una comanda a BotigaVerda —quatre operacions sobre tres taules— és un desastre esperant a passar si no és atòmic; entendràs l'autocommit, la font número u de malentesos amb les transaccions; aprendràs BEGIN, COMMIT i ROLLBACK amb el seu cicle de vida complet; i coneixeràs el format de dues sessions amb què es demostra tota la resta del mòdul. Al final tindràs dues terminals psql obertes alhora i hauràs vist, per primera vegada al curs, que dues sessions no veuen el mateix al mateix temps.
Contingut
- Què és una transacció
- El cas de BotigaVerda: confirmar una comanda són quatre operacions
- Què queda si falla el tercer pas
- Autocommit: cada sentència ja és una transacció
BEGIN,COMMITiROLLBACK: el cicle de vida- Com saber si ets dins d'una transacció
- El format de dues sessions
- Tancar la sessió sense confirmar, i què passa si el servidor cau
- L'estat avortat
- Transaccions de només lectura
- Durada: transaccions curtes i el problema d'idle in transaction
- Errors habituals i consells
- Exercicis
- Conclusió
- Què és una transacció
Una transacció és un conjunt de sentències SQL que el motor executa com una sola operació indivisible: o s'apliquen totes o no se n'aplica cap.
L'exemple de manual és la transferència bancària. Moure 100 € d'un compte a un altre són dues operacions:
UPDATE comptes SET saldo = saldo - 100 WHERE id = 1; -- restar de l'origen
UPDATE comptes SET saldo = saldo + 100 WHERE id = 2; -- sumar al destíSi el sistema cau entre les dues, els 100 € han deixat d'existir. I no hi ha cap consulta que ho pugui detectar després: les dues files són individualment vàlides; només la relació entre totes dues està trencada, i aquesta relació no viu en cap columna.
La transacció resol exactament això: converteix les dues sentències en una. El subllenguatge que la controla és el TCL (Transaction Control Language), el quart dels que vam anomenar a 01-01 i l'únic que encara no havies fet servir a fons.
- El cas de BotigaVerda: confirmar una comanda són quatre operacions
Oblidem els bancs: a BotigaVerda el cas és més ric i el tens a la base de dades. Un client confirma el seu carretó i el sistema ha de fer quatre coses sobre tres taules:
flowchart LR
A["1 · INSERT<br/>capçalera a <b>comandes</b><br/>estat 'pendent'"] --> B["2 · INSERT<br/>una fila per article<br/>a <b>linies_comanda</b>"]
B --> C["3 · UPDATE<br/>descomptar l'estoc<br/>a <b>productes</b>"]
C --> D["4 · UPDATE<br/>estat = 'pagat'<br/>a <b>comandes</b>"]
Pau Llorens Vidal (client 6) compra per web dos olis d'oliva, un te matcha i —sense saber que està esgotat— un pack d'espelmes de cera de soja. Els quatre passos, tal com els executa l'aplicació:
-- Pas 1: la capçalera → RETURNING retorna id = 21
INSERT INTO comandes (client_id, empleat_id, data_comanda, estat, metode_pagament, despeses_enviament)
VALUES (6, NULL, DATE '2026-03-05', 'pendent', 'targeta', 4.95)
RETURNING id;
-- Pas 2: les línies
INSERT INTO linies_comanda (comanda_id, producte_id, quantitat, preu_unitari, descompte) VALUES
(21, 1, 2, 12.50, 0.00),
(21, 15, 1, 22.00, 0.00),
(21, 13, 1, 13.75, 0.00)
RETURNING id, producte_id, quantitat, preu_unitari;| id | producte_id | quantitat | preu_unitari |
|---|---|---|---|
| 48 | 1 | 2 | 12.50 |
| 49 | 15 | 1 | 22.00 |
| 50 | 13 | 1 | 13.75 |
Seixanta euros amb setanta-cinc de producte més 4,95 € de ports: 65,70 €. Ara el pas que falla:
-- Pas 3: descomptar estoc, línia a línia (és el que fa l'ORM en recórrer el carretó)
UPDATE productes SET stock = stock - 2 WHERE id = 1;
UPDATE productes SET stock = stock - 1 WHERE id = 15;
UPDATE productes SET stock = stock - 1 WHERE id = 13;UPDATE 1 UPDATE 1 ERROR: new row for relation "productes" violates check constraint "productes_stock_check" DETAIL: Failing row contains (13, Espelmes de cera de soja (pack 2), 3, 5, 13.75, 6.90, -1, t, 2025-03-01).
El producte 13 té stock = 0 i el CHECK (stock >= 0) de 05-01 impedeix baixar-lo a −1. El pas 3 ha fallat a mitges. I el pas 4 —marcar la comanda com a pagada— no arriba a executar-se mai.
- Què queda si falla el tercer pas
Aquesta és la pregunta que cal mirar de cara. Sense transacció, cada sentència es va confirmar sola i això és el que hi ha ara a la base de dades:
| Taula | Estat real després de l'errada | És correcte? |
|---|---|---|
comandes |
La comanda 21 existeix, en estat pendent |
A mitges: existeix però ningú no la cobrarà |
linies_comanda |
Tres línies (48, 49, 50) per valor de 60,75 € | Sí, però d'una comanda que no es va completar |
productes.stock (id 1) |
120 → 118 | No: s'han reservat 2 unitats d'una comanda inexistent |
productes.stock (id 15) |
40 → 39 | No: el mateix amb el matcha |
productes.stock (id 13) |
0 → 0 | Sí… però el client creu que l'ha comprat |
| id | nom | stock |
|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | 118 |
| 13 | Espelmes de cera de soja (pack 2) | 0 |
| 15 | Te verd matcha cerimonial 30 g | 39 |
Tres unitats d'inventari han desaparegut del sistema sense que ningú les hagi comprat. Multiplica-ho per cent comandes al dia i en un mes l'inventari de l'ERP no s'assembla al del magatzem. I no hi ha cap error registrat enlloc: l'aplicació va tornar un missatge al client, el client va tancar la pestanya, i les files s'hi van quedar. El mateix, dins d'una transacció:
BEGIN;
-- els quatre passos, exactament iguals
ROLLBACK; -- el motor ho desfà TOT: la capçalera, les tres línies i els dos descomptes
SELECT (SELECT COUNT(*) FROM comandes) AS comandes,
(SELECT COUNT(*) FROM linies_comanda) AS linies,
(SELECT stock FROM productes WHERE id = 1) AS stock_oli,
(SELECT stock FROM productes WHERE id = 15) AS stock_matcha;| comandes | linies | stock_oli | stock_matcha |
|---|---|---|---|
| 20 | 47 | 120 | 40 |
Com si no hagués passat mai. 20 comandes, 47 línies, els estocs intactes. Aquesta és tota la idea.
- Autocommit: cada sentència ja és una transacció
Aquí hi ha el malentès número u, i convé dir-ho sense embuts:
En SQL no existeix "estar fora d'una transacció". Tota sentència s'executa dins d'una. Si no n'obres una explícitament, el motor n'obre una d'implícita que dura exactament el que dura aquesta sentència i es confirma sola en acabar. Això és l'autocommit.
Dues conseqüències que cal interioritzar:
- Una sola sentència sempre és atòmica. L'
UPDATE productes SET preu = preu * 1.05;de 05-03 afecta 20 files: o canvien les 20, o no en canvia cap. Per aixòINSERT ... ON CONFLICT(05-05) és segur iSELECT+INSERTno ho és: una sentència és atòmica i dues no. - El
ROLLBACKno existeix per al que ja s'ha confirmat. Quan veusUPDATE 20en autocommit, ja és al disc. No hi ha marxa enrere.
I el problema és que l'autocommit no es comporta igual a tot arreu:
| Entorn | Estat per omissió | Com s'obre una transacció explícita | Com es desactiva l'autocommit |
|---|---|---|---|
psql (PostgreSQL) |
Autocommit activat | BEGIN; |
\set AUTOCOMMIT off — a partir d'aquí, psql obre un BEGIN implícit abans de la primera sentència i cal fer COMMIT a mà |
MySQL / MariaDB (client mysql) |
Autocommit activat | START TRANSACTION; o BEGIN; |
SET autocommit = 0; |
| SQL Server (SSMS) | Autocommit activat | BEGIN TRANSACTION; |
SET IMPLICIT_TRANSACTIONS ON; |
| Oracle (SQL*Plus) | Desactivat: tota sentència obre transacció i cal fer COMMIT |
Implícita, amb la primera sentència DML | És el comportament per omissió |
| SQLite (CLI) | Autocommit activat | BEGIN; |
No hi ha opció: s'obre explícitament |
| psycopg 3 (Python) | Autocommit desactivat: obre transacció sol | Automàtica amb la primera sentència | conn.autocommit = True per al contrari |
| JDBC (Java) | Autocommit activat | conn.setAutoCommit(false) i després conn.commit() |
— |
| SQLAlchemy | Autocommit desactivat: la Session manté transacció oberta |
Automàtica | session.commit() / session.rollback() |
Nota de dialecte: Oracle és el cas que més sorprèn: allà un
UPDATEsenseCOMMITposterior no ha passat per a ningú més que per a tu, i si tanques la sessió es perd. I a l'extrem oposat, psycopg i SQLAlchemy fan el contrari del que la majoria espera: la teva aplicació Python ja és dins d'una transacció oberta des de la primera consulta, encara que tu no hagis escritBEGINenlloc. Hi tornarem a 09-03, perquè explica la meitat dels bloquejos misteriosos en producció.
BEGIN, COMMIT i ROLLBACK: el cicle de vida
BEGIN, COMMIT i ROLLBACK: el cicle de vidaTres paraules i ja ho saps tot:
| Sentència | Què fa | Sinònims acceptats a PostgreSQL |
|---|---|---|
BEGIN; |
Obre una transacció explícita | START TRANSACTION;, BEGIN WORK;, BEGIN TRANSACTION; |
COMMIT; |
Confirma: tot el que s'ha fet es torna permanent i visible per als altres | END;, COMMIT WORK; |
ROLLBACK; |
Desfà: la base torna a l'estat que tenia abans del BEGIN |
ABORT;, ROLLBACK WORK; |
START TRANSACTION és la forma de l'estàndard SQL i funciona a PostgreSQL, MySQL i SQL Server; BEGIN és la més curta i la que fa servir tothom a PostgreSQL. Compte: a MySQL, BEGIN és a més l'arrencada d'un bloc de codi en procediments, així que allà convé escriure START TRANSACTION perquè no hi hagi ambigüitat.
El cicle de vida complet, amb l'estat avortat que veurem a l'apartat 9:
stateDiagram-v2
[*] --> Autocommit: sessió connectada
Autocommit --> Activa: BEGIN
Activa --> Activa: INSERT / UPDATE / DELETE / SELECT
Activa --> Avortada: ERROR en una sentència
Avortada --> Avortada: qualsevol sentència<br/>→ 25P02
Activa --> Confirmada: COMMIT
Activa --> Desfeta: ROLLBACK
Avortada --> Desfeta: ROLLBACK
Avortada --> Desfeta: COMMIT<br/>(es comporta com un ROLLBACK!)
Confirmada --> Autocommit
Desfeta --> Autocommit
Fixa't en la transició més perillosa del diagrama: un COMMIT sobre una transacció avortada no confirma res, fa un ROLLBACK. PostgreSQL respon ROLLBACK en lloc de COMMIT, i si el teu script no mira aquesta resposta, creurà que va desar les dades.
- Com saber si ets dins d'una transacció
És la pregunta pràctica més freqüent, i psql te la respon al mateix indicador:
| Indicador | Significat |
|---|---|
botigaverda=> |
Fora de transacció (autocommit) |
botigaverda=*> |
Dins d'una transacció oberta |
botigaverda=!> |
Dins d'una transacció avortada |
botigaverda-> |
Sentència incompleta: falta el ; |
El caràcter final és > per a un usuari normal i # per a un superusuari, així que un administrador dins d'una transacció veu botigaverda=*#. El que importa sempre és l'asterisc: apareix just després del BEGIN i desapareix amb el COMMIT o el ROLLBACK. Des de SQL:
SELECT pg_current_xact_id_if_assigned() AS xid,
(pg_current_xact_id_if_assigned() IS NOT NULL) AS ha_escrit;| xid | ha_escrit |
|---|---|
| (null) | false |
Retorna NULL mentre la transacció no hagi escrit res, perquè PostgreSQL no gasta un identificador de transacció en qui només llegeix; així que facis un INSERT o un UPDATE, apareix un número. La seva germana pg_current_xact_id() (abans txid_current(), que continua funcionant) força l'assignació, així que retorna sempre un número — i per això no serveix per diagnosticar: en preguntar, canvia la resposta.
I dos comodins de psql: \echo :ROW_COUNT imprimeix les files afectades per l'última sentència —l'UPDATE N de 05-03, però utilitzable dins d'un script— i \set ON_ERROR_STOP on fa que psql avorti el fitxer al primer error en lloc de continuar llançant sentències contra una transacció ja morta. És obligatori en qualsevol script de migració.
- El format de dues sessions
Tot el que queda del mòdul tracta del que passa quan dues persones treballen alhora, i això no es veu en una sortida de psql normal. A partir d'aquí ho mostrarem sempre així:
Com reproduir-ho tu. Obre dues terminals i a cadascuna llança
psql -h localhost -U curs_sql -d botigaverda. Anomenarem Sessió A la primera i Sessió B la segona. Executa les sentències en l'ordre dels instantst1,t2,t3… alternant de terminal. Tots els exemples del mòdul estan pensats per fer-se així, i no n'entendràs cap de debò fins que els teclegis.
La primera demostració: què veu cada sessió abans i després del COMMIT.
| Instant | Sessió A | Sessió B |
|---|---|---|
| t1 | BEGIN; |
|
| t2 | SELECT stock FROM productes WHERE id = 15; → 40 |
|
| t3 | UPDATE productes SET stock = 39 WHERE id = 15; → UPDATE 1 |
|
| t4 | SELECT stock FROM productes WHERE id = 15; → 39 |
|
| t5 | SELECT stock FROM productes WHERE id = 15; → 40 |
|
| t6 | COMMIT; |
|
| t7 | SELECT stock FROM productes WHERE id = 15; → 39 |
Llegeix-ho a poc a poc, perquè en aquestes set línies hi ha el mòdul sencer:
- A t4, A veu 39. És el seu propi canvi: tota transacció veu sempre el que ella mateixa ha fet.
- A t5, B veu 40. El canvi d'A existeix, està escrit, però no està confirmat, i per a B és com si no existís. B no es bloqueja, no espera, no rep cap avís: simplement llegeix el valor bo anterior.
- A t7, després del
COMMIT, B veu 39. El canvi s'ha fet públic de cop, i per a B va passar sencer en l'instant delCOMMIT, no repartit entre t3 i t6.
Això que acabes de veure és l'aïllament, la tercera lletra d'ACID, i l'estudiarem a fons a 09-02 i 09-04. I el mecanisme que ho fa possible sense que B hagi d'esperar —dos valors de la mateixa fila coexistint— és MVCC, la resposta a la pregunta que va deixar oberta 08-05 sobre el bloat.
Quan l'exemple requereixi veure el SQL complet en lloc de resumit, farem servir dos blocs etiquetats amb l'instant:
-- Sessió B
SELECT stock FROM productes WHERE id = 15; -- t5 → 40
SELECT stock FROM productes WHERE id = 15; -- t7 → 39I per als interbloquejos de 09-05, diagrames mermaid de seqüència. Sigui quin sigui el format, la regla no canvia: sempre s'indica què veu cada sessió a cada instant i quina es queda esperant.
- Tancar la sessió sense confirmar, i què passa si el servidor cau
Els dos finals imprevistos tenen la mateixa resposta, i és tranquil·litzadora:
| Situació | Què passa |
|---|---|
Escrius \q o tanques la terminal amb una transacció oberta |
ROLLBACK implícit. PostgreSQL desfà tot el que no s'ha confirmat |
| Es talla la xarxa entre el client i el servidor | Igual: en detectar la desconnexió, el servidor desfà la transacció |
| El procés del servidor mor, o se'n va la llum | En arrencar, PostgreSQL fa recuperació: reaplica des del WAL el que s'ha confirmat i descarta el que no. Les transaccions a mitges desapareixen |
Fas COMMIT i un microsegon després se'n va la llum |
Les dades hi són. Això és la durabilitat, i el mecanisme que la garanteix (el WAL) és l'apartat central de 09-02 |
La regla mental: el que s'ha confirmat sobreviu a tot; el que no s'ha confirmat no sobreviu a res. No hi ha estat intermedi, ni manera que una transacció quedi "mig aplicada" després d'una caiguda.
- L'estat avortat
Aquest missatge el veuràs, segur, i probablement avui mateix:
Passa així:
botigaverda=> BEGIN; BEGIN botigaverda=*> UPDATE productes SET stock = stock - 1 WHERE id = 1; UPDATE 1 botigaverda=*> UPDATE productes SET stock = stock - 1 WHERE id = 13; ERROR: new row for relation "productes" violates check constraint "productes_stock_check" botigaverda=!> SELECT COUNT(*) FROM productes; ERROR: current transaction is aborted, commands ignored until end of transaction block botigaverda=!> ROLLBACK; ROLLBACK botigaverda=>
Fixa't en l'indicador: va passar de =*> a =!> tan bon punt hi va haver un error. A partir d'aquí PostgreSQL rebutja qualsevol sentència, fins i tot un SELECT COUNT(*) inofensiu, amb el codi 25P02.
Per què és així, i per què és el correcte. La transacció va prometre atomicitat: o tot o res. Tan bon punt una sentència falla, "tot" ja és impossible, així que l'única promesa que el motor pot continuar complint és "res". Deixar-te continuar seria permetre que confirmessis un resultat parcial creient que està complet — exactament el desastre de l'apartat 3, però amb el segell de qualitat d'una transacció al damunt.
Com sortir-ne: hi ha dues portes i totes dues acaben la transacció. ROLLBACK; ho desfà tot, i és la sortida honesta. COMMIT; respon ROLLBACK i ho desfà tot igualment. N'existeix una tercera que no tanca la transacció i salva la feina ja feta: tornar a un SAVEPOINT anterior a l'error, una de les raons de ser dels savepoints (09-03).
Nota de dialecte — i és una divergència enorme. MySQL/InnoDB no avorta la transacció sencera. Si una sentència falla, es desfà només aquesta sentència i la transacció continua viva i acceptant ordres; pots fer
COMMITi confirmaràs tot el que hi havia abans de l'error. SQL Server queda al mig: depèn de la gravetat de l'error i deSET XACT_ABORT ON(que el fa comportar-se com PostgreSQL, i és el recomanat). Oracle també desfà només la sentència fallida. Conseqüència pràctica: un script provat a MySQL que "funciona" pot estar confirmant resultats parcials; el mateix script a PostgreSQL fallarà sorollosament. La versió sorollosa és la bona.
- Transaccions de només lectura
Es declaren així:
I si intentes escriure-hi dins: ERROR: cannot execute UPDATE in a read-only transaction. Quatre raons per declarar-les:
- És una xarxa de seguretat contra tu mateix. Un informe mensual no hauria de poder modificar res; amb
READ ONLY, unUPDATEenganxat per error és un error i no un incident. - No consumeix un identificador de transacció, cosa que redueix la pressió sobre MVCC i sobre la congelació de transaccions (09-02).
- És obligatòria en una rèplica de només lectura. Si el teu informe apunta a un secundari, val més que l'escriptura falli al teu portàtil.
- Habilita
DEFERRABLE, que permet executar un informe llarg en modeSERIALIZABLEsense risc que avorti per conflicte (09-03).
- Durada: transaccions curtes i el problema d'idle in transaction
Aquesta és l'única regla operativa que cal memoritzar d'aquesta lliçó:
Una transacció s'ha d'obrir com més tard millor i tancar-se com més aviat millor. Mai, mai de la vida, no s'espera dins d'una transacció oberta: ni entrada de l'usuari, ni una crida HTTP, ni una lectura de fitxer, ni un
sleep.
Una transacció oberta i ociosa (l'estat idle in transaction) fa tres danys simultanis:
| Dany | Detall |
|---|---|
| Reté bloquejos | Les files que va tocar continuen bloquejades per a qui les vulgui modificar. Si és la del producte més venut, has aturat la botiga (09-05) |
| Reté una instantània | VACUUM no pot netejar cap versió de fila que aquesta transacció encara pogués necessitar. Amb una transacció oberta des de fa hores, les files mortes s'acumulen a tota la base: és la causa clàssica del bloat de 08-05 |
| Ocupa una connexió | I les connexions són un recurs escàs i car |
El cas real és sempre el mateix: un formulari que obre transacció, mostra una pantalla de confirmació i espera. L'usuari se'n va a dinar. La botiga s'atura. Com es detecta:
SELECT pid, state, now() - xact_start AS duracio, left(query, 55) AS ultima_consulta
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY xact_start;| pid | state | duracio | ultima_consulta |
|---|---|---|---|
| 41287 | idle in transaction | 01:42:19 | UPDATE productes SET stock = stock - 1 WHERE id = 15 |
Una hora i quaranta-dos minuts amb la fila del matcha bloquejada. La defensa automàtica és un paràmetre que tot servidor de producció hauria de tenir posat —SET idle_in_transaction_session_timeout = '5min';— fixable per sessió, per usuari, per base de dades o a postgresql.conf. Al costat de statement_timeout i lock_timeout forma el kit de supervivència que detalla 09-05.
Errors habituals i consells
- Creure que "no estic fent servir transaccions". Sí que en fas servir: cada sentència solta n'és una. La pregunta no és si hi ha transacció, sinó on comença i on acaba.
- Suposar que l'autocommit es comporta igual a tot arreu. A Oracle està desactivat; a psycopg i SQLAlchemy la teva aplicació ja és dins d'una transacció oberta sense que hagis escrit
BEGIN. - Fer
BEGINi oblidar de tancar. L'error operatiu més car del mòdul: bloqueges files, impedeixes elVACUUMi ocupes una connexió. Mira l'asterisc de l'indicador abans d'aixecar-te. - Ignorar l'estat avortat, o no comprovar la resposta del
COMMIT. Després d'un error tot falla amb25P02fins alROLLBACK; i sobre una transacció avortada,COMMITretornaROLLBACKi no desa res. Si el teu script no ho mira, creurà que va desar. - Executar un fitxer
.sqlsense\set ON_ERROR_STOP on.psqlcontinuarà llançant cent sentències contra una transacció morta i l'error real quedarà sepultat entre cent missatges de25P02. - Descomptar estoc línia a línia sense transacció. És l'apartat 3: inventari trencat en silenci i sense traça.
- Esperar dins d'una transacció oberta. Entrada d'usuari, crida a la passarel·la de pagament, lectura d'un fitxer gran. Prepara les dades a fora, obre, escriu, tanca.
- Consell: adopta
BEGIN… verificar …COMMIT/ROLLBACKcom a reflex en qualsevolUPDATEoDELETEmanual, iBEGIN TRANSACTION READ ONLYper als teus informes: costa dues paraules i fa impossible l'accident. - Consell: tingues sempre dues terminals
psqlobertes mentre estudies aquest mòdul. És l'única manera de veure la concurrència.
Exercicis
Treballa sobre la base acabada de recarregar (botigaverda.sql) i amb dues terminals psql obertes.
Exercici 1
Reprodueix el desastre de l'apartat 3 i després la seva versió correcta.
- Sense transacció, executa els quatre passos de confirmació de la comanda d'en Pau amb les tres línies (productes 1, 15 i 13). Anota què falla.
- Escriu una consulta que demostri la incoherència: comandes, línies i estocs dels tres productes implicats.
- Recarrega l'script i repeteix-ho tot dins de
BEGIN…ROLLBACK. Comprova amb la mateixa consulta que no en queda ni rastre. - Què hauria passat si el pas 3 s'hagués escrit com un sol
UPDATE ... FROM linies_comanda(05-03) en lloc de tres sentències? Continuaria fent falta la transacció?
Exercici 2
Anomenant INSERIR_COMANDA la sentència INSERT INTO comandes (client_id, empleat_id, data_comanda, estat, metode_pagament, despeses_enviament) VALUES (6, NULL, DATE '2026-03-05', 'pendent', 'targeta', 4.95) RETURNING id;, prediu què veurà cada sessió abans d'executar res, i després comprova-ho amb dues terminals:
| Instant | Sessió A | Sessió B | Què veu B? |
|---|---|---|---|
| t1 | BEGIN; |
||
| t2 | INSERIR_COMANDA |
||
| t3 | SELECT COUNT(*) FROM comandes; |
? | |
| t4 | ROLLBACK; |
||
| t5 | SELECT COUNT(*) FROM comandes; |
? | |
| t6 | INSERIR_COMANDA |
quin id? |
La pregunta de t6 és la interessant: quin identificador rep la comanda de B, sabent que la d'A es va desfer?
Exercici 3
En una sessió, provoca deliberadament l'estat avortat i surt-ne de les dues maneres possibles.
BEGIN;, unUPDATEvàlid sobreproductesi després unINSERTque violi una clau forana (per exemple, una comanda ambclient_id = 999).- Intenta executar
SELECT 1;. Copia el missatge exacte. - Surt-ne amb
COMMIT;i anota què respon el servidor. Comprova després si l'UPDATEvàlid es va arribar a aplicar. - Repeteix-ho tot sortint-ne amb
ROLLBACK;i compara. - Explica en dues frases per què MySQL es comportaria de manera diferent i quin dels dos comportaments prefereixes per a un script de facturació.
Solucions
Solució 1
1 i 2. El tercer UPDATE falla amb productes_stock_check, perquè el producte 13 està a 0. La consulta que revela l'estropici:
SELECT (SELECT COUNT(*) FROM comandes) AS comandes,
(SELECT COUNT(*) FROM linies_comanda) AS linies,
(SELECT estat FROM comandes WHERE id = 21) AS estat_21,
(SELECT stock FROM productes WHERE id = 1) AS stock_1,
(SELECT stock FROM productes WHERE id = 15) AS stock_15,
(SELECT stock FROM productes WHERE id = 13) AS stock_13;| comandes | linies | estat_21 | stock_1 | stock_15 | stock_13 |
|---|---|---|---|---|---|
| 21 | 50 | pendent | 118 | 39 | 0 |
Una comanda que ningú no cobrarà, tres línies òrfenes de propòsit i tres unitats d'inventari evaporades.
3. Amb BEGIN al principi i ROLLBACK al final, la mateixa consulta retorna 20, 47, (null), 120, 40, 0. Estat inicial exacte.
4. Amb un sol UPDATE ... FROM, el pas 3 sí que seria atòmic per si mateix: en violar el CHECK en una de les tres files, no se n'aplicaria cap, i els estocs quedarien a 120 i 40. Però la transacció continua fent falta, i per dos motius: la comanda 21 i les seves tres línies ja estan inserides i confirmades pels passos 1 i 2, així que la incoherència persisteix; i el pas 4 tampoc no s'executa. L'atomicitat d'una sentència no dóna atomicitat al procés: la unitat de treball és la comanda, no l'UPDATE.
Solució 2
A t3, B veu 20: l'INSERT d'A no està confirmat i per a B no existeix. A t5, B continua veient 20, perquè A va fer ROLLBACK i la comanda no va existir mai per a ningú. I a t6, l'id que rep B és 22, no 21. Aquest 22 és la part important. Les seqüències no es desfan amb un ROLLBACK, com ja van avisar 05-02 i 05-05: A va consumir el valor 21 en inserir i aquest valor es va perdre en desfer. És deliberat — si nextval respectés les transaccions, dues sessions haurien d'esperar-se l'una a l'altra per obtenir un identificador, i això destruiria el rendiment de qualsevol sistema amb insercions concurrents. El preu és que els id tenen forats, i la conseqüència pràctica (per què una PK no s'ha de fer servir com a número de factura) es desenvolupa a 09-05.
Solució 3
2. El missatge, literal: ERROR: current transaction is aborted, commands ignored until end of transaction block.
3. El servidor respon a COMMIT; amb ROLLBACK, i l'UPDATE vàlid no s'ha aplicat: la transacció sencera es va desfer. Aquesta resposta discordant —demanes COMMIT i et contesten ROLLBACK— és el senyal que estàs confirmant una transacció morta.
4. Amb ROLLBACK; el resultat és idèntic, però honest: vas demanar desfer i va desfer. La diferència no és a les dades, sinó que en el primer cas un script que no llegeixi la resposta creurà que va desar.
5. A MySQL/InnoDB, l'INSERT fallit s'hauria desfet només a si mateix i la transacció hauria continuat viva; el COMMIT hauria confirmat l'UPDATE de productes. Per a un script de facturació és preferible el comportament de PostgreSQL: una factura a mitges és pitjor que cap factura, i l'error sorollós obliga a mirar. El de MySQL és més còmode en càrregues massives on es toleren files rebutjades, però exigeix comprovar el resultat de cada sentència a mà.
Conclusió
Ja tens la unitat de treball que faltava:
- Una transacció és un conjunt de sentències que s'apliquen totes o cap. Es controla amb el TCL:
BEGIN,COMMITiROLLBACK. - El cas de BotigaVerda: confirmar una comanda són quatre operacions sobre tres taules, i si la tercera falla a mitges queden una comanda sense cobrar, tres línies òrfenes i tres unitats d'inventari evaporades — sense cap error registrat.
- L'autocommit no és l'absència de transaccions: és una transacció per sentència. Per això una sentència sempre és atòmica i dues mai no ho són. I per això importa saber que Oracle no l'activa, i que psycopg i SQLAlchemy fan just el contrari del que gairebé tothom suposa.
- El cicle de vida: activa → confirmada / desfeta, amb el desviament a l'estat avortat tan bon punt una sentència falla. Allà PostgreSQL rebutja tot amb
25P02i unCOMMITresponROLLBACK; MySQL, en canvi, només desfà la sentència fallida. I per saber on ets: l'asterisc de l'indicador (botigaverda=*>), l'admiració si està avortada (=!>) ipg_current_xact_id_if_assigned(). - El format de dues sessions que farem servir tot el mòdul, i la seva primera lliçó: abans del
COMMIT, B veu el valor vell sense esperar ni assabentar-se'n; després, el veu sencer i de cop. - Tancar la sessió o caure el servidor equivalen a un
ROLLBACK; el que s'ha confirmat sobreviu sempre. Transaccions de només lectura per als informes, i la regla d'or: curtes, i mai esperant ningú. Una transaccióidle in transactionbloqueja files, impedeix elVACUUM—el bloat de 08-05— i ocupa una connexió.
Has vist què fa una transacció. Falta què garanteix exactament, i aquesta resposta té quatre lletres. A Propietats ACID desmuntarem una a una l'atomicitat (i el mecanisme que permet desfer), la consistència (on acaba la responsabilitat de la base de dades i comença la teva, amb l'estoc que no pot quedar negatiu com a cas d'estudi), l'aïllament (per què B veia 40 mentre A veia 39) i la durabilitat (el WAL, fsync i per què les teves dades sobreviuen a un tall de corrent). I per fi arribarà MVCC: com PostgreSQL desa diverses versions de cada fila, com es veuen amb SELECT xmin, xmax, *, i per què d'aquí surten les files mortes, el bloat i la necessitat de VACUUM que 08-05 va deixar pendent.
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
