Hi ha una idea molt estesa i molt falsa: que SQL serveix per treure les dades i que l'anàlisi de debò es fa en un altre lloc. A la pràctica, la major part de la feina analítica d'una empresa —les mètriques del mes, la sèrie temporal, el desglossament per país, el Pareto de clients, les cohorts— cap sencera en SQL, s'executa on són les dades i no necessita moure ni un sol fitxer. Aquesta lliçó converteix les funcions de finestra de 10-03 en informes complets. Però abans de la primera consulta hi ha una cosa més important, i és la raó per la qual un analista amb criteri val molt més que un de ràpid: la meitat dels errors d'anàlisi no són de SQL, són de definició. "Vendes" inclou els ports? I la comanda cancel·lada? "Client actiu" és el que va comprar alguna vegada o el que ha comprat aquest any? Cadascuna d'aquestes preguntes canvia la xifra, i cap no la resol el motor.
Contingut
- El flux de treball de l'analista
- Les definicions de BotigaVerda
- Mètriques fonamentals
- Anàlisi temporal
- Segmentació
- Anàlisi ABC / Pareto
- Cohorts i retenció
- Presentació: pivotar amb
CASEi ambcrosstab - Reproductibilitat i on encaixa SQL
- Errors d'anàlisi freqüents
- Errors habituals i consells
- Exercicis
- Conclusió
- El flux de treball de l'analista
flowchart LR
A["<b>Pregunta de negoci</b><br/>'venem més que l'any passat?'"] --> B["<b>Mètrica definida</b><br/>què se suma, què s'exclou,<br/>quin període, quina granularitat"] --> C["<b>Consulta</b>"]
C --> D["<b>Validació</b><br/>quadra amb un total conegut?"] --> E["<b>Presentació</b><br/>taula, gràfic, quadre de comandament"]
D -.->|"no quadra"| B
Els dos passos que se salten sempre són el segon i el quart, i són els que separen un número correcte d'un número plausible. Definir obliga a parlar amb qui fa la pregunta —i moltes vegades descobreixes que la pregunta era una altra—. Validar és comprovar el resultat contra alguna cosa que ja sabies: un total, un recompte, una xifra de l'any passat. Si un desglossament per país no suma el mateix que el total general, el desglossament està malament, i tant és com d'elegant sigui la consulta.
- Les definicions de BotigaVerda
Aquestes són les definicions que fa servir el curs. No són "les correctes": són les que hem triat, i l'important és que estiguin escrites.
| Mètrica | Definició exacta | Valor |
|---|---|---|
| Facturació de producte ("vendes") | SUM(quantitat * preu_unitari * (1 - descompte)) sobre linies_comanda, sense ports, tots els estats |
727,95 € |
| Ingressos totals | Facturació de producte + despeses d'enviament | 846,20 € |
| Vendes netes de cancel·lacions | Facturació de producte excloent-hi les comandes cancellat |
701,20 € |
| Comandes | Files de comandes, tots els estats |
20 |
| Tiquet mitjà | Facturació de producte / nombre de comandes | 36,40 € |
| Unitats per comanda | SUM(quantitat) / nombre de comandes |
5,65 |
| Client comprador / recurrent | Amb ≥ 1 comanda / amb ≥ 2 comandes | 12 de 15 / 7 |
| Comanda nova / recurrent | La primera d'aquell client / les següents | 12 / 8 |
| Cohort | Mes de data_registre del client |
8 cohorts |
| Taxa de devolució (comandes) | Comandes amb devolució / comandes | 15,00 % |
| Taxa de devolució (import) | Import retornat / facturació de producte | 11,07 % |
I les tres decisions que hi ha al darrere:
- Els ports no són vendes. Són un servei repercutit, no marge comercial. Si els hi inclouessis, la facturació seria 846,20 € i el tiquet mitjà 42,31 €: xifres igual de "veritables" que responen a una altra pregunta. El greu no és triar malament, és barrejar les dues en el mateix informe.
- La comanda cancel·lada (la 6) compta a la facturació bruta i no a la neta. Els seus 26,75 € es van demanar de debò i es van retornar sencers: un informe de demanda l'ha d'incloure, un d'ingressos no. I "tots els estats" significa que les comandes encara no lliurades (2
pagat, 1pendent, 2enviat) compten; per a ingressos cobrats caldria filtrar per estat i el número seria un altre.
- Mètriques fonamentals
Les set primeres, en una consulta amb FILTER (04-04):
SELECT COUNT(DISTINCT co.id) AS comandes,
COUNT(DISTINCT co.client_id) AS compradors,
SUM(lc.quantitat) AS unitats,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
FILTER (WHERE co.estat <> 'cancellat'), 2) AS facturacio_neta,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
/ COUNT(DISTINCT co.id), 2) AS tiquet_mitja,
ROUND(SUM(lc.quantitat)::numeric / COUNT(DISTINCT co.id), 2) AS unitats_per_comanda
FROM comandes AS co JOIN linies_comanda AS lc ON lc.comanda_id = co.id;| comandes | compradors | unitats | facturacio | facturacio_neta | tiquet_mitja | unitats_per_comanda |
|---|---|---|---|---|---|---|
| 20 | 12 | 113 | 727.95 | 701.20 | 36.40 | 5.65 |
La validació immediata: 20 comandes i 113 unitats són les xifres que el curs arrossega des de 01-06, i 727,95 − 26,75 = 701,20 quadra amb la devolució de la comanda cancel·lada. Si algun dels tres no donés, caldria aturar-se.
Taxa de devolució
SELECT COUNT(*) AS devolucions, ROUND(SUM(d.import), 2) AS import_retornat,
ROUND(100.0 * COUNT(DISTINCT d.comanda_id) / (SELECT COUNT(*) FROM comandes), 2) AS taxa_comandes_pct,
ROUND(100 * SUM(d.import) / 727.95, 2) AS taxa_import_pct
FROM devolucions AS d;| devolucions | import_retornat | taxa_comandes_pct | taxa_import_pct |
|---|---|---|---|
| 3 | 80.57 | 15.00 | 11.07 |
Les dues taxes diuen coses diferents i cal publicar quina és: el 15 % de les comandes va tenir alguna devolució, però només es va retornar l'11 % dels diners, perquè dues de les tres són parcials —de la comanda 10 es va retornar la crema (34,02 €) de 48,27 €, i de la 13 les bosses (19,80 €) de 30,30 €—. Només la comanda 6, cancel·lada, es va retornar sencera.
Productes actius sense vendes i clients nous enfront de recurrents
SELECT p.id, p.nom, p.stock FROM productes AS p
WHERE p.actiu AND NOT EXISTS (SELECT 1 FROM linies_comanda AS lc WHERE lc.producte_id = p.id)
ORDER BY p.id;| id | nom | stock |
|---|---|---|
| 13 | Espelmes de cera de soja (pack 2) | 0 |
| 19 | Desodorant natural en barra 50 g | 75 |
Dos productes actius que no s'han venut mai, amb lectures de negoci oposades: les espelmes tenen estoc 0 —potser no van arribar mai a estar disponibles— i el desodorant té 75 unitats esperant. El tercer sense vendes, les càpsules d'espirulina, no hi apareix perquè està descatalogat: aquest WHERE p.actiu és una decisió de definició, no un detall.
WITH primera AS (SELECT client_id, MIN(data_comanda) AS primera_comanda FROM comandes GROUP BY client_id)
SELECT to_char(co.data_comanda, 'YYYY-MM') AS mes,
COUNT(*) FILTER (WHERE co.data_comanda = pr.primera_comanda) AS comandes_noves,
COUNT(*) FILTER (WHERE co.data_comanda > pr.primera_comanda) AS comandes_recurrents
FROM comandes AS co JOIN primera AS pr ON pr.client_id = co.client_id
GROUP BY 1 ORDER BY 1;| mes | comandes_noves | comandes_recurrents |
|---|---|---|
| 2025-03 | 2 | 0 |
| 2025-10 | 2 | 0 |
| 2025-12 | 0 | 2 |
| 2026-02 | 0 | 2 |
(4 de 12 files; el total és 12 de noves i 8 de recurrents.) La lectura és la que espera qualsevol negoci jove: fins al novembre gairebé totes les comandes són de clients nous, i des del desembre totes són de clients que repeteixen. És un senyal bo —hi ha retenció— i un altre de preocupant: la captació s'ha aturat. Cap dels dos no es veu mirant només la facturació total.
- Anàlisi temporal
Les tres columnes que demana qualsevol quadre de comandament, sobre la sèrie mensual de 10-03:
WITH mensual AS (
SELECT date_trunc('month', co.data_comanda)::date AS mes,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM comandes AS co JOIN linies_comanda AS lc ON lc.comanda_id = co.id GROUP BY 1)
SELECT to_char(mes, 'YYYY-MM') AS mes, facturacio,
SUM(facturacio) OVER w AS acumulat,
ROUND(AVG(facturacio) OVER (ORDER BY mes
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW), 2) AS mitjana_mobil_3,
ROUND(100 * (facturacio - LAG(facturacio) OVER w)
/ LAG(facturacio) OVER w, 2) AS var_pct
FROM mensual WINDOW w AS (ORDER BY mes) ORDER BY mes;| mes | facturacio | acumulat | mitjana_mobil_3 | var_pct |
|---|---|---|---|---|
| 2025-03 | 68.80 | 68.80 | 68.80 | (null) |
| 2025-09 | 32.76 | 410.04 | 41.88 | -32.13 |
| 2025-10 | 97.20 | 507.24 | 59.41 | 196.70 |
| 2025-12 | 64.58 | 603.52 | 64.49 | 103.72 |
| 2026-02 | 49.33 | 727.95 | 63.00 | -34.31 |
(5 de 12 files; la sèrie completa és a 10-03.) Tres coses que un informe ha de dir i que la taula sola no diu:
- L'acumulat tanca en 727,95 € i passa per 603,52 € al desembre: són les dues xifres canòniques, i la seva coincidència valida la sèrie sencera.
- La mitjana mòbil de 3 mesos és el que cal ensenyar al gràfic, no la sèrie crua: la facturació real oscil·la entre 31,70 € i 97,20 € i la mitjana mòbil entre 41,88 € i 71,87 €. Amb volums petits, la sèrie crua és sobretot soroll.
- El
+196,70 %d'octubre no és cap notícia: és que el setembre va tenir una sola comanda, i publicar aquesta variació sense la base sobre la qual es calcula és enganyar de bona fe. Regla: no publiquis una variació percentual si el denominador és petit; publica la xifra absoluta i el nombre de comandes al costat.
La comparació interanual que no es pot fer
La pregunta "venem més que l'any passat per aquestes dates?" és la més freqüent del món, i a BotigaVerda no té resposta: la sèrie comença al març del 2025 i acaba al febrer del 2026, així que el gener i el febrer del 2026 no tenen amb què comparar-se. El correcte és dir-ho, no calcular un NULL i deixar que algú l'interpreti. I el que sí que es pot fer, amb la mateixa honestedat: comparar els dos anys parcials, deixant clar que no són comparables en durada.
| any | mesos amb dades | comandes | facturacio |
|---|---|---|---|
| 2025 | 10 (mar-des) | 16 | 603.52 |
| 2026 | 2 (gen-feb) | 4 | 124.43 |
Els 124,43 € del 2026 no són "una caiguda del 79 %": són dos mesos enfront de deu. El comparable és la mitjana mensual —60,35 € el 2025 enfront de 62,22 € el 2026— o els mateixos mesos de calendari, que aquí no existeixen.
- Segmentació
Un mateix total, tallat per quatre dimensions. El patró és sempre el mateix GROUP BY, i l'important és que els quatre desglossaments sumen 727,95 €:
SELECT cat.nom AS categoria, COUNT(DISTINCT co.id) AS comandes,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio,
ROUND(100 * SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
/ SUM(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))) OVER (), 2) AS pct
FROM linies_comanda AS lc
JOIN productes AS p ON p.id = lc.producte_id
JOIN categories AS cat ON cat.id = p.categoria_id
JOIN comandes AS co ON co.id = lc.comanda_id
GROUP BY cat.nom ORDER BY facturacio DESC;| categoria | comandes | facturacio | pct |
|---|---|---|---|
| Alimentació | 10 | 256.27 | 35.20 |
| Begudes | 8 | 195.28 | 26.83 |
| Cosmètica natural | 6 | 156.32 | 21.47 |
| Llar sostenible | 4 | 88.58 | 12.17 |
| Higiene personal | 3 | 31.50 | 4.33 |
Complements no hi apareix, i això també és un resultat: el seu únic producte està descatalogat. Un informe honest ho diu; un que ensenya cinc files deixa creure que hi ha cinc categories. Els altres tres talls:
| País del client | Comandes | Facturació | Tiquet mitjà |
|---|---|---|---|
| Espanya | 14 | 433.70 | 30.98 |
| Portugal | 3 | 156.48 | 52.16 |
| França | 3 | 137.77 | 45.92 |
| Mètode de pagament | Comandes | Facturació | Canal | Comandes | Facturació |
|---|---|---|---|---|---|
| targeta | 11 | 398.00 | Telèfon (amb comercial) | 10 | 378.83 |
| paypal | 4 | 155.05 | Web (sense comercial) | 10 | 349.12 |
| transferencia | 3 | 121.70 | — | — | — |
| contrareemborsament | 2 | 53.20 | — | — | — |
Tres lectures que cap total general no donava. Espanya aporta el 60 % de la facturació però té el tiquet mitjà més baix (30,98 € enfront dels 52,16 € de Portugal): moltes comandes petites contra poques de grans, cosa que canvia completament l'estratègia de ports. La targeta concentra el 55 % de la facturació en 11 de les 20 comandes. I els dos canals estan empatats en nombre, amb el telèfon lleugerament al davant en import (378,83 € contra 349,12 €): que el canal atès facturi més per comanda és el que justificaria tenir comercials, però és una hipòtesi contrastable, no una conclusió.
- Anàlisi ABC / Pareto
El principi de Pareto —"pocs elements expliquen la major part del total"— es calcula amb un acumulat de finestra (10-03) i es classifica amb CASE:
WITH vendes AS (
SELECT c.id, c.nom || ' ' || c.cognoms AS client, c.pais,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio
FROM comandes AS co
JOIN clients AS c ON c.id = co.client_id
JOIN linies_comanda AS lc ON lc.comanda_id = co.id
GROUP BY c.id, c.nom, c.cognoms, c.pais),
acum AS (SELECT *, ROUND(100 * SUM(facturacio) OVER (ORDER BY facturacio DESC, id
ROWS UNBOUNDED PRECEDING)
/ SUM(facturacio) OVER (), 2) AS pct_acum FROM vendes)
SELECT client, pais, facturacio, pct_acum,
CASE WHEN pct_acum <= 80 THEN 'A' WHEN pct_acum <= 95 THEN 'B' ELSE 'C' END AS classe
FROM acum ORDER BY facturacio DESC;| client | pais | facturacio | pct_acum | classe |
|---|---|---|---|---|
| Sofia Moreira Costa | Portugal | 111.88 | 15.37 | A |
| Lucía Martínez Soler | Espanya | 107.60 | 30.15 | A |
| Camille Dubois | França | 70.87 | 39.89 | A |
| Carlos Ferrer Ibáñez | Espanya | 59.46 | 65.89 | A |
| Pau Llorens Vidal | Espanya | 57.33 | 73.76 | A |
(5 de les 7 files de classe A —hi falten Julien Moreau, 4t amb 66,90 €, i Javier Ortega Ruiz, 5è amb 62,93 €—. Després vénen l'Ana amb 54,85 €, el Tiago amb 44,60 € i el Diego amb 31,70 € en classe B, i l'Elena amb 30,30 € i la Marta amb 29,53 € en classe C.) El resum per classe, per a clients i per a productes:
| Classe | Clients | % facturació | Productes | % facturació |
|---|---|---|---|---|
| A (fins al 80 % acumulat) | 7 | 73.76 % | 10 | 77.41 % |
| B (fins al 95 %) | 3 | 18.02 % | 4 | 14.66 % |
| C (la resta) | 2 | 8.22 % | 3 | 7.93 % |
El Pareto de BotigaVerda és suau: els 6 primers clients expliquen el 65,89 % i en calen 7 per arribar al 73,76 %; en el clàssic 80/20 n'hi hauria prou amb 2 o 3 de 12. És una dada de negoci: la botiga no depèn d'un client gran, cosa que redueix el risc i alhora indica que no hi ha comptes clau per cultivar. I una advertència metodològica: el tall 80/95 és una convenció, i cal escriure quina fas servir —incloent-hi o no la fila que creua el llindar— perquè canvia qui entra a cada grup.
- Cohorts i retenció
Una cohort agrupa clients pel seu moment d'entrada i els segueix en el temps. Sobre BotigaVerda, amb el mes de data_registre:
WITH v AS (SELECT co.client_id, COUNT(DISTINCT co.id) AS comandes,
SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)) AS facturacio
FROM comandes AS co JOIN linies_comanda AS lc ON lc.comanda_id = co.id GROUP BY 1)
SELECT to_char(c.data_registre, 'YYYY-MM') AS cohort,
COUNT(*) AS clients,
COUNT(v.client_id) AS compradors,
ROUND(100.0 * COUNT(v.client_id) / COUNT(*), 1) AS conversio_pct,
COALESCE(SUM(v.comandes), 0) AS comandes,
COALESCE(ROUND(SUM(v.facturacio), 2), 0.00) AS facturacio
FROM clients AS c LEFT JOIN v ON v.client_id = c.id
GROUP BY 1 ORDER BY 1;| cohort | clients | compradors | conversio_pct | comandes | facturacio |
|---|---|---|---|---|---|
| 2025-01 | 2 | 2 | 100.0 | 5 | 167.06 |
| 2025-02 | 3 | 3 | 100.0 | 5 | 147.31 |
| 2025-03 | 2 | 2 | 100.0 | 4 | 169.21 |
| 2025-04 | 2 | 2 | 100.0 | 3 | 115.47 |
| 2025-05 | 2 | 2 | 100.0 | 2 | 97.20 |
| 2025-06 | 2 | 1 | 50.0 | 1 | 31.70 |
| 2025-09 | 1 | 0 | 0.0 | 0 | 0.00 |
| 2026-01 | 1 | 0 | 0.0 | 0 | 0.00 |
La consulta és correcta i l'anàlisi seria una ximpleria. Les cohorts tenen un, dos o tres clients: un sol client que no compra converteix la cohort de 2025-09 en un "0 % de conversió" que no significa res. I les cohorts antigues guanyen per definició, perquè porten més temps comprant: comparar les 5 comandes de la del gener amb l'1 de la del juny és comparar deu mesos amb vuit. I aquesta és la lliçó d'anàlisi, no la de SQL. Un resultat amb mostres de mida 1 o 2 no és un resultat: és una anècdota amb format de taula. El que cal fer és (a) dir-ho a l'informe, (b) agrupar en cohorts més grans —per trimestre en lloc de per mes— i (c) comparar sempre a la mateixa edat: "comandes en els 90 dies següents a l'alta", que posa totes les cohorts en igualtat i és la manera estàndard de fer retenció. Amb 15 clients ni això no salvaria l'anàlisi; amb 15.000, és exactament l'informe que demanarà direcció.
Quan un desglossament deixa de tenir sentit: quan algun grup baixa d'unes desenes d'observacions, el percentatge que calculis oscil·larà més que el senyal que busques. Abans de partir un total en vint trossos, mira quantes files queden al tros més petit.
- Presentació: pivotar amb
CASE i amb crosstab
CASE i amb crosstabUn informe de direcció gairebé mai no vol files: vol una matriu, amb les categories a les files i els anys a les columnes. La forma portable és la de 06-05, un agregat condicional per columna (SUM(CASE …), o la seva forma moderna amb FILTER):
SELECT cat.nom AS categoria,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
FILTER (WHERE co.data_comanda < '2026-01-01'), 2) AS a2025,
ROUND(COALESCE(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
FILTER (WHERE co.data_comanda >= '2026-01-01'), 0), 2) AS a2026,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS total
FROM linies_comanda AS lc
JOIN productes AS p ON p.id = lc.producte_id
JOIN categories AS cat ON cat.id = p.categoria_id
JOIN comandes AS co ON co.id = lc.comanda_id
GROUP BY cat.nom ORDER BY total DESC;| categoria | a2025 | a2026 | total |
|---|---|---|---|
| Alimentació | 215.67 | 40.60 | 256.27 |
| Begudes | 146.55 | 48.73 | 195.28 |
| Cosmètica natural | 128.22 | 28.10 | 156.32 |
| Llar sostenible | 88.58 | 0.00 | 88.58 |
| Higiene personal | 24.50 | 7.00 | 31.50 |
Les columnes sumen 603,52 € i 124,43 €: les xifres canòniques del 2025 i del 2026. I Llar sostenible surt amb 0,00 € el 2026 gràcies al COALESCE, cosa que és informativa: va deixar de vendre's.
crosstab de l'extensió tablefunc
Aquí es tanca la promesa de 06-05. PostgreSQL porta l'extensió tablefunc, amb una funció crosstab() que pivota a partir d'una consulta de tres columnes: fila, columna i valor.
CREATE EXTENSION IF NOT EXISTS tablefunc;
SELECT * FROM crosstab(
$$SELECT cat.nom, to_char(co.data_comanda, 'YYYY'),
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2)
FROM linies_comanda AS lc
JOIN productes AS p ON p.id = lc.producte_id
JOIN categories AS cat ON cat.id = p.categoria_id
JOIN comandes AS co ON co.id = lc.comanda_id
GROUP BY 1, 2 ORDER BY 1, 2$$,
$$SELECT unnest(ARRAY['2025','2026'])$$ -- 2a consulta: les columnes, en ordre
) AS t(categoria text, a2025 numeric, a2026 numeric); -- ⬅️ cal declarar-les a mà| categoria | a2025 | a2026 |
|---|---|---|
| Alimentació | 215.67 | 40.60 |
| Begudes | 146.55 | 48.73 |
| Cosmètica natural | 128.22 | 28.10 |
| Higiene personal | 24.50 | 7.00 |
| Llar sostenible | 88.58 | (null) |
Les mateixes xifres, amb dues diferències que importen. L'ordre és alfabètic per la columna de fila, perquè crosstab exigeix ORDER BY 1, 2 a la consulta d'origen i no n'admet cap altre. I Llar sostenible surt NULL el 2026, no 0,00, perquè per a crosstab "no hi ha fila" i "hi ha una fila amb zero" són coses diferents — i en un gràfic, NULL és un forat. S'arregla embolcallant amb COALESCE(a2026, 0) a fora.
SUM(CASE …) (06-05) |
crosstab |
|
|---|---|---|
| Portabilitat | Total: SQL estàndard | Només PostgreSQL, i cal instal·lar l'extensió |
| Escriure 12 columnes | 12 CASE: tediós |
Una consulta curta |
| Columnes dinàmiques | Cal conèixer-les per endavant | També: la llista de sortida es declara a mà |
| Absència de dades | 0 (amb l'ELSE 0) |
NULL |
| Llegibilitat | Verbosa però evident | Compacta i críptica: dues consultes imbricades en $$ |
El criteri: per a dues, tres o quatre columnes, SUM(CASE …) guanya per claredat i portabilitat; crosstab compensa a partir de vuit o deu columnes fixes i conegudes. I per a columnes de debò dinàmiques —"una per mes, els que hi hagi"— cap de les dues no serveix: SQL retorna un nombre fix de columnes decidit en planificar. Aquest pivot es fa a la capa de presentació (BI, full de càlcul, pandas.pivot_table), i aquest és el repartiment natural de la feina.
- Reproductibilitat i on encaixa SQL
Una anàlisi que no es pot repetir d'aquí a tres mesos no és una anàlisi, és una captura de pantalla:
- Les consultes viuen al repositori, en fitxers
.sqlamb un comentari que digui quina pregunta responen i quina definició fan servir ("vendes = producte, sense ports, tots els estats"). Un quadre de comandament amb el SQL amagat dins de l'eina és un quadre de comandament que ningú no pot auditar. - Vistes i vistes materialitzades com a capa semàntica (10-01):
v_detall_vendesdefineix una vegada què és l'import d'una línia, i a partir d'aquí ningú no torna a escriurequantitat * preu_unitari * (1 - descompte)— que és exactament on algú s'oblidarà del descompte.mv_vendes_mensualsfa el mateix amb la sèrie, i a més evita recalcular-la a cada consulta. - Números que es validen sols. Si cada informe inclou un total que ja coneixes, un desglossament mal fet es delata a l'instant. I el repartiment amb les altres eines, que és la pregunta que tot analista es fa:
| Eina | Fa bé | Fa malament |
|---|---|---|
| SQL | Filtrar, unir, agregar, ordenar, finestres; treballar on són les dades sense moure-les; volums que no caben en memòria | Estadística avançada, models, gràfics, bucles, text lliure complex |
| Python / pandas / R | Models, sèries, neteja complexa, gràfics, reproductibilitat en quaderns | Escala: si has de portar 50 milions de files per agrupar, agrupa-les en SQL |
| BI (Power BI, Metabase, Looker, Superset) | Publicar, explorar, filtrar interactivament, distribuir | Definir mètriques: si cada panell defineix "vendes" a la seva manera, tindràs cinc xifres diferents |
El criteri del curs: agrega en SQL, modela i dibuixa fora. La regla operativa: el que redueixi files, fes-ho al més a prop possible de la base de dades. Portar 2 milions de files a pandas per a un groupby que en retorna 12 és llençar xarxa, memòria i temps, i és la versió analítica de l'antipatró de 08-04.
- Errors d'anàlisi freqüents
| Error | Com es manifesta | Com s'evita |
|---|---|---|
Comptar files duplicades per un JOIN |
20 comandes es converteixen en 47; el tiquet mitjà es divideix per 2,35 | COUNT(DISTINCT co.id); comprovar el recompte després de cada JOIN |
| Mitjana de mitjanes | Fer la mitjana dels tiquets mitjans dels tres països dona 43,02 €, no 36,40 € | Sumar numeradors i denominadors |
Ignorar els NULL |
AVG els omet; COUNT(columna) no els compta; un NOT IN amb nuls retorna 0 files |
Decidir explícitament: COALESCE, FILTER, NOT EXISTS (04-03) |
| Comparar períodes incomplets | "El febrer cau un 34 %" quan el febrer encara no ha acabat | Comparar períodes tancats, o el mateix nombre de dies |
| Publicar un percentatge sobre pocs casos | El +196,70 % d'octubre sobre una única comanda del setembre |
Publicar la xifra absoluta i la mida de la mostra al costat |
| Confondre correlació amb causalitat | "Les comandes amb comercial facturen més → posem-hi més comercials" | Els comercials atenen trucades, que ja solen ser comandes més grans. Per afirmar-ne la causa cal un experiment |
| Canviar la definició a mig informe | Una taula amb ports i la següent sense | Escriure la definició una vegada i encapsular-la en una vista |
La penúltima fila és la més perillosa pel raonable que sona: el canal telefònic factura 378,83 € enfront dels 349,12 € del web, però això no demostra que el comercial generi més venda — amb 10 comandes per canal la diferència és de 3 € per comanda. La manera de saber-ho és un experiment, no una consulta.
Errors habituals i consells
- Començar per la consulta i no per la definició. "Dona'm les vendes del mes" té almenys quatre respostes correctes. Pregunta abans d'escriure. I no validar contra un total conegut: és la comprovació més barata i detecta el 90 % dels errors de
JOIN. - Arrodonir a cada pas. Arrodoneix només en presentar: arrodonir un intermedi i després sumar acumula l'error. I fer servir
AVGd'una columna que ja és una mitjana: la mitjana de mitjanes només coincideix amb la global si tots els grups tenen la mateixa mida. - Presentar un gràfic sense els mesos buits. El calendari amb
generate_series(11-01) no és un adorn: sense ell la tendència és una altra. I tractar un percentatge sobre 1 o 2 casos com a informació: amb mostres petites, els percentatges menteixen més que informen. - Consell: escriu la definició en un comentari dins de la mateixa consulta. L'informe i la seva definició viatgen junts, i qui l'hereti sabrà què està mirant.
- Consell: desa la xifra de control. Cada informe recurrent hauria de portar una fila o columna que delati que alguna cosa s'ha trencat — l'equivalent analític d'una prova automàtica. I si un resultat et sorprèn, sospita del SQL abans que del negoci: nou de cada deu sorpreses són un
JOINque multiplica o un filtre que faltava.
Exercicis
Exercici 1
Direcció demana "el marge per categoria". (1) Enumera tres decisions de definició que cal prendre abans d'escriure res. (2) Escriu la consulta fent servir productes.cost i l'import de línia del curs. (3) Per què el marge calculat així pot estar malament encara que la consulta sigui correcta?
Exercici 2
Calcula, per a cada mes, la facturació, el nombre de clients diferents que van comprar i la facturació mitjana per client, i ordena-ho per mes. (1) Escriu-ho. (2) Per què la suma de "clients diferents per mes" no dona 12? (3) Quin mes té la facturació mitjana per client més alta i quina precaució cal prendre abans de destacar-lo?
Exercici 3
Un company presenta aquesta conclusió: "Portugal és el nostre millor mercat: el seu tiquet mitjà és un 68 % superior al d'Espanya". (1) És certa la dada? (2) Dona tres raons per les quals la conclusió no se sosté. (3) Quina anàlisi proposaries al seu lloc?
Solucions
Solució 1
1. (a) Marge sobre el preu real de venda (amb descompte) o sobre el de tarifa? El descompte surt del marge, així que de l'import de línia. (b) El cost és l'actual (productes.cost) o el del moment de la venda? L'esquema només desa l'actual: cal dir-ho. (c) S'hi inclouen les comandes cancel·lades? Un marge sobre vendes que es van retornar no és marge.
-- 2
SELECT cat.nom AS categoria,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS ingressos,
ROUND(SUM(lc.quantitat * p.cost), 2) AS cost,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
- SUM(lc.quantitat * p.cost), 2) AS marge
FROM linies_comanda AS lc
JOIN productes AS p ON p.id = lc.producte_id
JOIN categories AS cat ON cat.id = p.categoria_id
JOIN comandes AS co ON co.id = lc.comanda_id
WHERE co.estat <> 'cancellat'
GROUP BY cat.nom ORDER BY marge DESC;3. Perquè cost és el cost actual, no el de la venda: és el problema que preu_unitari sí que resol per al preu (01-06) i que l'esquema no resol per al cost, així que si els costos han pujat el marge històric sortirà subestimat. La solució de disseny seria desar cost_unitari a linies_comanda; mentre no existeixi, el número es publica com a estimació i es diu per què.
Solució 2
SELECT to_char(co.data_comanda, 'YYYY-MM') AS mes,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS facturacio,
COUNT(DISTINCT co.client_id) AS clients,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte))
/ COUNT(DISTINCT co.client_id), 2) AS mitjana_per_client
FROM comandes AS co JOIN linies_comanda AS lc ON lc.comanda_id = co.id GROUP BY 1 ORDER BY 1;2. Perquè un client pot comprar en diversos mesos i es compta a cadascun: COUNT(DISTINCT client_id) és diferent dins de cada grup, i els grups no són disjunts per client — sumar aquella columna compta la Lucía tres vegades. El total de compradors diferents, 12, només surt d'una consulta sense GROUP BY per mes; és el mateix error que sumar "usuaris actius diaris" per obtenir els mensuals. 3. El més alt és 2025-10, amb 48,60 € per client (97,20 € entre 2 clients), i la precaució és la de l'apartat 7: són dos clients, així que el "rècord" l'explica una sola comanda gran.
Solució 3
1. La dada és certa: 52,16 € enfront de 30,98 € són un 68,4 % més; l'aritmètica està bé. 2. (a) Mida de la mostra: Portugal són 3 comandes de 2 clients. Una sola comanda gran mou el tiquet mitjà desenes d'euros; no hi ha base per a una conclusió. (b) "Millor mercat" no és "tiquet mitjà": Espanya aporta 433,70 €, gairebé el triple que Portugal (156,48 €), amb 14 comandes i 8 clients. Si "millor" és volum, la conclusió s'inverteix. (c) Falten els costos: enviar a Portugal costa 9,90 € enfront dels 4,95 € o 0 € nacionals, i aquest port s'endú part de l'avantatge; el marge podria ser menor. I una quarta, de mètode: el tiquet mitjà més alt pot ser degut al fet que els ports internacionals empenyen el client a ajuntar més articles per comanda —l'enviament gratuït a partir de cert import—, que és un efecte del mateix esquema de preus i no una propietat del mercat.
3. Comparar marge per client i per període, no tiquet mitjà: facturació menys cost de producte menys cost real d'enviament, dividit entre clients actius, amb el nombre d'observacions publicat al costat de cada xifra. I si la pregunta real és "on invertim en captació?", la resposta necessita el cost d'adquisició i la repetició de compra per país, no una mitjana de tres comandes.
Conclusió
SQL és una eina analítica de ple dret, i l'ofici va menys de sintaxi que de criteri:
- El flux és pregunta → definició → consulta → validació → presentació, i els passos que tothom es salta són el segon i el quart. Definir obliga a decidir si "vendes" inclou ports (727,95 € enfront de 846,20 €) i si la comanda cancel·lada compta (727,95 € enfront de 701,20 €); validar és comprovar contra un total que ja coneixies. Les mètriques fonamentals de BotigaVerda: 20 comandes, 12 compradors, 113 unitats, 36,40 € de tiquet mitjà, 5,65 unitats per comanda, 15,00 % de taxa de devolució per comandes i 11,07 % per import —dos números diferents que responen a preguntes diferents—, 2 productes actius sense vendes i un repartiment de 12 comandes noves enfront de 8 de recurrents que revela que la captació s'ha aturat.
- L'anàlisi temporal amb acumulat (que tanca en 727,95 €), mitjana mòbil de 3 mesos (la que cal ensenyar) i variació mensual (el
+196,70 %d'octubre que no és cap notícia). I la comparació interanual que no es pot fer, perquè dir-ho és part de la feina. La segmentació per categoria, país, mètode de pagament i canal, amb els quatre desglossaments sumant 727,95 €, i la troballa que cap total no donava: Espanya factura més però amb el tiquet mitjà més baix. El Pareto és suau —6 clients expliquen el 65,89 %—, amb classes A/B/C de 7/3/2 clients i 10/4/3 productes. - Les cohorts surten ben escrites i mal fonamentades: amb un o dos clients per cohort, el resultat és una anècdota amb format de taula. Dir-ho, agrupar més gruixut i comparar a la mateixa edat. El pivot amb
SUM(CASE …)—portable, amb zeros— i ambcrosstabdetablefunc—compacte, ambNULLon no hi ha dades i amb les columnes declarades a mà—, tancant la promesa de 06-05. Cap dels dos no fa columnes realment dinàmiques: això és de la capa de presentació. - Reproductibilitat: consultes al repositori amb la seva definició escrita, vistes i materialitzades com a capa semàntica, i el repartiment: agrega en SQL, modela i dibuixa fora.
Tot això s'executa a psql o en una eina de BI. Però el SQL que de debò s'executa més vegades al dia no l'escriu un analista: el llança una aplicació, centenars de vegades per segon, des d'un procés web que obre connexions, executa consultes i les tanca. A la lliçó següent, SQL en el desenvolupament web, tanques el mòdul: la connexió i el pool; com s'executa una consulta parametritzada des del codi i com es maneja la transacció; ORM enfront de SQL a mà amb el criteri del curs; els antipatrons que maten un web, començant pel N+1 que 08-04 va deixar pendent; els patrons útils —paginació per cursor, LIMIT defensiu, cues amb SKIP LOCKED, JSON directe des de PostgreSQL—; i la llista de comprovació per quan algú diu que "el web va lent".
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
