A la lliçó anterior vas obtenir una xifra per a tota l'empresa: 727,95 € de facturació. És una dada, però no és una anàlisi. El que la direcció pregunta de debò no és "quant hem venut?", sinó "quant hem venut de cada cosa?": per categoria, per client, per país, per mes, per comercial. Aquella paraula —per— és el senyal inequívoc que toca GROUP BY.
GROUP BY parteix el conjunt de files en grups i aplica l'agregat a cada grup per separat, retornant una fila per grup en comptes d'una per a tot. És, sense exagerar, la clàusula que converteix SQL en una eina d'anàlisi. En aquesta lliçó aprendràs a agrupar per una columna, per diverses i per una expressió; ampliaràs per fi el diagrama de l'ordre lògic d'execució amb GROUP BY i HAVING —ho vam prometre a 02-01—; veuràs per què els NULL formen el seu propi grup; dominaràs el patró central de l'anàlisi, que és GROUP BY combinat amb JOIN; i descobriràs per què la categoria 6 de BotigaVerda desapareix dels teus informes i què cal fer perquè hi aparegui amb un honest 0.
Contingut
GROUP BY: partir en grups i agregar cadascun- L'ordre lògic d'execució, ampliat
- La regla d'or: agrupada o agregada
- Agrupar per una columna
- Agrupar per diverses columnes
- Agrupar per una expressió
GROUP BYi l'àlies delSELECT- Grups i
NULL GROUP BYambJOIN: el patró central de l'anàlisi- Grups buits: per què la categoria 6 no apareix
- Ordenar per l'agregat i quedar-se amb el top N
ROLLUP,GROUPING SETSiCUBE- Errors habituals i consells
- Exercicis
- Conclusió
GROUP BY: partir en grups i agregar cadascun
GROUP BY: partir en grups i agregar cadascunLa idea és visual. Sense GROUP BY, totes les files van a un únic munt. Amb GROUP BY, es reparteixen en munts segons el valor d'una columna, i l'agregat es calcula a cada munt:
flowchart LR
subgraph A["20 comandes"]
direction TB
A1["lliurat ×14"]
A2["enviat ×2"]
A3["pagat ×2"]
A4["cancellat ×1"]
A5["pendent ×1"]
end
A --> B["GROUP BY estat"]
B --> C["5 grups"]
C --> D["COUNT(*) a cadascun<br/>→ 5 files de resultat"]
I la consulta:
| estat | comandes |
|---|---|
| lliurat | 14 |
| enviat | 2 |
| pagat | 2 |
| cancellat | 1 |
| pendent | 1 |
5 files, una per estat. Compara-ho amb el que hauries hagut de fer sense GROUP BY: cinc consultes amb WHERE estat = '…', o una amb cinc COUNT(*) FILTER (...). I si demà aparegués un sisè estat, GROUP BY el mostraria sol, mentre que les altres dues versions caldria reescriure-les.
El principi general: GROUP BY produeix una fila per cada combinació diferent de valors de les columnes agrupades. El nombre de files del resultat és exactament el nombre de valors diferents, ni un més ni un menys.
- L'ordre lògic d'execució, ampliat
Des de 02-01 véns construint un diagrama de l'ordre en què SQL entén una consulta. El mòdul 2 el va deixar en cinc passos i el mòdul 3 va mostrar que els JOIN passen dins del FROM. Ara hi inserim les dues peces que faltaven, entre WHERE i SELECT:
flowchart LR
A["1 · FROM / JOIN<br/>d'on surten les files"] --> B["2 · WHERE<br/>filtra FILES"]
B --> C["3 · GROUP BY<br/>forma els GRUPS"]
C --> D["4 · HAVING<br/>filtra GRUPS"]
D --> E["5 · SELECT<br/>projecta i calcula<br/>neixen els àlies"]
E --> F["5b · DISTINCT<br/>elimina duplicats"]
F --> G["6 · ORDER BY<br/>ordena el resultat"]
G --> H["7 · LIMIT / OFFSET<br/>retalla"]
| Pas | Clàusula | Què fa | Treballa sobre | Lliçó |
|---|---|---|---|---|
| 1 | FROM / JOIN |
Determina el conjunt de files de partida | Taules | 02-01 / mòdul 3 |
| 2 | WHERE |
Descarta files | Files individuals | 02-03 |
| 3 | GROUP BY |
Reparteix les files supervivents en grups | Files | 04-05 |
| 4 | HAVING |
Descarta grups sencers | Grups | 04-06 |
| 5 | SELECT |
Calcula i projecta les columnes. Aquí neixen els àlies | Grups (o files, si no hi ha GROUP BY) |
02-01 / 02-02 |
| 5b | DISTINCT |
Elimina files duplicades del resultat | Resultat | 02-04 |
| 6 | ORDER BY |
Ordena | Resultat | 02-05 |
| 7 | LIMIT / OFFSET |
Retalla | Resultat | 02-06 |
Aquest diagrama no és decoració: explica per si sol gairebé tot el que ve després. Tres conseqüències immediates:
| Conseqüència | Per què |
|---|---|
WHERE no pot fer servir funcions d'agregació |
S'executa al pas 2, abans que existeixin els grups. Encara no hi ha res per agregar. D'aquí l'error aggregate functions are not allowed in WHERE |
HAVING sí que les pot fer servir |
S'executa al pas 4, quan els grups ja estan formats i els seus agregats calculats. És la lliçó 04-06 sencera |
Ni WHERE ni GROUP BY ni HAVING no veuen els àlies del SELECT |
Els àlies neixen al pas 5. ORDER BY, que va després, sí que els veu (02-05) |
Aquesta última té un matís important a PostgreSQL, i li dediquem la secció 7 sencera.
El raonament que cal tenir sempre present: un cop s'executa GROUP BY, les files individuals deixen d'existir com a tals. A partir del pas 3, el conjunt de treball ja no són 47 línies de comanda: són 5 grups. Tot el que escriguis d'aquí endavant ha de tenir sentit a nivell de grup.
- La regla d'or: agrupada o agregada
És una sola frase, i d'ella se'n dedueix tota la resta:
Tota expressió del
SELECTha d'estar (a) alGROUP BY, o (b) dins d'una funció d'agregació. Sense excepcions.
Veure-la fallar és la millor manera d'entendre-la:
-- ⚠️ INCORRECTA
SELECT cat.nom AS categoria,
p.nom AS producte,
COUNT(*) AS productes
FROM productes AS p
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY cat.nom;ERROR: column "p.nom" must appear in the GROUP BY clause or be used in an aggregate function
LINE 3: p.nom AS producte,
^I el motor té raó. La fila del grup "Alimentació" representa cinc productes: l'oli, l'arròs, la mel, la pasta i el tomàquet. Quin dels cinc noms hauria d'aparèixer en aquella cel·la? No hi ha cap resposta possible, així que PostgreSQL es nega a inventar-se'n una.
Les tres sortides vàlides:
-- ✅ a) Agrupar també pel nom del producte (però aleshores no hi ha grups: cadascun és únic)
GROUP BY cat.nom, p.nom
-- ✅ b) Agregar el nom del producte
STRING_AGG(p.nom, ', ' ORDER BY p.id) AS productes
-- ✅ c) Treure la columna del SELECT
SELECT cat.nom, COUNT(*) ...L'opció b és especialment útil, i demostra que la regla no és un caprici:
SELECT cat.nom AS categoria,
COUNT(*) AS productes,
STRING_AGG(p.nom, ' · ' ORDER BY p.id) AS llistat
FROM productes AS p
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY cat.nom
ORDER BY productes DESC, categoria;| categoria | productes | llistat |
|---|---|---|
| Alimentació | 5 | Oli d'oliva verge extra 500 ml · Arròs integral ecològic 1 kg · Mel de tarongina crua 500 g · Pasta d'espelta 500 g · Tomàquet triturat ecològic 400 g |
| Begudes | 4 | Infusió de camamilla ecològica 20 u · Te verd matcha cerimonial 30 g · Kombutxa de gingebre 750 ml · Suc de taronja premsat en fred 1 L |
| Cosmètica natural | 4 | Crema facial d'àloe vera 50 ml · Xampú sòlid de romaní 80 g · Oli corporal d'ametlles 200 ml · Bàlsam labial de calèndula 15 ml |
| Llar sostenible | 4 | Detergent ecològic concentrat 1 L · Fregall vegetal de lufa (pack 3) · Bosses reutilitzables de cotó (pack 5) · Espelmes de cera de soja (pack 2) |
| Higiene personal | 2 | Raspall de dents de bambú · Desodorant natural en barra 50 g |
| Complements | 1 | Càpsules d'espirulina 120 u |
6 files, una per categoria, amb els 20 productes repartits: 5 + 4 + 4 + 4 + 2 + 1 = 20. STRING_AGG ha respost a "quins són?" sense trencar la regla, perquè és un agregat.
Una matisació sobre la regla
PostgreSQL és una mica més llest del que suggereix l'enunciat: si agrupes per la clau primària d'una taula, et deixa seleccionar qualsevol altra columna d'aquella mateixa taula, perquè la PK determina funcionalment la resta de valors.
-- ✅ CORRECTA: cat.id és la PK, així que cat.nom queda determinat
SELECT cat.id,
cat.nom AS categoria,
COUNT(p.id) AS productes
FROM categories AS cat
LEFT JOIN productes AS p ON p.categoria_id = cat.id
GROUP BY cat.id
ORDER BY cat.id;| id | categoria | productes |
|---|---|---|
| 1 | Alimentació | 5 |
| 2 | Cosmètica natural | 4 |
| 3 | Llar sostenible | 4 |
| 4 | Begudes | 4 |
| 5 | Higiene personal | 2 |
| 6 | Complements | 1 |
Funciona perquè cat.id és PRIMARY KEY: dins d'un grup amb el mateix id només hi pot haver un nom, així que no hi ha ambigüitat. És una comoditat molt pràctica —evita haver de repetir cinc columnes al GROUP BY— i forma part de l'estàndard SQL des del 1999.
Nota de dialecte — i per què la permissivitat de MySQL és un parany.
Motor Permet una columna solta amb un agregat? Què retorna PostgreSQL No (llevat de dependència funcional de la PK) Error explícit MySQL amb ONLY_FULL_GROUP_BY(per defecte des de 5.7.5)No Error explícit MySQL amb ONLY_FULL_GROUP_BYdesactivatSí Un valor arbitrari de qualsevol fila del grup SQLite Sí, sempre Un valor arbitrari (amb l'excepció documentada de MIN/MAX, on retorna el d'aquella fila)SQL Server No Error explícit Oracle No Error explícit El cas de MySQL permissiu és el parany: la consulta no falla, i en proves amb poques dades fins i tot sembla retornar "el primer", que sol ser el que un esperava. En producció, amb un altre pla d'execució, en retorna un altre. Un informe que deia "Alimentació — Oli d'oliva — 5 productes" comença a dir "Alimentació — Tomàquet triturat — 5 productes" sense que ningú no hagi tocat res. Si treballes amb MySQL, comprova que
ONLY_FULL_GROUP_BYestà actiu i no el desactivis.
- Agrupar per una columna
4.1. Comandes per mètode de pagament
SELECT metode_pagament,
COUNT(*) AS comandes,
SUM(despeses_enviament) AS ports_totals,
ROUND(AVG(despeses_enviament), 2) AS ports_mitjans
FROM comandes
GROUP BY metode_pagament
ORDER BY comandes DESC, metode_pagament;| metode_pagament | comandes | ports_totals | ports_mitjans |
|---|---|---|---|
| targeta | 11 | 52.10 | 4.74 |
| paypal | 4 | 29.70 | 7.43 |
| transferencia | 3 | 17.45 | 5.82 |
| contrareemborsament | 2 | 19.00 | 9.50 |
4 files que sumen 20 comandes i 118,25 € de ports: les mateixes xifres mestres de 04-04, ara desglossades. I ja s'hi llegeix una història: la targeta domina (11 de 20) i el contrareemborsament és el mètode amb els ports més cars (9,50 € de mitjana), cosa que té sentit perquè són els enviaments més llunyans.
4.2. Clients per país
SELECT pais,
COUNT(*) AS clients,
COUNT(DISTINCT ciutat) AS ciutats,
MIN(data_registre) AS primera_alta,
MAX(data_registre) AS ultima_alta
FROM clients
GROUP BY pais
ORDER BY clients DESC, pais;| pais | clients | ciutats | primera_alta | ultima_alta |
|---|---|---|---|---|
| Espanya | 11 | 7 | 2025-01-10 | 2026-01-08 |
| França | 2 | 2 | 2025-04-18 | 2025-05-02 |
| Portugal | 2 | 2 | 2025-03-21 | 2025-04-04 |
3 files. Fixa't en COUNT(DISTINCT ciutat): 11 clients espanyols repartits en només 7 ciutats, perquè València en concentra quatre i Barcelona dues.
4.3. Productes per categoria, amb estadístiques de preu
SELECT categoria_id,
COUNT(*) AS productes,
ROUND(AVG(preu), 2) AS preu_mitja,
MIN(preu) AS mes_barat,
MAX(preu) AS mes_car,
SUM(stock) AS stock_total
FROM productes
GROUP BY categoria_id
ORDER BY categoria_id;| categoria_id | productes | preu_mitja | mes_barat | mes_car | stock_total |
|---|---|---|---|---|---|
| 1 | 5 | 6.18 | 1.95 | 12.50 | 850 |
| 2 | 4 | 11.54 | 4.60 | 18.90 | 330 |
| 3 | 4 | 10.09 | 5.50 | 13.75 | 265 |
| 4 | 4 | 8.90 | 3.25 | 22.00 | 370 |
| 5 | 2 | 5.65 | 3.50 | 7.80 | 315 |
| 6 | 1 | 16.40 | 16.40 | 16.40 | 55 |
6 files, les 6 categories, perquè estem agrupant la taula productes i tots els productes tenen categoria. Guarda't aquest detall: a la secció 10 veuràs que agrupar les vendes per categoria només dona 5 files, i entendre per què és la diferència entre un informe honest i un d'incomplet.
Fixa't en el que passa amb la categoria 6: un sol producte, així que AVG, MIN i MAX coincideixen. Els agregats sobre un grup d'una fila retornen aquell mateix valor.
- Agrupar per diverses columnes
En llistar diverses columnes al GROUP BY, el motor forma un grup per cada combinació diferent de valors.
SELECT c.pais,
co.estat,
COUNT(*) AS comandes,
SUM(co.despeses_enviament) AS ports
FROM comandes AS co
JOIN clients AS c ON co.client_id = c.id
GROUP BY c.pais, co.estat
ORDER BY c.pais, co.estat;| pais | estat | comandes | ports |
|---|---|---|---|
| Espanya | cancellat | 1 | 4.95 |
| Espanya | enviat | 1 | 4.95 |
| Espanya | lliurat | 10 | 31.25 |
| Espanya | pagat | 2 | 9.90 |
| França | lliurat | 2 | 25.00 |
| França | pendent | 1 | 12.50 |
| Portugal | enviat | 1 | 9.90 |
| Portugal | lliurat | 2 | 19.80 |
8 files. Compte amb un punt important: no són 3 països × 5 estats = 15 files. GROUP BY produeix una fila per cada combinació que existeix a les dades, no per cada combinació possible. No hi ha cap comanda francesa cancel·lada, així que aquella fila no hi apareix — no hi apareix amb un zero, és que directament no existeix. (Si necessitessis la graella completa, incloses les combinacions buides, el camí seria un CROSS JOIN de 03-06 amb un LEFT JOIN a sobre.)
L'ordre de les columnes al GROUP BY no canvia el resultat, només la interpretació mental. GROUP BY c.pais, co.estat i GROUP BY co.estat, c.pais retornen els mateixos 8 grups. El que sí que canvia l'aspecte de l'informe és l'ORDER BY.
I el cas clàssic d'anàlisi: vendes per categoria i any.
SELECT cat.nom AS categoria,
EXTRACT(YEAR FROM co.data_comanda) AS any_,
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
JOIN productes AS p ON lc.producte_id = p.id
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY cat.nom, EXTRACT(YEAR FROM co.data_comanda)
ORDER BY categoria, any_;| categoria | any_ | linies | facturacio |
|---|---|---|---|
| Alimentació | 2025 | 14 | 215.67 |
| Alimentació | 2026 | 2 | 40.60 |
| Begudes | 2025 | 9 | 146.55 |
| Begudes | 2026 | 2 | 48.73 |
| Cosmètica natural | 2025 | 8 | 128.22 |
| Cosmètica natural | 2026 | 2 | 28.10 |
| Higiene personal | 2025 | 2 | 24.50 |
| Higiene personal | 2026 | 1 | 7.00 |
| Llar sostenible | 2025 | 7 | 88.58 |
9 files, no 10: Llar sostenible no ha venut res el 2026, així que aquella combinació no existeix. És el mateix fenomen d'abans, i en un informe d'evolució és exactament la mena de buit que cal saber llegir.
(EXTRACT(YEAR FROM …) extreu l'any d'una data. Les funcions de data s'estudien a fons a 06-03; aquí és només un instrument.)
- Agrupar per una expressió
No estàs limitat a columnes: pots agrupar per qualsevol expressió calculada a partir d'elles. És el que acabes de fer amb EXTRACT, i és el que permet construir trams, cohorts i segmentacions.
Agrupem el catàleg per rang de preu:
SELECT CASE
WHEN preu < 5 THEN '1 · menys de 5 €'
WHEN preu < 10 THEN '2 · de 5 a 10 €'
WHEN preu < 15 THEN '3 · de 10 a 15 €'
ELSE '4 · 15 € o més'
END AS rang_preu,
COUNT(*) AS productes,
ROUND(AVG(preu), 2) AS preu_mitja,
MIN(preu) AS minim,
MAX(preu) AS maxim
FROM productes
GROUP BY CASE
WHEN preu < 5 THEN '1 · menys de 5 €'
WHEN preu < 10 THEN '2 · de 5 a 10 €'
WHEN preu < 15 THEN '3 · de 10 a 15 €'
ELSE '4 · 15 € o més'
END
ORDER BY rang_preu;| rang_preu | productes | preu_mitja | minim | maxim |
|---|---|---|---|---|
| 1 · menys de 5 € | 7 | 3.56 | 1.95 | 4.95 |
| 2 · de 5 a 10 € | 6 | 7.79 | 5.40 | 9.90 |
| 3 · de 10 a 15 € | 4 | 12.93 | 11.20 | 14.25 |
| 4 · 15 € o més | 3 | 19.10 | 16.40 | 22.00 |
4 trams que sumen els 20 productes. El catàleg de BotigaVerda està clarament escorat cap al producte barat: 13 de 20 referències costen menys de 10 €.
Dues coses d'aquesta consulta mereixen comentari:
- L'expressió apareix dues vegades, idèntica, al
SELECTi alGROUP BY. És lletja i és necessària, pel motiu de sempre: l'àliesrang_preuencara no existeix quan s'executa elGROUP BY. (Llevat a PostgreSQL, que fa una concessió — secció 7.) Les maneres d'evitar la repetició són les subconsultes del mòdul 7 i les CTE del mòdul 10. - El prefix numèric de les etiquetes (
1 ·,2 ·…) no és decoratiu.ORDER BY rang_preuordena alfabèticament, i sense el prefix l'ordre seria "de 10 a 15 €", "de 5 a 10 €", "15 € o més", "menys de 5 €": un desastre. És un truc habitual en construir trams.
CASE s'estudia a fons a 06-05; aquí es fa servir com a eina per classificar.
GROUP BY i l'àlies del SELECT
GROUP BY i l'àlies del SELECTAquest punt s'explica malament en molts llocs, així que serem precisos.
Segons l'ordre lògic, el GROUP BY (pas 3) s'executa abans que el SELECT (pas 5), on neixen els àlies. Per tant, l'estàndard SQL no permet fer servir un àlies del SELECT al GROUP BY.
A la pràctica, PostgreSQL fa una concessió: accepta que un element del GROUP BY sigui el nom d'una columna de sortida (un àlies) o el seu número ordinal. És una extensió del motor, documentada i molt còmoda:
-- ✅ Funciona a PostgreSQL: 'any_' és un àlies del SELECT
SELECT EXTRACT(YEAR FROM data_comanda) AS any_,
COUNT(*) AS comandes,
SUM(despeses_enviament) AS ports
FROM comandes
GROUP BY any_
ORDER BY any_;| any_ | comandes | ports |
|---|---|---|
| 2025 | 16 | 85.95 |
| 2026 | 4 | 32.30 |
També funciona per número ordinal, GROUP BY 1, encara que aquella forma és fràgil: si algú afegeix una columna al principi del SELECT, l'1 passa a referir-se a una altra cosa.
Ara les tres limitacions d'aquella concessió, que són les que produeixen els errors confusos.
Limitació 1: només un àlies nu, no una expressió que el faci servir
Tan bon punt l'àlies entra en una expressió més gran, PostgreSQL deixa de resoldre'l com a nom de sortida i el busca com a columna de la taula, on no existeix.
Limitació 2: en cas d'ambigüitat, guanya la columna d'entrada
Aquesta és la perillosa. Si un àlies del SELECT coincideix amb el nom d'una columna real de la taula, PostgreSQL fa servir la columna de la taula, no el teu àlies:
-- ⚠️ INCORRECTA: 'categoria_id' és alhora àlies i columna real
SELECT proveidor_id AS categoria_id,
COUNT(*) AS productes
FROM productes
GROUP BY categoria_id;ERROR: column "productes.proveidor_id" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT proveidor_id AS categoria_id,
^El missatge sembla absurd —"però si he escrit GROUP BY categoria_id, que és justament l'àlies de proveidor_id"— fins que entens la regla: el GROUP BY ha resolt categoria_id com a productes.categoria_id, la columna real. I aleshores proveidor_id queda solta al SELECT, que és exactament el que l'error denuncia. Un àlies que ombreja el nom d'una columna és sempre mala idea.
Limitació 3: HAVING no accepta àlies
-- ⚠️ INCORRECTA
SELECT categoria_id, COUNT(*) AS productes
FROM productes
GROUP BY categoria_id
HAVING productes > 3;La concessió de PostgreSQL cobreix GROUP BY i ORDER BY, però no HAVING. Allà cal repetir l'agregat sencer: HAVING COUNT(*) > 3. Ho desenvolupa la lliçó 04-06.
Resum per clàusula i per motor
| Clàusula | Veu els àlies del SELECT? |
|---|---|
WHERE |
No, a cap motor |
GROUP BY |
Sí a PostgreSQL, MySQL i SQLite (extensió). No a SQL Server ni a Oracle anterior a 23ai |
HAVING |
No a PostgreSQL, SQL Server ni Oracle. Sí a MySQL i SQLite |
ORDER BY |
Sí a tots |
Recomanació del curs: encara que PostgreSQL t'ho permeti, repeteix l'expressió completa al
GROUP BY. És més llarg, sí, però és portable, no depèn de regles de resolució de noms i funciona igual a les quatre clàusules. Reserva els àlies per a l'ORDER BY, on són estàndard i no tenen sorpreses.
- Grups i
NULL
NULLA 04-03 vas veure la taula d'on els NULL es consideren iguals entre si, i GROUP BY era a la llista. Aquí el tens en acció, i és una de les coses més útils de tota la lliçó:
SELECT empleat_id,
COUNT(*) AS comandes,
SUM(despeses_enviament) AS ports
FROM comandes
GROUP BY empleat_id
ORDER BY empleat_id NULLS LAST;| empleat_id | comandes | ports |
|---|---|---|
| 4 | 4 | 22.40 |
| 5 | 4 | 32.30 |
| 6 | 2 | 17.45 |
| (null) | 10 | 46.10 |
4 grups, i el quart és el dels NULL: 10 comandes del canal web amb 46,10 € de ports. GROUP BY ha ajuntat els deu nuls en un sol grup, tot i que NULL = NULL sigui UNKNOWN.
I això és exactament el que vols: el canal web és una categoria de negoci real i mereix la seva fila. Compara-ho amb el que hauria passat si el disseny hagués fet servir un sentinella (empleat_id = 0): el grup existiria igual, però a més s'esmunyiria a COUNT(DISTINCT empleat_id) com si fos un comercial de debò, donant 4 en lloc de 3.
Perquè l'informe es llegeixi bé, la fila del nul necessita una etiqueta. Amb COALESCE (06-04) o CASE (06-05) es resol; de moment, el NULLS LAST de l'ORDER BY (02-05) almenys la col·loca on toca.
El mateix amb els referits:
SELECT referit_per_id,
COUNT(*) AS clients
FROM clients
GROUP BY referit_per_id
ORDER BY clients DESC, referit_per_id NULLS LAST;| referit_per_id | clients |
|---|---|
| (null) | 7 |
| 1 | 3 |
| 2 | 1 |
| 5 | 1 |
| 6 | 1 |
| 7 | 1 |
| 9 | 1 |
7 files. El grup nul (7 clients que van arribar pel seu compte) és el més gran, i la Lucía Martínez Soler (id 1) és la millor prescriptora amb 3 recomanats. Aquesta és una dada accionable que cap consulta anterior del curs no podia donar.
GROUP BY amb JOIN: el patró central de l'anàlisi
GROUP BY amb JOIN: el patró central de l'anàlisiArribem al que de debò es fa cada dia a qualsevol empresa. La consulta canònica de quatre taules de 03-02 continua sent la base; l'únic que canvia és que ara hi posem GROUP BY a sobre.
9.1. Vendes per categoria
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
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 | Llar sostenible | 7 | 10 | 88.58 |
| 5 | Higiene personal | 3 | 9 | 31.50 |
Aquestes són les xifres de facturació per categoria de BotigaVerda. Sumen 727,95 €, el total de 04-04. I ja expliquen una història comercial: Alimentació lidera en facturació i en unitats (49 de 113), mentre que Cosmètica natural factura 156,32 € amb només 16 unitats — el seu tiquet per unitat és gairebé cinc vegades més gran.
Cinc files, no sis. Hi falta Complements. Hi tornarem a la secció 10.
9.2. Vendes per client
SELECT c.id,
c.nom || ' ' || c.cognoms AS client,
c.pais,
COUNT(DISTINCT co.id) AS comandes,
COUNT(*) AS linies,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS total_gastat
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
JOIN productes AS p ON lc.producte_id = p.id
GROUP BY c.id, c.nom, c.cognoms, c.pais
ORDER BY total_gastat DESC, c.id;| id | client | pais | comandes | linies | total_gastat |
|---|---|---|---|---|---|
| 7 | Sofia Moreira Costa | Portugal | 2 | 5 | 111.88 |
| 1 | Lucía Martínez Soler | Espanya | 3 | 9 | 107.60 |
| 9 | Camille Dubois | França | 2 | 4 | 70.87 |
| 10 | Julien Moreau | França | 1 | 3 | 66.90 |
| 4 | Javier Ortega Ruiz | Espanya | 2 | 4 | 62.93 |
| 2 | Carlos Ferrer Ibáñez | Espanya | 2 | 4 | 59.46 |
| 6 | Pau Llorens Vidal | Espanya | 2 | 3 | 57.33 |
| 5 | Ana Belmonte Roca | Espanya | 2 | 4 | 54.85 |
| 8 | Tiago Almeida Nunes | Portugal | 1 | 3 | 44.60 |
| 12 | Diego Ramos Herrera | Espanya | 1 | 3 | 31.70 |
| 11 | Elena Navarro Puig | Espanya | 1 | 2 | 30.30 |
| 3 | Marta Sanchis Gil | Espanya | 1 | 3 | 29.53 |
12 files —els 12 clients que han comprat— i el rànquing de clients de BotigaVerda: Sofia Moreira Costa lidera amb 111,88 €, seguida molt de prop per Lucía Martínez Soler amb 107,60 € repartits en tres comandes.
Dos detalls tècnics que cal veure:
COUNT(DISTINCT co.id)és imprescindible.COUNT(*)compta línies, no comandes: la Lucía té 9 línies en 3 comandes. És la multiplicació de files de 03-02, iDISTINCTés la manera de desfer-la en comptar.- Totes les columnes no agregades són al
GROUP BY. Com quec.idés la PK declients, PostgreSQL ens permetria escriure nomésGROUP BY c.id; s'han llistat totes perquè la consulta sigui portable.
I l'avís, ara resolt: si afegissis SUM(co.despeses_enviament) a aquesta consulta obtindries un número inflat, perquè cada comanda hi apareix tantes vegades com línies tingui. La Lucía pagaria els seus ports nou vegades. És exactament el problema de 04-04 secció 11, i la solució és la mateixa: agregar els ports en una consulta a part (o, des del mòdul 7, amb una subconsulta que col·lapsi les línies abans d'unir).
9.3. Unitats per producte
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
ORDER BY unitats DESC, p.id
LIMIT 8;| 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 primeres de 17 files.)
17 files en total, no 20: els productes 13, 19 i 20 no s'han venut mai i no hi apareixen. I observa el contrast entre les dues primeres columnes: l'arròs i el tomàquet empaten en unitats (14), però l'arròs factura més del doble perquè costa el doble. El producte més venut en unitats i el que més factura gairebé mai no són el mateix, i per això un informe de vendes necessita totes dues mètriques.
- Grups buits: per què la categoria 6 no apareix
Torna a la secció 9.1: cinc categories a l'informe de vendes, sis al catàleg. Complements ha desaparegut.
No és un error del GROUP BY. És l'INNER JOIN de 03-02 fent el que fa: descartar el que no casa. L'únic producte de Complements (l'espirulina) no apareix a linies_comanda, així que cap fila del FROM no pertany a aquella categoria, i un grup que no té files no existeix.
GROUP BY no inventa grups: només reparteix les files que li arriben.
flowchart TD
A["categories: 6 files"] --> B["INNER JOIN amb les vendes"]
B --> C["Complements no casa<br/>❌ es descarta al FROM"]
C --> D["GROUP BY només veu 5 categories<br/>→ 5 files"]
A --> E["LEFT JOIN des de categories"]
E --> F["Complements es conserva<br/>amb les vendes a NULL"]
F --> G["GROUP BY veu 6 categories<br/>→ 6 files"]
La solució: LEFT JOIN des de la taula que ha de sortir sencera
SELECT cat.id,
cat.nom AS categoria,
COUNT(lc.id) AS linies,
COALESCE(SUM(lc.quantitat), 0) AS unitats,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM categories AS cat
LEFT JOIN productes AS p ON p.categoria_id = cat.id
LEFT JOIN linies_comanda AS lc ON lc.producte_id = p.id
GROUP BY cat.id, cat.nom
ORDER BY cat.id;| id | categoria | linies | unitats | facturacio |
|---|---|---|---|---|
| 1 | Alimentació | 16 | 49 | 256.27 |
| 2 | Cosmètica natural | 10 | 16 | 156.32 |
| 3 | Llar sostenible | 7 | 10 | 88.58 |
| 4 | Begudes | 11 | 29 | 195.28 |
| 5 | Higiene personal | 3 | 9 | 31.50 |
| 6 | Complements | 0 | 0 | (null) |
6 files. Aquí tens Complements, amb la veritat: 0 línies, 0 unitats i una facturació nul·la.
Tres decisions d'aquella consulta mereixen explicació, i són la part més important de la secció:
1. COUNT(lc.id) i no COUNT(*). Això és crític. La fila de Complements existeix al resultat del LEFT JOIN —amb totes les columnes de linies_comanda a NULL—, així que COUNT(*) la comptaria i retornaria 1, no 0. COUNT(lc.id) compta només els valors no nuls i retorna l'honest 0.
-- La diferència, sobre la mateixa consulta
COUNT(*) -- Complements: 1 ❌ hi ha una fila, però no hi ha cap venda
COUNT(lc.id) -- Complements: 0 ✅És la regla de 04-04 secció 3 aplicada al cas que més importa. En qualsevol GROUP BY sobre un LEFT JOIN, compta sempre una columna de la taula de la dreta, mai *.
2. La facturació surt *(null)* i no 0.00. Perquè SUM d'un conjunt buit és NULL (04-04, secció 7). Per presentar-lo com a 0.00 caldria COALESCE(SUM(...), 0), que és el que hem fet amb les unitats. COALESCE és de 06-04; aquí s'ha fet servir una vegada perquè vegis el contrast entre les dues columnes.
3. Els dos JOIN són LEFT. Recorda la regla de 03-03 secció 7: un INNER JOIN després d'un LEFT JOIN n'anul·la l'efecte. Si el segon salt fos JOIN linies_comanda, Complements tornaria a desaparèixer.
El mateix patró sobre productes
SELECT p.id,
p.nom AS producte,
p.stock,
p.actiu,
COUNT(lc.id) AS vegades_venut,
COALESCE(SUM(lc.quantitat), 0) AS unitats
FROM productes AS p
LEFT JOIN linies_comanda AS lc ON lc.producte_id = p.id
GROUP BY p.id, p.nom, p.stock, p.actiu
HAVING COUNT(lc.id) = 0
ORDER BY p.id;| id | producte | stock | actiu | vegades_venut | unitats |
|---|---|---|---|---|---|
| 13 | Espelmes de cera de soja (pack 2) | 0 | true | 0 | 0 |
| 19 | Desodorant natural en barra 50 g | 75 | true | 0 | 0 |
| 20 | Càpsules d'espirulina 120 u | 55 | false | 0 | 0 |
3 files, els mateixos tres productes mai venuts que vas trobar amb l'anti-join de 03-03, ara amb el diagnòstic al costat: sense estoc, amb problema comercial, descatalogat. (El HAVING és la lliçó següent; aquí apareix de passada perquè és la manera natural de filtrar per un recompte.)
La regla que cal endur-se: un
INNER JOINrespon a "quant ha venut cada categoria que ha venut alguna cosa?"; unLEFT JOINrespon a "quant ha venut cada categoria?". La segona és gairebé sempre la pregunta del negoci, perquè un zero també és informació: li diu a la direcció que Complements no està funcionant. Un informe que amaga els zeros amaga precisament els problemes.
- Ordenar per l'agregat i quedar-se amb el top N
ORDER BY s'executa després del SELECT (pas 6 del diagrama), així que pot ordenar per un agregat o pel seu àlies sense problema:
SELECT p.id,
p.nom AS producte,
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
ORDER BY facturacio DESC, p.id
LIMIT 5;| id | producte | facturacio |
|---|---|---|
| 1 | Oli d'oliva verge extra 500 ml | 109.53 |
| 15 | Te verd matcha cerimonial 30 g | 88.00 |
| 6 | Crema facial d'àloe vera 50 ml | 70.42 |
| 16 | Kombutxa de gingebre 750 ml | 56.43 |
| 2 | Arròs integral ecològic 1 kg | 54.60 |
El top 5 de productes per facturació de BotigaVerda. L'oli d'oliva és el producte estrella amb 109,53 €, el 15 % de tota la facturació.
Fixa't en dues coses:
- L'
ORDER BYacaba ambp.id, una columna única, com mana la convenció del curs des de 02-05. Sense ella, dos productes amb la mateixa facturació podrien sortir en ordre diferent a cada execució i la paginació seria inestable. - També es pot ordenar per un agregat que no és al
SELECT:ORDER BY SUM(lc.quantitat) DESCés perfectament legal encara que no mostris les unitats. És legal i de vegades confús, així que fes-ho servir amb compte.
La combinació GROUP BY + ORDER BY agregat DESC + LIMIT n és el patró top N, i és probablement la consulta analítica més demanada que existeix: els 10 clients que més compren, els 5 productes que menys roten, els 3 comercials amb més vendes.
Nota: si el que vols és un top N dins de cada grup —"els 3 productes més venuts de cada categoria"—,
GROUP BYiLIMITno en tenen prou: cal una funció de finestra (ROW_NUMBER() OVER (PARTITION BY ...)), i això és el mòdul 10.
ROLLUP, GROUPING SETS i CUBE
ROLLUP, GROUPING SETS i CUBEUn informe real gairebé sempre necessita subtotals i un total general al costat del detall. Escriure això amb UNION ALL de diverses consultes és tediós i lent, perquè obliga a recórrer la taula diverses vegades. SQL ofereix tres extensions del GROUP BY per resoldre-ho en una sola passada:
| Construcció | Què afegeix |
|---|---|
ROLLUP (a, b) |
Els grups (a,b), més els subtotals per a, més el total general. Jeràrquic |
CUBE (a, b) |
Totes les combinacions: (a,b), (a), (b) i el total general |
GROUPING SETS ((a,b), (a), ()) |
Exactament els conjunts que tu enumeris. És la forma general; ROLLUP i CUBE en són dreceres |
Un exemple mínim amb ROLLUP, sobre les vendes per categoria i any de la secció 5:
SELECT cat.nom AS categoria,
EXTRACT(YEAR FROM co.data_comanda) AS any_,
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
JOIN productes AS p ON lc.producte_id = p.id
JOIN categories AS cat ON p.categoria_id = cat.id
GROUP BY ROLLUP (cat.nom, EXTRACT(YEAR FROM co.data_comanda))
ORDER BY categoria NULLS LAST, any_ NULLS LAST;| categoria | any_ | facturacio |
|---|---|---|
| Alimentació | 2025 | 215.67 |
| Alimentació | 2026 | 40.60 |
| Alimentació | (null) | 256.27 |
| Begudes | 2025 | 146.55 |
| Begudes | 2026 | 48.73 |
| Begudes | (null) | 195.28 |
| Cosmètica natural | 2025 | 128.22 |
| Cosmètica natural | 2026 | 28.10 |
| Cosmètica natural | (null) | 156.32 |
| Higiene personal | 2025 | 24.50 |
| Higiene personal | 2026 | 7.00 |
| Higiene personal | (null) | 31.50 |
| Llar sostenible | 2025 | 88.58 |
| Llar sostenible | (null) | 88.58 |
| (null) | (null) | 727.95 |
15 files: les 9 combinacions reals, 5 subtotals per categoria i el total general de 727,95 € a l'última. Les files de subtotal es reconeixen perquè les columnes agrupades de nivell inferior valen NULL.
I aquí hi ha l'única complicació de ROLLUP: aquells NULL de subtotal són indistingibles d'un NULL de dada. La funció GROUPING(columna) serveix per diferenciar-los (retorna 1 si la fila és un subtotal per aquella columna), i combinada amb CASE (06-05) permet etiquetar les files com a "Total categoria" o "TOTAL GENERAL".
No hi aprofundim més: ROLLUP i companyia es fan servir molt en informes de direcció i en eines de business intelligence, però el seu lloc natural és després de dominar CASE i les subconsultes.
Nota de dialecte:
ROLLUP,CUBEiGROUPING SETSexisteixen a PostgreSQL 9.5+, SQL Server i Oracle. MySQL només téGROUP BY ... WITH ROLLUP(sintaxi diferent i senseCUBEniGROUPING SETS). SQLite no en té cap: cal emular-los ambUNION ALL.
Errors habituals i consells
- Posar al
SELECTuna columna que no està agrupada ni agregada.column ... must appear in the GROUP BY clause. És l'error més freqüent de la lliçó, i a MySQL permissiu o a SQLite no dona error: retorna un valor arbitrari. - Fer servir
COUNT(*)en unGROUP BYsobre unLEFT JOIN. Retorna 1 on hauria de retornar 0. Compta una columna de la taula dreta:COUNT(lc.id). - Esperar que apareguin els grups buits amb un
INNER JOIN. La categoria 6 no hi surt. Si l'informe ha de llistar totes les categories, comença percategoriesambLEFT JOIN. - Ficar un
INNER JOINdesprés delLEFT JOINa la mateixa cadena. Anul·la elLEFTi els grups buits tornen a desaparèixer (03-03). - Sumar una columna de capçalera després d'unir amb el detall. Continua sent l'error de 04-04: els ports de la Lucía es comptarien nou vegades.
- Comptar comandes amb
COUNT(*)en una consulta que parteix delinies_comanda. Compta línies. Fes servirCOUNT(DISTINCT co.id). - Fer servir un àlies del
SELECTalHAVING.column "..." does not exista PostgreSQL. Repeteix l'agregat. - Donar per fet que
GROUP BYgenera totes les combinacions possibles. Només genera les que existeixen a les dades: 8 files de país × estat, no 15. - Ordenar trams alfabèticament sense prefix numèric. "de 10 a 15 €" va abans que "de 5 a 10 €". Numera les etiquetes.
- Oblidar la columna única al final de l'
ORDER BY. Amb empats, l'ordre deixa de ser determinista. - Consell: escriu primer la consulta sense agregar i mira les files. Si el detall no és el que esperes, l'agregat que hi posis a sobre estarà malament encara que compili.
- Consell: valida que els grups sumin el total. 256,27 + 195,28 + 156,32 + 88,58 + 31,50 = 727,95 €. Si no quadra amb la xifra global, hi ha files perdudes o duplicades.
- Consell: compta els grups que esperes abans d'executar. Si agrupes per
estathan de sortir com a màxim 5 files (el domini delCHECK); si en surten 6, hi ha un valor inesperat a les dades.
Exercicis
Exercici 1
Logística vol un informe d'activitat per comercial. Escriu una consulta sobre comandes i empleats que retorni, per als 8 empleats (apareguin o no a comandes): el seu id, nom complet, càrrec, nombre de comandes gestionades i suma de despeses d'enviament d'aquelles comandes.
Ordena per nombre de comandes descendent. Després respon:
- Quantes files retorna i per què?
- Què hauria passat amb un
INNER JOIN? - Per què no hi pot aparèixer el canal web?
Exercici 2
Màrqueting vol segmentar el catàleg per proveïdor. Escriu una consulta que retorni, per a cada proveïdor: el seu nom, el seu país, si està actiu, quants productes subministra, el preu mitjà d'aquells productes (dos decimals) i les unitats venudes de tots ells.
Hi ha d'aparèixer també el proveïdor inactiu. Després respon: quantes unitats ha venut EcoNordic Supplies tot i estar inactiu, i què et diu això del negoci?
Exercici 3
Direcció vol l'informe de vendes per mes de tot l'històric, amb aquestes columnes: mes (en format AAAA-MM), nombre de comandes diferents, nombre de línies, unitats i facturació.
- Escriu-lo. (Pista: pots agrupar per l'expressió
TO_CHAR(co.data_comanda, 'YYYY-MM'), una funció de cadena que veuràs a fons a 06-03.) - Identifica el millor mes i el pitjor.
- Explica per què la suma de les facturacions mensuals ha de donar 727,95 € i comprova-ho.
Solucions
Solució 1
SELECT e.id,
e.nom || ' ' || e.cognoms AS empleat,
e.carrec,
COUNT(co.id) AS comandes,
COALESCE(SUM(co.despeses_enviament), 0.00) AS ports
FROM empleats AS e
LEFT JOIN comandes AS co ON co.empleat_id = e.id
GROUP BY e.id, e.nom, e.cognoms, e.carrec
ORDER BY comandes DESC, e.id;| id | empleat | carrec | comandes | ports |
|---|---|---|---|---|
| 4 | Óscar Peris Blasco | Comercial | 4 | 22.40 |
| 5 | Laia Puig Sanchis | Comercial | 4 | 32.30 |
| 6 | Marc Estévez Roig | Atenció al client | 2 | 17.45 |
| 1 | Rosa Alcázar Vives | Directora general | 0 | 0.00 |
| 2 | Andrés Company Talens | Responsable de vendes | 0 | 0.00 |
| 3 | Beatriz Nadal Ripoll | Responsable de logística | 0 | 0.00 |
| 7 | Irene Salvador Mira | Operària de magatzem | 0 | 0.00 |
| 8 | Daniel Vercher Lluch | Analista de dades | 0 | 0.00 |
1. 8 files, els 8 empleats. El LEFT JOIN des d'empleats conserva els cinc que no han gestionat mai cap comanda, i COUNT(co.id) els dona un 0 correcte (amb COUNT(*) haurien sortit amb 1). COALESCE converteix el NULL de SUM en un 0.00 presentable.
2. Amb INNER JOIN en sortirien 3 files: només l'Óscar, la Laia i en Marc. Desapareixerien la Rosa, l'Andrés, la Beatriz, la Irene i en Daniel — que és exactament el que passava a 03-04, on els vas conèixer com "els empleats que no han gestionat mai una comanda". Un informe de recursos humans amb 3 de 8 persones no és un informe.
3. El canal web no hi pot aparèixer perquè aquelles 10 comandes tenen empleat_id a NULL i no casen amb cap fila d'empleats. En partir d'empleats amb LEFT JOIN, aquelles files queden al costat dret sense parella i es descarten: la suma de la columna comandes dona 10, no 20. Per veure-les caldria partir de comandes (GROUP BY empleat_id, secció 8) o fer servir un FULL OUTER JOIN (03-05). És l'asimetria del LEFT JOIN en estat pur: decideix quin costat surt sencer, i l'altre perd els seus orfes.
Solució 2
SELECT pr.id,
pr.nom AS proveidor,
pr.pais,
pr.actiu,
COUNT(DISTINCT p.id) AS productes,
ROUND(AVG(p.preu), 2) AS preu_mitja,
COALESCE(SUM(lc.quantitat), 0) AS unitats_venudes
FROM proveidors AS pr
LEFT JOIN productes AS p ON p.proveidor_id = pr.id
LEFT JOIN linies_comanda AS lc ON lc.producte_id = p.id
GROUP BY pr.id, pr.nom, pr.pais, pr.actiu
ORDER BY unitats_venudes DESC, pr.id;| id | proveidor | pais | actiu | productes | preu_mitja | unitats_venudes |
|---|---|---|---|---|---|---|
| 1 | Huerta del Turia | Espanya | true | 5 | 6.73 | 53 |
| 2 | BioSierra Ibérica | Espanya | true | 3 | 5.58 | 21 |
| 4 | Maison Nature | França | true | 4 | 10.57 | 14 |
| 3 | Verde Atlántico | Portugal | true | 4 | 13.52 | 13 |
| 5 | EcoNordic Supplies | Alemanya | false | 4 | 9.01 | 12 |
5 files, els cinc proveïdors, amb els 20 productes repartits: 5 + 3 + 4 + 4 + 4 = 20. Dos detalls importants d'aquesta consulta:
COUNT(DISTINCT p.id)i noCOUNT(p.id). Després del segonLEFT JOIN, cada producte apareix tantes vegades com s'hagi venut: l'oli hi seria cinc vegades.COUNT(p.id)donaria 16 per a Huerta del Turia en lloc de 5. És la multiplicació de files de 03-02, iDISTINCTn'és la correcció.AVG(p.preu)també n'està afectada, i aquesta sí que no té arranjament ambDISTINCT. El preu mitjà de Huerta del Turia surt 6,73 €, no la mitjana simple dels seus cinc productes (que és 5,74 €), perquè la mitjana queda ponderada pel nombre de vegades que s'ha venut cada referència: l'oli de 12,50 € hi entra cinc vegades i el tomàquet d'1,95 € només dues. És una mitjana legítima —"preu mitjà del que es factura"— però no és la que demanava l'enunciat. Per al preu mitjà de catàleg cal calcular-lo en una consulta a part sobreproductes, o amb una subconsulta (mòdul 7). És exactament el parany de granularitat de 04-04, ara sobre una mitjana en lloc d'una suma, i és més insidiós perquè el número resultant sembla plausible.
EcoNordic Supplies ha venut 12 unitats tot i estar inactiu, perquè proveidors.actiu = false significa "ja no li comprem", no "els seus productes desapareixen del catàleg". Les seves quatre referències són el detergent (10), les espelmes (13), el raspall de bambú (18) i l'espirulina (20); dues d'elles no s'han venut mai, però el raspall de bambú ha col·locat 9 unitats i el detergent 3. La dada accionable és clara: hi ha dos productes que es venen amb normalitat i el proveïdor dels quals ja no està operatiu. És un problema d'aprovisionament a tocar, i aquesta consulta és exactament la que el detecta.
Solució 3
SELECT TO_CHAR(co.data_comanda, 'YYYY-MM') AS mes,
COUNT(DISTINCT co.id) AS comandes,
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 comandes AS co ON lc.comanda_id = co.id
GROUP BY TO_CHAR(co.data_comanda, 'YYYY-MM')
ORDER BY mes;| mes | comandes | linies | unitats | facturacio |
|---|---|---|---|---|
| 2025-03 | 2 | 5 | 10 | 68.80 |
| 2025-04 | 2 | 5 | 14 | 61.28 |
| 2025-05 | 2 | 5 | 6 | 58.85 |
| 2025-06 | 2 | 5 | 14 | 95.48 |
| 2025-07 | 1 | 3 | 9 | 44.60 |
| 2025-08 | 1 | 2 | 3 | 48.27 |
| 2025-09 | 1 | 2 | 13 | 32.76 |
| 2025-10 | 2 | 5 | 13 | 97.20 |
| 2025-11 | 1 | 3 | 6 | 31.70 |
| 2025-12 | 2 | 5 | 7 | 64.58 |
| 2026-01 | 2 | 4 | 6 | 75.10 |
| 2026-02 | 2 | 3 | 12 | 49.33 |
2. El millor mes és l'octubre del 2025 amb 97,20 €, seguit molt de prop pel juny del 2025 amb 95,48 €. El pitjor és el novembre del 2025 amb 31,70 €, un mes amb una sola comanda. Amb volums tan petits, un mes bo o dolent depèn de si va caure una comanda gran, i això és una lliçó d'anàlisi: amb 20 comandes no es pot parlar d'estacionalitat.
3. La suma ha de donar 727,95 € perquè cadascuna de les 47 línies pertany exactament a una comanda, cada comanda té exactament una data, i cada data pertany exactament a un mes. Els grups són una partició del conjunt de línies: no se solapen i no deixen res fora. Comprovació:
68.80 + 61.28 + 58.85 + 95.48 + 44.60 + 48.27
+ 32.76 + 97.20 + 31.70 + 64.58 + 75.10 + 49.33 = 727.95I el recompte de línies: 5+5+5+5+3+2+2+5+3+5+4+3 = 47. Totes dues quadren.
Aquesta comprovació —els grups sumen el total?— és la millor validació que existeix per a una consulta agregada, i mereix convertir-se en un reflex. Si no quadra, o has perdut files (un INNER JOIN que descarta) o les has duplicat (un JOIN que multiplica).
Conclusió
GROUP BY és la clàusula que converteix SQL en anàlisi:
- Parteix les files en grups i aplica l'agregat a cadascun, retornant una fila per cada combinació diferent que existeixi a les dades — mai per combinacions que no existeixin.
- L'ordre lògic d'execució queda complet:
FROM/JOIN→WHERE(files) →GROUP BY(grups) →HAVING(grups) →SELECT(àlies) →DISTINCT→ORDER BY→LIMIT. D'aquí en surt queWHEREno pugui fer servir agregats iHAVINGsí. - La regla d'or: tota columna del
SELECTestà agrupada o agregada. PostgreSQL, SQL Server i Oracle ho exigeixen; MySQL ambONLY_FULL_GROUP_BYdesactivat i SQLite retornen un valor arbitrari, i això és un parany, no una comoditat. - Pots agrupar per una columna, per diverses (una fila per combinació existent) i per una expressió (
EXTRACT,CASE,TO_CHAR), repetint-la íntegra alGROUP BY. - PostgreSQL accepta un àlies del
SELECTalGROUP BYcom a extensió, però només nu, amb la columna real guanyant en cas d'ambigüitat, i mai aHAVING. Repetir l'expressió és sempre més segur. - Els
NULLformen el seu propi grup: les 10 comandes del canal web i els 7 clients espontanis apareixen com una fila amb*(null)*, i aquella fila és informació valuosa. GROUP BYambJOINés el patró central de l'anàlisi. Ja tens les xifres clau de BotigaVerda: facturació per categoria (Alimentació 256,27 € · Begudes 195,28 € · Cosmètica 156,32 € · Llar 88,58 € · Higiene 31,50 €), rànquing de clients (Sofia 111,88 € · Lucía 107,60 €) i top de productes (oli 109,53 € · matcha 88,00 €).- Els grups buits no existeixen per a un
INNER JOIN. La categoria 6 només apareix amb unLEFT JOINdes decategoriesiCOUNT(lc.id)en lloc deCOUNT(*), que és el que la converteix en un0honest en comptes d'un fals1. - El patró top N és
GROUP BY+ORDER BY agregat DESC+LIMIT; el top N per grup necessita funcions de finestra (mòdul 10), igual que qualsevol càlcul que hagi d'agregar sense col·lapsar les files. ROLLUP,CUBEiGROUPING SETSafegeixen subtotals i totals en una sola passada: elROLLUPde categoria i any va donar 15 files amb els 5 subtotals i el total general de 727,95 €.
A l'última lliçó del mòdul, la clàusula HAVING, tancaràs el cercle. Ja saps formar grups; ara aprendràs a filtrar-los: categories amb més de N productes, clients amb més d'una comanda, productes que superen cert volum. Veuràs per què HAVING pot fer servir agregats i WHERE no —el diagrama de la secció 2 t'ho dirà sol—, per què WHERE és sempre preferible quan la condició es pot avaluar fila a fila, i tindràs per fi la taula que compara els tres llocs on es pot filtrar en SQL: ON, WHERE i HAVING, tancant el fil que 03-03 va deixar obert.
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
