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

  1. GROUP BY: partir en grups i agregar cadascun
  2. L'ordre lògic d'execució, ampliat
  3. La regla d'or: agrupada o agregada
  4. Agrupar per una columna
  5. Agrupar per diverses columnes
  6. Agrupar per una expressió
  7. GROUP BY i l'àlies del SELECT
  8. Grups i NULL
  9. GROUP BY amb JOIN: el patró central de l'anàlisi
  10. Grups buits: per què la categoria 6 no apareix
  11. Ordenar per l'agregat i quedar-se amb el top N
  12. ROLLUP, GROUPING SETS i CUBE
  13. Errors habituals i consells
  14. Exercicis
  15. Conclusió

  1. GROUP BY: partir en grups i agregar cadascun

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

SELECT estat,
       COUNT(*) AS comandes
FROM comandes
GROUP BY estat
ORDER BY comandes DESC, estat;
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.

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

  1. La regla d'or: agrupada o agregada

És una sola frase, i d'ella se'n dedueix tota la resta:

Tota expressió del SELECT ha d'estar (a) al GROUP 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_BY desactivat Un valor arbitrari de qualsevol fila del grup
SQLite , 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_BY està actiu i no el desactivis.

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

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

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

  1. L'expressió apareix dues vegades, idèntica, al SELECT i al GROUP BY. És lletja i és necessària, pel motiu de sempre: l'àlies rang_preu encara no existeix quan s'executa el GROUP 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.
  2. El prefix numèric de les etiquetes (1 · , 2 · …) no és decoratiu. ORDER BY rang_preu ordena 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.

  1. GROUP BY i l'àlies del SELECT

Aquest 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

-- ⚠️ INCORRECTA
SELECT ROUND(preu) AS preu_enter, COUNT(*)
FROM productes
GROUP BY preu_enter + 0;
ERROR:  column "preu_enter" does not exist
LINE 3: GROUP BY preu_enter + 0;
                 ^

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;
ERROR:  column "productes" does not exist
LINE 4: 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 a PostgreSQL, MySQL i SQLite (extensió). No a SQL Server ni a Oracle anterior a 23ai
HAVING No a PostgreSQL, SQL Server ni Oracle. a MySQL i SQLite
ORDER BY 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.

  1. Grups i NULL

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

  1. GROUP BY amb JOIN: el patró central de l'anàlisi

Arribem 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, i DISTINCT és la manera de desfer-la en comptar.
  • Totes les columnes no agregades són al GROUP BY. Com que c.id és la PK de clients, PostgreSQL ens permetria escriure només GROUP 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.

  1. 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 JOIN respon a "quant ha venut cada categoria que ha venut alguna cosa?"; un LEFT JOIN respon 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.

  1. 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 BY acaba amb p.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 BY i LIMIT no en tenen prou: cal una funció de finestra (ROW_NUMBER() OVER (PARTITION BY ...)), i això és el mòdul 10.

  1. ROLLUP, GROUPING SETS i CUBE

Un 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, CUBE i GROUPING SETS existeixen a PostgreSQL 9.5+, SQL Server i Oracle. MySQL només té GROUP BY ... WITH ROLLUP (sintaxi diferent i sense CUBE ni GROUPING SETS). SQLite no en té cap: cal emular-los amb UNION ALL.

Errors habituals i consells

  • Posar al SELECT una 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 un GROUP BY sobre un LEFT 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 per categories amb LEFT JOIN.
  • Ficar un INNER JOIN després del LEFT JOIN a la mateixa cadena. Anul·la el LEFT i 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 de linies_comanda. Compta línies. Fes servir COUNT(DISTINCT co.id).
  • Fer servir un àlies del SELECT al HAVING. column "..." does not exist a PostgreSQL. Repeteix l'agregat.
  • Donar per fet que GROUP BY genera 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 estat han de sortir com a màxim 5 files (el domini del CHECK); 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:

  1. Quantes files retorna i per què?
  2. Què hauria passat amb un INNER JOIN?
  3. 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ó.

  1. 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.)
  2. Identifica el millor mes i el pitjor.
  3. 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 no COUNT(p.id). Després del segon LEFT 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, i DISTINCT n'és la correcció.
  • AVG(p.preu) també n'està afectada, i aquesta sí que no té arranjament amb DISTINCT. 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 sobre productes, 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.95

I 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/JOINWHERE (files) → GROUP BY (grups) → HAVING (grups) → SELECT (àlies) → DISTINCTORDER BYLIMIT. D'aquí en surt que WHERE no pugui fer servir agregats i HAVING sí.
  • La regla d'or: tota columna del SELECT està agrupada o agregada. PostgreSQL, SQL Server i Oracle ho exigeixen; MySQL amb ONLY_FULL_GROUP_BY desactivat 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 al GROUP BY.
  • PostgreSQL accepta un àlies del SELECT al GROUP BY com a extensió, però només nu, amb la columna real guanyant en cas d'ambigüitat, i mai a HAVING. Repetir l'expressió és sempre més segur.
  • Els NULL formen 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 BY amb JOIN é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 un LEFT JOIN des de categories i COUNT(lc.id) en lloc de COUNT(*), que és el que la converteix en un 0 honest en comptes d'un fals 1.
  • 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, CUBE i GROUPING SETS afegeixen subtotals i totals en una sola passada: el ROLLUP de 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

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