Hi ha una operació que els sistemes reals necessiten constantment i que cap de les tres instruccions anteriors no resol: "insereix aquesta fila si no existeix, i actualitza-la si ja existeix".
Sincronitzar el catàleg amb el fitxer que envia un proveïdor cada setmana: uns articles ja estan donats d'alta i només canvien de preu, altres són nous. Registrar l'estoc rebut d'una referència que potser encara no existeix. Desar la puntuació d'una ressenya que el client pot haver escrit fa mesos. Donar d'alta un client que tal vegada ja s'ha registrat. En els quatre casos, la resposta a "INSERT o UPDATE?" és depèn del que hi hagi a la taula, i no ho saps fins que ho mires.
La solució que tothom escriu la primera vegada —mirar i després decidir— és incorrecta, i ho és d'una manera que no es manifesta en desenvolupament i sí en producció. Aquesta lliçó comença explicant per què, i continua amb les dues eines que PostgreSQL ofereix per resoldre-ho en una sola sentència atòmica: INSERT ... ON CONFLICT, la seva solució pròpia des de la versió 9.5, i MERGE, la de l'estàndard SQL, disponible des de PostgreSQL 15.
Contingut
- El problema: inserir o actualitzar segons el que hi hagi
- Per què la solució ingènua és incorrecta
INSERT ... ON CONFLICT: la sintaxiDO NOTHINGiDO UPDATE SET- La destinació del conflicte i per què exigeix una restricció única
- La pseudotaula
EXCLUDEDi el patró d'acumulació WHEREalDO UPDATE: actualitzar només si alguna cosa canviaRETURNINGamb upsert, i com saber si va ser alta o modificacióMERGE: l'upsert de l'estàndard SQLON CONFLICTdavant deMERGE- Suport per motor
- Tres casos de BotigaVerda
- Errors habituals i consells
- Exercicis
- Conclusió
- El problema: inserir o actualitzar segons el que hi hagi
Huerta del Turia (proveïdor 1) envia cada dilluns un fitxer amb el seu catàleg actualitzat. Aquesta setmana porta cinc referències:
| nom | preu | cost | unitats enviades |
|---|---|---|---|
| Oli d'oliva verge extra 500 ml | 12.90 | 7.95 | 60 |
| Arròs integral ecològic 1 kg | 3.90 | 2.10 | 100 |
| Tomàquet triturat ecològic 400 g | 2.10 | 0.95 | 200 |
| Kombutxa de gingebre 750 ml | 5.25 | 2.45 | 40 |
| Cigrons ecològics 500 g | 2.60 | 1.15 | 140 |
Quatre ja són al catàleg (amb altres preus); el cinquè és nou. I ni tan sols tots els que hi són canvien: l'arròs continua a 3,90 € i 2,10 €.
Amb el que saps fins ara, hauries de separar el fitxer a mà en dos grups i escriure un INSERT per a l'un i un UPDATE per a l'altre. És tediós, i amb cinc-centes referències és directament inviable. El que necessites és una sentència que decideixi per si sola, fila a fila.
Això és un UPSERT: update + insert.
- Per què la solució ingènua és incorrecta
La primera idea de qualsevol, i la que escriuen la majoria de les aplicacions:
-- ⚠️ INCORRECTA tan bon punt hi ha més d'un usuari
SELECT COUNT(*) FROM productes WHERE nom = 'Cigrons ecològics 500 g';
-- si retorna 0 → INSERT
-- si retorna 1 → UPDATEFunciona perfectament… mentre siguis l'única persona connectada. Tan bon punt hi ha dos processos treballant alhora, apareix una condició de cursa:
sequenceDiagram
participant A as Sessió A
participant DB as Base de dades
participant B as Sessió B
A->>DB: SELECT ... WHERE nom = 'Cigrons'
DB-->>A: 0 files → "no existeix, inseriré"
B->>DB: SELECT ... WHERE nom = 'Cigrons'
DB-->>B: 0 files → "no existeix, inseriré"
A->>DB: INSERT ... 'Cigrons'
DB-->>A: INSERT 0 1 ✅
B->>DB: INSERT ... 'Cigrons'
DB-->>B: 💥 duplicate key value violates<br/>unique constraint
Les dues sessions miren, totes dues veuen que no existeix, totes dues decideixen inserir. La primera se'n surt; la segona peta. I hi ha una variant pitjor: si no hi hagués restricció única, la segona inserció tindria èxit i acabaries amb dues files duplicades sense cap error.
L'interval entre el SELECT i l'INSERT pot semblar minúscul —mil·lisegons—, però en un sistema que processa milers d'operacions per minut aquella finestra es creua constantment. No és un cas rar: és un cas garantit. La regla general que hi ha al darrere:
Comprovar i després actuar en dues sentències separades no és mai segur davant de la concurrència. Entre la comprovació i l'acció, el món ha pogut canviar.
Les tres formes de resoldre-ho, de pitjor a millor:
| Enfocament | Problema |
|---|---|
SELECT i després INSERT/UPDATE |
Condició de cursa. Incorrecte |
Intentar l'INSERT i capturar l'error de duplicat a l'aplicació |
Funciona, però converteix un cas normal en una excepció, embruta el codi i en alguns motors invalida la transacció sencera |
Una sola sentència atòmica: ON CONFLICT o MERGE |
El motor resol el conflicte internament, amb els bloqueigs adequats. Correcte |
Per què la tercera opció és segura. Quan PostgreSQL executa un
INSERT ... ON CONFLICT, la comprovació i l'escriptura passen dins de la mateixa operació, amb el bloqueig de fila i índex adequat: no hi ha finestra entre "mirar" i "actuar". El mecanisme complet —què es bloqueja, durant quant de temps i què veuen les altres sessions— és el mòdul 9. Aquí n'hi ha prou de saber que una sentència és atòmica i dues no ho són.
INSERT ... ON CONFLICT: la sintaxi
INSERT ... ON CONFLICT: la sintaxiINSERT INTO taula (columnes)
VALUES (...)
ON CONFLICT (columna_o_columnes) -- o: ON CONSTRAINT nom_restriccio
DO NOTHING;
-- o bé:
INSERT INTO taula (columnes)
VALUES (...)
ON CONFLICT (columna_o_columnes)
DO UPDATE SET columna = valor [, ...]
[WHERE condició];Es llegeix literalment: "insereix això; si xoca amb la unicitat d'aquestes columnes, no facis res / o fes aquest UPDATE al seu lloc".
Comencem pel cas que no necessita res nou, perquè BotigaVerda ja té la restricció: clients.email és UNIQUE.
DO NOTHING i DO UPDATE SET
DO NOTHING i DO UPDATE SET(Els tres exemples d'aquest apartat s'executen en ordre sobre la base acabada de recarregar. Fixa't en els id: el motiu que surtin els que surten s'explica al final.)
DO NOTHING: ignora el conflicte
INSERT INTO clients (nom, cognoms, email, ciutat, pais, data_registre)
VALUES ('Lucía', 'Martínez Soler', '[email protected]', 'Alacant', 'Espanya', DATE '2026-03-01')
ON CONFLICT (email) DO NOTHING;Zero files inserides, zero errors. El client ja existia amb aquell email, així que la sentència s'ha limitat a no fer res.
| id | nom | cognoms | ciutat | data_registre | |
|---|---|---|---|---|---|
| 1 | Lucía | Martínez Soler | [email protected] | València | 2025-01-10 |
Intacte: continua a València i amb la seva data de registre original.
DO NOTHING és ideal per a dades mestres reexecutables: llavors de desenvolupament, catàlegs de referència, càrregues inicials. Converteix un script fràgil en un d'idempotent (05-03).
DO UPDATE SET: l'upsert de debò
INSERT INTO clients (nom, cognoms, email, ciutat, pais, data_registre)
VALUES ('Lucía', 'Martínez Soler', '[email protected]', 'Alacant', 'Espanya', DATE '2026-03-01')
ON CONFLICT (email) DO UPDATE
SET ciutat = EXCLUDED.ciutat
RETURNING id, nom, cognoms, email, ciutat, data_registre;| id | nom | cognoms | ciutat | data_registre | |
|---|---|---|---|---|---|
| 1 | Lucía | Martínez Soler | [email protected] | Alacant | 2025-01-10 |
La ciutat ha passat de València a Alacant, i data_registre no ha canviat: només s'actualitza el que apareix al SET. És exactament el que volem — la data d'alta original d'un client no s'ha de reescriure perquè ens enviïn les seves dades una altra vegada.
I amb un email nou, la mateixa sentència insereix:
INSERT INTO clients (nom, cognoms, email, ciutat, pais, data_registre)
VALUES ('Aitor', 'Zubizarreta Egaña', '[email protected]', 'Bilbao', 'Espanya', DATE '2026-03-01')
ON CONFLICT (email) DO UPDATE
SET ciutat = EXCLUDED.ciutat
RETURNING id, nom, cognoms, email, ciutat, data_registre;| id | nom | cognoms | ciutat | data_registre | |
|---|---|---|---|---|---|
| 18 | Aitor | Zubizarreta Egaña | [email protected] | Bilbao | 2026-03-01 |
La mateixa sentència ha inserit en un cas i ha actualitzat en l'altre, sense que tu hagis hagut de decidir res. Això és l'upsert.
Per què l'
idés 18 i no 16. BotigaVerda té 15 clients i la seva seqüència va quedar a 15 després delsetvalde l'script. Però un upsert que acaba en conflicte també consumeix un valor de la seqüència: PostgreSQL construeix la fila completa —avaluant tots elsDEFAULT, inclòsnextval— abans de comprovar l'índex únic. Els dos exemples anteriors sobre la Lucía es van endur el 16 i el 17, i l'Aitor ha rebut el 18.És coherent amb el que vas veure a 05-02: les seqüències no es desfan. Conseqüència pràctica: en una taula amb upserts freqüents, els
idtenen buits grans i no compten files. Si això t'importa, revisa'n el tipus: unINTEGERs'esgota a 2 147 483 647, i amb upserts massius això arriba abans del que sembla.BIGINTés la resposta habitual.
- La destinació del conflicte i per què exigeix una restricció única
L'ON CONFLICT (...) no és decoratiu: li diu a PostgreSQL quin conflicte ha d'interceptar. I només pot interceptar violacions d'una restricció UNIQUE, PRIMARY KEY o d'un índex únic.
Hi ha dues formes d'indicar-ho:
ON CONFLICT (email) -- per columna (inferència)
ON CONFLICT ON CONSTRAINT clients_email_key -- per nom de restricció| Forma | Avantatge | Inconvenient |
|---|---|---|
| Per columna | No depèn del nom de la restricció; sobreviu a un RENAME CONSTRAINT |
Ambigua si hi ha dues restriccions sobre les mateixes columnes |
| Per nom | Totalment explícita | Es trenca si algú reanomena la restricció (un altre argument a favor d'anomenar-les bé, 05-01) |
Si indiques columnes que no estan cobertes per cap restricció única, falla:
-- ⚠️ INCORRECTA: productes.nom no és UNIQUE a BotigaVerda
INSERT INTO productes (nom, categoria_id, proveidor_id, preu, cost, stock)
VALUES ('Oli d''oliva verge extra 500 ml', 1, 1, 12.90, 7.95, 60)
ON CONFLICT (nom) DO UPDATE SET preu = EXCLUDED.preu;I això és una limitació de disseny, no un caprici. Perquè PostgreSQL pugui decidir atòmicament "ja existeix", necessita un índex únic que l'hi digui sense recórrer la taula; sense ell hauria de cercar, i tornaríem a la finestra de cursa de l'apartat 2.
Així que per sincronitzar el catàleg per nom cal afegir la restricció:
-- Exemple puntual d'aquesta lliçó: NO forma part de l'esquema
-- canònic de BotigaVerda. Afegeix-la per practicar i treu-la després.
ALTER TABLE productes
ADD CONSTRAINT uq_productes_nom UNIQUE (nom);Funciona perquè els vint noms del catàleg són diferents. I de passada il·lustra una cosa que veuràs a fons a 05-06: afegir un UNIQUE crea un índex per sota, i aquell índex és el que fa possible l'upsert (les estructures i el seu cost, al mòdul 8).
Ara sí:
INSERT INTO productes (nom, categoria_id, proveidor_id, preu, cost, stock)
VALUES ('Oli d''oliva verge extra 500 ml', 1, 1, 12.90, 7.95, 60)
ON CONFLICT (nom) DO UPDATE
SET preu = EXCLUDED.preu,
cost = EXCLUDED.cost
RETURNING id, nom, preu, cost, stock;| id | nom | preu | cost | stock |
|---|---|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | 12.90 | 7.95 | 120 |
Preu i cost actualitzats; l'estoc continua a 120 perquè no és al SET. Les 60 unitats del fitxer s'han ignorat, cosa que en aquest cas és correcta: són unitats enviades, no l'estoc total.
- La pseudotaula
EXCLUDED i el patró d'acumulació
EXCLUDED i el patró d'acumulacióEXCLUDED és la clau de tot el mecanisme. És una pseudotaula que conté la fila que s'ha intentat inserir i ha estat rebutjada pel conflicte.
Dins del DO UPDATE SET conviuen dos móns:
| Referència | A què apunta |
|---|---|
EXCLUDED.columna |
El valor proposat (el que venia al VALUES o al SELECT) |
productes.columna o taula.columna |
El valor actual de la fila que ja és a la taula |
Amb tots dos pots escriure qualsevol regla de fusió:
-- Quedar-se amb el valor nou
SET preu = EXCLUDED.preu
-- Conservar l'actual (equival a no posar-lo)
SET preu = productes.preu
-- Quedar-se amb el més gran dels dos
SET stock = GREATEST(productes.stock, EXCLUDED.stock)
-- ACUMULAR: sumar el nou a l'existent
SET stock = productes.stock + EXCLUDED.stockAquest últim és el patró d'acumulació, i és probablement l'ús més valuós de l'upsert. Registrar la recepció de mercaderia és exactament això: "si el producte ja està donat d'alta, suma-li les unitats; si no, crea'l amb aquelles unitats".
| id | nom | stock |
|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | 120 |
| 2 | Arròs integral ecològic 1 kg | 200 |
| 5 | Tomàquet triturat ecològic 400 g | 300 |
| 16 | Kombutxa de gingebre 750 ml | 60 |
INSERT INTO productes (nom, categoria_id, proveidor_id, preu, cost, stock) VALUES
('Oli d''oliva verge extra 500 ml', 1, 1, 12.90, 7.95, 60),
('Arròs integral ecològic 1 kg', 1, 1, 3.90, 2.10, 100),
('Tomàquet triturat ecològic 400 g', 1, 1, 2.10, 0.95, 200),
('Kombutxa de gingebre 750 ml', 4, 1, 5.25, 2.45, 40),
('Cigrons ecològics 500 g', 1, 1, 2.60, 1.15, 140)
ON CONFLICT (nom) DO UPDATE
SET stock = productes.stock + EXCLUDED.stock
RETURNING id, nom, preu, stock;| id | nom | preu | stock |
|---|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | 12.50 | 180 |
| 2 | Arròs integral ecològic 1 kg | 3.90 | 300 |
| 5 | Tomàquet triturat ecològic 400 g | 1.95 | 500 |
| 16 | Kombutxa de gingebre 750 ml | 4.95 | 100 |
| 21 | Cigrons ecològics 500 g | 2.60 | 140 |
Cinc files: quatre acumulades i una creada. 120+60=180, 200+100=300, 300+200=500, 60+40=100, i els cigrons entren amb les seves 140 unitats i l'id 21.
Fixa't en un detall revelador: els preus de les quatre files actualitzades continuen sent els antics (12,50 €, 3,90 €, 1,95 €, 4,95 €), perquè el SET només toca stock. Només la fila nova ha estrenat el preu del fitxer. L'upsert et deixa controlar camp a camp què es fusiona i què es conserva.
I l'avís obligatori, germà del d'UPDATE ... FROM (05-03):
⚠️
EXCLUDEDno acumula entre tuples de la mateixa sentència. Si el fitxer portés dues vegades el mateix nom, PostgreSQL fallaria ambON CONFLICT DO UPDATE command cannot affect row a second time. No suma les dues: s'hi nega. Dedupica l'origen abans de fer upsert.
ERROR: ON CONFLICT DO UPDATE command cannot affect row a second time HINT: Ensure that no rows proposed for insertion within the same command have duplicate constrained values.
Aquell error és en realitat una bona notícia: el motor es nega a fer una cosa ambigua en lloc d'inventar-se un resultat. Compara'l amb UPDATE ... FROM, que en la mateixa situació tria una coincidència a l'atzar sense avisar.
WHERE al DO UPDATE: actualitzar només si alguna cosa canvia
WHERE al DO UPDATE: actualitzar només si alguna cosa canviaEl DO UPDATE admet el seu propi WHERE, que decideix si l'actualització s'aplica o es descarta:
INSERT INTO productes (nom, categoria_id, proveidor_id, preu, cost, stock) VALUES
('Oli d''oliva verge extra 500 ml', 1, 1, 12.90, 7.95, 60),
('Arròs integral ecològic 1 kg', 1, 1, 3.90, 2.10, 100),
('Tomàquet triturat ecològic 400 g', 1, 1, 2.10, 0.95, 200),
('Kombutxa de gingebre 750 ml', 4, 1, 5.25, 2.45, 40),
('Cigrons ecològics 500 g', 1, 1, 2.60, 1.15, 140)
ON CONFLICT (nom) DO UPDATE
SET preu = EXCLUDED.preu,
cost = EXCLUDED.cost
WHERE productes.preu IS DISTINCT FROM EXCLUDED.preu
OR productes.cost IS DISTINCT FROM EXCLUDED.cost
RETURNING id, nom, preu, cost;| id | nom | preu | cost |
|---|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | 12.90 | 7.95 |
| 5 | Tomàquet triturat ecològic 400 g | 2.10 | 0.95 |
| 16 | Kombutxa de gingebre 750 ml | 5.25 | 2.45 |
| 21 | Cigrons ecològics 500 g | 2.60 | 1.15 |
Quatre files en lloc de cinc. L'arròs integral venia amb 3,90 € i 2,10 €, exactament el que ja tenia, així que el WHERE ha descartat la seva actualització i ni tan sols apareix al RETURNING.
Per què val la pena aquella línia de més:
| Motiu | Detall |
|---|---|
| Menys escriptures | Cada UPDATE crea una versió nova de la fila encara que el valor sigui idèntic. Amb milers de files i un 5 % de canvis reals, l'estalvi és enorme |
| Menys bloqueigs | Una fila no actualitzada no es bloqueja per a les altres sessions (mòdul 9) |
| Auditoria honesta | Si hi ha triggers d'auditoria (mòdul 10), no registren canvis que no han passat |
El RETURNING diu la veritat |
Et retorna només el que realment ha canviat |
Fixa't en l'ús d'IS DISTINCT FROM en lloc de <>. És directament 04-03: si productes.cost fos NULL, la comparació productes.cost <> EXCLUDED.cost donaria UNKNOWN, el WHERE la descartaria i el cost no s'actualitzaria mai. IS DISTINCT FROM tracta dos nuls com a iguals i un nul davant d'un valor com a diferents, que és el que aquí necessitem. És un dels llocs on aquella lliçó es cobra el deute.
RETURNING amb upsert, i com saber si va ser alta o modificació
RETURNING amb upsert, i com saber si va ser alta o modificacióRETURNING funciona igual que a INSERT i UPDATE: retorna les files afectades, tant si són inserides com actualitzades. El que no et diu de manera directa és quina va ser quina.
Existeix un truc molt estès basat en la columna de sistema xmax (exemple sobre la base acabada de recarregar):
INSERT INTO clients (nom, cognoms, email, ciutat, pais, data_registre) VALUES
('Lucía', 'Martínez Soler', '[email protected]', 'Alacant', 'Espanya', DATE '2026-03-01'),
('Marisol', 'Aguirre Peña', '[email protected]', 'Màlaga', 'Espanya', DATE '2026-03-01')
ON CONFLICT (email) DO UPDATE
SET ciutat = EXCLUDED.ciutat
RETURNING id,
nom,
email,
ciutat,
(xmax = 0) AS va_ser_insercio;| id | nom | ciutat | va_ser_insercio | |
|---|---|---|---|---|
| 1 | Lucía | [email protected] | Alacant | false |
| 17 | Marisol | [email protected] | Màlaga | true |
La Lucía s'ha actualitzat (false), la Marisol s'ha creat (true) — amb l'id 17, perquè l'intent fallit sobre la Lucía es va endur el 16.
Com funciona. xmax és una columna oculta que PostgreSQL fa servir internament per al control de versions: desa l'identificador de la transacció que va esborrar o bloquejar la fila. En una fila acabada d'inserir val 0; en una d'actualitzada dins de l'upsert, no.
⚠️ Fes-lo servir amb reserves.
xmaxés un detall d'implementació, no una interfície documentada. Pot canviar entre versions, i hi ha situacions (files bloquejades per altres transaccions,SELECT ... FOR UPDATEprevis) en què unxmaxdiferent de zero no significa el que et penses. Serveix perfectament per depurar i per a un informe intern; no construeixis lògica de negoci crítica sobre ell.
I una limitació que sí que importa: amb DO NOTHING, les files en conflicte no apareixen al RETURNING en absolut. Només veuràs les que realment s'han inserit:
INSERT INTO categories (nom, descripcio) VALUES
('Begudes', 'Ja existeix'),
('Rebost a granel', 'Llegums, cereals i fruita seca sense envàs')
ON CONFLICT (nom) DO NOTHING
RETURNING id, nom;| id | nom |
|---|---|
| 7 | Rebost a granel |
Una de les dues. Si necessites saber també què s'ha ignorat, DO NOTHING no t'ho dirà.
MERGE: l'upsert de l'estàndard SQL
MERGE: l'upsert de l'estàndard SQLON CONFLICT és una extensió de PostgreSQL. L'estàndard SQL:2003 defineix per al mateix la instrucció MERGE, que PostgreSQL va incorporar a la versió 15.
MERGE INTO taula_desti AS d
USING origen AS o
ON d.clau = o.clau
WHEN MATCHED [AND condició] THEN
UPDATE SET columna = valor [, ...]
WHEN MATCHED [AND condició] THEN
DELETE
WHEN NOT MATCHED [AND condició] THEN
INSERT (columnes) VALUES (valors)
[WHEN NOT MATCHED THEN DO NOTHING];
La lògica és la d'un JOIN: s'aparellen destinació i origen per la condició de l'ON, i per a cada fila s'executa la primera clàusula WHEN que es compleixi.
L'origen pot ser una taula, una consulta o una llista de valors. Per al cas de BotigaVerda, el natural és una taula de staging amb el contingut del fitxer del proveïdor:
-- Taula auxiliar d'aquesta lliçó: NO forma part de l'esquema canònic
CREATE TEMP TABLE cataleg_proveidor (
nom VARCHAR(150) NOT NULL PRIMARY KEY,
categoria_id INTEGER NOT NULL,
preu NUMERIC(10,2) NOT NULL CHECK (preu >= 0),
cost NUMERIC(10,2) NOT NULL CHECK (cost >= 0),
unitats INTEGER NOT NULL CHECK (unitats > 0)
);
INSERT INTO cataleg_proveidor (nom, categoria_id, preu, cost, unitats) VALUES
('Oli d''oliva verge extra 500 ml', 1, 12.90, 7.95, 60),
('Arròs integral ecològic 1 kg', 1, 3.90, 2.10, 100),
('Tomàquet triturat ecològic 400 g', 1, 2.10, 0.95, 200),
('Kombutxa de gingebre 750 ml', 4, 5.25, 2.45, 40),
('Cigrons ecològics 500 g', 1, 2.60, 1.15, 140);Aquell COPY a una taula de staging seguit d'una fusió cap a la taula definitiva és el patró professional d'integració de dades que anunciava 05-02.
I ara el mateix cas de l'apartat 7, resolt amb MERGE:
MERGE INTO productes AS p
USING cataleg_proveidor AS c
ON p.nom = c.nom
WHEN MATCHED AND (p.preu IS DISTINCT FROM c.preu
OR p.cost IS DISTINCT FROM c.cost) THEN
UPDATE SET preu = c.preu,
cost = c.cost
WHEN NOT MATCHED THEN
INSERT (nom, categoria_id, proveidor_id, preu, cost, stock)
VALUES (c.nom, c.categoria_id, 1, c.preu, c.cost, c.unitats);Quatre files afectades: tres actualitzacions (oli, tomàquet i kombutxa) i una inserció (cigrons). L'arròs no compleix la condició del WHEN MATCHED i, en no haver-hi més clàusules aplicables, es queda com està.
| id | nom | preu | cost | stock |
|---|---|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | 12.90 | 7.95 | 120 |
| 2 | Arròs integral ecològic 1 kg | 3.90 | 2.10 | 200 |
| 5 | Tomàquet triturat ecològic 400 g | 2.10 | 0.95 | 300 |
| 16 | Kombutxa de gingebre 750 ml | 5.25 | 2.45 | 60 |
| 17 | Suc de taronja premsat en fred 1 L | 5.40 | 2.60 | 90 |
| 21 | Cigrons ecològics 500 g | 2.60 | 1.15 | 140 |
Fixa't en el suc de taronja (17): és de Huerta del Turia però no venia al fitxer, i ha quedat intacte. Ni ON CONFLICT ni aquest MERGE no fan res amb les files de la destinació que l'origen no esmenta. Si el criteri de negoci fos "el que no és al fitxer es descataloga", caldrien dues operacions… o una clàusula WHEN NOT MATCHED BY SOURCE, que PostgreSQL 16 encara no té (va arribar a la 17).
El que MERGE sap fer i ON CONFLICT no
Diverses condicions i DELETE. Aquest MERGE sincronitza el catàleg i a més descataloga el que el proveïdor envia a preu 0:
MERGE INTO productes AS p
USING cataleg_proveidor AS c
ON p.nom = c.nom
WHEN MATCHED AND c.preu = 0 THEN
UPDATE SET actiu = FALSE, stock = 0
WHEN MATCHED AND (p.preu IS DISTINCT FROM c.preu) THEN
UPDATE SET preu = c.preu, cost = c.cost
WHEN NOT MATCHED AND c.preu > 0 THEN
INSERT (nom, categoria_id, proveidor_id, preu, cost, stock)
VALUES (c.nom, c.categoria_id, 1, c.preu, c.cost, c.unitats);Tres regles diferents en una sentència, avaluades en ordre: la primera que es compleixi guanya. Amb ON CONFLICT això no es pot expressar; caldrien diverses sentències.
Limitacions de MERGE a PostgreSQL 16
| Limitació | Detall |
|---|---|
Sense RETURNING |
MERGE ... RETURNING va arribar a PostgreSQL 17. A la 16 només tens el comptador MERGE N |
Sense WHEN NOT MATCHED BY SOURCE |
També de la 17 |
| No és immune a la concurrència | Aquest és l'important: sota càrrega concurrent, un MERGE pot fallar amb duplicate key value si una altra sessió insereix la mateixa clau entre l'aparellament i l'escriptura. ON CONFLICT sí que garanteix que això no passi |
| No agrupa l'origen | Si cataleg_proveidor portés dues files amb el mateix nom, el MERGE falla amb MERGE command cannot affect row a second time, igual que l'upsert |
Aquella tercera limitació és contraintuïtiva i convé retenir-la: la instrucció estàndard és menys robusta davant de la concurrència que l'extensió propietària, perquè ON CONFLICT es recolza directament en l'índex únic i MERGE no l'exigeix.
ON CONFLICT davant de MERGE
ON CONFLICT davant de MERGEINSERT ... ON CONFLICT |
MERGE |
|
|---|---|---|
| Estàndard SQL | No: extensió de PostgreSQL | Sí (SQL:2003) |
| Disponible des de | PostgreSQL 9.5 | PostgreSQL 15 |
| Exigeix restricció única | Sí, obligatòriament | No: n'hi ha prou amb la condició de l'ON |
| Atòmic davant de la concurrència | Sí, garantit | No sempre: pot fallar amb clau duplicada |
| Accions possibles | DO NOTHING, DO UPDATE |
UPDATE, INSERT, DELETE, DO NOTHING |
| Diverses condicions | Una de sola (DO UPDATE ... WHERE) |
Diverses clàusules WHEN, avaluades en ordre |
| Origen | VALUES o SELECT |
Taula, consulta o VALUES |
RETURNING |
Sí | No a PostgreSQL 16 (sí a la 17) |
| Accés a la fila existent i a la proposada | taula.col i EXCLUDED.col |
desti.col i origen.col |
| Llegibilitat amb regles complexes | Es complica | Millor |
Quan fer servir cadascun:
ON CONFLICTquan el cas sigui el clàssic "inserir o actualitzar" sobre una clau única, quan hi hagi concurrència real, o quan necessitisRETURNING. És el 90 % dels casos.MERGEquan necessitis diverses regles (actualitzar-ne uns, esborrar-ne d'altres, inserir la resta), quan l'aparellament no sigui per una clau única, quan l'origen sigui una consulta complexa, o quan el codi hagi de ser portable a Oracle o SQL Server.
- Suport per motor
| Motor | Sintaxi | Notes |
|---|---|---|
| PostgreSQL 15+ | INSERT ... ON CONFLICT i MERGE |
Totes dues. ON CONFLICT és la recomanada per al cas simple |
| MySQL / MariaDB | INSERT ... ON DUPLICATE KEY UPDATE |
No s'indica la restricció: es dispara amb qualsevol clau única violada. Per llegir la fila proposada: AS new (MySQL 8.0.19+) o l'antiga funció VALUES(col). MySQL 8 no té MERGE |
| SQLite 3.24+ | INSERT ... ON CONFLICT |
Copiat de PostgreSQL, amb excluded en minúscula. Sense MERGE |
| SQL Server | MERGE |
Des del 2008. Històricament amb diversos errors de concurrència documentats; molts equips prefereixen UPDATE + INSERT en transacció |
| Oracle | MERGE |
Des de 9i, molt madur i d'ús massiu. No té ON CONFLICT |
El cas especial d'INSERT OR REPLACE de SQLite
SQLite ofereix a més INSERT OR REPLACE INTO ..., que molta gent fa servir creient que és un upsert. No ho és, i la diferència és greu:
ON CONFLICT DO UPDATE |
INSERT OR REPLACE |
|
|---|---|---|
| Què fa amb la fila existent | La modifica | L'esborra i en crea una de nova |
| Columnes no esmentades | Conserven el seu valor | Es perden: prenen el seu DEFAULT o NULL |
Clau primària (rowid) |
Es conserva | Canvia |
ON DELETE CASCADE de les filles |
No es dispara | Es dispara: les files filles s'esborren |
| Triggers d'esborrat | No | Sí |
Aplicat a BotigaVerda seria una catàstrofe silenciosa: un INSERT OR REPLACE sobre productes esborraria la fila, i amb ella se n'anirien en cascada totes les seves ressenyes (ressenyes.producte_id és ON DELETE CASCADE). L'"upsert" hauria destruït dades d'una altra taula sense esmentar-la.
La regla: a SQLite fes servir
ON CONFLICT ... DO UPDATE, maiINSERT OR REPLACE, llevat que l'esborrat-i-recreació sigui exactament el que vols.
- Tres casos de BotigaVerda
12.1. Sincronitzar el catàleg amb el fitxer del proveïdor
Resolt als apartats 7 i 9, de les dues formes. Requereix la restricció uq_productes_nom per a la versió amb ON CONFLICT; el MERGE funcionaria igual sense ella.
12.2. Registrar o incrementar l'estoc rebut
Resolt a l'apartat 6 amb el patró d'acumulació SET stock = productes.stock + EXCLUDED.stock. És el cas on l'upsert brilla: una sola sentència processa un albarà sencer, tant si dóna d'alta referències noves com si suma a les existents.
Una versió més completa, que a més actualitza el preu de compra i anota la data d'alta només a les referències noves:
INSERT INTO productes (nom, categoria_id, proveidor_id, preu, cost, stock, data_alta)
SELECT c.nom, c.categoria_id, 1, c.preu, c.cost, c.unitats, DATE '2026-03-02'
FROM cataleg_proveidor AS c
ON CONFLICT (nom) DO UPDATE
SET stock = productes.stock + EXCLUDED.stock,
cost = EXCLUDED.cost
RETURNING id, nom, preu, cost, stock, data_alta, (xmax = 0) AS alta_nova;| id | nom | preu | cost | stock | data_alta | alta_nova |
|---|---|---|---|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | 12.50 | 7.95 | 180 | 2025-01-15 | false |
| 2 | Arròs integral ecològic 1 kg | 3.90 | 2.10 | 300 | 2025-01-15 | false |
| 5 | Tomàquet triturat ecològic 400 g | 1.95 | 0.95 | 500 | 2025-01-20 | false |
| 16 | Kombutxa de gingebre 750 ml | 4.95 | 2.45 | 100 | 2025-02-20 | false |
| 21 | Cigrons ecològics 500 g | 2.60 | 1.15 | 140 | 2026-03-02 | true |
Quatre acumulacions i una alta. I observa la columna data_alta: les quatre existents conserven la seva (2025) i només la nova estrena la d'avui, perquè data_alta no apareix al SET. Aquella asimetria —uns camps es fusionen i altres només s'omplen en crear— és just el que fa útil el DO UPDATE davant d'un REPLACE.
Fixa't també que l'origen és un SELECT, no un VALUES. INSERT ... SELECT ... ON CONFLICT combina el de 05-02 amb el d'aquesta lliçó, i és la forma habitual de processar una taula de staging sencera.
12.3. Actualitzar la puntuació d'una ressenya ja existent
Un client torna a valorar un producte que ja havia ressenyat. La regla de negoci: una ressenya per client i producte, i l'última substitueix l'anterior.
Avui ressenyes no impedeix duplicats, així que primer cal declarar aquella regla:
-- Exemple puntual d'aquesta lliçó: NO forma part de l'esquema
-- canònic de BotigaVerda.
ALTER TABLE ressenyes
ADD CONSTRAINT uq_ressenyes_producte_client UNIQUE (producte_id, client_id);Funciona perquè les dotze ressenyes actuals tenen parells (producte_id, client_id) diferents. Si hi hagués duplicats, l'ALTER TABLE fallaria — que és exactament el que ha de fer.
Ara l'upsert. El client 6 (Pau Llorens Vidal) havia posat un 2 a la Kombutxa; n'han canviat la recepta i vol pujar-lo a 4:
SELECT id, producte_id, client_id, puntuacio, comentari, data
FROM ressenyes WHERE producte_id = 16 AND client_id = 6;| id | producte_id | client_id | puntuacio | comentari | data |
|---|---|---|---|---|---|
| 6 | 16 | 6 | 2 | Massa gingebre per al meu gust, gairebé no es pot beure. | 2025-06-20 |
INSERT INTO ressenyes (producte_id, client_id, puntuacio, comentari, data)
VALUES (16, 6, 4, 'Han suavitzat el gingebre i ara està molt equilibrada.', DATE '2026-03-02')
ON CONFLICT (producte_id, client_id) DO UPDATE
SET puntuacio = EXCLUDED.puntuacio,
comentari = EXCLUDED.comentari,
data = EXCLUDED.data
RETURNING id, producte_id, client_id, puntuacio, comentari, data, (xmax = 0) AS nova;| id | producte_id | client_id | puntuacio | comentari | data | nova |
|---|---|---|---|---|---|---|
| 6 | 16 | 6 | 4 | Han suavitzat el gingebre i ara està molt equilibrada. | 2026-03-02 | false |
Mateixa fila (id 6), contingut nou. I amb un client que no ha ressenyat mai aquell producte, la mateixa sentència crea la ressenya:
INSERT INTO ressenyes (producte_id, client_id, puntuacio, comentari, data)
VALUES (16, 1, 5, 'La millor kombutxa que he tastat.', DATE '2026-03-02')
ON CONFLICT (producte_id, client_id) DO UPDATE
SET puntuacio = EXCLUDED.puntuacio,
comentari = EXCLUDED.comentari,
data = EXCLUDED.data
RETURNING id, producte_id, client_id, puntuacio, data, (xmax = 0) AS nova;| id | producte_id | client_id | puntuacio | data | nova |
|---|---|---|---|---|---|
| 14 | 16 | 1 | 5 | 2026-03-02 | true |
L'id és 14 i no 13 pel mateix de l'apartat 4: l'upsert anterior, que va acabar en DO UPDATE, ja s'havia endut el 13 de la seqüència.
I l'efecte sobre la valoració del producte:
SELECT p.id,
p.nom,
COUNT(r.id) AS ressenyes,
ROUND(AVG(r.puntuacio), 2) AS mitjana
FROM productes AS p
LEFT JOIN ressenyes AS r ON r.producte_id = p.id
WHERE p.id = 16
GROUP BY p.id, p.nom;| id | nom | ressenyes | mitjana |
|---|---|---|---|
| 16 | Kombutxa de gingebre 750 ml | 2 | 4.50 |
D'un 2 solitari a una mitjana de 4,50 amb dues ressenyes. La Kombutxa deixa de ser el pitjor producte del catàleg.
Recorda desfer els exemples. Les dues restriccions d'aquesta lliçó (
uq_productes_nomiuq_ressenyes_producte_client) no formen part de l'esquema del curs. Treu-les ambALTER TABLE ... DROP CONSTRAINT ...o, més simple, recarregabotigaverda.sql. La sintaxi completa d'ALTER TABLEés la lliçó següent.
Errors habituals i consells
- Resoldre l'upsert amb
SELECT+INSERT/UPDATE. Condició de cursa garantida sota concurrència. Una sentència és atòmica; dues no. - Fer servir
ON CONFLICTsobre columnes sense restricció única.there is no unique or exclusion constraint matching the ON CONFLICT specification. L'índex únic és el mecanisme, no un requisit burocràtic. - Oblidar el prefix
EXCLUDED.alDO UPDATE.SET preu = preués una assignació circular: la fila es queda com estava i no dóna error. - Portar el mateix valor de clau dues vegades a la mateixa sentència.
cannot affect row a second time. Dedupica l'origen abans. - Comparar amb
<>en lloc d'IS DISTINCT FROMalWHEREdelDO UPDATE. Amb unNULLpel mig la comparació dónaUNKNOWNi la fila no s'actualitza mai (04-03). - Esperar que
DO NOTHINGt'informi del que ha ignorat. No apareix alRETURNING. Si necessites saber-ho, fes servirDO UPDATEambWHERE. - Construir lògica de negoci sobre
xmax = 0. És un detall d'implementació. Val per depurar; no per facturar. - Creure que l'upsert actualitza també el que l'origen no esmenta. No ho fa. Les files de la destinació absents del fitxer queden intactes.
- Fer servir
INSERT OR REPLACEa SQLite creient que és un upsert. Esborra i recrea: perd columnes, canvia elrowidi dispara les cascades d'esborrat. - Suposar que
MERGEés segur davant de la concurrència. No ho és a PostgreSQL 16: pot fallar amb clau duplicada.ON CONFLICTsí que ho és. - Consell: per a un albarà o un fitxer, carrega a una taula de staging i fusiona des d'allà.
COPY+INSERT ... SELECT ... ON CONFLICT(oMERGE) és el patró professional d'integració. - Consell: decideix camp a camp què es fusiona.
preusí,data_altano,stockacumulant. Aquella granularitat és tot el valor delDO UPDATE. - Consell: posa sempre el
WHERE ... IS DISTINCT FROMalDO UPDATE. Menys escriptures, menys bloqueigs, auditoria honesta i unRETURNINGque diu la veritat.
Exercicis
Treballa sobre la base acabada de recarregar, dins de BEGIN … ROLLBACK.
Exercici 1
BotigaVerda rep un fitxer d'altes i baixes de clients procedent d'una campanya de màrqueting:
| nom | cognoms | ciutat | pais | |
|---|---|---|---|---|
| Lucía | Martínez Soler | [email protected] | Gandia | Espanya |
| Sofia | Moreira Costa | [email protected] | Coïmbra | Portugal |
| Aitor | Zubizarreta Egaña | [email protected] | Bilbao | Espanya |
| Nadia | Benali Torres | [email protected] | València | Espanya |
Escriu una sola sentència que doni d'alta els que no existeixin i actualitzi la ciutat dels que sí, amb aquestes regles:
data_registreha de ser2026-03-02per als nous i no s'ha de modificar en els existents.- La ciutat només s'ha d'actualitzar si realment canvia.
- El resultat ha d'indicar, per fila, si va ser alta o modificació.
Després respon: quantes files retorna el RETURNING i per què?
Exercici 2
Resol el mateix cas de l'exercici 1 amb MERGE, carregant abans les dades en una taula temporal clients_campanya. Després compara les dues solucions responent aquestes preguntes:
- Quina diferència hi ha en la sortida de
psql? - Quina de les dues és segura si dos processos executen la campanya alhora?
- Quina escriuries si a més calgués desactivar els clients que no apareixen al fitxer? Es pot fer a PostgreSQL 16?
Exercici 3
Un company vol sincronitzar el catàleg i escriu això:
-- ⚠️ INCORRECTA
INSERT INTO productes (nom, categoria_id, proveidor_id, preu, cost, stock) VALUES
('Mel de tarongina crua 500 g', 1, 2, 10.20, 5.60, 40),
('Pasta d''espelta 500 g', 1, 2, 2.95, 1.40, 80),
('Mel de tarongina crua 500 g', 1, 2, 10.50, 5.75, 25)
ON CONFLICT (id) DO UPDATE
SET preu = preu,
stock = stock + EXCLUDED.stock;Té tres errors diferents. Troba'ls, explica el símptoma de cadascun i reescriu la sentència correctament, indicant què hauria d'estar declarat a l'esquema perquè funcionés i quin resultat donaria.
Solucions
Solució 1
BEGIN;
INSERT INTO clients (nom, cognoms, email, ciutat, pais, data_registre) VALUES
('Lucía', 'Martínez Soler', '[email protected]', 'Gandia', 'Espanya', DATE '2026-03-02'),
('Sofia', 'Moreira Costa', '[email protected]', 'Coïmbra', 'Portugal', DATE '2026-03-02'),
('Aitor', 'Zubizarreta Egaña', '[email protected]', 'Bilbao', 'Espanya', DATE '2026-03-02'),
('Nadia', 'Benali Torres', '[email protected]', 'València', 'Espanya', DATE '2026-03-02')
ON CONFLICT (email) DO UPDATE
SET ciutat = EXCLUDED.ciutat
WHERE clients.ciutat IS DISTINCT FROM EXCLUDED.ciutat
RETURNING id,
nom || ' ' || cognoms AS client,
email,
ciutat,
data_registre,
(xmax = 0) AS alta_nova;| id | client | ciutat | data_registre | alta_nova | |
|---|---|---|---|---|---|
| 1 | Lucía Martínez Soler | [email protected] | Gandia | 2025-01-10 | false |
| 7 | Sofia Moreira Costa | [email protected] | Coïmbra | 2025-03-21 | false |
| 18 | Aitor Zubizarreta Egaña | [email protected] | Bilbao | 2026-03-02 | true |
| 19 | Nadia Benali Torres | [email protected] | València | 2026-03-02 | true |
Quatre files, i no és casualitat que surtin les quatre:
| Client | Què passa | Per què |
|---|---|---|
| Lucía (1) | Actualització | Existia a València, passa a Gandia: la ciutat canvia |
| Sofia (7) | Actualització | Existia a Lisboa, passa a Coïmbra: la ciutat canvia |
| Aitor | Alta | Email nou |
| Nadia | Alta | Email nou |
Si el fitxer hagués portat la Lucía amb la seva ciutat actual (València), el WHERE ... IS DISTINCT FROM hauria descartat aquella actualització i el RETURNING hauria retornat tres files.
Les tres decisions de l'enunciat:
data_registrefora delSET. Apareix a l'INSERT(per als nous) però no alDO UPDATE, així que els existents conserven la seva: 2025-01-10 i 2025-03-21. És el mateix principi que ambdata_altaa 12.2.WHERE clients.ciutat IS DISTINCT FROM EXCLUDED.ciutat, no<>: si algun client tingués la ciutat aNULL(la columna ho admet),<>donariaUNKNOWNi no s'actualitzaria mai.(xmax = 0) AS alta_nova, amb la reserva de l'apartat 8: serveix per a l'informe de la campanya, no per a lògica crítica.
Solució 2
BEGIN;
CREATE TEMP TABLE clients_campanya (
email VARCHAR(120) PRIMARY KEY,
nom VARCHAR(60) NOT NULL,
cognoms VARCHAR(90) NOT NULL,
ciutat VARCHAR(80),
pais VARCHAR(60) NOT NULL
);
INSERT INTO clients_campanya (email, nom, cognoms, ciutat, pais) VALUES
('[email protected]', 'Lucía', 'Martínez Soler', 'Gandia', 'Espanya'),
('[email protected]', 'Sofia', 'Moreira Costa', 'Coïmbra', 'Portugal'),
('[email protected]', 'Aitor', 'Zubizarreta Egaña', 'Bilbao', 'Espanya'),
('[email protected]', 'Nadia', 'Benali Torres', 'València', 'Espanya');
MERGE INTO clients AS c
USING clients_campanya AS k
ON c.email = k.email
WHEN MATCHED AND c.ciutat IS DISTINCT FROM k.ciutat THEN
UPDATE SET ciutat = k.ciutat
WHEN NOT MATCHED THEN
INSERT (nom, cognoms, email, ciutat, pais, data_registre)
VALUES (k.nom, k.cognoms, k.email, k.ciutat, k.pais, DATE '2026-03-02');SELECT id, nom || ' ' || cognoms AS client, email, ciutat, data_registre
FROM clients
WHERE email IN (SELECT email FROM clients_campanya)
ORDER BY id;| id | client | ciutat | data_registre | |
|---|---|---|---|---|
| 1 | Lucía Martínez Soler | [email protected] | Gandia | 2025-01-10 |
| 7 | Sofia Moreira Costa | [email protected] | Coïmbra | 2025-03-21 |
| 16 | Aitor Zubizarreta Egaña | [email protected] | Bilbao | 2026-03-02 |
| 17 | Nadia Benali Torres | [email protected] | València | 2026-03-02 |
Resultat idèntic en el contingut, amb un detall revelador als id: aquí són 16 i 17, no 18 i 19 com amb ON CONFLICT. El motiu és que MERGE només avalua els DEFAULT de les files que realment insereix, mentre que l'upsert els avalua també a les que acaben en conflicte. MERGE malgasta menys valors de seqüència. (Amb tot, no comptis amb id consecutius en cap dels dos: les seqüències tenen buits per disseny.)
Les tres respostes:
1. La sortida. MERGE 4 davant d'INSERT 0 4, i sobretot: el MERGE de PostgreSQL 16 no admet RETURNING, així que cal consultar després per veure què ha passat — i aquella consulta ja no pot distingir altes de modificacions. És la pèrdua més notable en canviar d'instrucció.
2. Concurrència. La segura és ON CONFLICT. Si dos processos llancen la campanya alhora, el MERGE pot fallar amb duplicate key value violates unique constraint "clients_email_key", perquè el seu aparellament no es recolza en l'índex únic de la mateixa manera. ON CONFLICT està dissenyat precisament per a aquell escenari.
3. Desactivar els absents. Seria la feina d'una clàusula WHEN NOT MATCHED BY SOURCE THEN UPDATE SET ..., que PostgreSQL 16 no té (va arribar a la 17). A la 16 cal fer-ho en dues sentències dins de la mateixa transacció: el MERGE (o l'upsert) i després un UPDATE ... WHERE email NOT IN (SELECT email FROM clients_campanya). Compte amb aquell NOT IN i els nuls: 04-02 ho va advertir, i aquí clients_campanya.email és PRIMARY KEY, així que és segur. A més, clients no té columna de baixa, així que a BotigaVerda l'operació ni tan sols seria expressable sense un ALTER TABLE previ — lliçó següent.
Solució 3
Els tres errors:
| # | Error | Símptoma |
|---|---|---|
| 1 | ON CONFLICT (id) quan l'INSERT no indica cap id |
Cada fila rep un id nou de la seqüència, així que no hi ha mai conflicte: s'insereixen tres productes duplicats en lloc d'actualitzar els existents. No dóna cap error |
| 2 | SET preu = preu sense EXCLUDED. |
Assignació circular: assigna a preu el seu propi valor. No dóna error i no fa res. Havia de ser EXCLUDED.preu |
| 3 | La mel apareix dues vegades al mateix VALUES |
Amb la destinació de conflicte corregida, PostgreSQL falla amb ON CONFLICT DO UPDATE command cannot affect row a second time |
Hi ha un quart detall que no és un error de sintaxi però sí de criteri: SET stock = stock + EXCLUDED.stock funciona perquè stock sense qualificar es resol a la fila existent, però és ambigu de llegir. Escriu sempre productes.stock + EXCLUDED.stock.
Què caldria a l'esquema. Per aparellar per nom cal la restricció de l'apartat 5:
La versió corregida, amb l'origen deduplicat (ens quedem amb l'últim enviament de la mel, el de 10,50 €, i sumem les dues quantitats: 40 + 25 = 65):
-- ✅ CORRECTA
INSERT INTO productes (nom, categoria_id, proveidor_id, preu, cost, stock) VALUES
('Mel de tarongina crua 500 g', 1, 2, 10.50, 5.75, 65),
('Pasta d''espelta 500 g', 1, 2, 2.95, 1.40, 80)
ON CONFLICT (nom) DO UPDATE
SET preu = EXCLUDED.preu,
cost = EXCLUDED.cost,
stock = productes.stock + EXCLUDED.stock
WHERE productes.preu IS DISTINCT FROM EXCLUDED.preu
OR EXCLUDED.stock > 0
RETURNING id, nom, preu, cost, stock, (xmax = 0) AS alta_nova;| id | nom | preu | cost | stock | alta_nova |
|---|---|---|---|---|---|
| 3 | Mel de tarongina crua 500 g | 10.50 | 5.75 | 145 | false |
| 4 | Pasta d'espelta 500 g | 2.95 | 1.40 | 230 | false |
Els dos productes existien (ids 3 i 4, de BioSierra Ibérica): la mel passa de 9,75 € a 10,50 € i de 80 a 145 unitats (80 + 65); la pasta passa de 2,80 € a 2,95 € i de 150 a 230 unitats (150 + 80).
I la lliçó de fons de l'exercici: dels tres errors, dos no produeixen cap missatge. L'ON CONFLICT (id) hauria duplicat el catàleg en silenci i el SET preu = preu hauria deixat els preus sense tocar sense que ningú no se n'assabentés. Només el tercer —el duplicat a l'origen— provoca un error visible. Un upsert mal escrit falla callat molt més sovint que sorollosament, i per això cal comprovar el RETURNING fila a fila la primera vegada que es posa en producció.
Conclusió
L'upsert resol l'operació que faltava:
- El problema: "insereix si no existeix, actualitza si existeix", omnipresent en sincronització de catàlegs, recepció de mercaderia, altes de client i valoracions.
- La solució ingènua és incorrecta:
SELECTi després decidir obre una condició de cursa entre la comprovació i l'escriptura. Dues sessions veuen "no existeix" i totes dues insereixen. Una sentència és atòmica; dues no. INSERT ... ON CONFLICT, amb les seves dues accions:DO NOTHING(ideal per a dades mestres reexecutables,INSERT 0 0sense error) iDO UPDATE SET(l'upsert pròpiament dit).- La destinació del conflicte, per columna o per
ON CONSTRAINT, i per què exigeix una restricció única: sense l'índex, PostgreSQL no podria decidir atòmicament i tornaria la finestra de cursa. EXCLUDED, la pseudotaula amb la fila proposada, davant detaula.columnaamb la fila existent. D'aquí surten tots els patrons: quedar-se amb el nou, conservar el vell,GREATEST, i sobretot acumular (stock = productes.stock + EXCLUDED.stock). I el seu límit: el mateix valor de clau dues vegades en una sentència falla, cal deduplicar l'origen.WHEREalDO UPDATEambIS DISTINCT FROM(04-03) per no escriure quan no canvia res: menys escriptures, menys bloqueigs, auditoria honesta.RETURNINGamb upsert, i el truc dexmax = 0per distingir alta de modificació, amb les seves reserves.MERGE, l'estàndard SQL:2003 disponible des de PostgreSQL 15:WHEN MATCHED/WHEN NOT MATCHED, diverses condicions avaluades en ordre i la capacitat d'esborrar. Amb les seves tres limitacions a PostgreSQL 16: senseRETURNING, senseWHEN NOT MATCHED BY SOURCE, i no immune a la concurrència.- Quan fer servir cadascun:
ON CONFLICTper al cas clàssic amb concurrència real;MERGEper a regles múltiples, aparellaments sense clau única i portabilitat. - El suport per motor, i l'avís sobre
INSERT OR REPLACEde SQLite, que esborra i recrea la fila: perd columnes, canvia elrowidi dispara les cascades d'esborrat.
Amb això has tancat el DML: saps crear, llegir, inserir, modificar, esborrar i fusionar. Tot això sobre un esquema que fins ara ha estat immutable: el mateix que vas crear a 05-01 i que no has tornat a tocar. Però els esquemes canvien. Cal una columna nova, un tipus es queda curt, una restricció arriba tard, un nom resulta ser un error. A l'última lliçó del mòdul, Modificar l'esquema: ALTER TABLE i migracions segures, aprendràs totes les operacions d'ALTER TABLE i —el que de debò separa una migració innòcua d'una caiguda de producció— quines bloquegen la taula i quines no, el patró expand/contract per canviar un esquema sense aturar el servei, i per què cap canvi d'estructura no s'hauria d'escriure mai a mà en una consola de producció.
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
