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

  1. El flux de treball de l'analista
  2. Les definicions de BotigaVerda
  3. Mètriques fonamentals
  4. Anàlisi temporal
  5. Segmentació
  6. Anàlisi ABC / Pareto
  7. Cohorts i retenció
  8. Presentació: pivotar amb CASE i amb crosstab
  9. Reproductibilitat i on encaixa SQL
  10. Errors d'anàlisi freqüents
  11. Errors habituals i consells
  12. Exercicis
  13. Conclusió

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

  1. 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, 1 pendent, 2 enviat) compten; per a ingressos cobrats caldria filtrar per estat i el número seria un altre.

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

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

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

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

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

  1. Presentació: pivotar amb CASE i amb crosstab

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

  1. 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 .sql amb 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_vendes defineix una vegada què és l'import d'una línia, i a partir d'aquí ningú no torna a escriure quantitat * preu_unitari * (1 - descompte) — que és exactament on algú s'oblidarà del descompte. mv_vendes_mensuals fa 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.

  1. 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 AVG d'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 JOIN que 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 amb crosstab de tablefunc —compacte, amb NULL on 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

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