Ja saps formar grups i calcular agregats sobre ells. Falta l'última peça: filtrar els grups. No "les comandes del 2025", que és filtrar files i això ja ho fa WHERE, sinó "els clients que han fet més d'una comanda", "les categories que facturen més de 150 €", "els productes que han venut més de 8 unitats". Aquestes condicions no es poden avaluar mirant una fila: cal haver format el grup i haver calculat el seu agregat abans de poder decidir.
Per a això existeix HAVING. La seva definició cap en una línia —WHERE filtra files, HAVING filtra grups— i tota la resta es dedueix del diagrama de l'ordre lògic que vas completar ahir. En aquesta lliçó veuràs per què HAVING pot fer servir agregats i WHERE no, per què tot i així WHERE és gairebé sempre preferible, com es combinen tots dos a la mateixa consulta, i tancaràs el fil que 03-03 va deixar obert amb la taula definitiva dels tres llocs on es pot filtrar en SQL.
I amb això es tanca el mòdul 4.
Contingut
HAVINGa l'ordre lògic d'execució- El primer
HAVING: clients amb més d'una comanda - Per què
HAVINGpot fer servir agregats iWHEREno - La mateixa pregunta resolta de les dues maneres
- Per què
WHEREés preferible: filtrar abans d'agrupar WHEREiHAVINGa la mateixa consultaHAVINGsenseGROUP BY- Diverses condicions a
HAVING HAVINGsobre un agregat que no és alSELECT- Casos de negoci de BotigaVerda
- Els tres llocs on es filtra:
ON,WHEREiHAVING - Errors habituals i consells
- Exercicis
- Conclusió del mòdul
HAVING a l'ordre lògic d'execució
HAVING a l'ordre lògic d'execucióRecupera el diagrama de 04-05 i fixa't en el pas 4:
flowchart LR
A["1 · FROM / JOIN<br/>d'on surten les files"] --> B["2 · WHERE<br/>filtra FILES<br/>(sense agregats)"]
B --> C["3 · GROUP BY<br/>forma els GRUPS"]
C --> D["4 · HAVING<br/>filtra GRUPS<br/>(sí agregats)"]
D --> E["5 · SELECT<br/>projecta · neixen els àlies"]
E --> F["5b · DISTINCT"]
F --> G["6 · ORDER BY"]
G --> H["7 · LIMIT / OFFSET"]
HAVING és després de GROUP BY i abans de SELECT. D'aquella única posició en surten les cinc propietats de la clàusula:
| Propietat | Per què |
|---|---|
| Pot fer servir funcions d'agregació | Quan s'executa, els grups ja estan formats i els seus agregats calculats |
Pot fer servir les columnes del GROUP BY |
Són les que defineixen cada grup i tenen un únic valor per grup |
| No pot fer servir columnes soltes | Una columna no agrupada no té un valor únic dins del grup. Mateix error que al SELECT |
No veu els àlies del SELECT |
El SELECT és el pas 5, posterior. Cal repetir l'agregat sencer |
| Descarta grups sencers, no files | Si un grup no supera la condició, desapareix amb totes les seves files |
La sintaxi es col·loca sempre entre GROUP BY i ORDER BY:
SELECT columnes, agregats
FROM taules
WHERE condició_sobre_files
GROUP BY columnes
HAVING condició_sobre_grups
ORDER BY …
LIMIT …
- El primer
HAVING: clients amb més d'una comanda
HAVING: clients amb més d'una comandaLa pregunta clàssica de fidelització: quins clients han repetit?
SELECT c.id,
c.nom || ' ' || c.cognoms AS client,
c.pais,
COUNT(*) AS comandes,
MIN(co.data_comanda) AS primera_comanda,
MAX(co.data_comanda) AS ultima_comanda
FROM comandes AS co
JOIN clients AS c ON co.client_id = c.id
GROUP BY c.id, c.nom, c.cognoms, c.pais
HAVING COUNT(*) > 1
ORDER BY comandes DESC, c.id;| id | client | pais | comandes | primera_comanda | ultima_comanda |
|---|---|---|---|---|---|
| 1 | Lucía Martínez Soler | Espanya | 3 | 2025-03-04 | 2025-12-02 |
| 2 | Carlos Ferrer Ibáñez | Espanya | 2 | 2025-03-12 | 2025-09-09 |
| 4 | Javier Ortega Ruiz | Espanya | 2 | 2025-04-19 | 2025-12-19 |
| 5 | Ana Belmonte Roca | Espanya | 2 | 2025-05-23 | 2026-01-27 |
| 6 | Pau Llorens Vidal | Espanya | 2 | 2025-06-11 | 2026-02-09 |
| 7 | Sofia Moreira Costa | Portugal | 2 | 2025-06-28 | 2026-01-13 |
| 9 | Camille Dubois | França | 2 | 2025-08-03 | 2026-02-21 |
7 clients de 15 han repetit. Sense el HAVING, la consulta retornaria 12 files (els 12 compradors); amb ell, s'hi queden només els grups el COUNT(*) dels quals supera 1. S'han descartat cinc grups sencers: la Marta, en Tiago, en Julien, l'Elena i en Diego, cadascun amb la seva única comanda.
Fixa't que aquesta condició és impossible d'escriure al WHERE. En avaluar-se el WHERE, la consulta està mirant la comanda 1 de la Lucía i no té manera de saber que existiran la 5 i la 15: els grups encara no s'han format.
- Per què
HAVING pot fer servir agregats i WHERE no
HAVING pot fer servir agregats i WHERE noProvem-ho, per veure l'error:
-- ⚠️ INCORRECTA
SELECT client_id, COUNT(*) AS comandes
FROM comandes
WHERE COUNT(*) > 1
GROUP BY client_id;El missatge és taxatiu i el diagrama explica per què. WHERE s'executa al pas 2, quan el motor està examinant una fila cada vegada i encara no existeix cap grup. COUNT(*) no hi significaria res: el recompte de què?
HAVING s'executa al pas 4, amb els grups ja construïts i els seus agregats ja calculats. Pot preguntar per COUNT(*), SUM(...), AVG(...) o qualsevol altre, perquè són valors que ja existeixen.
flowchart TD
A["20 files de comandes"] --> B["2 · WHERE<br/>mira una fila cada vegada<br/>❌ no hi ha grups: COUNT(*) no existeix"]
B --> C["3 · GROUP BY client_id<br/>12 grups"]
C --> D["Es calcula COUNT(*)<br/>de cada grup"]
D --> E["4 · HAVING COUNT(*) > 1<br/>✅ els agregats ja existeixen<br/>→ 7 grups"]
I el simètric: HAVING no pot fer servir una columna que no estigui agrupada ni agregada, exactament igual que el SELECT:
-- ⚠️ INCORRECTA
SELECT client_id, COUNT(*) AS comandes
FROM comandes
GROUP BY client_id
HAVING estat = 'lliurat';ERROR: column "comandes.estat" must appear in the GROUP BY clause or be used in an aggregate function
LINE 4: HAVING estat = 'lliurat';
^És la regla d'or de 04-05, aplicada a HAVING. El grup del client 5 conté una comanda cancel·lada i una altra de pagada: quin és "l'estat" d'aquell grup? No hi ha resposta. Si el que volies era filtrar per estat, això és un filtre de fila i va al WHERE.
I una tercera restricció, que ja vas veure a 04-05: HAVING no accepta àlies del SELECT a PostgreSQL.
-- ⚠️ INCORRECTA a PostgreSQL (funciona a MySQL i SQLite)
SELECT client_id, COUNT(*) AS comandes
FROM comandes
GROUP BY client_id
HAVING comandes > 1;Cal repetir l'agregat: HAVING COUNT(*) > 1. És redundant i és el que exigeix l'estàndard, perquè els àlies neixen al pas 5.
- La mateixa pregunta resolta de les dues maneres
Quan la condició recau sobre una columna del GROUP BY, sí que es pot escriure als dos llocs, i el resultat és idèntic. És el millor experiment per entendre la diferència.
La pregunta: "quantes comandes es van pagar amb targeta?"
Versió A — filtrant files amb WHERE:
-- ✅ CORRECTA i preferible
SELECT metode_pagament,
COUNT(*) AS comandes,
SUM(despeses_enviament) AS ports
FROM comandes
WHERE metode_pagament = 'targeta'
GROUP BY metode_pagament;| metode_pagament | comandes | ports |
|---|---|---|
| targeta | 11 | 52.10 |
Versió B — filtrant grups amb HAVING:
-- ✅ CORRECTA però pitjor
SELECT metode_pagament,
COUNT(*) AS comandes,
SUM(despeses_enviament) AS ports
FROM comandes
GROUP BY metode_pagament
HAVING metode_pagament = 'targeta';| metode_pagament | comandes | ports |
|---|---|---|
| targeta | 11 | 52.10 |
Resultat idèntic. Totes dues són vàlides i metode_pagament és legal al HAVING perquè és al GROUP BY. Però no fan la mateixa feina:
flowchart TD
subgraph A["Versió A · WHERE"]
A1["20 files"] --> A2["WHERE metode_pagament='targeta'<br/>→ 11 files"]
A2 --> A3["GROUP BY sobre 11 files<br/>→ 1 grup"]
A3 --> A4["✅ 1 fila"]
end
subgraph B["Versió B · HAVING"]
B1["20 files"] --> B2["GROUP BY sobre les 20<br/>→ 4 grups"]
B2 --> B3["S'agreguen els 4 grups"]
B3 --> B4["HAVING en descarta 3<br/>→ 1 grup"]
B4 --> B5["✅ 1 fila"]
end
La versió A agrupa 11 files i forma 1 grup. La versió B agrupa 20 files, forma 4 grups, calcula quatre sumes i després en llença tres a les escombraries. Amb 20 comandes la diferència és imperceptible; amb vint milions, és la diferència entre una consulta instantània i una que fa suar el servidor.
La regla: si la condició es pot avaluar mirant una sola fila, va al
WHERE. ReservaHAVINGper al que exigeix haver calculat un agregat.
- Per què
WHERE és preferible: filtrar abans d'agrupar
WHERE és preferible: filtrar abans d'agruparEl principi es va enunciar a 02-03 i aquí assoleix la seva expressió més clara: filtrar aviat és filtrar barat. Cada fila descartada pel WHERE és una fila que no cal ordenar, ni ficar en una taula hash, ni agregar.
| Aspecte | WHERE (abans d'agrupar) |
HAVING (després d'agrupar) |
|---|---|---|
Files que arriben al GROUP BY |
Només les que passen el filtre | Totes |
| Grups que es construeixen | Només els necessaris | Tots, i després es descarten |
| Agregats que es calculen | Només els que es mostraran | Tots, inclosos els que es llençaran |
| Pot aprofitar un índex | Sí | No: actua sobre resultats calculats |
| Pot fer servir agregats | No | Sí |
Aquell punt de l'índex és el més important en taules grans. Un WHERE data_comanda >= '2025-01-01' es pot resoldre amb un índex sobre data_comanda, llegint només les files rellevants. Un HAVING mai no ho pot fer: opera sobre valors que no existien fins que la consulta els va calcular.
Veure-ho requereix EXPLAIN, l'eina que compara plans d'execució, i això és el mòdul 8. Però la intuïció ja la tens: a la versió A de l'apartat anterior, PostgreSQL pot descartar 9 de les 20 files abans de tocar el GROUP BY; a la versió B, no en pot descartar cap.
Consell pràctic: quan escriguis una consulta amb
GROUP BY, repassa cada condició delHAVINGi pregunta't: "això depèn de més d'una fila?". Si la resposta és no, mou-ho alWHERE. És l'optimització més barata que existeix: no requereix índexs, ni configuració, ni entendre el planificador.
WHERE i HAVING a la mateixa consulta
WHERE i HAVING a la mateixa consultaL'habitual no és triar entre un i altre: és fer servir tots dos, cadascun per al que li toca. WHERE acota l'univers de files, HAVING selecciona quins grups mereixen aparèixer.
La pregunta: "de les vendes del 2025, quins clients van gastar més de 50 €?"
SELECT c.id,
c.nom || ' ' || c.cognoms AS client,
c.pais,
COUNT(DISTINCT co.id) AS comandes_2025,
COUNT(*) AS linies,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS total_2025
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
WHERE co.data_comanda >= DATE '2025-01-01'
AND co.data_comanda < DATE '2026-01-01'
GROUP BY c.id, c.nom, c.cognoms, c.pais
HAVING SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)) > 50
ORDER BY total_2025 DESC, c.id;| id | client | pais | comandes_2025 | linies | total_2025 |
|---|---|---|---|---|---|
| 1 | Lucía Martínez Soler | Espanya | 3 | 9 | 107.60 |
| 10 | Julien Moreau | França | 1 | 3 | 66.90 |
| 7 | Sofia Moreira Costa | Portugal | 1 | 3 | 64.88 |
| 4 | Javier Ortega Ruiz | Espanya | 2 | 4 | 62.93 |
| 2 | Carlos Ferrer Ibáñez | Espanya | 2 | 4 | 59.46 |
5 files. El repartiment de responsabilitats és perfectament nítid:
| Clàusula | Condició | Què descarta |
|---|---|---|
WHERE |
data_comanda al 2025 |
Les 7 línies de les 4 comandes del 2026. S'avalua fila a fila |
GROUP BY |
Per client | Forma 12 grups amb les 40 línies supervivents |
HAVING |
Suma > 50 € | Descarta 7 grups sencers el total dels quals no arriba a 50 € |
Sense el HAVING el resultat tindria 12 files; sense el WHERE, els totals inclourien el 2026 i la Sofia pujaria de 64,88 € a 111,88 €, canviant fins i tot l'ordre del rànquing. Els dos filtres són necessaris i cap no pot substituir l'altre.
I observa un detall d'escriptura: l'expressió de l'import està repetida al SELECT i al HAVING. És obligatori (l'àlies total_2025 encara no existeix) i és lleig. Les CTE del mòdul 10 ho resoldran definitivament.
HAVING sense GROUP BY
HAVING sense GROUP BYHAVING pot aparèixer sense GROUP BY. Quan això passa, SQL tracta tota la taula com un únic grup, exactament igual que un agregat sense GROUP BY (04-04, secció 8). El resultat és una consulta que retorna una fila o cap.
| productes | preu_mitja |
|---|---|
| 20 | 9.04 |
Com que el catàleg té 20 productes i 20 > 5, el grup únic sobreviu i en surt la fila. Ara amb una condició que no es compleix:
SELECT COUNT(*) AS productes,
ROUND(AVG(preu), 2) AS preu_mitja
FROM productes
HAVING COUNT(*) > 100;Zero files. És l'única manera que una consulta agregada sense GROUP BY no en retorni cap, i per això desconcerta: sense HAVING, SELECT COUNT(*) FROM productes WHERE 1 = 0 retornaria una fila amb un 0.
Per a què serveix? Gairebé per a res en el dia a dia. El seu únic ús raonable és com a guarda d'una comprovació: "retorna'm el resum només si hi ha dades suficients", o "avisa'm només si el total supera un llindar". Fora d'això, un HAVING sense GROUP BY sol ser un WHERE mal escrit:
Regla pràctica: si escrius
HAVINGi no hi haGROUP BYa la consulta, atura't i comprova que era això el que volies. En nou de cada deu casos, no ho era.
- Diverses condicions a
HAVING
HAVINGHAVING accepta condicions compostes amb AND, OR, NOT i parèntesis, amb la mateixa precedència i els mateixos paranys que el WHERE de 02-03. AND s'avalua abans que OR, i barrejar-los sense parèntesis produeix resultats incorrectes sense donar cap error.
La pregunta: "categories amb almenys 10 línies de venda i més de 150 € facturats".
SELECT cat.id,
cat.nom AS categoria,
COUNT(*) AS linies,
SUM(lc.quantitat) AS unitats,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM linies_comanda AS lc
JOIN productes AS p ON lc.producte_id = p.id
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY cat.id, cat.nom
HAVING COUNT(*) >= 10
AND SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)) > 150
ORDER BY facturacio DESC, cat.id;| id | categoria | linies | unitats | facturacio |
|---|---|---|---|---|
| 1 | Alimentació | 16 | 49 | 256.27 |
| 4 | Begudes | 11 | 29 | 195.28 |
| 2 | Cosmètica natural | 10 | 16 | 156.32 |
3 files de 5. S'han descartat Llar sostenible (7 línies, no arriba a 10) i Higiene personal (3 línies i 31,50 €, falla les dues condicions).
I ara la versió amb OR, que respon una altra pregunta:
donaria les mateixes 3 files en aquest conjunt de dades —les tres compleixen totes dues condicions—, cosa que és una coincidència perillosa: si demà una categoria arribés a 12 línies amb 90 € de facturació, AND l'excloiria i OR la inclouria. Que dues consultes retornin el mateix avui no significa que siguin equivalents.
Recordatori de 02-03: sempre que en un
HAVINGconvisquinANDiOR, posa-hi parèntesis. La precedència és la mateixa i el dany també.
HAVING sobre un agregat que no és al SELECT
HAVING sobre un agregat que no és al SELECTÉs perfectament legal filtrar per un agregat que no mostres. També és una de les coses que més confonen qui llegeix la consulta després.
SELECT cat.id,
cat.nom AS categoria,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM linies_comanda AS lc
JOIN productes AS p ON lc.producte_id = p.id
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY cat.id, cat.nom
HAVING SUM(lc.quantitat) > 25
ORDER BY facturacio DESC, cat.id;| id | categoria | facturacio |
|---|---|---|
| 1 | Alimentació | 256.27 |
| 4 | Begudes | 195.28 |
2 files, i qui llegeixi aquest resultat no pot saber per què. Ni Cosmètica natural (156,32 €) ni Llar sostenible no hi apareixen, tot i facturar més que moltes coses. La raó està amagada al HAVING: només Alimentació (49 unitats) i Begudes (29 unitats) superen les 25 unitats venudes, i aquella columna no es mostra.
És legal, funciona i de vegades és el que vols (un informe executiu no té per què ensenyar els criteris de tall). Però com a norma d'higiene:
Si filtres per un agregat, mostra'l. Afegir
SUM(lc.quantitat) AS unitatsalSELECTcosta una línia i converteix un resultat misteriós en un d'autoexplicatiu. El teu jo d'aquí a sis mesos t'ho agrairà.
- Casos de negoci de BotigaVerda
Cinc preguntes reals, resoltes.
10.1. Categories amb més de tres productes al catàleg
SELECT cat.id,
cat.nom AS categoria,
COUNT(*) AS productes,
ROUND(AVG(p.preu), 2) AS preu_mitja
FROM productes AS p
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY cat.id, cat.nom
HAVING COUNT(*) > 3
ORDER BY productes DESC, cat.id;| id | categoria | productes | preu_mitja |
|---|---|---|---|
| 1 | Alimentació | 5 | 6.18 |
| 2 | Cosmètica natural | 4 | 11.54 |
| 3 | Llar sostenible | 4 | 10.09 |
| 4 | Begudes | 4 | 8.90 |
4 categories de 6. En queden fora Higiene personal (2 productes) i Complements (1). És el diagnòstic d'un catàleg amb dues àrees clarament infradesenvolupades.
10.2. Productes que han venut més de 8 unitats
SELECT p.id,
p.nom AS producte,
SUM(lc.quantitat) AS unitats,
COUNT(*) AS vegades_venut,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM linies_comanda AS lc
JOIN productes AS p ON lc.producte_id = p.id
GROUP BY p.id, p.nom
HAVING SUM(lc.quantitat) > 8
ORDER BY unitats DESC, p.id;| id | producte | unitats | vegades_venut | facturacio |
|---|---|---|---|---|
| 2 | Arròs integral ecològic 1 kg | 14 | 4 | 54.60 |
| 5 | Tomàquet triturat ecològic 400 g | 14 | 2 | 23.79 |
| 16 | Kombutxa de gingebre 750 ml | 12 | 3 | 56.43 |
| 1 | Oli d'oliva verge extra 500 ml | 9 | 5 | 109.53 |
| 14 | Infusió de camamilla ecològica 20 u | 9 | 3 | 29.25 |
| 18 | Raspall de dents de bambú | 9 | 3 | 31.50 |
6 productes dels 17 que s'han venut alguna vegada. Són els de més rotació, i l'equip de compres n'hauria de vigilar l'estoc amb prioritat.
10.3. Categories la facturació de les quals supera els 150 €
SELECT cat.id,
cat.nom AS categoria,
COUNT(*) AS linies,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM linies_comanda AS lc
JOIN productes AS p ON lc.producte_id = p.id
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY cat.id, cat.nom
HAVING SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)) > 150
ORDER BY facturacio DESC, cat.id;| id | categoria | linies | facturacio |
|---|---|---|---|
| 1 | Alimentació | 16 | 256.27 |
| 4 | Begudes | 11 | 195.28 |
| 2 | Cosmètica natural | 10 | 156.32 |
3 categories concentren 607,87 € dels 727,95 € facturats, un 83,5 %. És la regla de Pareto traient el cap en un conjunt de dades de vint comandes.
10.4. Comercials amb més de tres comandes gestionades
SELECT e.id,
e.nom || ' ' || e.cognoms AS comercial,
e.carrec,
COUNT(*) AS comandes,
SUM(co.despeses_enviament) AS ports
FROM comandes AS co
JOIN empleats AS e ON co.empleat_id = e.id
GROUP BY e.id, e.nom, e.cognoms, e.carrec
HAVING COUNT(*) > 3
ORDER BY comandes DESC, e.id;| id | comercial | carrec | comandes | ports |
|---|---|---|---|---|
| 4 | Óscar Peris Blasco | Comercial | 4 | 22.40 |
| 5 | Laia Puig Sanchis | Comercial | 4 | 32.30 |
2 files. Aquí convé recordar l'avís de 04-05: aquesta consulta fa servir INNER JOIN, així que les 10 comandes del canal web no hi són i els cinc empleats sense comandes tampoc. Per a aquest informe concret tant se val —preguntem per qui en gestiona més de tres, i qui en gestiona zero no és candidat—, però si la pregunta fos "repartiment de la càrrega comercial", hi faltaria la meitat de l'empresa.
10.5. Clients el tiquet mitjà dels quals supera la mitjana general
Aquesta és la que encara no es pot resoldre, i val la pena entendre exactament per què.
Comencem pel que sí que sabem calcular. El tiquet mitjà de cada client:
SELECT c.id,
c.nom || ' ' || c.cognoms AS client,
COUNT(DISTINCT co.id) AS comandes,
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
ORDER BY tiquet_mitja DESC, c.id;| id | client | comandes | tiquet_mitja |
|---|---|---|---|
| 10 | Julien Moreau | 1 | 66.90 |
| 7 | Sofia Moreira Costa | 2 | 55.94 |
| 8 | Tiago Almeida Nunes | 1 | 44.60 |
| 1 | Lucía Martínez Soler | 3 | 35.87 |
| 9 | Camille Dubois | 2 | 35.44 |
| 12 | Diego Ramos Herrera | 1 | 31.70 |
| 4 | Javier Ortega Ruiz | 2 | 31.47 |
| 11 | Elena Navarro Puig | 1 | 30.30 |
| 2 | Carlos Ferrer Ibáñez | 2 | 29.73 |
| 3 | Marta Sanchis Gil | 1 | 29.53 |
| 6 | Pau Llorens Vidal | 2 | 28.67 |
| 5 | Ana Belmonte Roca | 2 | 27.43 |
I també sabem calcular la mitjana general dels 20 tiquets, en una altra consulta: 36,40 €.
El que no podem fer és escriure HAVING ... > (la mitjana general) dins de la mateixa consulta, perquè aquell valor és al seu torn un agregat calculat sobre un altre conjunt de files. HAVING pot comparar l'agregat d'un grup amb una constant (> 50) o amb un altre agregat del mateix grup (SUM(a) > SUM(b)), però no amb un agregat global.
La solució és una subconsulta que calculi la mitjana general i la injecti al HAVING, i això és la lliçó 07-01. Allà escriuràs alguna cosa amb aquesta forma:
-- Avançament de 07-01: no ho escriguis encara
HAVING SUM(...) / COUNT(DISTINCT co.id) > (SELECT AVG(...) FROM ...)Quan hi arribis, sabràs que la resposta són tres clients —Julien Moreau, Sofia Moreira Costa i Tiago Almeida Nunes—, els únics que superen els 36,40 € de tiquet mitjà general. De moment, la manera honesta de resoldre-ho és en dues consultes i comparant a mà; saber quan una pregunta necessita una eina que encara no tens és tan important com saber fer servir les que ja tens.
- Els tres llocs on es filtra:
ON, WHERE i HAVING
ON, WHERE i HAVINGAquí es tanca el fil que 03-03 va deixar obert. SQL té tres llocs on posar una condició, i cadascun actua en un moment diferent de l'ordre lògic:
flowchart LR
A["FROM<br/>taules"] --> B["ON<br/>① condiciona<br/>l'EMPARELLAMENT"]
B --> C["WHERE<br/>② filtra FILES<br/>ja emparellades"]
C --> D["GROUP BY<br/>forma grups"]
D --> E["HAVING<br/>③ filtra GRUPS"]
E --> F["SELECT"]
ON |
WHERE |
HAVING |
|
|---|---|---|---|
| Quan actua | Pas 1, dins del FROM |
Pas 2 | Pas 4 |
| Sobre què actua | Parelles candidates de files | Files del resultat del FROM |
Grups |
| Pot fer servir agregats? | No | No | Sí |
Pot fer servir àlies del SELECT? |
No | No | No (a PostgreSQL) |
| Pot referir-se a columnes no agrupades? | Sí | Sí | No |
Efecte en un INNER JOIN |
Equivalent a posar-ho al WHERE |
Equivalent a posar-ho a l'ON |
— |
Efecte en un LEFT JOIN |
Conserva les files esquerres sense parella | Degrada el LEFT a INNER |
— |
| Pregunta típica | "Tots els X amb els seus Y que compleixin Z" | "Només les files que compleixen Z" | "Només els grups que compleixen Z" |
L'arbre de decisió
flowchart TD
Q{"Què vull filtrar?"}
Q -->|"Quines files de la taula dreta<br/>s'emparellen en un OUTER JOIN"| ON["ON<br/>(03-03)"]
Q -->|"Quines files individuals<br/>entren al càlcul"| W["WHERE<br/>(02-03)"]
Q -->|"Quins grups apareixen<br/>al resultat"| H["HAVING<br/>(04-06)"]
W --> N["Si la condició<br/>necessita un agregat,<br/>NO hi cap → HAVING"]
H --> M["Si la condició es pot<br/>avaluar fila a fila,<br/>NO hauria de ser aquí → WHERE"]
I la comprovació final amb els tres alhora, en una única consulta de BotigaVerda:
SELECT c.id,
c.nom || ' ' || c.cognoms AS client,
COUNT(co.id) AS comandes_lliurades
FROM clients AS c
LEFT JOIN comandes AS co
ON co.client_id = c.id -- ① ON: només emparella les lliurades
AND co.estat = 'lliurat'
WHERE c.pais = 'Espanya' -- ② WHERE: només clients espanyols
GROUP BY c.id, c.nom, c.cognoms
HAVING COUNT(co.id) >= 2 -- ③ HAVING: només qui en té 2 o més
ORDER BY comandes_lliurades DESC, c.id;| id | client | comandes_lliurades |
|---|---|---|
| 1 | Lucía Martínez Soler | 3 |
| 2 | Carlos Ferrer Ibáñez | 2 |
2 files, i les tres clàusules han fet una feina diferent i insubstituïble:
- L'
ONha restringit l'emparellament a les comandes lliurades, sense eliminar els clients que no en tenen cap (si aquella condició hagués anat alWHERE, elLEFT JOINs'hauria degradat aINNER, tal com va demostrar 03-03). - El
WHEREha filtrat per una columna de la taula esquerra, cosa que és segura i no degrada res: hi queden els 11 clients espanyols. - El
HAVINGha descartat els 9 clients espanyols amb menys de dues comandes lliurades — entre ells l'Ana Belmonte Roca, que té dues comandes però cap de lliurada, i la Núria, l'Hugo i la Inés, que hi apareixien amb un honest 0 gràcies alLEFT JOINi aCOUNT(co.id).
Tota la lògica de filtratge del curs, en onze línies.
Errors habituals i consells
- Posar un agregat al
WHERE.ERROR: aggregate functions are not allowed in WHERE. Va aHAVING. - Posar al
HAVINGuna condició que es pot avaluar fila a fila. Funciona, però agrupa files que s'anaven a llençar. Mou-ho alWHERE. - Fer servir al
HAVINGuna columna que no és alGROUP BY.column ... must appear in the GROUP BY clause. Si és un filtre de fila, va alWHERE. - Fer servir un àlies del
SELECTalHAVING.column "..." does not exista PostgreSQL, encara que MySQL i SQLite ho permetin. Repeteix l'agregat. - Escriure
HAVINGsenseGROUP BYper error. Tracta tota la taula com un grup i retorna una fila o cap. Gairebé sempre volies unWHERE. - Barrejar
ANDiORalHAVINGsense parèntesis. Mateixa precedència i mateix dany silenciós que alWHERE(02-03). - Filtrar per un agregat que no mostres. És legal i deixa un resultat incomprensible. Afegeix-lo al
SELECT. - Oblidar que un
INNER JOINa la consulta ja ha descartat grups. ElHAVINGfiltra el que li arriba; si elJOINva perdre files abans, l'informe ja estava incomplet (04-05, secció 10). - Intentar comparar l'agregat d'un grup amb un agregat global. Necessita subconsulta: 07-01.
- Consell: escriu la consulta per capes. Primer el
FROMamb els seusJOINi compta files; després elWHERE; després elGROUP BYi comprova el nombre de grups; i només al final elHAVING. Depurar una consulta agregada escrita d'una tirada és un suplici. - Consell: comprova quants grups descarta el
HAVING. Executa la consulta sense ell i compara: 12 clients contra 7, 5 categories contra 3. Aquell delta et confirma que la condició fa el que et penses. - Consell: llegeix la pregunta de negoci buscant-hi el subjecte. "Clients que hagin fet més d'una comanda" → el subjecte és el client i la condició és sobre el seu conjunt de comandes:
GROUP BYclient +HAVING. "Comandes de més de 50 €" → el subjecte és la comanda: pot ser que siguiWHERE.
Exercicis
Exercici 1
Compres vol identificar proveïdors amb catàleg rellevant. Escriu una consulta que retorni, per a cada proveïdor amb almenys 4 productes al catàleg: el seu nom, el seu país, el nombre de productes, el preu mitjà (dos decimals) i el preu del producte més car.
Ordena per nombre de productes descendent. Després respon: quin proveïdor en queda fora i per què?
Exercici 2
Direcció vol saber en quins mesos del 2025 es van superar els 60 € de facturació. Escriu una única consulta que faci servir WHERE i HAVING, mostrant el mes (AAAA-MM), el nombre de comandes diferents, el de línies i la facturació.
Després indica, per a cadascuna de les tres clàusules de filtratge, quina condició li correspon i per què no podria anar a les altres.
Exercici 3
Un company et passa aquesta consulta amb el comentari "vull les categories amb més de 20 unitats venudes, però em falten categories i no sé si el número està bé":
-- ⚠️ INCORRECTA
SELECT cat.nom AS categoria,
SUM(lc.quantitat) AS unitats,
SUM(co.despeses_enviament) AS ports
FROM linies_comanda AS lc
JOIN comandes AS co ON lc.comanda_id = co.id
JOIN productes AS p ON lc.producte_id = p.id
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY cat.nom
HAVING SUM(lc.quantitat) > 20;- Quines categories retorna i quines categories "falten"? És un error del
HAVING? - La columna
portsestà malament. Explica per què i calcula quant val realment per a Alimentació davant del que hauria de valer. - Reescriu la consulta corregint el que es pugui corregir avui i indica quina part necessita el mòdul 7.
Solucions
Solució 1
SELECT pr.id,
pr.nom AS proveidor,
pr.pais,
COUNT(*) AS productes,
ROUND(AVG(p.preu), 2) AS preu_mitja,
MAX(p.preu) AS mes_car
FROM productes AS p
JOIN proveidors AS pr ON p.proveidor_id = pr.id
GROUP BY pr.id, pr.nom, pr.pais
HAVING COUNT(*) >= 4
ORDER BY productes DESC, pr.id;| id | proveidor | pais | productes | preu_mitja | mes_car |
|---|---|---|---|---|---|
| 1 | Huerta del Turia | Espanya | 5 | 5.74 | 12.50 |
| 3 | Verde Atlántico | Portugal | 4 | 12.91 | 22.00 |
| 4 | Maison Nature | França | 4 | 9.93 | 18.90 |
| 5 | EcoNordic Supplies | Alemanya | 4 | 11.21 | 16.40 |
4 files. En queda fora BioSierra Ibérica, que només subministra 3 productes (la mel, la pasta d'espelta i la infusió de camamilla) i no arriba al llindar de 4. Comprovació: 5 + 4 + 4 + 4 + 3 = 20 productes.
Dues observacions sobre aquests preus mitjans, que ara sí que són els de catàleg: són diferents dels que sortien a la solució 2 de 04-05 (6,73 € per a Huerta del Turia en lloc de 5,74 €). La diferència és que aquí partim de productes i cada referència compta una vegada; allà partíem d'un LEFT JOIN amb linies_comanda i cada referència comptava tantes vegades com s'hagués venut. Les dues mitjanes són correctes i responen preguntes diferents: "preu mitjà del catàleg" enfront de "preu mitjà del que es ven". Comprovar de quina taula parteix una consulta abans d'interpretar-ne la mitjana és un hàbit que evita molts disgustos.
I fixa't que EcoNordic Supplies hi apareix tot i estar inactiu: la consulta no filtra per pr.actiu. Si la pregunta fos "proveïdors operatius amb catàleg rellevant", caldria afegir-hi WHERE pr.actiu — un filtre de fila, no de grup.
Solució 2
SELECT TO_CHAR(co.data_comanda, 'YYYY-MM') AS mes,
COUNT(DISTINCT co.id) AS comandes,
COUNT(*) AS linies,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM linies_comanda AS lc
JOIN comandes AS co ON lc.comanda_id = co.id
WHERE co.data_comanda >= DATE '2025-01-01'
AND co.data_comanda < DATE '2026-01-01'
GROUP BY TO_CHAR(co.data_comanda, 'YYYY-MM')
HAVING SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)) > 60
ORDER BY facturacio DESC, mes;| mes | comandes | linies | facturacio |
|---|---|---|---|
| 2025-10 | 2 | 5 | 97.20 |
| 2025-06 | 2 | 5 | 95.48 |
| 2025-03 | 2 | 5 | 68.80 |
| 2025-12 | 2 | 5 | 64.58 |
| 2025-04 | 2 | 5 | 61.28 |
5 mesos dels 10 del 2025 van superar els 60 €. I hi apareix un patró interessant: els cinc són mesos amb 2 comandes i 5 línies. Amb aquest volum, facturar bé un mes és simplement que caiguin dues comandes en lloc d'una.
Repartiment de responsabilitats:
| Clàusula | Condició | Per què va aquí |
|---|---|---|
ON |
lc.comanda_id = co.id |
És l'emparellament, no un filtre. Sense ell no hi ha consulta |
WHERE |
Dates del 2025 | S'avalua fila a fila: cada línia sap la data de la seva comanda. Descartar-la abans evita agrupar les 7 línies del 2026. A més pot fer servir un índex sobre data_comanda |
HAVING |
Facturació > 60 € | Necessita el SUM del mes sencer. Cap línia individual no pot decidir si el seu mes supera els 60 € |
La condició del WHERE no podria anar al HAVING: co.data_comanda no és al GROUP BY (agrupem per mes, no per dia), així que donaria l'error must appear in the GROUP BY clause. I la del HAVING no podria anar al WHERE: aggregate functions are not allowed in WHERE. Cadascuna només cap on és.
Solució 3
1. Què retorna i què hi falta.
| categoria | unitats | ports |
|---|---|---|
| Alimentació | 49 | 86.75 |
| Begudes | 29 | 90.10 |
2 files. Les categories que "falten" són:
- Cosmètica natural (16 unitats), Llar sostenible (10) i Higiene personal (9): no falten per error, és el
HAVINGfent la seva feina. Cap no supera les 20 unitats. Correcte. - Complements (0 unitats): aquesta sí que falta per un motiu diferent. No la descarta el
HAVING, la va descartar l'INNER JOINmolt abans, al pas 1. És el problema de 04-05 secció 10. Ara bé: com que la seva condició seria0 > 20, tampoc no hi hauria aparegut ambLEFT JOIN. El resultat és el mateix, però per raons diferents, i aquella distinció importa — amb el llindar a> 20tant se val, amb el llindar a>= 0no seria igual en absolut.
2. Per què ports està malament. despeses_enviament viu a comandes, i després del JOIN amb linies_comanda cada comanda apareix tantes vegades com línies tingui. És l'error de 04-04 secció 11, ara dins d'un GROUP BY.
Per a Alimentació, la consulta dona 86,75 €; per a Begudes, 90,10 €. Sumades totes les categories donarien 278,70 €, el mateix número inflat de 04-04 secció 11, davant dels 118,25 € reals.
I què hauria de donar? La pregunta ni tan sols està ben plantejada: les despeses d'enviament no es poden repartir per categoria, perquè una comanda amb productes de tres categories paga uns ports, no tres. Els 4,95 € de la comanda 1 no són "4,95 € d'Alimentació": són 4,95 € de la comanda, i la comanda 1 porta productes d'Alimentació i de Begudes.
Aquest és el punt més profund de l'exercici. El número està malament, però el problema real és que la mètrica no existeix al nivell de granularitat que es demana. És exactament la mena de pregunta que cal tornar a qui la formula.
3. La versió corregida:
-- ✅ CORRECTA per al que sí que es pot respondre avui
SELECT cat.id,
cat.nom AS categoria,
COUNT(*) AS linies,
SUM(lc.quantitat) AS unitats,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM linies_comanda AS lc
JOIN productes AS p ON lc.producte_id = p.id
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY cat.id, cat.nom
HAVING SUM(lc.quantitat) > 20
ORDER BY unitats DESC, cat.id;| id | categoria | linies | unitats | facturacio |
|---|---|---|---|---|
| 1 | Alimentació | 16 | 49 | 256.27 |
| 4 | Begudes | 11 | 29 | 195.28 |
Canvis aplicats:
| Canvi | Motiu |
|---|---|
S'elimina el JOIN amb comandes |
Ja no cal: sense despeses_enviament, linies_comanda i productes en tenen prou |
S'elimina SUM(co.despeses_enviament) |
La mètrica no existeix a nivell de categoria |
S'afegeix facturacio, que sí que viu a la línia |
És la mètrica correcta a aquesta granularitat |
S'afegeix cat.id al GROUP BY i a l'ORDER BY |
Desempat determinista i agrupació per PK |
Què necessita el mòdul 7. Si la pregunta fos "quant dels ports és atribuïble a cada categoria, prorratejat pel pes de cada categoria en l'import de la comanda?", això és una mètrica legítima però exigeix calcular primer el total de cada comanda, després el pes de cada línia sobre aquell total, i tot seguit repartir. Aquell "calcular un agregat i fer-lo servir en un altre càlcul" és precisament el que resolen les subconsultes del mòdul 7 i les CTE del mòdul 10.
Conclusió del mòdul
HAVING tanca la lògica de filtratge d'SQL:
WHEREfiltra files,HAVINGfiltra grups. Tota la resta es dedueix de la seva posició a l'ordre lògic:FROM/JOIN→WHERE→GROUP BY→HAVING→SELECT→DISTINCT→ORDER BY→LIMIT.HAVINGpot fer servir agregats iWHEREno, perquè quan s'executa elWHEREels grups encara no existeixen. IHAVINGno pot fer servir columnes no agrupades, per la mateixa regla d'or que elSELECT.- Quan la condició recau sobre una columna del
GROUP BY, les dues escriptures donen el mateix resultat però no la mateixa feina:WHEREagrupa 11 files,HAVINGn'agrupa 20 i llença 3 grups. Filtrar abans d'agrupar és sempre preferible, i a més és l'única cosa que pot aprofitar un índex (EXPLAIN, mòdul 8). - El normal és fer servir tots dos alhora:
WHEREacota l'univers (2025) iHAVINGselecciona els grups que importen (més de 50 €). HAVINGsenseGROUP BYtracta la taula sencera com un grup i retorna una fila o cap. Gairebé sempre és unWHEREmal escrit.- Els tres llocs on es filtra són
ON(condiciona l'emparellament i conserva les files esquerres d'unLEFT JOIN),WHERE(filtra files i degrada elLEFTaINNERsi toca la taula dreta) iHAVING(filtra grups). El fil obert a 03-03 queda tancat. - Has resolt els casos de negoci reals de BotigaVerda: 7 clients recurrents, 4 categories amb més de 3 productes, 6 productes amb més de 8 unitats venudes, 3 categories que concentren el 83,5 % de la facturació. I saps quines preguntes encara no pots respondre: comparar l'agregat d'un grup amb un agregat global exigeix subconsultes (07-01).
I amb això es tanca el mòdul 4. Repassa el que has guanyat en sis lliçons: cerques per patrons de text amb LIKE, ILIKE i expressions regulars, i saps quin d'ells pot fer servir un índex; filtres per llistes i rangs amb IN i BETWEEN, i coneixes l'error més car d'SQL —NOT IN amb un NULL—; entens la lògica de tres valors i amb ella se t'han resolt de cop cinc misteris que arrossegaves des del mòdul 2; calcules amb COUNT, SUM, AVG, MIN i MAX, i saps que totes ignoren els nuls menys COUNT(*); parteixes les dades en grups amb GROUP BY combinat amb JOIN, que és el patró central de tota l'anàlisi de dades; i filtres aquells grups amb HAVING. De regal, has liquidat l'avís que el mòdul 3 et va repetir tres vegades: 278,70 € davant de 118,25 €, i saps exactament per què.
Fins aquí, però, només has llegit. Les quatre instruccions que has fet servir —SELECT, FROM, WHERE, GROUP BY— pertanyen totes al mateix subllenguatge, el DQL del qual parlava 01-01, i cap d'elles no ha canviat mai ni un sol byte de BotigaVerda. Hauries pogut treballar tot el curs sobre una base de dades de només lectura i no hauries notat la diferència.
Però una botiga que no pot donar d'alta un producte, registrar una comanda, corregir un preu ni cancel·lar una compra no és una botiga: és un catàleg mort. Al mòdul 5, Manipulació de dades, creuaràs a l'altre costat. Aprendràs a crear taules amb CREATE TABLE —amb els tipus, les claus i les restriccions que fa cinc mòduls que llegeixes a l'esquema de BotigaVerda—, a inserir files amb INSERT, a modificar-les amb UPDATE, a eliminar-les amb DELETE, a resoldre el clàssic "insereix-lo si no existeix i actualitza'l si existeix" amb UPSERT, i a canviar l'estructura d'una taula en producció amb ALTER TABLE. Canvia també el nivell de responsabilitat: un SELECT mal escrit retorna un número equivocat, però un UPDATE sense WHERE modifica les vint files de la taula i no hi ha manera de desfer-ho. Aquell WHERE que fa quatre mòduls que afines deixa de ser una qüestió de precisió analítica i passa a ser la teva xarxa de seguretat.
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
