Fins ara, totes les nostres consultes han retornat detall: una fila del resultat per cada fila de la base de dades. Això respon preguntes del tipus "què?" —quins préstecs hi ha oberts, quins exemplars no ha tocat ningú—, però la direcció de BiblioRed en fa unes altres: quants préstecs vam fer el trimestre passat?, quina sucursal presta més?, quin és el títol estrella?, quant hem recaptat en recàrrecs?, quins socis superen els dos préstecs?.
Aquestes preguntes exigeixen passar del detall al resum: condensar moltes files en una sola xifra. És el que fan les funcions d'agregat i la clàusula GROUP BY, i és el pas que converteix una base de dades en una eina de gestió.
Aquesta lliçó també aclareix una cosa que arrossegues des de la primera consulta: en quin ordre s'executa realment una instrucció SQL. No és l'ordre en què s'escriu, i entendre-ho resol de cop mitja dotzena d'errors aparentment inexplicables.
Donem per sabuts els JOIN i les subconsultes de la lliçó 02-04: els farem servir sense tornar-los a explicar.
Contingut
- Les funcions d'agregat
COUNTi les seves tres formesSUM,AVG,MINiMAX- Com tracten els agregats els valors
NULL GROUP BY: agregar per grups- La regla de què pot aparèixer al
SELECT GROUP BYper diverses columnesHAVINGdavant deWHERE- L'ordre lògic d'execució d'una consulta
- Agregació combinada amb
JOIN - El problema de les files inflades per un
JOIN COALESCEiCASE WHENdins d'agregats- Introducció a les funcions de finestra
- Errors habituals i consells
- Exercicis
- Conclusió
- Les funcions d'agregat
Una funció d'agregat rep molts valors i en retorna un de sol. Les cinc de l'estàndard SQL, presents a tots els gestors:
| Funció | Què retorna | Tipus acceptats |
|---|---|---|
COUNT |
Nombre de files o de valors | Qualsevol |
SUM |
Suma | Numèrics |
AVG |
Mitjana aritmètica | Numèrics |
MIN |
Valor mínim | Numèrics, text, dates |
MAX |
Valor màxim | Numèrics, text, dates |
Sense GROUP BY, una funció d'agregat tracta tota la taula com un únic grup i retorna una sola fila:
En pots combinar diverses a la mateixa consulta, sempre que totes resumeixin el mateix conjunt de files:
SELECT COUNT(*) AS total_prestecs,
MIN(data_prestec) AS primer_prestec,
MAX(data_prestec) AS ultim_prestec,
SUM(recarrec) AS recarrecs_cobrats
FROM prestecs;| total_prestecs | primer_prestec | ultim_prestec | recarrecs_cobrats |
|---|---|---|---|
| 12 | 2026-03-02 | 2026-07-25 | 7.00 |
Dotze préstecs entre el 2 de març i el 25 de juliol de 2026, amb 7,00 € recaptats en recàrrecs.
Fixa't en un detall essencial: el resultat té una fila, no dotze. La consulta ja no parla de préstecs individuals, parla del conjunt. Per això no pots barrejar lliurement agregats amb columnes de detall; ho veurem a l'apartat 6.
COUNT i les seves tres formes
COUNT i les seves tres formesCOUNT sembla la funció més simple i és la que més confusió genera, perquè té tres variants que compten coses diferents.
SELECT COUNT(*) AS files,
COUNT(email) AS amb_email,
COUNT(DISTINCT sucursal_id) AS sucursals_diferents
FROM socis;| files | amb_email | sucursals_diferents |
|---|---|---|
| 10 | 9 | 4 |
| Forma | Compta | Resultat aquí |
|---|---|---|
COUNT(*) |
Files, sense mirar el contingut | 10 socis |
COUNT(columna) |
Valors no nuls d'aquesta columna | 9 (Pau Miralles no té correu) |
COUNT(DISTINCT columna) |
Valors no nuls diferents | 4 sucursals diferents |
La diferència entre les dues primeres és la que més vegades es passa per alt, i a prestecs és especialment eloqüent:
SELECT COUNT(*) AS prestecs,
COUNT(data_devolucio) AS tancats,
COUNT(*) - COUNT(data_devolucio) AS oberts,
COUNT(DISTINCT soci_id) AS socis_diferents
FROM prestecs;| prestecs | tancats | oberts | socis_diferents |
|---|---|---|---|
| 12 | 8 | 4 | 8 |
COUNT(data_devolucio) compta 8 perquè els quatre préstecs oberts tenen aquesta columna a NULL. És un truc molt útil: comptar els no nuls d'una columna equival a comptar les files que compleixen certa condició, si aquesta condició es reflecteix en la nul·litat.
I COUNT(DISTINCT soci_id) retorna 8, no 12: hi ha vuit socis diferents amb préstecs, perquè alguns en tenen més d'un.
Sobre el rendiment: existeix la llegenda que
COUNT(1)és més ràpid queCOUNT(*). És fals a PostgreSQL i a qualsevol gestor modern: són idèntics.COUNT(DISTINCT columna)sí que és notablement més car, perquè obliga a deduplicar.
SUM, AVG, MIN i MAX
SUM, AVG, MIN i MAXSELECT COUNT(*) AS llibres,
MIN(any_publicacio) AS mes_antic,
MAX(any_publicacio) AS mes_recent,
ROUND(AVG(any_publicacio), 2) AS any_mitja
FROM llibres;| llibres | mes_antic | mes_recent | any_mitja |
|---|---|---|---|
| 9 | 1904 | 2023 | 2000.89 |
ROUND(expressió, decimals) no és una funció d'agregat: arrodoneix el resultat. Sense ella, AVG sobre NUMERIC a PostgreSQL retorna un número amb moltíssims decimals (2000.8888888888888889), poc pràctic per a un informe.
MIN i MAX funcionen també sobre text (ordre alfabètic) i sobre dates (ordre cronològic):
SELECT MIN(cognoms) AS primer_alfabeticament,
MAX(cognoms) AS ultim_alfabeticament,
MIN(data_alta) AS soci_mes_antic,
MAX(data_alta) AS soci_mes_recent
FROM socis;| primer_alfabeticament | ultim_alfabeticament | soci_mes_antic | soci_mes_recent |
|---|---|---|---|
| Alsina | Vendrell | 2018-01-22 | 2024-10-01 |
Atenció a un parany clàssic: aquesta consulta et diu quin és el cognom mínim i quina és la data mínima, però no et diu que siguin de la mateixa persona. Cada agregat es calcula per separat. Per obtenir la fila completa del soci més antic cal una altra cosa:
SELECT soci_id, nom, cognoms, data_alta
FROM socis
WHERE data_alta = (SELECT MIN(data_alta) FROM socis);| soci_id | nom | cognoms | data_alta |
|---|---|---|---|
| 11 | Álvaro | Ferran | 2018-01-22 |
És la subconsulta escalar de la lliçó anterior, ara amb un agregat a dins. A l'apartat 13 veurem una alternativa amb funcions de finestra.
- Com tracten els agregats els valors
NULL
NULLAquí hi ha una regla i una excepció, i convé gravar-les:
Totes les funcions d'agregat ignoren els
NULL. L'única excepció ésCOUNT(*), que compta files i no mira el contingut.
Vegem-ho amb la columna recarrec, que té vuit valors i quatre NULL:
SELECT COUNT(*) AS files,
COUNT(recarrec) AS amb_recarrec,
SUM(recarrec) AS suma,
ROUND(AVG(recarrec), 4) AS mitjana_ignorant_nuls,
ROUND(SUM(recarrec) / COUNT(*), 4) AS mitjana_comptant_nuls_com_zero,
MIN(recarrec) AS minim,
MAX(recarrec) AS maxim
FROM prestecs;| files | amb_recarrec | suma | mitjana_ignorant_nuls | mitjana_comptant_nuls_com_zero | minim | maxim |
|---|---|---|---|---|---|---|
| 12 | 8 | 7.00 | 0.8750 | 0.5833 | 0.00 | 4.20 |
Dues mitjanes diferents per a les mateixes dades, i cap de les dues no està malament: signifiquen coses diferents.
AVG(recarrec)= 7,00 / 8 = 0,875 €. És "el recàrrec mitjà dels préstecs ja tancats", perquè els oberts encara no tenen recàrrec calculat.SUM(recarrec) / COUNT(*)= 7,00 / 12 = 0,583 €. És "el recàrrec mitjà per préstec realitzat", tractant els pendents com a zero.
La pregunta que t'has de fer sempre és: què significa el NULL en aquesta columna? Si significa "encara no se sap", ignorar-lo és correcte. Si significa "zero", ignorar-lo distorsiona la mitjana. A l'apartat 12 veurem COALESCE, l'eina per convertir explícitament NULL en zero quan aquesta és la semàntica correcta.
Un cas extrem que sorprèn:
SELECT SUM(recarrec), COUNT(recarrec), AVG(recarrec)
FROM prestecs
WHERE data_devolucio IS NULL; -- els quatre préstecs oberts| sum | count | avg |
|---|---|---|
| (NULL) | 0 | (NULL) |
SUM d'un conjunt on tots els valors són NULL retorna NULL, no zero. I el mateix si el conjunt és buit. COUNT, en canvi, retorna 0: és l'única funció d'agregat que mai no retorna NULL.
GROUP BY: agregar per grups
GROUP BY: agregar per grupsFins ara resumíem la taula sencera. GROUP BY la parteix en grups i calcula els agregats dins de cada grup, retornant una fila per grup.
| sucursal_id | exemplars |
|---|---|
| 1 | 6 |
| 2 | 4 |
| 3 | 3 |
| 4 | 2 |
Quatre files, una per sucursal, i la suma dels recomptes és 15: el total d'exemplars. Conceptualment passa això:
flowchart LR
T["exemplars<br/>15 files"] --> G1["sucursal_id = 1<br/>6 files"]
T --> G2["sucursal_id = 2<br/>4 files"]
T --> G3["sucursal_id = 3<br/>3 files"]
T --> G4["sucursal_id = 4<br/>2 files"]
G1 --> R["Resultat<br/>4 files,<br/>una per grup"]
G2 --> R
G3 --> R
G4 --> R
Un altre exemple, agrupant per una columna de text:
| estat | quants |
|---|---|
| disponible | 9 |
| prestat | 4 |
| baixa | 1 |
| reparacio | 1 |
Observa que es pot ordenar per l'agregat fent servir el seu àlies. I observa també que els grups surten de les dades que hi ha: si cap exemplar no estigués en reparació, aquesta fila simplement no apareixeria. GROUP BY no inventa mai grups buits.
Un tercer exemple, amb més d'un agregat per grup:
SELECT editorial,
COUNT(*) AS titols,
MIN(any_publicacio) AS mes_antic,
MAX(any_publicacio) AS mes_recent
FROM llibres
GROUP BY editorial
ORDER BY titols DESC, editorial;| editorial | titols | mes_antic | mes_recent |
|---|---|---|---|
| Edicions Marlia | 3 | 2012 | 2017 |
| Editorial Andana | 3 | 1989 | 2021 |
| Premsa Tècnica Nord | 2 | 2019 | 2023 |
| Ajuntament de Vallmar | 1 | 1904 | 1904 |
- La regla de què pot aparèixer al
SELECT
SELECTAquesta és la regla que més errors produeix en començar:
Tota columna que aparegui al
SELECTi no estigui dins d'una funció d'agregat ha de figurar alGROUP BY.
El motiu és pur sentit comú. Si agrupes els exemplars per sucursal, cada fila del resultat representa sis exemplars diferents (els de la sucursal 1). Quin codi hauria de mostrar? El de quin dels sis? La pregunta no té resposta, així que el gestor la rebutja:
ERROR: column "exemplars.codi" must appear in the GROUP BY clause
or be used in an aggregate functionLes tres maneres legítimes d'arreglar-ho, segons el que vulguis de veritat:
-- a) Afegir la columna al GROUP BY: canvien els grups (i aquí ja no agrupa res,
-- perquè codi és únic)
SELECT sucursal_id, codi, COUNT(*) FROM exemplars GROUP BY sucursal_id, codi;
-- b) Embolcallar-la en un agregat: "el codi més petit de cada sucursal"
SELECT sucursal_id, MIN(codi) AS primer_codi, COUNT(*) AS exemplars
FROM exemplars GROUP BY sucursal_id ORDER BY sucursal_id;
-- c) Treure-la del SELECT
SELECT sucursal_id, COUNT(*) FROM exemplars GROUP BY sucursal_id;Resultat de l'opció b):
| sucursal_id | primer_codi | exemplars |
|---|---|---|
| 1 | EJ-3082 | 6 |
| 2 | EJ-3081 | 4 |
| 3 | EJ-3083 | 3 |
| 4 | EJ-3088 | 2 |
L'excepció de la dependència funcional
PostgreSQL, des de la versió 9.1, admet una relaxació molt pràctica: si agrupes per la clau primària d'una taula, pots seleccionar qualsevol altra columna d'aquesta mateixa taula sense llistar-la, perquè la clau primària la determina de manera unívoca.
-- Legal a PostgreSQL: socis.cognoms depèn funcionalment de socis.soci_id
SELECT s.soci_id, s.nom, s.cognoms, COUNT(p.prestec_id) AS prestecs
FROM socis s
LEFT JOIN prestecs p ON p.soci_id = s.soci_id
GROUP BY s.soci_id
ORDER BY s.soci_id;És una comoditat real, però no és portable: en altres gestors hauries d'escriure GROUP BY s.soci_id, s.nom, s.cognoms. Si el teu SQL ha de funcionar en diversos motors, llista totes les columnes.
I el perill de SQLite
SQLite no aplica aquesta regla en absolut. La consulta que a PostgreSQL dóna error, a SQLite s'executa i retorna un codi qualsevol dels del grup, escollit de manera no documentada. No hi ha error, no hi ha avís, i l'informe és incorrecte.
sqlite> SELECT sucursal_id, codi, COUNT(*) FROM exemplars GROUP BY sucursal_id;
1|EJ-3095|6
2|EJ-3090|4
...Aquest EJ-3095 no representa res. És una de les diferències més perilloses entre tots dos gestors: la permissivitat de SQLite converteix un error en una dada falsa. Escriu el SQL com si PostgreSQL et vigilés, encara que siguis a SQLite.
GROUP BY per diverses columnes
GROUP BY per diverses columnesEn agrupar per dues columnes, els grups són les combinacions diferents de totes dues:
SELECT sucursal_id, estat, COUNT(*) AS quants
FROM exemplars
GROUP BY sucursal_id, estat
ORDER BY sucursal_id, estat;| sucursal_id | estat | quants |
|---|---|---|
| 1 | disponible | 4 |
| 1 | prestat | 2 |
| 2 | disponible | 2 |
| 2 | prestat | 2 |
| 3 | baixa | 1 |
| 3 | disponible | 2 |
| 4 | disponible | 1 |
| 4 | reparacio | 1 |
Vuit grups, els recomptes dels quals sumen 15. Igual que abans: només hi apareixen les combinacions que existeixen. La sucursal 1 no té cap exemplar en reparació, així que aquesta fila no hi és —no surt amb un 0—. Si necessites la graella completa amb zeros inclosos, hi ha dos camins: un CROSS JOIN que generi totes les combinacions (lliçó 02-04) i un LEFT JOIN contra les dades, o la tabulació amb CASE WHEN de l'apartat 12.
L'ordre de les columnes al GROUP BY no altera els grups (GROUP BY a, b i GROUP BY b, a produeixen els mateixos), però sí que convé que coincideixi amb l'ORDER BY perquè l'informe es llegeixi bé.
HAVING davant de WHERE
HAVING davant de WHEREWHERE filtra files, abans d'agrupar. HAVING filtra grups, després d'agrupar. La diferència és de moment, no de sintaxi.
Pregunta: quins socis tenen dos o més préstecs?
SELECT s.soci_id,
s.nom || ' ' || s.cognoms AS soci,
COUNT(*) AS prestecs
FROM prestecs p
INNER JOIN socis s ON s.soci_id = p.soci_id
GROUP BY s.soci_id, s.nom, s.cognoms
HAVING COUNT(*) >= 2
ORDER BY prestecs DESC, soci;| soci_id | soci | prestecs |
|---|---|---|
| 14 | Marta Alsina | 3 |
| 15 | Ivan Pereda | 2 |
| 16 | Núria Bastos | 2 |
Intentar el mateix amb WHERE és impossible:
SELECT s.soci_id, COUNT(*) FROM prestecs p
INNER JOIN socis s ON s.soci_id = p.soci_id
WHERE COUNT(*) >= 2
GROUP BY s.soci_id;I és lògic: quan s'avalua el WHERE, els grups encara no existeixen, així que no hi ha res per comptar.
Fer servir tots dos alhora
És l'habitual, i cadascun fa la seva feina:
-- Files: només els préstecs amb recàrrec. Grups: només els socis que superen 1 €.
SELECT s.soci_id,
s.cognoms,
COUNT(*) AS prestecs_amb_recarrec,
SUM(p.recarrec) AS total_recarrec
FROM prestecs p
INNER JOIN socis s ON s.soci_id = p.soci_id
WHERE p.recarrec > 0
GROUP BY s.soci_id, s.cognoms
HAVING SUM(p.recarrec) > 1.00
ORDER BY total_recarrec DESC;| soci_id | cognoms | prestecs_amb_recarrec | total_recarrec |
|---|---|---|---|
| 12 | Quiroga | 1 | 4.20 |
| 15 | Pereda | 1 | 1.40 |
| 18 | Vendrell | 1 | 1.40 |
| Clàusula | Què filtra | Quan actua | Admet agregats? |
|---|---|---|---|
WHERE |
Files individuals | Abans de GROUP BY |
No |
HAVING |
Grups ja formats | Després de GROUP BY |
Sí |
Regla pràctica d'eficiència: si una condició es pot expressar en WHERE, posa-la al WHERE. Filtrar abans d'agrupar significa agrupar menys files. Posar al HAVING una condició que no fa servir agregats (HAVING s.sucursal_id = 2) funciona en molts gestors, però és més lent i més confús.
- L'ordre lògic d'execució d'una consulta
Ara s'entén tot. Una consulta SQL no s'avalua en l'ordre en què s'escriu. L'ordre lògic és aquest:
flowchart TD
A["1. FROM<br/>pren les taules de partida"] --> B["2. JOIN ... ON<br/>aparella files"]
B --> C["3. WHERE<br/>descarta files individuals"]
C --> D["4. GROUP BY<br/>parteix el resultat en grups"]
D --> E["5. HAVING<br/>descarta grups sencers"]
E --> F["6. SELECT<br/>calcula columnes i agregats,<br/>aplica els àlies"]
F --> G["7. DISTINCT<br/>elimina files repetides"]
G --> H["8. ORDER BY<br/>ordena el resultat"]
H --> I["9. LIMIT / OFFSET<br/>retalla"]
Aquest diagrama explica d'un cop d'ull quatre comportaments que fins ara semblaven capricis:
WHEREno pot fer servir agregats. S'executa al pas 3; els grups es formen al 4.WHEREno pot fer servir els àlies delSELECT. ElSELECTés el pas 6, posterior.WHERE prestecs >= 2dóna error de columna inexistent.ORDER BYsí que pot fer servir els àlies delSELECT. És el pas 8, posterior al 6. Per aixòORDER BY prestecs DESCfuncionava.LIMITés l'últim. Retalla el resultat final, no les files llegides:LIMIT 3en una consulta ambGROUP BYretorna tres grups, no tres files de la taula.
-- ERROR: 'prestecs' és un àlies definit al pas 6, i WHERE és el pas 3
SELECT s.soci_id, COUNT(*) AS prestecs
FROM prestecs p JOIN socis s ON s.soci_id = p.soci_id
WHERE prestecs >= 2
GROUP BY s.soci_id;Insistim en això de lògic: és l'ordre en què cal raonar la consulta. L'optimitzador (lliçó 01-04) és lliure d'executar les coses en un altre ordre físic mentre el resultat sigui el mateix.
- Agregació combinada amb
JOIN
JOINAquí és on les dues últimes lliçons s'ajunten i BiblioRed comença a produir informes de veritat.
Préstecs per sucursal
Compte amb el matís: la sucursal d'un préstec és la de l'exemplar prestat, que no té per què ser la d'alta del soci.
SELECT su.nom AS sucursal,
COUNT(*) AS prestecs
FROM prestecs p
INNER JOIN exemplars e ON e.exemplar_id = p.exemplar_id
INNER JOIN sucursals su ON su.sucursal_id = e.sucursal_id
GROUP BY su.sucursal_id, su.nom
ORDER BY prestecs DESC;| sucursal | prestecs |
|---|---|
| Nord | 6 |
| Centre | 4 |
| Sud | 1 |
| Est | 1 |
La sucursal Nord concentra la meitat de l'activitat. Sumen 12 ✓.
Els llibres més prestats
SELECT l.titol,
COUNT(*) AS vegades_prestat
FROM prestecs p
INNER JOIN exemplars e ON e.exemplar_id = p.exemplar_id
INNER JOIN llibres l ON l.llibre_id = e.llibre_id
GROUP BY l.llibre_id, l.titol
ORDER BY vegades_prestat DESC, l.titol
LIMIT 5;| titol | vegades_prestat |
|---|---|
| El mapa del temps | 4 |
| Els pilars de la Terra | 3 |
| Àlgebra per a impacients | 1 |
| La casa de les marees | 1 |
| L'hivern dels ocells | 1 |
Important: en aquest llistat falten dos llibres. "Manual de jardineria urbana" i "Memòria de l'Eixample (1904)" no s'han prestat mai, i l'INNER JOIN els elimina abans d'agrupar. Si l'informe els ha d'incloure amb un 0, cal combinar LEFT JOIN amb COUNT(columna):
SELECT l.titol,
COUNT(p.prestec_id) AS vegades_prestat
FROM llibres l
LEFT JOIN exemplars e ON e.llibre_id = l.llibre_id
LEFT JOIN prestecs p ON p.exemplar_id = e.exemplar_id
GROUP BY l.llibre_id, l.titol
ORDER BY vegades_prestat DESC, l.titol;| titol | vegades_prestat |
|---|---|
| El mapa del temps | 4 |
| Els pilars de la Terra | 3 |
| Àlgebra per a impacients | 1 |
| La casa de les marees | 1 |
| L'hivern dels ocells | 1 |
| Quaderns de Ravenna | 1 |
| Rutes del delta | 1 |
| Manual de jardineria urbana | 0 |
| Memòria de l'Eixample (1904) | 0 |
Aquí hi ha una lliçó que val per si sola: COUNT(*) hauria retornat 1 en lloc de 0 per als dos últims, perquè el LEFT JOIN genera una fila plena de NULL i COUNT(*) compta files. COUNT(p.prestec_id) compta valors no nuls, i allà no n'hi ha cap.
Amb
LEFT JOIN, maiCOUNT(*): sempreCOUNT(columna_de_la_taula_dreta).
Préstecs per soci, inclosos els que no en tenen cap
SELECT s.soci_id,
s.nom || ' ' || s.cognoms AS soci,
COUNT(p.prestec_id) AS prestecs,
MAX(p.data_prestec) AS ultim_prestec
FROM socis s
LEFT JOIN prestecs p ON p.soci_id = s.soci_id
GROUP BY s.soci_id, s.nom, s.cognoms
ORDER BY prestecs DESC, soci;| soci_id | soci | prestecs | ultim_prestec |
|---|---|---|---|
| 14 | Marta Alsina | 3 | 2026-07-14 |
| 15 | Ivan Pereda | 2 | 2026-07-18 |
| 16 | Núria Bastos | 2 | 2026-05-04 |
| 11 | Álvaro Ferran | 1 | 2026-05-08 |
| 17 | Diego Salom | 1 | 2026-07-21 |
| 18 | Lucía Vendrell | 1 | 2026-04-12 |
| 19 | Pau Miralles | 1 | 2026-07-25 |
| 12 | Sonia Quiroga | 1 | 2026-05-19 |
| 20 | Elena Roig | 0 | (NULL) |
| 13 | Ramón Etxebarri | 0 | (NULL) |
Aquest és l'informe complet d'activitat de socis, amb els inactius inclosos. Compara'l amb l'anti-join de la lliçó anterior: allà només obteníem qui no tenia préstecs; ara tenim la foto sencera.
- El problema de les files inflades per un
JOIN
JOINReprenem l'exercici que vam deixar a mitges a la lliçó 02-04. Un responsable de BiblioRed demana: "quants socis i quants exemplars hi ha a cada sucursal". La consulta que surt sola és aquesta:
SELECT su.nom AS sucursal,
COUNT(s.soci_id) AS socis,
COUNT(e.exemplar_id) AS exemplars
FROM sucursals su
LEFT JOIN socis s ON s.sucursal_id = su.sucursal_id
LEFT JOIN exemplars e ON e.sucursal_id = su.sucursal_id
GROUP BY su.sucursal_id, su.nom
ORDER BY su.sucursal_id;| sucursal | socis | exemplars |
|---|---|---|
| Centre | 24 | 24 |
| Nord | 12 | 12 |
| Sud | 6 | 6 |
| Est | 2 | 2 |
Tots els números són incorrectes, i el fet que les dues columnes siguin idèntiques és el senyal d'alarma. Centre té 4 socis i 6 exemplars, no 24 i 24.
La causa: socis i exemplars no estan relacionades entre si; totes dues pengen de sucursals de manera independent. En unir-les, dins de cada sucursal es produeix un producte cartesià: 4 socis × 6 exemplars = 24 files, i cada COUNT compta aquestes 24. És l'explosió de files (fan trap).
Les tres solucions, de pitjor a millor:
-- a) COUNT(DISTINCT ...): funciona, i és el més ràpid d'escriure
SELECT su.nom AS sucursal,
COUNT(DISTINCT s.soci_id) AS socis,
COUNT(DISTINCT e.exemplar_id) AS exemplars
FROM sucursals su
LEFT JOIN socis s ON s.sucursal_id = su.sucursal_id
LEFT JOIN exemplars e ON e.sucursal_id = su.sucursal_id
GROUP BY su.sucursal_id, su.nom
ORDER BY su.sucursal_id;| sucursal | socis | exemplars |
|---|---|---|
| Centre | 4 | 6 |
| Nord | 3 | 4 |
| Sud | 2 | 3 |
| Est | 1 | 2 |
Correcte. Però COUNT(DISTINCT) és car i només salva els recomptes: un SUM continuaria inflat, perquè sumaria el mateix valor diverses vegades. Si en lloc de comptar exemplars sumessis imports, el resultat seria fals i DISTINCT no ho arreglaria.
-- b) Subconsultes escalars: cada xifra es calcula per separat
SELECT su.nom AS sucursal,
(SELECT COUNT(*) FROM socis s WHERE s.sucursal_id = su.sucursal_id) AS socis,
(SELECT COUNT(*) FROM exemplars e WHERE e.sucursal_id = su.sucursal_id) AS exemplars
FROM sucursals su
ORDER BY su.sucursal_id;
-- c) Agregar cada branca per separat i unir després: la forma canònica
WITH socis_per_sucursal AS (
SELECT sucursal_id, COUNT(*) AS socis FROM socis GROUP BY sucursal_id
),
exemplars_per_sucursal AS (
SELECT sucursal_id, COUNT(*) AS exemplars FROM exemplars GROUP BY sucursal_id
)
SELECT su.nom AS sucursal,
COALESCE(sp.socis, 0) AS socis,
COALESCE(ep.exemplars, 0) AS exemplars
FROM sucursals su
LEFT JOIN socis_per_sucursal sp ON sp.sucursal_id = su.sucursal_id
LEFT JOIN exemplars_per_sucursal ep ON ep.sucursal_id = su.sucursal_id
ORDER BY su.sucursal_id;Les tres retornen la taula correcta. L'opció c) és la que has d'interioritzar: quan calgui resumir dues branques independents, agrega cadascuna pel seu costat i uneix els resums. És l'única que escala a qualsevol nombre de branques i a qualsevol agregat, no només a COUNT.
Com detectar el problema a la pràctica: si un total et sembla sospitosament gran, treu els agregats i executa la consulta amb SELECT *. Si en surten moltes més files de les que esperaves, tens una explosió.
COALESCE i CASE WHEN dins d'agregats
COALESCE i CASE WHEN dins d'agregatsCOALESCE: substituir NULL per un valor
COALESCE(a, b, c, ...) retorna el primer argument que no sigui NULL. És l'eina per decidir explícitament què fer amb l'absència de valor.
SELECT s.soci_id,
s.cognoms,
COUNT(p.prestec_id) AS prestecs,
COALESCE(SUM(p.recarrec), 0) AS recarrec_total
FROM socis s
LEFT JOIN prestecs p ON p.soci_id = s.soci_id
GROUP BY s.soci_id, s.cognoms
ORDER BY recarrec_total DESC, s.soci_id;| soci_id | cognoms | prestecs | recarrec_total |
|---|---|---|---|
| 12 | Quiroga | 1 | 4.20 |
| 15 | Pereda | 2 | 1.40 |
| 18 | Vendrell | 1 | 1.40 |
| 11 | Ferran | 1 | 0.00 |
| 13 | Etxebarri | 0 | 0.00 |
| 14 | Alsina | 3 | 0.00 |
| 16 | Bastos | 2 | 0.00 |
| 17 | Salom | 1 | 0.00 |
| 19 | Miralles | 1 | 0.00 |
| 20 | Roig | 0 | 0.00 |
Sense el COALESCE, quatre socis mostrarien NULL en lloc de 0.00: els dos que no tenen cap préstec (Etxebarri i Roig) i els dos que tenen l'únic préstec encara obert i per tant amb el recàrrec sense calcular (Salom i Miralles). Un informe amb NULL en una columna de diners és un informe que ningú no sap llegir.
Fixa't en l'ordre en què s'aplica: COALESCE embolcalla l'agregat, no al revés. SUM(COALESCE(p.recarrec, 0)) també funcionaria i seria fins i tot més precís semànticament (tracta cada NULL individual com a zero), però per al total tant se val… llevat que sigui amb AVG, on sí que canvia el resultat, perquè altera el nombre de valors promitjats. Comprova-ho:
SELECT ROUND(AVG(recarrec), 4) AS avg_ignorant_nuls, -- 0.8750
ROUND(AVG(COALESCE(recarrec, 0)), 4) AS avg_tractant_nuls_zero -- 0.5833
FROM prestecs;CASE WHEN: comptar condicionalment
CASE és l'estructura condicional de SQL:
Ficat dins d'un SUM o d'un COUNT, permet tabular: produir diverses columnes que compten coses diferents del mateix grup. És la manera de construir un quadre de comandament en una sola consulta.
SELECT su.nom AS sucursal,
COUNT(*) AS total,
SUM(CASE WHEN e.estat = 'disponible' THEN 1 ELSE 0 END) AS disponibles,
SUM(CASE WHEN e.estat = 'prestat' THEN 1 ELSE 0 END) AS prestats,
SUM(CASE WHEN e.estat IN ('reparacio','baixa') THEN 1 ELSE 0 END) AS fora_de_servei
FROM exemplars e
INNER JOIN sucursals su ON su.sucursal_id = e.sucursal_id
GROUP BY su.sucursal_id, su.nom
ORDER BY su.nom;| sucursal | total | disponibles | prestats | fora_de_servei |
|---|---|---|---|---|
| Centre | 6 | 4 | 2 | 0 |
| Est | 2 | 1 | 0 | 1 |
| Nord | 4 | 2 | 2 | 0 |
| Sud | 3 | 2 | 0 | 1 |
Això és exactament la graella completa que GROUP BY sucursal_id, estat no podia donar: aquí sí que hi apareixen els zeros, perquè les columnes estan fixades per la consulta i no depenen de les dades.
Com funciona: per a cada fila, el CASE produeix 1 o 0, i SUM els suma. Una alternativa molt utilitzada és COUNT(CASE WHEN condició THEN 1 END) —sense ELSE, de manera que les files que no la compleixen donen NULL i COUNT les ignora—.
PostgreSQL ofereix a més una sintaxi estàndard més elegant, la clàusula FILTER:
SELECT su.nom AS sucursal,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE e.estat = 'disponible') AS disponibles,
COUNT(*) FILTER (WHERE e.estat = 'prestat') AS prestats
FROM exemplars e
INNER JOIN sucursals su ON su.sucursal_id = e.sucursal_id
GROUP BY su.sucursal_id, su.nom
ORDER BY su.nom;Mateix resultat, molt més llegible. FILTER no existeix a SQLite ni a MySQL; allà toca CASE WHEN. I CASE WHEN funciona a tot arreu, així que és l'opció segura.
- Introducció a les funcions de finestra
Acabem amb una capacitat que resol un problema que GROUP BY no pot: calcular un agregat sense perdre el detall.
Fixa't en la limitació. Aquesta consulta et diu quants préstecs té cada soci, però perd els préstecs individuals:
I si vols veure cada préstec i, al costat, quants en té aquest soci en total? Aquí entren les funcions de finestra (window functions): calculen un agregat sobre un conjunt de files relacionades, però retornen una fila per cada fila original.
SELECT s.cognoms,
p.prestec_id,
p.data_prestec,
COUNT(*) OVER (PARTITION BY p.soci_id) AS prestecs_del_soci
FROM prestecs p
INNER JOIN socis s ON s.soci_id = p.soci_id
ORDER BY s.cognoms, p.data_prestec;| cognoms | prestec_id | data_prestec | prestecs_del_soci |
|---|---|---|---|
| Alsina | 1 | 2026-03-02 | 3 |
| Alsina | 4 | 2026-04-06 | 3 |
| Alsina | 9 | 2026-07-14 | 3 |
| Bastos | 3 | 2026-03-11 | 2 |
| Bastos | 6 | 2026-05-04 | 2 |
| Ferran | 7 | 2026-05-08 | 1 |
| Miralles | 12 | 2026-07-25 | 1 |
| Pereda | 2 | 2026-03-05 | 2 |
| Pereda | 10 | 2026-07-18 | 2 |
| Quiroga | 8 | 2026-05-19 | 1 |
| Salom | 11 | 2026-07-21 | 1 |
| Vendrell | 5 | 2026-04-12 | 1 |
Dotze files, una per préstec, cadascuna amb el total del seu soci repetit al costat. Això és impossible amb GROUP BY.
GROUP BY |
Funció de finestra (OVER) |
|
|---|---|---|
| Files del resultat | Una per grup | Una per fila original |
| Es perd el detall? | Sí | No |
| Sintaxi | COUNT(*) ... GROUP BY soci_id |
COUNT(*) OVER (PARTITION BY soci_id) |
| Per a què serveix | Informes de totals | Comparar cada fila amb el seu grup, numerar, rànquings |
PARTITION BY és a les finestres el que GROUP BY és als agregats: defineix els subconjunts. Si l'omets (OVER ()), la finestra és la taula sencera.
Numerar i classificar: ROW_NUMBER i RANK
SELECT s.cognoms,
p.data_prestec,
ROW_NUMBER() OVER (PARTITION BY p.soci_id ORDER BY p.data_prestec) AS n_prestec
FROM prestecs p
INNER JOIN socis s ON s.soci_id = p.soci_id
ORDER BY s.cognoms, n_prestec;| cognoms | data_prestec | n_prestec |
|---|---|---|
| Alsina | 2026-03-02 | 1 |
| Alsina | 2026-04-06 | 2 |
| Alsina | 2026-07-14 | 3 |
| Bastos | 2026-03-11 | 1 |
| Bastos | 2026-05-04 | 2 |
| Ferran | 2026-05-08 | 1 |
| Miralles | 2026-07-25 | 1 |
| Pereda | 2026-03-05 | 1 |
| Pereda | 2026-07-18 | 2 |
| Quiroga | 2026-05-19 | 1 |
| Salom | 2026-07-21 | 1 |
| Vendrell | 2026-04-12 | 1 |
"El préstec número N de cada soci". Aquest patró —numerar dins de cada grup i quedar-se amb el n_prestec = 1— és la manera estàndard d'obtenir "el registre més recent de cada X", una consulta que sense finestres és sorprenentment incòmoda.
RANK és com ROW_NUMBER però empata:
WITH recompte AS (
SELECT l.llibre_id, l.titol, COUNT(*) AS prestecs
FROM prestecs p
INNER JOIN exemplars e ON e.exemplar_id = p.exemplar_id
INNER JOIN llibres l ON l.llibre_id = e.llibre_id
GROUP BY l.llibre_id, l.titol
)
SELECT titol,
prestecs,
RANK() OVER (ORDER BY prestecs DESC) AS posicio,
DENSE_RANK() OVER (ORDER BY prestecs DESC) AS posicio_densa,
ROW_NUMBER() OVER (ORDER BY prestecs DESC, titol) AS ordre
FROM recompte
ORDER BY prestecs DESC, titol;| titol | prestecs | posicio | posicio_densa | ordre |
|---|---|---|---|---|
| El mapa del temps | 4 | 1 | 1 | 1 |
| Els pilars de la Terra | 3 | 2 | 2 | 2 |
| Àlgebra per a impacients | 1 | 3 | 3 | 3 |
| La casa de les marees | 1 | 3 | 3 | 4 |
| L'hivern dels ocells | 1 | 3 | 3 | 5 |
| Quaderns de Ravenna | 1 | 3 | 3 | 6 |
| Rutes del delta | 1 | 3 | 3 | 7 |
Les tres funcions es diferencien justament en els empats:
ROW_NUMBERno empata mai: numera 1, 2, 3, 4, 5, 6, 7 encara que els valors siguin iguals.RANKempata i salta: els cinc llibres amb un préstec són tots "posició 3"; si hi hagués un sisè valor diferent, seria la posició 8.DENSE_RANKempata i no salta: el següent valor diferent seria la posició 4.
Aquí ho deixem. Les funcions de finestra donen per a molt més —mitjanes mòbils, sumes acumulades, LAG i LEAD per comparar amb la fila anterior, marcs de finestra amb ROWS BETWEEN— i són el pa de cada dia de l'anàlisi de dades. Estan disponibles a PostgreSQL des de la versió 8.4 i a SQLite des de la 3.25.
Errors Habituals i Consells
- Oblidar una columna al
GROUP BY. A PostgreSQL dóna error; a SQLite s'executa i retorna un valor arbitrari, que és infinitament pitjor. Escriu sempre elGROUP BYcomplet. - Fer servir
COUNT(*)amb unLEFT JOIN. Retorna 1 on hauria de retornar 0, perquè compta la fila plena deNULL. Fes servirCOUNT(columna_de_la_taula_dreta). - Comptar sobre un
JOINque infla files. Si dues taules independents pengen d'una tercera, cadaCOUNTcompta el producte cartesià. Agrega cada branca per separat. - Posar una condició amb agregat al
WHERE. Error segur: els agregats van alHAVING. - Posar al
HAVINGuna condició que no fa servir agregats. Funciona, però filtra més tard del necessari i confon qui ho llegeixi. Va alWHERE. - Fer servir un àlies del
SELECTdins delWHERE. ElSELECTs'avalua després. AORDER BYsí que es pot. - Suposar que
SUMde pursNULLdóna zero. DónaNULL. Embolcalla'l enCOALESCE(SUM(x), 0)si l'informe necessita un número. - Comparar
AVGsense decidir què fer amb elsNULL.AVG(x)iAVG(COALESCE(x, 0))donen xifres diferents i totes dues poden ser correctes: la pregunta és què significa l'absència. - Creure que
MIN(a)iMAX(b)vénen de la mateixa fila. No ho fan. Cada agregat es calcula pel seu compte. - Consell: quan un agregat et doni un número que no et quadri, treu les funcions i executa la consulta en mode detall amb
SELECT *. Comptar les files a ull revela les explosions a l'instant. - Consell: escriu sempre el
GROUP BYcomençant per la clau primària de la taula que estàs resumint (GROUP BY l.llibre_id, l.titol, no nomésGROUP BY l.titol). Si hi hagués dos llibres amb el mateix títol, agrupar pel títol els fondria en un.
Exercicis
Exercici 1: Recomptes i totals
- Quants socis hi ha a cada sucursal? Mostra el nom de la sucursal, incloses les que no en tinguessin cap.
- Quants llibres hi ha per idioma?
- Quants exemplars té cada llibre? Inclou el títol i ordena de més a menys.
- Quin és el recàrrec total cobrat a cada sucursal (per la sucursal de l'exemplar prestat)? Mostra
0.00on no n'hi hagi hagut cap.
Exercici 2: Agrupar i filtrar grups
- Quins autors tenen més d'una obra al fons?
- Quins llibres tenen exemplars a tres o més sucursals diferents?
- Quins socis han retornat algun llibre amb retard, i quantes vegades? Ordena de més a menys.
- Quines sucursals tenen tres o més exemplars disponibles?
Exercici 3: Informes complets
- Construeix el quadre de préstecs per mes: any-mes, nombre de préstecs i recàrrec total. Pista: a PostgreSQL,
TO_CHAR(data_prestec, 'YYYY-MM'); a SQLite,strftime('%Y-%m', data_prestec). - Per a cada soci, mostra el seu nom, el nombre de préstecs, el nombre de reserves i el recàrrec acumulat, en una sola consulta i amb les xifres correctes. Vés amb compte amb l'explosió de files.
- Fent servir funcions de finestra, mostra cada préstec amb: el soci, la data, el recàrrec i el recàrrec total acumulat per aquest soci.
Solucions
Solució 1
-- 1
SELECT su.nom AS sucursal, COUNT(s.soci_id) AS socis
FROM sucursals su
LEFT JOIN socis s ON s.sucursal_id = su.sucursal_id
GROUP BY su.sucursal_id, su.nom
ORDER BY socis DESC, su.nom;| sucursal | socis |
|---|---|
| Centre | 4 |
| Nord | 3 |
| Sud | 2 |
| Est | 1 |
| idioma | llibres |
|---|---|
| es | 8 |
| ca | 1 |
-- 3
SELECT l.titol, COUNT(e.exemplar_id) AS exemplars
FROM llibres l
LEFT JOIN exemplars e ON e.llibre_id = l.llibre_id
GROUP BY l.llibre_id, l.titol
ORDER BY exemplars DESC, l.titol;| titol | exemplars |
|---|---|
| El mapa del temps | 3 |
| Àlgebra per a impacients | 2 |
| Els pilars de la Terra | 2 |
| L'hivern dels ocells | 2 |
| Manual de jardineria urbana | 2 |
| La casa de les marees | 1 |
| Memòria de l'Eixample (1904) | 1 |
| Quaderns de Ravenna | 1 |
| Rutes del delta | 1 |
-- 4
SELECT su.nom AS sucursal, COALESCE(SUM(p.recarrec), 0) AS recarrec_total
FROM sucursals su
LEFT JOIN exemplars e ON e.sucursal_id = su.sucursal_id
LEFT JOIN prestecs p ON p.exemplar_id = e.exemplar_id
GROUP BY su.sucursal_id, su.nom
ORDER BY recarrec_total DESC;| sucursal | recarrec_total |
|---|---|
| Est | 4.20 |
| Nord | 1.40 |
| Sud | 1.40 |
| Centre | 0.00 |
Total: 7,00 €, que coincideix amb el SUM(recarrec) global de l'apartat 1. Aquí no hi ha explosió de files, perquè les tres taules estan encadenades en línia (sucursal → exemplar → préstec), no penjant en paral·lel.
Solució 2
-- 1
SELECT a.cognoms, COUNT(*) AS obres
FROM llibres l
INNER JOIN autors a ON a.autor_id = l.autor_id
GROUP BY a.autor_id, a.cognoms
HAVING COUNT(*) > 1;| cognoms | obres |
|---|---|
| Barreda | 2 |
-- 2
SELECT l.titol, COUNT(DISTINCT e.sucursal_id) AS sucursals
FROM llibres l
INNER JOIN exemplars e ON e.llibre_id = l.llibre_id
GROUP BY l.llibre_id, l.titol
HAVING COUNT(DISTINCT e.sucursal_id) >= 3;| titol | sucursals |
|---|---|
| El mapa del temps | 3 |
El DISTINCT és imprescindible: si un llibre tingués dos exemplars a la mateixa sucursal, COUNT(e.sucursal_id) els comptaria dues vegades.
-- 3
SELECT s.nom || ' ' || s.cognoms AS soci, COUNT(*) AS retards
FROM prestecs p
INNER JOIN socis s ON s.soci_id = p.soci_id
WHERE p.data_devolucio > p.data_devolucio_prevista
GROUP BY s.soci_id, s.nom, s.cognoms
ORDER BY retards DESC, soci;| soci | retards |
|---|---|
| Ivan Pereda | 1 |
| Lucía Vendrell | 1 |
| Sonia Quiroga | 1 |
Nota: la condició va al WHERE perquè filtra files (préstecs), no grups. És el filtratge eficient.
-- 4
SELECT su.nom AS sucursal, COUNT(*) AS disponibles
FROM exemplars e
INNER JOIN sucursals su ON su.sucursal_id = e.sucursal_id
WHERE e.estat = 'disponible'
GROUP BY su.sucursal_id, su.nom
HAVING COUNT(*) >= 3
ORDER BY disponibles DESC;| sucursal | disponibles |
|---|---|
| Centre | 4 |
Solució 3
-- 1 (PostgreSQL)
SELECT TO_CHAR(data_prestec, 'YYYY-MM') AS mes,
COUNT(*) AS prestecs,
COALESCE(SUM(recarrec), 0) AS recarrec
FROM prestecs
GROUP BY TO_CHAR(data_prestec, 'YYYY-MM')
ORDER BY mes;
-- SQLite: substitueix TO_CHAR(...) per strftime('%Y-%m', data_prestec)| mes | prestecs | recarrec |
|---|---|---|
| 2026-03 | 3 | 1.40 |
| 2026-04 | 2 | 1.40 |
| 2026-05 | 3 | 4.20 |
| 2026-07 | 4 | 0.00 |
Els quatre préstecs de juliol són oberts, així que el seu recàrrec és NULL i SUM retorna NULL; el COALESCE el converteix en 0.00. I fixa't que el juny no hi apareix: no hi va haver cap préstec aquell mes i GROUP BY no inventa grups buits. Perquè surti amb un 0 caldria generar la sèrie de mesos i fer-hi un LEFT JOIN.
-- 2 Tres branques independents: cal agregar cadascuna per separat
WITH prest AS (
SELECT soci_id, COUNT(*) AS prestecs, SUM(recarrec) AS recarrec
FROM prestecs GROUP BY soci_id
),
resv AS (
SELECT soci_id, COUNT(*) AS reserves
FROM reserves GROUP BY soci_id
)
SELECT s.soci_id,
s.nom || ' ' || s.cognoms AS soci,
COALESCE(pr.prestecs, 0) AS prestecs,
COALESCE(rv.reserves, 0) AS reserves,
COALESCE(pr.recarrec, 0) AS recarrec
FROM socis s
LEFT JOIN prest pr ON pr.soci_id = s.soci_id
LEFT JOIN resv rv ON rv.soci_id = s.soci_id
ORDER BY s.soci_id;| soci_id | soci | prestecs | reserves | recarrec |
|---|---|---|---|---|
| 11 | Álvaro Ferran | 1 | 1 | 0.00 |
| 12 | Sonia Quiroga | 1 | 0 | 4.20 |
| 13 | Ramón Etxebarri | 0 | 0 | 0.00 |
| 14 | Marta Alsina | 3 | 1 | 0.00 |
| 15 | Ivan Pereda | 2 | 1 | 1.40 |
| 16 | Núria Bastos | 2 | 1 | 0.00 |
| 17 | Diego Salom | 1 | 0 | 0.00 |
| 18 | Lucía Vendrell | 1 | 1 | 1.40 |
| 19 | Pau Miralles | 1 | 0 | 0.00 |
| 20 | Elena Roig | 0 | 0 | 0.00 |
Si ho haguessis resolt amb dos LEFT JOIN directes a prestecs i reserves, la Marta Alsina hauria sortit amb 3 préstecs i 3 reserves (3 × 1 = 3 files), i la Núria Bastos amb 2 i 2. L'explosió de files en estat pur.
-- 3
SELECT s.cognoms,
p.data_prestec,
COALESCE(p.recarrec, 0) AS recarrec,
SUM(COALESCE(p.recarrec, 0)) OVER (PARTITION BY p.soci_id
ORDER BY p.data_prestec) AS acumulat
FROM prestecs p
INNER JOIN socis s ON s.soci_id = p.soci_id
ORDER BY s.cognoms, p.data_prestec;| cognoms | data_prestec | recarrec | acumulat |
|---|---|---|---|
| Alsina | 2026-03-02 | 0.00 | 0.00 |
| Alsina | 2026-04-06 | 0.00 | 0.00 |
| Alsina | 2026-07-14 | 0.00 | 0.00 |
| Bastos | 2026-03-11 | 0.00 | 0.00 |
| Bastos | 2026-05-04 | 0.00 | 0.00 |
| Ferran | 2026-05-08 | 0.00 | 0.00 |
| Miralles | 2026-07-25 | 0.00 | 0.00 |
| Pereda | 2026-03-05 | 1.40 | 1.40 |
| Pereda | 2026-07-18 | 0.00 | 1.40 |
| Quiroga | 2026-05-19 | 4.20 | 4.20 |
| Salom | 2026-07-21 | 0.00 | 0.00 |
| Vendrell | 2026-04-12 | 1.40 | 1.40 |
En afegir ORDER BY dins de l'OVER, SUM deixa de ser un total i passa a ser una suma acumulada: cada fila inclou totes les anteriors de la seva partició. És el mecanisme que hi ha darrere de qualsevol gràfic d'evolució acumulada.
Conclusió
Amb aquesta lliçó BiblioRed ja pot respondre no només "què hi ha", sinó "quant n'hi ha":
- Les funcions d'agregat
COUNT,SUM,AVG,MINiMAXcondensen moltes files en un valor. SenseGROUP BY, la taula sencera és un sol grup. COUNTté tres formes:COUNT(*)compta files,COUNT(columna)compta valors no nuls iCOUNT(DISTINCT columna)compta valors diferents.- Tots els agregats ignoren els
NULLllevat deCOUNT(*), iSUMde purs nuls retornaNULL, no zero. Decidir què significa l'absència és una decisió de negoci, no tècnica. GROUP BYparteix el resultat en grups i retorna una fila per grup; tota columna delSELECTque no sigui agregat ha de ser alGROUP BY(i SQLite no ho comprova, cosa que és un parany seriós).HAVINGfiltra grups;WHEREfiltra files. El que es pugui posar alWHERE, va alWHERE.- L'ordre lògic d'execució —
FROM→JOIN→WHERE→GROUP BY→HAVING→SELECT→DISTINCT→ORDER BY→LIMIT— explica per quèWHEREno veu els àlies ni els agregats iORDER BYsí. - L'agregació combinada amb
JOINprodueix els informes reals: préstecs per sucursal, llibres més prestats, activitat per soci. Amb una regla d'or: ambLEFT JOIN,COUNT(columna), maiCOUNT(*). - L'explosió de files infla els recomptes quan dues taules independents pengen d'una tercera. Es detecta perquè les xifres surten sospitosament altes i idèntiques, i es resol agregant cada branca per separat amb CTE.
COALESCEconverteixNULLen un valor presentable iCASE WHEN(oFILTERa PostgreSQL) permet tabular diverses columnes condicionals en una sola passada.- Les funcions de finestra amb
OVER (PARTITION BY ...)calculen agregats sense perdre el detall, iROW_NUMBER,RANKiDENSE_RANKnumeren i classifiquen amb un tractament diferent dels empats.
Ja sabem definir l'esquema, poblar-lo, consultar-lo, creuar-lo i resumir-lo. Falta la pregunta que sosté tota la resta: qui garanteix que aquestes dades continuïn sent certes d'aquí a cinc anys? Els nostres informes són fiables només perquè tots els soci_id de prestecs apunten a socis que existeixen, i tots els llibre_id d'exemplars, a llibres del catàleg.
Això ho garanteix la integritat referencial, i és el tema de la lliçó 02-06, amb la qual tanquem el mòdul: com es declaren les claus foranes, què comprova el gestor a cada INSERT, UPDATE i DELETE, què fer quan s'esborra una fila de la qual en depenen d'altres (CASCADE, RESTRICT, SET NULL…), com detectar i netejar les files òrfenes que ja existeixen, i per què SQLite no protegeix res si no li ho demanes expressament.
Fonaments de Bases de Dades
Mòdul 1: Introducció a les Bases de Dades
- Conceptes Bàsics de Bases de Dades
- Tipus de Bases de Dades
- Història i Evolució de les Bases de Dades
- Sistemes Gestors de Bases de Dades i Arquitectura
Mòdul 2: Bases de Dades Relacionals
- Model Relacional
- Llenguatge SQL
- Operacions Bàsiques en SQL
- Consultes Multitaula: JOIN i Subconsultes
- Agregació i Agrupació de Dades
- Integritat Referencial
Mòdul 3: Bases de Dades No Relacionals
- Introducció a NoSQL
- Tipus de Bases de Dades NoSQL
- Modelatge de Dades en NoSQL
- Comparació entre Bases de Dades Relacionals i No Relacionals
Mòdul 4: Disseny d'Esquemes
- Principis de Disseny d'Esquemes
- Diagrames Entitat-Relació (ER)
- Transformació de Diagrames ER a Esquemes Relacionals
- Tipus de Dades i Restriccions
Mòdul 5: Normalització
Mòdul 6: Transaccions, Rendiment i Seguretat
- Transaccions i Propietats ACID
- Concurrència i Nivells d'Aïllament
- Índexs i Optimització de Consultes
- Seguretat, Permisos i Còpies de Seguretat
Mòdul 7: Exercicis Pràctics
- Exercicis de SQL
- Exercicis de Disseny d'Esquemes
- Exercicis de Normalització
- Exercicis de Consultes Avançades i Transaccions
Mòdul 8: Casos d'Estudi
- Cas d'Estudi: Base de Dades Relacional
- Cas d'Estudi: Base de Dades No Relacional
- Cas d'Estudi: Persistència Poliglota
