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

  1. Què és una subconsulta: vocabulari, correlació i tipus
  2. Subconsulta escalar al WHERE
  3. La pregunta pendent de 04-06: el tiquet mitjà
  4. Subconsultes de llista: IN, ANY/SOME i ALL
  5. Els tres errors clàssics
  6. On pot aparèixer una subconsulta
  7. Errors habituals i consells
  8. Exercicis
  9. Conclusió

  1. 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 , fa servir un àlies de fora
Es pot executar sola? , 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 amb AND.

  1. Subconsulta escalar al WHERE

El 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:

-- ⚠️ INCORRECTA
SELECT id, nom, preu FROM productes WHERE preu > AVG(preu);
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 productes9.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.

  1. 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:

  1. FROM + dos JOIN: es parteix de les 47 línies de detall i es puja fins al client; és la consulta canònica de detall del curs. El GROUP BY forma 12 grups, un per client comprador (els clients 13, 14 i 15 ja els va descartar l'INNER JOIN).
  2. La subconsulta escalar s'avalua una vegada i retorna 36.39725, la mitjana exacta sense arrodonir.
  3. HAVING compara el tiquet mitjà de cada grup amb aquella constant i en descarta nou grups.
  4. SELECT projecta 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 HAVING i no al WHERE: la condició compara un agregat del grup amb la constant, i el WHERE no pot fer servir agregats. La subconsulta només aporta el número amb què comparar.

  1. Subconsultes de llista: IN, ANY/SOME i ALL

Quan 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.

  1. 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);
ERROR:  more than one row returned by a subquery used as an expression

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);
(0 files)

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);
(0 files)

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                                                              -- ✅ 3

Els 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.

  1. 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, un ORDER BY ... LIMIT 1, o canvia a IN/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 IN sobre una columna que admet nuls. Zero files, sempre. Fes servir NOT EXISTS o 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 BY dins 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 al SELECT extern (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);
  1. Funciona? Justifica la resposta mirant l'esquema de 01-06, sense executar-la.
  2. Dona'n el resultat i digues quants d'aquests clients sí que han comprat alguna vegada.
  3. Reescriu-la amb NOT EXISTS i 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 WHERE resol "per sobre de la mitjana": 9 dels 20 productes superen els 9,035 € del catàleg. I una escalar al HAVING tanca 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; > ALL compara amb el màxim i > ANY amb el mínim. I coneixes els tres errors clàssics: l'escalar amb diverses files (sorollosa), l'escalar amb zero files (silenciosa: dona NULL i buida el resultat) i NOT IN amb 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

Mòdul 2: Consultes bàsiques de SQL

Mòdul 3: Treballar amb múltiples taules

Mòdul 4: Filtratge avançat de dades

Mòdul 5: Manipulació de dades

Mòdul 6: Funcions avançades de SQL

Mòdul 7: Subconsultes i consultes imbricades

Mòdul 8: Índexs i optimització del rendiment

Mòdul 9: Transaccions i concurrència

Mòdul 10: Temes avançats

Mòdul 11: SQL a la pràctica

Mòdul 12: Projecte final

© Copyright 2026. Tots els drets reservats