A la lliçó anterior vas perdre tres clients, tres productes i deu comandes sense que ningú t'avisés. El LEFT JOIN és l'eina que els recupera. La seva regla és una sola frase: conserva totes les files de la taula esquerra, casin o no casin amb la dreta; quan no casen, les columnes de la dreta s'omplen amb NULL.
Sona senzill, i ho és. Però el LEFT JOIN amaga el parany més famós de tot l'SQL intermedi: posar una condició a l'ON o al WHERE deixa de ser indiferent. La diferència no dona error, no dona avís, i produeix dos resultats diferents que semblen igualment plausibles. Mitja lliçó està dedicada al fet que no hi caiguis mai.
Contingut
- La regla del
LEFT JOINi el diagrama de conjunts LEFT JOIN=LEFT OUTER JOIN- D'on surten exactament els
NULL - Els tres casos reals de BotigaVerda
- El patró anti-join: trobar el que no casa
- El parany: condició a l'
ONenfront de condició alWHERE LEFT JOINencadenats- Quan la taula dreta té diverses files per cada esquerra
- Errors habituals i consells
- Exercicis
- Conclusió
- La regla del
LEFT JOIN i el diagrama de conjunts
LEFT JOIN i el diagrama de conjuntsflowchart LR
subgraph R[" "]
direction LR
A(("només a l'esquerra<br/>✅ es conserva<br/>amb NULL a la dreta"))
I(("casen<br/>✅ resultat"))
B(("només a la dreta<br/>❌ fora"))
end
style A fill:#d9f0d9,stroke:#2b7a2b,stroke-width:3px
style I fill:#d9f0d9,stroke:#2b7a2b,stroke-width:3px
style B fill:#f8f8f8,stroke:#bbb,stroke-dasharray: 4 4
Com a flux de files, ampliant el diagrama de 03-02:
flowchart LR
A["taula esquerra"] --> C["cartesià"]
B["taula dreta"] --> C
C --> D["filtre ON"]
D --> E["✅ files que casen"]
D --> F["files esquerres<br/>sense parella"]
F --> G["✅ s'afegeixen igualment<br/>amb NULL a la dreta"]
D --> H["❌ files dretes<br/>sense parella: es descarten"]
Aquest pas extra —"s'afegeixen igualment amb NULL a la dreta"— és el pas 1d de l'ordre lògic que vas veure a 03-01. Passa dins del FROM, després d'aplicar l'ON i abans del WHERE. Tot el que explica aquesta lliçó es dedueix d'aquest fet.
La paraula "esquerra" és literal: és la taula que escrius abans de la paraula LEFT JOIN.
FROM clients AS c -- ← l'esquerra: es conserva sencera
LEFT JOIN comandes AS co -- ← la dreta: només aporta el que casi
ON co.client_id = c.idD'aquí en surt la conseqüència més important per escriure bé: el LEFT JOIN no és commutatiu. clients LEFT JOIN comandes conserva els 15 clients; comandes LEFT JOIN clients conserva les 20 comandes. Són preguntes diferents.
Com triar el costat esquerre: digues-te la pregunta de negoci en veu alta i busca el substantiu que ha d'aparèixer complet a l'informe. "Vull tots els clients, amb les seves comandes si en tenen" →
clientsés l'esquerra. "Vull totes les comandes, amb el seu comercial si en tenen" →comandesés l'esquerra.
LEFT JOIN = LEFT OUTER JOIN
LEFT JOIN = LEFT OUTER JOINLes dues escriptures són idèntiques:
FROM clients AS c LEFT JOIN comandes AS co ON co.client_id = c.id
FROM clients AS c LEFT OUTER JOIN comandes AS co ON co.client_id = c.idOUTER és opcional i pràcticament ningú no l'escriu. L'adjectiu extern (outer) descriu tota la família: LEFT, RIGHT i FULL són joins externs perquè conserven files que queden fora de l'emparellament; INNER és el join intern perquè només retorna el que queda dins.
| Escriptura | Equival a | Freqüència real |
|---|---|---|
LEFT JOIN |
LEFT OUTER JOIN |
L'habitual |
LEFT OUTER JOIN |
LEFT JOIN |
Poc freqüent, una mica més en documentació formal |
En aquest curs escrivim LEFT JOIN.
- D'on surten exactament els
NULL
NULLAquest punt es malinterpreta sovint, així que convé ser precisos: els NULL que apareixen en un LEFT JOIN no eren a la base de dades. Els fabrica el motor en el moment de construir el resultat.
Quan una fila de l'esquerra no troba parella, PostgreSQL la incorpora igualment al resultat i omple totes les columnes de la taula dreta amb NULL. No només la de la clau: totes.
flowchart LR
A["client 13<br/>Núria Bosch Ferrer"] --> B{"hi ha alguna comanda<br/>amb client_id = 13?"}
B -->|"no"| C["fila conservada<br/>co.id = NULL<br/>co.data_comanda = NULL<br/>co.estat = NULL<br/>co.despeses_enviament = NULL"]
D'aquí en surten dues conseqüències que faràs servir constantment:
- Pots detectar l'absència mirant qualsevol columna de la dreta. Si
co.id IS NULLen unLEFT JOINdes declients, és que no hi va haver parella. És la base de l'anti-join de la secció 5. - Convé mirar una columna
NOT NULLde la dreta. Si triessis una columna que pot ser nul·la de debò a les dades, no sabries distingir "no hi va haver parella" de "hi va haver parella i aquella dada era buida". Per això l'anti-join s'escriu sempre contra la clau primària de la taula dreta:co.id,lc.id,r.id. Una PK mai no ésNULLde forma legítima.
Un tercer efecte, més subtil, és l'aritmètica amb nuls que vas veure a 02-02: qualsevol operació amb NULL dona NULL. Si calcules co.despeses_enviament * 2 sobre una fila sense parella, el resultat és NULL, no zero. La funció COALESCE que ho resol s'estudia a 06-04; el tractament complet dels nuls, a 04-03.
- Els tres casos reals de BotigaVerda
Els tres buits deliberats del conjunt de dades existeixen precisament per a aquesta lliçó.
4.1. Tots els clients amb les seves comandes
SELECT c.id AS client_id,
c.nom || ' ' || c.cognoms AS client,
c.ciutat,
co.id AS comanda_id,
co.data_comanda,
co.estat
FROM clients AS c
LEFT JOIN comandes AS co ON co.client_id = c.id
ORDER BY c.id, co.id;| client_id | client | ciutat | comanda_id | data_comanda | estat |
|---|---|---|---|---|---|
| 1 | Lucía Martínez Soler | València | 1 | 2025-03-04 | lliurat |
| 1 | Lucía Martínez Soler | València | 5 | 2025-05-07 | lliurat |
| 1 | Lucía Martínez Soler | València | 15 | 2025-12-02 | lliurat |
| 2 | Carlos Ferrer Ibáñez | València | 2 | 2025-03-12 | lliurat |
| 2 | Carlos Ferrer Ibáñez | València | 11 | 2025-09-09 | lliurat |
| 3 | Marta Sanchis Gil | Castelló | 3 | 2025-04-02 | lliurat |
| 4 | Javier Ortega Ruiz | Madrid | 4 | 2025-04-19 | lliurat |
| 4 | Javier Ortega Ruiz | Madrid | 16 | 2025-12-19 | enviat |
| 5 | Ana Belmonte Roca | Barcelona | 6 | 2025-05-23 | cancellat |
| 5 | Ana Belmonte Roca | Barcelona | 18 | 2026-01-27 | pagat |
| 6 | Pau Llorens Vidal | València | 7 | 2025-06-11 | lliurat |
| 6 | Pau Llorens Vidal | València | 19 | 2026-02-09 | pagat |
| 7 | Sofia Moreira Costa | Lisboa | 8 | 2025-06-28 | lliurat |
| 7 | Sofia Moreira Costa | Lisboa | 17 | 2026-01-13 | enviat |
| 8 | Tiago Almeida Nunes | Porto | 9 | 2025-07-15 | lliurat |
| 9 | Camille Dubois | Lió | 10 | 2025-08-03 | lliurat |
| 9 | Camille Dubois | Lió | 20 | 2026-02-21 | pendent |
| 10 | Julien Moreau | París | 12 | 2025-10-01 | lliurat |
| 11 | Elena Navarro Puig | Alacant | 13 | 2025-10-22 | lliurat |
| 12 | Diego Ramos Herrera | Sevilla | 14 | 2025-11-14 | lliurat |
| 13 | Núria Bosch Ferrer | Barcelona | (null) | (null) | (null) |
| 14 | Hugo Iglesias Pardo | Saragossa | (null) | (null) | (null) |
| 15 | Inés Carrasco Vega | València | (null) | (null) | (null) |
23 files. Compara amb 03-02:
| Consulta | Files | Composició |
|---|---|---|
clients INNER JOIN comandes |
20 | Només comandes reals |
clients LEFT JOIN comandes |
23 | 20 comandes + 3 clients sense comandes |
Ja hi són la Núria, l'Hugo i la Inés, amb tota la part de comandes a NULL. L'aritmètica és exacta i val la pena interioritzar-la: el resultat d'un LEFT JOIN té tantes files com l'INNER JOIN més una fila per cada fila esquerra òrfena.
4.2. Tots els productes amb les seves línies de venda
Mateix patró, ara al catàleg. Retallem a cinc productes per veure el contrast amb claredat:
SELECT p.id AS producte_id,
p.nom AS producte,
lc.id AS linia_id,
lc.comanda_id,
lc.quantitat
FROM productes AS p
LEFT JOIN linies_comanda AS lc ON lc.producte_id = p.id
WHERE p.id IN (12, 13, 18, 19, 20)
ORDER BY p.id, lc.id;| producte_id | producte | linia_id | comanda_id | quantitat |
|---|---|---|---|---|
| 12 | Bosses reutilitzables de cotó (pack 5) | 13 | 5 | 1 |
| 12 | Bosses reutilitzables de cotó (pack 5) | 31 | 13 | 2 |
| 12 | Bosses reutilitzables de cotó (pack 5) | 40 | 16 | 1 |
| 13 | Espelmes de cera de soja (pack 2) | (null) | (null) | (null) |
| 18 | Raspall de dents de bambú | 23 | 9 | 4 |
| 18 | Raspall de dents de bambú | 32 | 13 | 3 |
| 18 | Raspall de dents de bambú | 47 | 20 | 2 |
| 19 | Desodorant natural en barra 50 g | (null) | (null) | (null) |
| 20 | Càpsules d'espirulina 120 u | (null) | (null) | (null) |
Els productes 12 i 18 s'han venut tres vegades cadascun i ocupen tres files. Els productes 13, 19 i 20 no s'han venut mai i ocupen una fila amb tot a NULL.
Sense el WHERE de retall, la consulta completa retorna 50 files: les 47 de linies_comanda més les 3 dels productes mai venuts.
4.3. Totes les comandes amb el seu comercial
El tercer cas és diferent dels dos anteriors, i convé fixar-se en el matís. Aquí no falta la fila filla: la clau forana és NULL.
SELECT co.id AS comanda_id,
co.data_comanda,
co.estat,
e.nom || ' ' || e.cognoms AS comercial
FROM comandes AS co
LEFT JOIN empleats AS e ON co.empleat_id = e.id
ORDER BY co.id;| comanda_id | data_comanda | estat | comercial |
|---|---|---|---|
| 1 | 2025-03-04 | lliurat | (null) |
| 2 | 2025-03-12 | lliurat | Óscar Peris Blasco |
| 3 | 2025-04-02 | lliurat | (null) |
| 4 | 2025-04-19 | lliurat | Laia Puig Sanchis |
| 5 | 2025-05-07 | lliurat | (null) |
| 6 | 2025-05-23 | cancellat | Óscar Peris Blasco |
| 7 | 2025-06-11 | lliurat | (null) |
| 8 | 2025-06-28 | lliurat | Laia Puig Sanchis |
| 9 | 2025-07-15 | lliurat | (null) |
| 10 | 2025-08-03 | lliurat | Óscar Peris Blasco |
| 11 | 2025-09-09 | lliurat | (null) |
| 12 | 2025-10-01 | lliurat | Laia Puig Sanchis |
| 13 | 2025-10-22 | lliurat | (null) |
| 14 | 2025-11-14 | lliurat | Marc Estévez Roig |
| 15 | 2025-12-02 | lliurat | (null) |
| 16 | 2025-12-19 | enviat | Óscar Peris Blasco |
| 17 | 2026-01-13 | enviat | (null) |
| 18 | 2026-01-27 | pagat | Laia Puig Sanchis |
| 19 | 2026-02-09 | pagat | (null) |
| 20 | 2026-02-21 | pendent | Marc Estévez Roig |
20 files: les 20 comandes. Enfront de les 10 que retornava l'INNER JOIN de 03-02. Els NULL de la columna comercial signifiquen aquí una cosa perfectament llegible per al negoci: comanda entrada per la web, sense comercial assignat. Deu de vint, exactament la proporció que 01-06 descrivia.
Fixa't en un detall: comercial val NULL per partida doble. Primer perquè co.empleat_id és NULL i no casa amb res; i segon perquè, encara que casés, la concatenació e.nom || ' ' || e.cognoms sobre columnes nul·les dona NULL (el parany de 02-02).
Resum dels tres casos
| Cas | Esquerra | Dreta | Files | Què recupera |
|---|---|---|---|---|
| Clients i comandes | clients (15) |
comandes |
23 | Clients 13, 14, 15 |
| Productes i vendes | productes (20) |
linies_comanda |
50 | Productes 13, 19, 20 |
| Comandes i comercial | comandes (20) |
empleats |
20 | Les 10 comandes web |
- El patró anti-join: trobar el que no casa
Fins ara hem fet servir el LEFT JOIN per conservar el que no casa. Ara el farem servir per quedar-nos només amb això, que és una de les consultes més demanades a qualsevol empresa: clients inactius, productes sense rotació, factures sense cobrar, usuaris sense verificar.
La tècnica es diu anti-join i s'escriu en dos moviments:
- Un
LEFT JOINque conserva tot el de l'esquerra. - Un
WHERE <clau primària de la dreta> IS NULLque es queda només amb les files que no van trobar parella.
flowchart LR
A["LEFT JOIN<br/>totes les esquerres"] --> B["files amb parella<br/>co.id té valor"]
A --> C["files sense parella<br/>co.id IS NULL"]
B --> D["❌ descartades pel WHERE"]
C --> E["✅ el resultat que busquem"]
5.1. Quins clients no han comprat mai
SELECT c.id,
c.nom,
c.cognoms,
c.email,
c.ciutat,
c.data_registre
FROM clients AS c
LEFT JOIN comandes AS co ON co.client_id = c.id
WHERE co.id IS NULL
ORDER BY c.id;| id | nom | cognoms | ciutat | data_registre | |
|---|---|---|---|---|---|
| 13 | Núria | Bosch Ferrer | [email protected] | Barcelona | 2025-06-20 |
| 14 | Hugo | Iglesias Pardo | [email protected] | Saragossa | 2025-09-12 |
| 15 | Inés | Carrasco Vega | [email protected] | València | 2026-01-08 |
Tres files. Aquesta és exactament la llista que demanaria el departament de màrqueting per llançar una campanya de primera compra.
5.2. Quins productes no s'han venut mai
SELECT p.id,
p.nom,
p.preu,
p.stock,
p.actiu
FROM productes AS p
LEFT JOIN linies_comanda AS lc ON lc.producte_id = p.id
WHERE lc.id IS NULL
ORDER BY p.id;| id | nom | preu | stock | actiu |
|---|---|---|---|---|
| 13 | Espelmes de cera de soja (pack 2) | 13.75 | 0 | true |
| 19 | Desodorant natural en barra 50 g | 7.80 | 75 | true |
| 20 | Càpsules d'espirulina 120 u | 16.40 | 55 | false |
Tres files, i amb un diagnòstic diferent per a cadascuna: les espelmes no es venen perquè no hi ha estoc; l'espirulina perquè està descatalogada (actiu = false); el desodorant té estoc i està actiu, així que el seu problema és comercial. Un JOIN ben plantejat no només retorna dades: apunta a la causa.
5.3. Les tres regles de l'anti-join
| Regla | Per què |
|---|---|
El JOIN ha de ser LEFT (o RIGHT), mai INNER |
Un INNER JOIN ja ha descartat les files sense parella: WHERE ... IS NULL retornaria sempre 0 files |
La condició IS NULL va al WHERE, mai a l'ON |
A l'ON formaria part de l'emparellament i no filtraria res d'útil |
La columna comprovada ha de ser NOT NULL a la taula dreta |
Si triessis una columna que admet nuls, confondries "sense parella" amb "parella amb la dada buida". Fes servir sempre la seva clau primària |
Sobre IS NULL: és l'operador correcte per preguntar per nuls, perquè = NULL mai no és cert (ho vas veure a 02-03 amb empleat_id = NULL retornant 0 files). El seu tractament complet, juntament amb IS NOT NULL i COALESCE, és la lliçó 04-03.
Nota: al mòdul 7 veuràs dues altres formes d'escriure el mateix:
NOT EXISTSamb una subconsulta correlacionada, iNOT IN. Totes tres tenen el mateix objectiu i diferent comportament davant dels nuls. L'anti-join ambLEFT JOINés el que pots escriure avui, i és perfectament idiomàtic.
- El parany: condició a l'
ON enfront de condició al WHERE
ON enfront de condició al WHEREAquí hi ha el contingut més important de la lliçó. Para atenció a la pregunta, perquè la clau és allà:
"Dona'm tots els clients, amb les seves comandes lliurades."
Fixa't en el "tots": l'informe ha de llistar els 15 clients, tinguin o no comandes lliurades. L'escriurem de les dues maneres possibles.
Versió A: la condició a l'ON
-- ✅ CORRECTA per a la pregunta plantejada
SELECT c.id AS client_id,
c.nom || ' ' || c.cognoms AS client,
co.id AS comanda_id,
co.data_comanda,
co.estat
FROM clients AS c
LEFT JOIN comandes AS co
ON co.client_id = c.id
AND co.estat = 'lliurat'
ORDER BY c.id, co.id;| client_id | client | comanda_id | data_comanda | estat |
|---|---|---|---|---|
| 1 | Lucía Martínez Soler | 1 | 2025-03-04 | lliurat |
| 1 | Lucía Martínez Soler | 5 | 2025-05-07 | lliurat |
| 1 | Lucía Martínez Soler | 15 | 2025-12-02 | lliurat |
| 2 | Carlos Ferrer Ibáñez | 2 | 2025-03-12 | lliurat |
| 2 | Carlos Ferrer Ibáñez | 11 | 2025-09-09 | lliurat |
| 3 | Marta Sanchis Gil | 3 | 2025-04-02 | lliurat |
| 4 | Javier Ortega Ruiz | 4 | 2025-04-19 | lliurat |
| 5 | Ana Belmonte Roca | (null) | (null) | (null) |
| 6 | Pau Llorens Vidal | 7 | 2025-06-11 | lliurat |
| 7 | Sofia Moreira Costa | 8 | 2025-06-28 | lliurat |
| 8 | Tiago Almeida Nunes | 9 | 2025-07-15 | lliurat |
| 9 | Camille Dubois | 10 | 2025-08-03 | lliurat |
| 10 | Julien Moreau | 12 | 2025-10-01 | lliurat |
| 11 | Elena Navarro Puig | 13 | 2025-10-22 | lliurat |
| 12 | Diego Ramos Herrera | 14 | 2025-11-14 | lliurat |
| 13 | Núria Bosch Ferrer | (null) | (null) | (null) |
| 14 | Hugo Iglesias Pardo | (null) | (null) | (null) |
| 15 | Inés Carrasco Vega | (null) | (null) | (null) |
18 files i els 15 clients presents. Els que no tenen cap comanda lliurada apareixen amb NULL: la Núria, l'Hugo i la Inés perquè no han comprat mai, i Ana Belmonte Roca perquè les seves dues comandes estan cancellat i pagat, cap de lliurada. L'Ana és el cas més interessant: existeix a comandes, però cap de les seves comandes no supera la condició de l'ON.
Versió B: la condició al WHERE
Canviem exactament dues paraules: AND passa a ser WHERE.
-- ⚠️ INCORRECTA per a la pregunta plantejada
SELECT c.id AS client_id,
c.nom || ' ' || c.cognoms AS client,
co.id AS comanda_id,
co.data_comanda,
co.estat
FROM clients AS c
LEFT JOIN comandes AS co ON co.client_id = c.id
WHERE co.estat = 'lliurat'
ORDER BY c.id, co.id;| client_id | client | comanda_id | data_comanda | estat |
|---|---|---|---|---|
| 1 | Lucía Martínez Soler | 1 | 2025-03-04 | lliurat |
| 1 | Lucía Martínez Soler | 5 | 2025-05-07 | lliurat |
| 1 | Lucía Martínez Soler | 15 | 2025-12-02 | lliurat |
| 2 | Carlos Ferrer Ibáñez | 2 | 2025-03-12 | lliurat |
| 2 | Carlos Ferrer Ibáñez | 11 | 2025-09-09 | lliurat |
| 3 | Marta Sanchis Gil | 3 | 2025-04-02 | lliurat |
| 4 | Javier Ortega Ruiz | 4 | 2025-04-19 | lliurat |
| 6 | Pau Llorens Vidal | 7 | 2025-06-11 | lliurat |
| 7 | Sofia Moreira Costa | 8 | 2025-06-28 | lliurat |
| 8 | Tiago Almeida Nunes | 9 | 2025-07-15 | lliurat |
| 9 | Camille Dubois | 10 | 2025-08-03 | lliurat |
| 10 | Julien Moreau | 12 | 2025-10-01 | lliurat |
| 11 | Elena Navarro Puig | 13 | 2025-10-22 | lliurat |
| 12 | Diego Ramos Herrera | 14 | 2025-11-14 | lliurat |
14 files i només 12 clients. Han desaparegut l'Ana, la Núria, l'Hugo i la Inés. El LEFT JOIN s'ha comportat com un INNER JOIN.
Per què passa
Torna a l'ordre lògic de 03-01 i segueix el recorregut de la fila de la Núria a cada versió:
flowchart TD
subgraph A["Versió A · condició a ON"]
A1["FROM: cartesià"] --> A2["ON: client_id = 13<br/>I estat = 'lliurat'<br/>→ cap parella"]
A2 --> A3["1d: s'afegeix la Núria<br/>amb comandes a NULL"]
A3 --> A4["WHERE: no n'hi ha<br/>→ ✅ la Núria sobreviu"]
end
subgraph B["Versió B · condició a WHERE"]
B1["FROM: cartesià"] --> B2["ON: client_id = 13<br/>→ cap parella"]
B2 --> B3["1d: s'afegeix la Núria<br/>amb comandes a NULL"]
B3 --> B4["WHERE: NULL = 'lliurat'<br/>no és TRUE<br/>→ ❌ la Núria s'elimina"]
end
El pas 1d sí que afegeix la Núria a les dues versions. La diferència és després: a la versió B, el WHERE avalua co.estat = 'lliurat' sobre una fila el co.estat de la qual és NULL. I NULL = 'lliurat' no és FALSE, és NULL, que tampoc no és TRUE, així que la fila es descarta. És el mateix mecanisme de 02-03 actuant en un lloc nou.
La regla que cal gravar: en un
LEFT JOIN, qualsevol condició alWHEREsobre una columna de la taula dreta converteix elLEFTen unINNER, perquè les files omplertes ambNULLno la poden satisfer.
Taula de decisió
| On posar la condició sobre la taula dreta | Efecte | Quan la vols |
|---|---|---|
A l'ON |
Restringeix què s'emparella. Les files esquerres sense parella es conserven amb NULL |
"Tots els X, amb els seus Y que compleixin Z" |
Al WHERE |
Filtra el resultat final. El LEFT JOIN es degrada a INNER JOIN |
"Només els X que tinguin algun Y que compleixi Z" |
Al WHERE, amb IS NULL |
Es queda només amb les files sense parella | Anti-join: "els X que no tenen cap Y" |
Les tres són vàlides: cadascuna respon a una pregunta diferent. L'error no és fer servir el WHERE, és fer-lo servir creient que fa el de l'ON.
Un cas que sí que és segur: una condició al WHERE sobre una columna de la taula esquerra no degrada res. WHERE c.pais = 'Espanya' filtra clients, i els que passin el filtre conserven el seu NULL a la dreta sense problema. El perill és exclusivament el de les columnes de la dreta.
LEFT JOIN encadenats
LEFT JOIN encadenatsAmb tres o més taules el LEFT JOIN manté la seva lògica, però apareix una regla que sorprèn: un INNER JOIN col·locat després d'un LEFT JOIN anul·la l'efecte del LEFT.
Volem "tots els clients, amb les seves comandes i el comercial de cada comanda". Escrit amb LEFT als dos salts:
-- ✅ CORRECTA: 23 files, els 15 clients
FROM clients AS c
LEFT JOIN comandes AS co ON co.client_id = c.id
LEFT JOIN empleats AS e ON co.empleat_id = e.idI ara el mateix camí amb un INNER JOIN al segon salt:
-- ⚠️ INCORRECTA: 10 files
FROM clients AS c
LEFT JOIN comandes AS co ON co.client_id = c.id
INNER JOIN empleats AS e ON co.empleat_id = e.id| Consulta | Files | Què queda |
|---|---|---|
LEFT + LEFT |
23 | Els 15 clients, les 20 comandes, amb NULL on falti |
LEFT + INNER |
10 | Només les 10 comandes que tenen comercial |
El raonament: els JOIN es resolen d'esquerra a dreta, així que (clients LEFT JOIN comandes) produeix 23 files; sobre aquestes 23, l'INNER JOIN amb empleats exigeix que co.empleat_id casi amb un empleat. Les 3 files de clients sense comandes tenen co.empleat_id = NULL i cauen; les 10 files de comandes web tenen empleat_id = NULL i també cauen. En queden 10.
Regla pràctica: un cop has obert una cadena amb
LEFT JOIN, tots els salts posteriors sobre aquella branca han de serLEFT JOIN. Un solINNERal mig desfà tota la feina, i ho fa sense cap avís.
És un error molt fàcil de cometre en ampliar una consulta existent: algú afegeix JOIN productes ON ... al final d'una consulta que començava amb LEFT JOIN, i l'informe perd files d'un dia per l'altre sense que ningú toqui el LEFT.
- Quan la taula dreta té diverses files per cada esquerra
El LEFT JOIN no protegeix de la multiplicació de files de 03-02. Continua havent-hi una fila per parella:
SELECT p.id AS producte_id,
p.nom AS producte,
r.id AS ressenya_id,
r.puntuacio
FROM productes AS p
LEFT JOIN ressenyes AS r ON r.producte_id = p.id
WHERE p.id IN (1, 2, 4, 6)
ORDER BY p.id, r.id;| producte_id | producte | ressenya_id | puntuacio |
|---|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | 1 | 5 |
| 1 | Oli d'oliva verge extra 500 ml | 7 | 5 |
| 2 | Arròs integral ecològic 1 kg | 2 | 4 |
| 2 | Arròs integral ecològic 1 kg | 10 | 5 |
| 4 | Pasta d'espelta 500 g | (null) | (null) |
| 6 | Crema facial d'àloe vera 50 ml | 3 | 5 |
| 6 | Crema facial d'àloe vera 50 ml | 9 | 4 |
Quatre productes produeixen set files: dues ressenyes per als productes 1, 2 i 6, i una fila buida per al 4. Sobre el catàleg sencer, productes LEFT JOIN ressenyes retorna 23 files: les 12 ressenyes més els 11 productes sense cap.
El compte general d'un LEFT JOIN és sempre aquest:
| Exemple | Casen | Òrfenes | Total |
|---|---|---|---|
clients LEFT JOIN comandes |
20 | 3 | 23 |
productes LEFT JOIN linies_comanda |
47 | 3 | 50 |
productes LEFT JOIN ressenyes |
12 | 11 | 23 |
comandes LEFT JOIN empleats |
10 | 10 | 20 |
I de nou l'avís del mòdul 4: si sobre el primer cas sumessis c.data_registre, o comptessis clients, obtindries xifres inflades, perquè la Lucía hi apareix tres vegades. Un LEFT JOIN conserva files; no les deduplica.
Errors habituals i consells
- Posar una condició sobre la taula dreta al
WHERE. Degrada elLEFT JOINaINNER JOINsense dir res. És *l'*error d'aquesta lliçó: 18 files contra 14, i quatre clients desapareguts. - Barrejar un
INNER JOINdesprés d'unLEFT JOIN. Mateix efecte, en cadena: 23 files es converteixen en 10. - Escriure les taules a l'inrevés.
comandes LEFT JOIN clientsno ésclients LEFT JOIN comandes. ElLEFT JOINno és commutatiu; l'esquerra és la que es conserva sencera. - Fer l'anti-join contra una columna que admet nuls.
WHERE co.empleat_id IS NULLno distingeix "no hi va haver parella" de "hi va haver parella amb la dada buida". Compara sempre contra la clau primària de la dreta. - Intentar un anti-join amb
INNER JOIN.INNER JOIN ... WHERE dreta.id IS NULLretorna sempre zero files: l'INNERja ha llençat aquelles files. - Fer servir
= NULLen lloc d'IS NULL.WHERE co.id = NULLretorna zero files sempre (02-03). - Suposar que
NULLal resultat significa zero.co.despeses_enviamentésNULL, no0.00, per a un client sense comandes. Operar-hi propaga el nul (COALESCE, a 06-04). - Oblidar que el
LEFT JOINcontinua multiplicant files. Conserva els orfes, però no impedeix que un client amb tres comandes ocupi tres files. - Consell: escriu primer l'
INNER JOIN, comprova el recompte, i després canvia'l aLEFT. La diferència entre tots dos recomptes et diu exactament quants orfes hi ha. - Consell: per a l'anti-join, digues la pregunta en veu alta. "Clients sense comandes", "productes mai venuts", "factures sense cobrar". El "sense" i el "mai" són el senyal que toca
LEFT JOIN ... WHERE ... IS NULL. - Consell: sospita de qualsevol
WHEREque anomeni la taula dreta d'unLEFT JOIN. Llevat que sigui unIS NULLdeliberat, gairebé sempre hauria de ser a l'ON.
Exercicis
Exercici 1
L'equip de producte vol saber quines referències del catàleg no han rebut cap ressenya, per llançar una campanya de sol·licitud d'opinions. Escriu la consulta que retorni l'id, el nom, la categoria i el preu d'aquests productes, ordenats per id.
Després respon: per què el JOIN amb categories pot ser un INNER JOIN sense que això trenqui l'anti-join?
Exercici 2
Logística necessita el llistat complet de les 20 comandes amb la informació de la seva devolució, si n'hi va haver: id de la comanda, data, estat, despeses d'enviament i —quan hi hagi devolució— el seu motiu i import.
- Escriu la consulta.
- Escriu després la variant que retorna només les comandes que no van tenir devolució, i indica quantes files dona.
Exercici 3
Un company et passa aquesta consulta i et diu: "vull tots els clients amb les seves comandes pagades amb targeta, però em falten clients".
SELECT c.id, c.nom, co.id AS comanda_id, co.metode_pagament
FROM clients AS c
LEFT JOIN comandes AS co ON co.client_id = c.id
WHERE co.metode_pagament = 'targeta';- Explica què està fent realment aquesta consulta.
- Corregeix-la perquè retorni el que ell vol.
- Prediu quantes files retorna cada versió i quants clients diferents apareixen a cadascuna.
Solucions
Solució 1
SELECT p.id,
p.nom AS producte,
cat.nom AS categoria,
p.preu
FROM productes AS p
INNER JOIN categories AS cat ON p.categoria_id = cat.id
LEFT JOIN ressenyes AS r ON r.producte_id = p.id
WHERE r.id IS NULL
ORDER BY p.id;| id | producte | categoria | preu |
|---|---|---|---|
| 3 | Mel de tarongina crua 500 g | Alimentació | 9.75 |
| 4 | Pasta d'espelta 500 g | Alimentació | 2.80 |
| 7 | Xampú sòlid de romaní 80 g | Cosmètica natural | 8.40 |
| 8 | Oli corporal d'ametlles 200 ml | Cosmètica natural | 14.25 |
| 9 | Bàlsam labial de calèndula 15 ml | Cosmètica natural | 4.60 |
| 11 | Fregall vegetal de lufa (pack 3) | Llar sostenible | 5.50 |
| 13 | Espelmes de cera de soja (pack 2) | Llar sostenible | 13.75 |
| 14 | Infusió de camamilla ecològica 20 u | Begudes | 3.25 |
| 17 | Suc de taronja premsat en fred 1 L | Begudes | 5.40 |
| 19 | Desodorant natural en barra 50 g | Higiene personal | 7.80 |
| 20 | Càpsules d'espirulina 120 u | Complements | 16.40 |
11 files, exactament els 11 productes sense ressenya que anunciava 01-06. Només 9 dels 20 productes han estat ressenyats alguna vegada.
Per què el JOIN amb categories pot ser INNER: perquè la secció 7 adverteix dels INNER JOIN que apareixen després d'un LEFT JOIN sobre la mateixa branca. Aquí no és el cas: categories penja de productes per una FK que apunta a una categoria que sempre existeix, així que aquell INNER JOIN no descarta cap producte. Les 20 files de partida continuen sent 20 abans d'aplicar el LEFT JOIN amb ressenyes. La regla precisa és: un INNER JOIN és segur mentre no pugui eliminar files de la taula protagonista.
(Si categoria_id admetés NULL amb dades reals nul·les, aquell INNER JOIN sí que perdria productes i caldria escriure'l també com a LEFT JOIN.)
Solució 2
1. Totes les comandes amb la seva devolució, si n'hi va haver:
SELECT co.id AS comanda_id,
co.data_comanda,
co.estat,
co.despeses_enviament,
d.motiu,
d.import AS import_reemborsat
FROM comandes AS co
LEFT JOIN devolucions AS d ON d.comanda_id = co.id
ORDER BY co.id;Retorna 20 files: les 3 amb devolució (comandes 6, 10 i 13) mostren motiu i import; les altres 17 mostren *(null)* en aquestes dues columnes.
2. Només les comandes sense devolució (anti-join):
SELECT co.id AS comanda_id,
co.data_comanda,
co.estat,
co.despeses_enviament
FROM comandes AS co
LEFT JOIN devolucions AS d ON d.comanda_id = co.id
WHERE d.id IS NULL
ORDER BY co.id;| comanda_id | data_comanda | estat | despeses_enviament |
|---|---|---|---|
| 1 | 2025-03-04 | lliurat | 4.95 |
| 2 | 2025-03-12 | lliurat | 0.00 |
| 3 | 2025-04-02 | lliurat | 4.95 |
| 4 | 2025-04-19 | lliurat | 4.95 |
| 5 | 2025-05-07 | lliurat | 0.00 |
| 7 | 2025-06-11 | lliurat | 6.50 |
| 8 | 2025-06-28 | lliurat | 9.90 |
| 9 | 2025-07-15 | lliurat | 9.90 |
| 11 | 2025-09-09 | lliurat | 0.00 |
| 12 | 2025-10-01 | lliurat | 12.50 |
| 14 | 2025-11-14 | lliurat | 4.95 |
| 15 | 2025-12-02 | lliurat | 0.00 |
| 16 | 2025-12-19 | enviat | 4.95 |
| 17 | 2026-01-13 | enviat | 9.90 |
| 18 | 2026-01-27 | pagat | 4.95 |
| 19 | 2026-02-09 | pagat | 4.95 |
| 20 | 2026-02-21 | pendent | 12.50 |
17 files = 20 comandes − 3 devolucions. Falten justament les comandes 6, 10 i 13.
Solució 3
1. Què fa realment. La condició co.metode_pagament = 'targeta' és al WHERE i s'aplica a una columna de la taula dreta. Tota fila de client sense comanda arriba al WHERE amb co.metode_pagament a NULL, i NULL = 'targeta' no és TRUE. Resultat: el LEFT JOIN es degrada a INNER JOIN i la consulta retorna només els clients que tenen almenys una comanda pagada amb targeta. És una pregunta legítima, però no la que ell volia.
2. Correcció: moure la condició a l'ON.
-- ✅ CORRECTA
SELECT c.id,
c.nom,
co.id AS comanda_id,
co.metode_pagament
FROM clients AS c
LEFT JOIN comandes AS co
ON co.client_id = c.id
AND co.metode_pagament = 'targeta'
ORDER BY c.id, co.id;3. Predicció de files. Les comandes amb metode_pagament = 'targeta' són els ids 1, 3, 5, 6, 8, 10, 11, 13, 15, 16 i 19: 11 comandes, de 9 clients diferents (la Lucía hi apareix tres vegades).
| Versió | Files | Clients diferents |
|---|---|---|
Original (WHERE) |
11 | 9 — només els que van pagar alguna vegada amb targeta |
Corregida (ON) |
17 | 15 — els 11 emparellaments + els 6 clients sense cap compra amb targeta, amb NULL |
Els 6 clients que apareixen amb NULL a la versió corregida són: en Tiago (8), en Julien (10), en Diego (12), la Núria (13), l'Hugo (14) i la Inés (15). Els tres últims perquè no han comprat mai; els tres primers perquè van comprar, però pagant amb PayPal o transferència.
Conclusió
El LEFT JOIN és, a la pràctica, el JOIN que més problemes de negoci resol:
- Conserva totes les files de la taula esquerra, casin o no, omplint amb
NULLles columnes de la dreta.LEFT JOINiLEFT OUTER JOINsón el mateix. - Els
NULLels fabrica el motor en construir el resultat; no eren a les dades. Per això pots detectar l'absència mirant la clau primària de la taula dreta. - Has recuperat els tres buits de BotigaVerda: els clients 13, 14 i 15 (23 files en comptes de 20), els productes 13, 19 i 20 (50 files en comptes de 47) i les 10 comandes web (20 files en comptes de 10).
- El patró anti-join
LEFT JOIN ... WHERE dreta.id IS NULLrespon a les preguntes amb "sense" i "mai": tres clients que no han comprat, tres productes que ningú no ha venut, onze productes sense ressenyes. - Saps que una condició sobre la taula dreta al
WHEREdegrada elLEFT JOINaINNER JOIN: 18 files i 15 clients contra 14 files i 12 clients, amb la mateixa pregunta escrita de dues formes. Si vols "tots els X amb els seus Y que compleixin Z", la condició va a l'ON. - Un
INNER JOINencadenat després d'unLEFT JOINanul·la el seu efecte: 23 files es queden en 10. Oberta una branca ambLEFT, continua ambLEFT. - El
LEFT JOINno deduplica: continua multiplicant files quan la dreta en té diverses per cada esquerra.
A la lliçó següent, RIGHT JOIN, veurem el simètric exacte del que acabes d'aprendre. Comprovaràs que A RIGHT JOIN B i B LEFT JOIN A retornen exactament el mateix, entendràs per què la majoria de guies d'estil prefereixen el LEFT tot i això, i veuràs per fi Irene Salvador Mira i Daniel Vercher Lluch, els dos empleats que mai no han gestionat una comanda.
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
