La lliçó anterior va acabar amb una llista de preguntes que encara no sabies respondre, i totes tenien la mateixa forma: comparar cada fila amb el resultat d'una altra consulta. Quins productes superen el preu mitjà del catàleg? Quins clients tenen un tiquet mitjà per sobre de la mitjana general —la pregunta que 04-06 va deixar explícitament reservada per a aquesta lliçó? Quines comandes inclouen el producte més car?
La resposta a totes elles és la mateixa eina: una subconsulta, una consulta escrita entre parèntesis dins d'una altra. Aquí n'aprendràs el vocabulari, els tres tipus de subconsulta segons el que retornen, com es fan servir amb IN, ANY i ALL, i els tres errors clàssics que produeixen des d'un missatge vermell fins a —molt pitjor— un resultat buit sense cap avís.
Contingut
- Què és una subconsulta: vocabulari, correlació i tipus
- Subconsulta escalar al
WHERE - La pregunta pendent de 04-06: el tiquet mitjà
- Subconsultes de llista:
IN,ANY/SOMEiALL - Els tres errors clàssics
- On pot aparèixer una subconsulta
- Errors habituals i consells
- Exercicis
- Conclusió
- Què és una subconsulta i com es diu cada part
Una subconsulta (o consulta imbricada) és una sentència SELECT completa, escrita entre parèntesis, que apareix dins d'una altra sentència SQL. El motor l'executa i fa servir el seu resultat com si fos un valor, una llista o una taula.
SELECT id, nom, preu
FROM productes
WHERE preu > (SELECT AVG(preu) FROM productes);
-- └──────── subconsulta ────────┘La consulta que conté l'altra és la consulta externa (outer query); la de dins, la subconsulta (subquery). Una subconsulta pot contenir-ne una altra, sense més límit que la llegibilitat. Tres regles de sintaxi sense excepció: els parèntesis són obligatoris; la subconsulta s'escriu sencera (SELECT, FROM, WHERE, GROUP BY… el que calgui); i un ORDER BY dins d'una subconsulta gairebé mai no serveix de res, llevat que vagi acompanyat d'un LIMIT.
No correlacionada enfront de correlacionada
Aquesta és la distinció que estructura el mòdul sencer, i convé fixar-la abans que cap altra.
| No correlacionada | Correlacionada | |
|---|---|---|
| Referència a la consulta externa | No | Sí, fa servir un àlies de fora |
| Es pot executar sola? | Sí, copiant i enganxant | No: dona error |
| Quantes vegades s'avalua | Una per a tota la consulta | Una per cada fila candidata |
| Cost conceptual | Constant | Proporcional al nombre de files |
| Lliçó | Aquesta (07-01) | La següent (07-02) |
-- NO CORRELACIONADA: la subconsulta no esmenta res de fora
SELECT id, nom FROM productes AS p
WHERE p.preu > (SELECT AVG(preu) FROM productes);
-- CORRELACIONADA: la subconsulta fa servir p.categoria_id, que ve de fora
SELECT id, nom FROM productes AS p
WHERE p.preu > (SELECT AVG(preu) FROM productes WHERE categoria_id = p.categoria_id);La prova pràctica és infal·lible: selecciona la subconsulta, executa-la sola i mira què passa. La primera retorna 9.0350000000000000. La segona dona ERROR: missing FROM-clause entry for table "p", perquè p no existeix fora de la consulta externa. Tota aquesta lliçó tracta de les no correlacionades.
Els tres tipus segons el que retornen
La segona classificació, transversal a l'anterior, es refereix a la forma del resultat, i determina on pot aparèixer la subconsulta i amb quins operadors es combina.
| Tipus | Retorna | Exemple | Es fa servir amb |
|---|---|---|---|
| Escalar | Una fila i una columna: un valor | (SELECT AVG(preu) FROM productes) |
=, >, <, >=, <=, <>; o com a columna del SELECT |
| De fila | Una fila amb diverses columnes | (SELECT MAX(preu), MIN(preu) FROM productes) |
Comparació de tuples: (a, b) = (SELECT ...) |
| De taula (multifila) | Diverses files | (SELECT client_id FROM comandes) |
IN, NOT IN, ANY, ALL, EXISTS, o al FROM |
Les escalars i les de taula cobreixen el 99 % del SQL que escriuràs. Les de fila són elegants però rares: WHERE (preu, stock) = (SELECT MAX(preu), 40 FROM productes) retorna una única fila, el matcha (22,00 € i 40 unitats).
Nota de dialecte: els constructors de fila
(a, b) = (...)funcionen a PostgreSQL, MySQL i MariaDB; SQLite i SQL Server no els admeten i exigeixen dues condicions unides ambAND.
- Subconsulta escalar al
WHERE
WHEREEl cas d'ús més comú de tots: comparar cada fila amb un valor calculat sobre el conjunt. La pregunta és "quins productes estan per sobre del preu mitjà del catàleg?", i l'intent ingenu és aquest:
ERROR: aggregate functions are not allowed in WHERE
LINE 1: ... nom, preu FROM productes WHERE preu > AVG(preu);
^És el mateix error de 04-06 i per la mateixa raó: quan s'executa el WHERE (pas 2 de l'ordre lògic) el motor mira una fila cada vegada i no ha calculat cap agregat. La subconsulta ho resol perquè és una altra consulta, amb el seu propi recorregut complet per la taula:
-- ✅ CORRECTA
SELECT p.id, p.nom AS producte, cat.nom AS categoria, p.preu
FROM productes AS p
JOIN categories AS cat ON p.categoria_id = cat.id
WHERE p.preu > (SELECT AVG(preu) FROM productes)
ORDER BY p.preu DESC;| id | producte | categoria | preu |
|---|---|---|---|
| 15 | Te verd matcha cerimonial 30 g | Begudes | 22.00 |
| 6 | Crema facial d'àloe vera 50 ml | Cosmètica natural | 18.90 |
| 20 | Càpsules d'espirulina 120 u | Complements | 16.40 |
| 8 | Oli corporal d'ametlles 200 ml | Cosmètica natural | 14.25 |
| 13 | Espelmes de cera de soja (pack 2) | Llar sostenible | 13.75 |
| 1 | Oli d'oliva verge extra 500 ml | Alimentació | 12.50 |
| 10 | Detergent ecològic concentrat 1 L | Llar sostenible | 11.20 |
| 12 | Bosses reutilitzables de cotó (pack 5) | Llar sostenible | 9.90 |
| 3 | Mel de tarongina crua 500 g | Alimentació | 9.75 |
9 files: 9 productes de 20 superen els 9,035 € de preu mitjà (SELECT ROUND(AVG(preu), 4) FROM productes → 9.0350, la xifra de 04-04). L'important és com s'executa: PostgreSQL avalua la subconsulta una sola vegada, obté 9.0350000000000000 i substitueix l'expressió per aquell número; a partir d'aquí la consulta externa és un WHERE preu > 9.0350000000000000 corrent. No hi ha cap bucle: és una constant calculada al vol. I compte amb un detall que importa: la comparació es fa amb el valor sense arrodonir — amb altres dades, un cèntim decideix si una fila hi entra o en surt. No arrodoneixis mai el valor de comparació; arrodoneix només allò que mostres.
- La pregunta pendent de 04-06: el tiquet mitjà
A 04-06 vas calcular el tiquet mitjà de cada client i vas comprovar que la mitjana global dels 20 tiquets és de 36,40 €, però no vas poder unir les dues coses: HAVING sap comparar l'agregat d'un grup amb una constant o amb un altre agregat del mateix grup, mai amb un agregat calculat sobre un altre conjunt de files. Una subconsulta escalar al HAVING és la peça que faltava.
Primer, el valor de referència. La mitjana global no és la mitjana de les 47 línies (això donaria els 15,49 € d'import mitjà de línia), sinó la mitjana dels 20 tiquets:
SELECT COUNT(DISTINCT comanda_id) AS comandes,
ROUND(SUM(quantitat * preu_unitari * (1 - descompte)), 2) AS facturacio,
ROUND(SUM(quantitat * preu_unitari * (1 - descompte))
/ COUNT(DISTINCT comanda_id), 2) AS tiquet_mitja_global
FROM linies_comanda;| comandes | facturacio | tiquet_mitja_global |
|---|---|---|
| 20 | 727.95 | 36.40 |
I ara la mateixa expressió, injectada al HAVING:
-- ✅ La consulta que 04-06 va deixar pendent
SELECT c.id,
c.nom || ' ' || c.cognoms AS client,
c.pais,
COUNT(DISTINCT co.id) AS comandes,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS total,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
/ COUNT(DISTINCT co.id), 2) AS tiquet_mitja
FROM linies_comanda AS lc
JOIN comandes AS co ON lc.comanda_id = co.id
JOIN clients AS c ON co.client_id = c.id
GROUP BY c.id, c.nom, c.cognoms, c.pais
HAVING SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
/ COUNT(DISTINCT co.id)
> (SELECT SUM(quantitat * preu_unitari * (1 - descompte))
/ COUNT(DISTINCT comanda_id)
FROM linies_comanda)
ORDER BY tiquet_mitja DESC;| id | client | pais | comandes | total | tiquet_mitja |
|---|---|---|---|---|---|
| 10 | Julien Moreau | França | 1 | 66.90 | 66.90 |
| 7 | Sofia Moreira Costa | Portugal | 2 | 111.88 | 55.94 |
| 8 | Tiago Almeida Nunes | Portugal | 1 | 44.60 | 44.60 |
Tres clients de dotze, exactament els que 04-06 va anticipar. Pas a pas:
FROM+ dosJOIN: es parteix de les 47 línies de detall i es puja fins al client; és la consulta canònica de detall del curs. ElGROUP BYforma 12 grups, un per client comprador (els clients 13, 14 i 15 ja els va descartar l'INNER JOIN).- La subconsulta escalar s'avalua una vegada i retorna
36.39725, la mitjana exacta sense arrodonir. HAVINGcompara el tiquet mitjà de cada grup amb aquella constant i en descarta nou grups.SELECTprojecta i arrodoneix. L'arrodoniment és només de presentació: la comparació ja s'ha fet amb tota la precisió.
I una lectura de negoci: els tres són clients estrangers, cap espanyol no supera la mitjana (el més proper és la Lucía, amb 35,87 €). Els ports a Portugal i França costen 9,90 € i 12,50 € enfront dels 4,95 € nacionals, i el client ho compensa fent comandes més grans. Això sí, dos dels tres tenen una sola comanda: el cas realment sòlid és la Sofia, amb dos tiquets de 64,88 € i 47,00 €.
Per què va al
HAVINGi no alWHERE: la condició compara un agregat del grup amb la constant, i elWHEREno pot fer servir agregats. La subconsulta només aporta el número amb què comparar.
- Subconsultes de llista:
IN, ANY/SOME i ALL
IN, ANY/SOME i ALLQuan la subconsulta retorna diverses files d'una sola columna, la forma natural de fer-la servir és IN. Ja coneixes l'operador de 04-02 amb una llista escrita a mà; ara la llista la calcula una altra consulta.
-- ✅ Clients que han fet alguna comanda
SELECT c.id, c.nom, c.cognoms, c.ciutat, c.pais
FROM clients AS c
WHERE c.id IN (SELECT client_id FROM comandes)
ORDER BY c.id LIMIT 3;| id | nom | cognoms | ciutat | pais |
|---|---|---|---|---|
| 1 | Lucía | Martínez Soler | València | Espanya |
| 2 | Carlos | Ferrer Ibáñez | València | Espanya |
| 3 | Marta | Sanchis Gil | Castelló | Espanya |
(3 primeres de 12 files: els clients 1 a 12, els 12 compradors.) Aquí hi ha un detall cabdal que reprendrem a 07-05: la subconsulta retorna 20 valors (un per comanda, amb repeticions), però el resultat té 12 files. IN no multiplica: pregunta si el valor és a la llista i respon sí o no una sola vegada per fila externa. Un INNER JOIN amb comandes hauria retornat 20 files.
Un segon exemple, amb una subconsulta agregada: productes de les categories que tenen més de tres productes.
SELECT COUNT(*) AS productes
FROM productes AS p
WHERE p.categoria_id IN (SELECT categoria_id FROM productes
GROUP BY categoria_id HAVING COUNT(*) > 3);| productes |
|---|
| 17 |
17 productes de 20. Les categories amb més de tres referències són Alimentació (5), Cosmètica natural (4), Llar sostenible (4) i Begudes (4); en queden fora Higiene personal (productes 18 i 19) i Complements (el 20). La subconsulta és una consulta agregada completa, amb el seu GROUP BY i el seu HAVING: pots calcular un conjunt de claus amb tota la maquinària del mòdul 4 i fer-lo servir com a filtre.
L'equivalència amb = ANY. L'estàndard defineix IN com a sucre sintàctic de = ANY: c.id IN (sub) i c.id = ANY (sub) produeixen el mateix pla i el mateix resultat. IN es llegeix millor i és el que veuràs al codi real; = ANY importa perquè explica la família sencera:
| Escriptura | Cert quan… | Equival a |
|---|---|---|
x = ANY (sub) |
x coincideix amb almenys un |
x IN (sub) |
x <> ALL (sub) |
x és diferent de tots |
x NOT IN (sub) |
x > ANY (sub) |
x en supera almenys un: supera el mínim |
x > (SELECT MIN(...) ...) |
x > ALL (sub) |
x els supera tots: supera el màxim |
x > (SELECT MAX(...) ...) |
x < ANY (sub) |
x és menor que el màxim |
x < (SELECT MAX(...) ...) |
x < ALL (sub) |
x és menor que el mínim |
x < (SELECT MIN(...) ...) |
SOME és un sinònim exacte d'ANY que no fa servir ningú. La forma que de debò apareix és > ALL, i la seva pregunta natural és "més car que qualsevol dels d'aquella categoria":
SELECT p.id, p.nom AS producte, p.preu
FROM productes AS p
WHERE p.preu > ALL (SELECT preu FROM productes WHERE categoria_id = 1)
ORDER BY p.preu DESC;| id | producte | preu |
|---|---|---|
| 15 | Te verd matcha cerimonial 30 g | 22.00 |
| 6 | Crema facial d'àloe vera 50 ml | 18.90 |
| 20 | Càpsules d'espirulina 120 u | 16.40 |
| 8 | Oli corporal d'ametlles 200 ml | 14.25 |
| 13 | Espelmes de cera de soja (pack 2) | 13.75 |
5 productes costen més que tots els d'Alimentació, el sostre de la qual és l'oli d'oliva (12,50 €). L'escriptura > (SELECT MAX(preu) FROM productes WHERE categoria_id = 1) retorna el mateix i es llegeix millor. Amb ANY en lloc d'ALL la condició seria "més car que el més barat d'Alimentació" (1,95 €) i en sortirien 19 productes.
Compte amb els conjunts buits. Si la subconsulta no retorna cap fila,
> ALLés cert per a totes (no hi ha contraexemple) i> ANYés fals per a totes (no hi ha cap cas favorable). Impecable en lògica i desconcertant a la pràctica:preu > ALL (SELECT preu FROM productes WHERE categoria_id = 99)retorna els 20 productes.
- Els tres errors clàssics
5.1. L'escalar que retorna més d'una fila
-- ⚠️ INCORRECTA
SELECT id, nom, preu
FROM productes
WHERE preu > (SELECT preu FROM productes WHERE categoria_id = 1);Alimentació té cinc productes, la subconsulta retorna cinc preus i > no sap amb quin comparar. Els arranjaments són tres i cadascun respon a una pregunta diferent: > (SELECT MAX(preu) ...) i > ALL (...) diuen "més car que el més car"; > ANY (...) diu "més car que algun". És el menys perillós dels tres perquè és sorollós, i és un error de dades, no de sintaxi: la mateixa consulta funcionaria si la categoria tingués un sol producte i rebentaria en donar d'alta el segon. Si la subconsulta retorna diverses columnes, el missatge és ERROR: subquery must return only one column.
5.2. L'escalar que retorna zero files — el perillós
-- ⚠️ Retorna 0 files, i no hi ha cap error
SELECT id, nom, preu
FROM productes
WHERE preu > (SELECT AVG(preu) FROM productes WHERE categoria_id = 99);Cap missatge, cap advertiment. La categoria 99 no existeix, la subconsulta no troba files, AVG sobre un conjunt buit retorna NULL (04-04) i preu > NULL s'avalua a UNKNOWN per a les vint files. Com que el WHERE només deixa passar el que és TRUE (04-03), el resultat es buida en silenci. Aquest és el més perillós dels tres, perquè un informe buit sembla un informe legítim: "aquest mes no n'hi va haver cap". Com defensar-se'n: executa sempre la subconsulta sola abans d'imbricar-la; embolcalla-la en COALESCE quan hi hagi un valor per omissió sensat (> COALESCE((SELECT AVG(...)...), 0), 06-04); i si la pregunta és d'existència, fes servir EXISTS, que mai no retorna NULL (07-03).
5.3. NOT IN amb NULL — el retrobament amb 04-02
A 04-02 el vas anomenar "l'error més car de SQL". Amb subconsultes és molt més fàcil de cometre, perquè ja no veus la llista: la calcula una altra consulta i no saps si porta nuls.
-- ⚠️ INCORRECTA: retorna 0 files
SELECT id, nom, cognoms, carrec
FROM empleats
WHERE id NOT IN (SELECT empleat_id FROM comandes);I tanmateix saps que hi ha cinc empleats sense comandes: només el 4, el 5 i el 6 apareixen a comandes. El que ha passat és que la subconsulta retorna {4, 5, 6, NULL} — les 10 comandes web tenen empleat_id a NULL. Per a l'empleada 1:
1 NOT IN (4, 5, 6, NULL) ≡ 1 <> 4 AND 1 <> 5 AND 1 <> 6 AND 1 <> NULL ≡ TRUE AND TRUE AND TRUE AND UNKNOWN ≡ UNKNOWN → no és TRUE → la fila es descarta
El mateix per a les vuit files. Amb un sol NULL a la llista, NOT IN no pot retornar TRUE mai. Els tres arranjaments —filtrar els nuls a dins, NOT EXISTS (07-03) i l'anti-join de 03-03— són aquests:
WHERE id NOT IN (SELECT empleat_id FROM comandes WHERE empleat_id IS NOT NULL) -- ✅ 1
WHERE NOT EXISTS (SELECT 1 FROM comandes AS co WHERE co.empleat_id = empleats.id) -- ✅ 2
FROM empleats AS e LEFT JOIN comandes AS co ON co.empleat_id = e.id
WHERE co.id IS NULL -- ✅ 3Els tres retornen el mateix:
| id | nom | cognoms | carrec |
|---|---|---|---|
| 1 | Rosa | Alcázar Vives | Directora general |
| 2 | Andrés | Company Talens | Responsable de vendes |
| 3 | Beatriz | Nadal Ripoll | Responsable de logística |
| 7 | Irene | Salvador Mira | Operària de magatzem |
| 8 | Daniel | Vercher Lluch | Analista de dades |
5 files. I hi ha una asimetria que sorprèn tothom: IN sí que funciona amb nuls. 4 IN (4, 5, 6, NULL) és TRUE, perquè n'hi ha prou que una comparació encerti. El problema és exclusiu de la negació. La comparació de les quatre formes de respondre "què no casa" arriba a 07-03, i la seva taula definitiva, a 07-05.
- On pot aparèixer una subconsulta
Gairebé a qualsevol lloc on càpiga un valor o una taula:
| Lloc | Quin tipus | Per a què | Lliçó |
|---|---|---|---|
SELECT |
Escalar | Columna calculada a partir d'una altra taula | 07-04 |
FROM / JOIN ... ON |
De taula | Taula derivada: agregar i tornar a agregar, o unir contra un agregat | 07-04 |
WHERE |
Qualsevol | Filtrar amb un valor o una llista calculats | Aquesta |
HAVING |
Escalar | Comparar l'agregat d'un grup amb un de global | Aquesta, secció 3 |
INSERT ... SELECT |
De taula | Inserir el resultat d'una consulta | 05-02 |
UPDATE ... SET |
Escalar | Calcular el nou valor des d'una altra taula | 05-03 |
UPDATE/DELETE ... WHERE |
Qualsevol | Triar quines files es modifiquen o s'esborren | 05-03, 05-04 |
Dos exemples del mòdul 5 revisitats, ara que saps com es diuen: UPDATE productes SET preu = ROUND(preu * 1.05, 2) WHERE id NOT IN (SELECT producte_id FROM linies_comanda) puja un 5 % el que no s'ha venut mai, i DELETE FROM ressenyes WHERE client_id NOT IN (SELECT id FROM clients) neteja ressenyes òrfenes. Els dos són segurs perquè linies_comanda.producte_id i clients.id són NOT NULL. Si alguna admetés nuls, l'UPDATE no modificaria res i el DELETE no esborraria res — la trampa de 5.3, ara amb conseqüències sobre les dades. I on no pot aparèixer una subconsulta: al GROUP BY, ni —a PostgreSQL— dins d'una restricció CHECK.
Errors habituals i consells
- Escriure un agregat al
WHERE.aggregate functions are not allowed in WHERE. El que necessites és una subconsulta escalar. - Fer servir una subconsulta multifila on s'espera un valor.
more than one row returned by a subquery used as an expression. Afegeix-hi un agregat, unORDER BY ... LIMIT 1, o canvia aIN/ANY/ALL. - No comprovar que l'escalar retorna alguna cosa. Si retorna zero files val
NULL, el filtre es buida en silenci i l'informe sembla correcte. És l'error més car de la lliçó. NOT INsobre una columna que admet nuls. Zero files, sempre. Fes servirNOT EXISTSo filtra els nuls dins de la subconsulta.- Arrodonir el valor de comparació.
> ROUND(AVG(preu), 2)no és> AVG(preu). Arrodoneix en mostrar, no en comparar. I evita l'ORDER BYdins d'una subconsulta de llista: no aporta res i costa temps. - Consell: executa sempre la subconsulta sola primer. És la tècnica de depuració número u del mòdul; si no es pot executar sola, és correlacionada.
- Consell: si necessites columnes de l'altra taula al resultat, no és una subconsulta: és un
JOIN. La subconsulta filtra i calcula; no aporta columnes alSELECTextern (07-05).
Exercicis
Exercici 1
Màrqueting vol una llista de productes cars mesurats amb un patró concret: els que superen la mitjana de preus de la categoria Cosmètica natural. Mostra id, nom, categoria i preu, ordenats per preu descendent, i no escriguis el llindar a mà. Després respon: quants dels productes que en surten són de Cosmètica natural, i per què els altres dos d'aquesta categoria no hi apareixen?
Exercici 2
Un company vol saber quins clients no han escrit mai una ressenya i ha escrit això:
-- ⚠️ Sospitosa
SELECT id, nom, cognoms FROM clients
WHERE id NOT IN (SELECT client_id FROM ressenyes);- Funciona? Justifica la resposta mirant l'esquema de 01-06, sense executar-la.
- Dona'n el resultat i digues quants d'aquests clients sí que han comprat alguna vegada.
- Reescriu-la amb
NOT EXISTSi amb un anti-join, i explica per què aquí les tres són equivalents.
Exercici 3
Direcció pregunta: "quines comandes inclouen el producte més car del catàleg?". Escriu una única consulta que retorni l'id de la comanda, la data, el client i l'estat, sense escriure a mà ni el nom ni l'id d'aquell producte. (Pista: necessitaràs dues subconsultes, una dins de l'altra.) Indica després què passaria si hi hagués dos productes empatats al preu màxim.
Solucions
Solució 1
SELECT p.id, p.nom AS producte, cat.nom AS categoria, p.preu
FROM productes AS p
JOIN categories AS cat ON p.categoria_id = cat.id
WHERE p.preu > (SELECT AVG(preu) FROM productes WHERE categoria_id = 2)
ORDER BY p.preu DESC;| id | producte | categoria | preu |
|---|---|---|---|
| 15 | Te verd matcha cerimonial 30 g | Begudes | 22.00 |
| 6 | Crema facial d'àloe vera 50 ml | Cosmètica natural | 18.90 |
| 20 | Càpsules d'espirulina 120 u | Complements | 16.40 |
| 8 | Oli corporal d'ametlles 200 ml | Cosmètica natural | 14.25 |
| 13 | Espelmes de cera de soja (pack 2) | Llar sostenible | 13.75 |
| 1 | Oli d'oliva verge extra 500 ml | Alimentació | 12.50 |
6 files. El llindar calculat és de 11,5375 € (46,15 € entre 4 productes). Només dos són de Cosmètica natural, la crema d'àloe i l'oli corporal; els altres dos de la categoria —xampú 8,40 € i bàlsam 4,60 €— queden per sota de la seva pròpia mitjana, que està estirada cap amunt precisament pels dos cars.
El punt de fons: el llindar surt d'una categoria però s'aplica a tot el catàleg, perquè una subconsulta no correlacionada s'avalua una vegada i val igual per a les vint files. Comparar cada producte amb la mitjana de la seva pròpia categoria exigeix una de correlacionada: lliçó següent.
Solució 2
1. Sí que funciona, i se sap sense executar-la: ressenyes.client_id està declarada NOT NULL (01-06, taula 3.8), així que la subconsulta no pot retornar cap NULL i NOT IN es comporta correctament. La sospita era el reflex correcte davant de qualsevol NOT IN; l'esquema l'esvaeix. 2. El resultat:
| id | nom | cognoms |
|---|---|---|
| 5 | Ana | Belmonte Roca |
| 10 | Julien | Moreau |
| 12 | Diego | Ramos Herrera |
| 13 | Núria | Bosch Ferrer |
| 14 | Hugo | Iglesias Pardo |
| 15 | Inés | Carrasco Vega |
6 clients de 15, i tres d'ells sí que han comprat: l'Ana (2 comandes), en Julien (1) i en Diego (1). Els altres tres són els coneguts 13, 14 i 15, que no han comprat mai i per tant no podien ressenyar res. Distingir els dos grups importa: a l'Ana, en Julien i en Diego se'ls pot demanar una opinió; a la Núria, l'Hugo i la Inés cal vendre'ls alguna cosa primer.
3. Les dues reescriptures, amb el mateix resultat de sis files:
SELECT c.id, c.nom, c.cognoms FROM clients AS c -- NOT EXISTS (07-03)
WHERE NOT EXISTS (SELECT 1 FROM ressenyes AS r WHERE r.client_id = c.id) ORDER BY c.id;
SELECT c.id, c.nom, c.cognoms FROM clients AS c -- anti-join (03-03)
LEFT JOIN ressenyes AS r ON r.client_id = c.id WHERE r.id IS NULL ORDER BY c.id;Les tres són equivalents aquí per una única raó: ressenyes.client_id és NOT NULL. Si demà es permetessin ressenyes anònimes amb client_id nul, la versió amb NOT IN passaria a retornar zero files i les altres dues continuarien funcionant. L'equivalència depèn de l'esquema, no de la sintaxi.
Solució 3
SELECT co.id AS comanda_id,
co.data_comanda,
c.nom || ' ' || c.cognoms AS client,
co.estat
FROM comandes AS co
JOIN clients AS c ON co.client_id = c.id
WHERE co.id IN (SELECT lc.comanda_id FROM linies_comanda AS lc
WHERE lc.producte_id = (SELECT id FROM productes
ORDER BY preu DESC LIMIT 1))
ORDER BY co.id;| comanda_id | data_comanda | client | estat |
|---|---|---|---|
| 4 | 2025-04-19 | Javier Ortega Ruiz | lliurat |
| 12 | 2025-10-01 | Julien Moreau | lliurat |
| 17 | 2026-01-13 | Sofia Moreira Costa | enviat |
3 comandes. El producte més car és el Te verd matcha cerimonial 30 g (22,00 €) i s'ha venut tres vegades. Hi ha dos nivells: la subconsulta interna retorna un valor escalar (l'id del producte més car) i la intermèdia, una llista de comanda_id.
Si hi hagués empat, la interna continuaria retornant una sola fila —LIMIT 1 talla arbitràriament— i perdries en silenci les comandes de l'altre producte. La versió robusta canvia l'= per un IN i elimina el LIMIT: WHERE lc.producte_id IN (SELECT id FROM productes WHERE preu = (SELECT MAX(preu) FROM productes)). Amb les dades actuals retorna el mateix, però no es trenca el dia que algú doni d'alta un segon producte de 22,00 €. ORDER BY ... LIMIT 1 dins d'una subconsulta és còmode i fràgil.
Conclusió
Has obert la porta del mòdul:
- Una subconsulta és una consulta entre parèntesis dins d'una altra; la que la conté és la consulta externa. Segons el que retornen n'hi ha tres tipus —escalar, de fila i de taula—, i cadascun admet uns operadors diferents.
- La distinció que organitza el mòdul és no correlacionada (no esmenta res de fora, s'executa sola, s'avalua una vegada) enfront de correlacionada (fa servir un àlies extern, no es pot executar aïllada, s'avalua una vegada per fila).
- Una escalar al
WHEREresol "per sobre de la mitjana": 9 dels 20 productes superen els 9,035 € del catàleg. I una escalar alHAVINGtanca la pregunta que 04-06 va deixar pendent: en Julien, la Sofia i en Tiago són els únics amb un tiquet mitjà superior als 36,40 € de mitjana global. IN (SELECT ...)filtra per una llista calculada —12 clients compradors, 17 productes de categories amb més de tres referències— i equival a= ANY;> ALLcompara amb el màxim i> ANYamb el mínim. I coneixes els tres errors clàssics: l'escalar amb diverses files (sorollosa), l'escalar amb zero files (silenciosa: donaNULLi buida el resultat) iNOT INamb nuls (zero files garantides).
Totes les subconsultes d'aquesta lliçó tenen una cosa en comú: es calculen una vegada i valen el mateix per a totes les files. Per això cap no ha pogut respondre a "quins productes superen la mitjana de la seva categoria": aquell llindar és diferent per a cada fila. A la lliçó següent, subconsultes correlacionades, la subconsulta començarà a mirar cap enfora —a la fila que la consulta externa està examinant en aquell moment— i s'executarà una vegada per cadascuna d'elles. Canvia el model mental, canvia el cost i apareix tota una família de preguntes noves.
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
