De totes les preguntes que es fa la direcció de BotigaVerda, la majoria porten una data a dins: quant hem facturat aquest mes?, quins clients no compren des de fa mig any?, quants dies va trigar a arribar aquella devolució?, creixem trimestre a trimestre? Fins ara només sabies comparar dates amb >= i <. Aquesta lliçó converteix la columna data_comanda en una eina d'anàlisi.
També salda el deute de 04-05, on EXTRACT va aparèixer de passada per agrupar per any i es va deixar explicat "per al mòdul 6". I porta la peça que faltava per als informes: DATE_TRUNC, la funció que converteix 20 comandes soltes en una sèrie mensual.
Nota sobre la reproductibilitat: els exemples que necessiten "avui" fan servir la data fixa DATE '2026-03-01' en lloc de CURRENT_DATE, perquè els resultats que veus aquí coincideixin amb els teus. En producció faries servir CURRENT_DATE.
Contingut
- Els tipus temporals, i per què BotigaVerda fa servir
DATE - L'"ara":
CURRENT_DATE,NOW()iCLOCK_TIMESTAMP() - Aritmètica de dates,
INTERVALiAGE() EXTRACTiDATE_PARTDATE_TRUNC: el patró de l'informe temporal- Formatar amb
TO_CHAR, analitzar ambTO_DATE - Informes sense buits amb
generate_series - Taula comparativa per motor
- Errors habituals i consells
- Exercicis
- Conclusió
- Els tipus temporals, i per què BotigaVerda fa servir
DATE
DATE| Tipus | Què desa | Exemple de literal | Mida |
|---|---|---|---|
DATE |
Una data de calendari, sense hora | DATE '2025-03-04' |
4 bytes |
TIME |
Una hora del dia, sense data | TIME '18:30:00' |
8 bytes |
TIMESTAMP |
Data + hora, sense zona horària | TIMESTAMP '2025-03-04 18:30:00' |
8 bytes |
TIMESTAMPTZ |
Data + hora, amb zona horària | TIMESTAMPTZ '2025-03-04 18:30:00+01' |
8 bytes |
INTERVAL |
Una durada, no un instant | INTERVAL '30 days' |
16 bytes |
BotigaVerda fa servir DATE a totes les seves columnes temporals —data_comanda, data_registre, data_alta, data_contractacio, ressenyes.data, devolucions.data— perquè en aquest model són fets de calendari: una comanda és "del 4 de març", no "de les 18:47:03 del 4 de març". Desar una hora que ningú no farà servir afegeix ambigüitat sense aportar informació.
Què canviaria amb marques de temps
En una botiga real voldries l'hora: per mesurar el temps de preparació, per saber a quina hora es compra més, per auditar qui va canviar què i quan. I tan bon punt hi afegeixes l'hora, apareix la zona horària:
| Situació | Amb DATE |
Amb TIMESTAMP |
Amb TIMESTAMPTZ |
|---|---|---|---|
| Comanda a les 23:50 a Lió (CET) | "4 de març", i prou | 2025-03-04 23:50 — hora d'on? |
Instant inequívoc; es mostra en la zona de cada usuari |
| Comparar amb una comanda de Lisboa (WET) | Directe | Incomparable: una hora de diferència invisible | Correcte: el motor normalitza |
| Canvi d'hora d'octubre | El problema no existeix | Dos instants amb la mateixa representació | Resolt |
data >= '2025-03-01' AND data < '2025-04-01' |
Exacte | Exacte | Depèn de la zona de sessió |
La recomanació professional: en una aplicació real,
TIMESTAMPTZés gairebé sempre l'elecció correcta.TIMESTAMPsense zona sembla més senzill i és una trampa: desa un número sense dir a quin rellotge pertany, i el dia que tinguis usuaris a dos països ja no hi ha manera de saber-ho.TIMESTAMPTZdesa internament un instant en UTC i el converteix a la zona de cada sessió en llegir-lo. IDATEés correcte només quan la dada és de debò una data de calendari: una data de naixement, un dia de facturació, un venciment.
Aquest és exactament el criteri pel qual BotigaVerda fa servir DATE, i pel qual la teva propera aplicació probablement no ho hauria de fer.
- L'"ara":
CURRENT_DATE, NOW() i CLOCK_TIMESTAMP()
CURRENT_DATE, NOW() i CLOCK_TIMESTAMP()| Funció | Què retorna | Quan s'avalua |
|---|---|---|
CURRENT_DATE / CURRENT_TIME |
DATE d'avui / TIME WITH TIME ZONE |
Inici de la transacció |
CURRENT_TIMESTAMP |
TIMESTAMPTZ |
Inici de la transacció |
NOW() |
Idèntica a CURRENT_TIMESTAMP |
Inici de la transacció |
STATEMENT_TIMESTAMP() |
TIMESTAMPTZ |
Inici de la sentència |
CLOCK_TIMESTAMP() |
TIMESTAMPTZ |
En l'instant de la crida |
La diferència entre NOW() i CLOCK_TIMESTAMP() sorprèn la primera vegada. Si obres BEGIN;, executes SELECT NOW(), CLOCK_TIMESTAMP();, esperes uns segons i repeteixes la consulta abans del COMMIT, la primera columna retorna exactament el mateix valor les dues vegades i la segona no. NOW() està congelada durant tota la transacció, i això és una garantia deliberada: si un procés insereix cent files amb NOW(), les cent comparteixen la mateixa marca i formen un lot identificable. Per mesurar quant triga una cosa dins d'una transacció, NOW() no serveix: necessites CLOCK_TIMESTAMP().
Conseqüència pràctica:
data_registre DATE NOT NULL DEFAULT CURRENT_DATE(elDEFAULTdeclientsa 01-06) és estable i correcte. UnDEFAULT clock_timestamp()seria volàtil i, com vas veure a 05-06, obligaria a reescriure la taula sencera en afegir la columna.
- Aritmètica de dates,
INTERVAL i AGE()
INTERVAL i AGE()| Operació | Tipus del resultat | Exemple | Resultat |
|---|---|---|---|
date - date |
INTEGER (dies) |
DATE '2026-02-21' - DATE '2025-03-04' |
354 |
date + enter |
DATE |
DATE '2025-03-04' + 30 |
2025-04-03 |
date + INTERVAL |
TIMESTAMP |
DATE '2025-03-04' + INTERVAL '30 days' |
2025-04-03 00:00:00 |
timestamp - timestamp |
INTERVAL |
— | 1 day 02:15:00 |
AGE(a, b) |
INTERVAL llegible |
AGE(DATE '2026-03-01', DATE '2025-01-10') |
1 year 1 mon 19 days |
AGE(x) |
INTERVAL des d'avui |
AGE(data_registre) |
equival a AGE(CURRENT_DATE, data_registre) |
Dues trampes en aquesta taula. date - date dona un enter i timestamp - timestamp dona un interval: el mateix signe - amb dues semàntiques, de manera que migrar una columna de DATE a TIMESTAMP canvia el tipus de totes les teves restes en silenci. I date + INTERVAL retorna un TIMESTAMP, no un DATE; si necessites una data, afegeix-hi ::DATE.
INTERVAL i les seves unitats
SELECT DATE '2025-01-31' + INTERVAL '1 month' AS fi_de_gener,
DATE '2025-03-04' + INTERVAL '2 weeks' AS dues_setmanes,
DATE '2026-02-21' - INTERVAL '6 months' AS fa_mig_any;| fi_de_gener | dues_setmanes | fa_mig_any |
|---|---|---|
| 2025-02-28 00:00:00 | 2025-03-18 00:00:00 | 2025-08-21 00:00:00 |
Fixa't en la primera: 31 de gener + 1 mes = 28 de febrer, perquè el 31 de febrer no existeix i PostgreSQL ajusta a l'últim dia del mes. Un mes no és una quantitat fixa de dies: INTERVAL '1 month' i INTERVAL '30 days' són coses diferents.
AGE(): l'antiguitat llegible
AGE no retorna dies: retorna anys, mesos i dies, com ho diria una persona.
SELECT id, CONCAT_WS(' ', nom, cognoms) AS client, data_registre,
DATE '2026-03-01' - data_registre AS dies,
AGE(DATE '2026-03-01', data_registre) AS antiguitat
FROM clients WHERE id IN (1, 4, 7, 12, 15) ORDER BY id;| id | client | data_registre | dies | antiguitat |
|---|---|---|---|---|
| 1 | Lucía Martínez Soler | 2025-01-10 | 415 | 1 year 1 mon 19 days |
| 4 | Javier Ortega Ruiz | 2025-02-14 | 380 | 1 year 15 days |
| 7 | Sofia Moreira Costa | 2025-03-21 | 345 | 11 mons 8 days |
| 12 | Diego Ramos Herrera | 2025-06-01 | 273 | 9 mons |
| 15 | Inés Carrasco Vega | 2026-01-08 | 52 | 1 mon 21 days |
Dos detalls: PostgreSQL omet els components que valen zero (en Javier no mostra "0 mons"; en Diego, ni mesos ni dies), i AGE és el que vols per mostrar a una persona mentre que la resta en dies és el que vols per ordenar i comparar.
Cas: dies entre la comanda i la devolució
SELECT d.id AS devolucio, d.comanda_id, co.data_comanda,
d.data AS data_devolucio,
d.data - co.data_comanda AS dies_transcorreguts,
d.import
FROM devolucions AS d
JOIN comandes AS co ON co.id = d.comanda_id
ORDER BY d.id;| devolucio | comanda_id | data_comanda | data_devolucio | dies_transcorreguts | import |
|---|---|---|---|---|---|
| 1 | 6 | 2025-05-23 | 2025-05-25 | 2 | 26.75 |
| 2 | 10 | 2025-08-03 | 2025-08-11 | 8 | 34.02 |
| 3 | 13 | 2025-10-22 | 2025-10-30 | 8 | 19.80 |
Les tres devolucions de BotigaVerda: la de la comanda cancel·lada es va tramitar en 2 dies i les dues per incidència de producte, en 8. Amb TIMESTAMP en lloc de DATE aquesta resta retornaria un INTERVAL com ara 8 days 03:12:00, més precís i menys còmode d'agregar.
EXTRACT i DATE_PART
EXTRACT i DATE_PARTEXTRACT(camp FROM data) treu un component. DATE_PART('camp', data) fa el mateix amb sintaxi de funció; EXTRACT és l'estàndard SQL i és la forma preferible.
| Camp | Què retorna | Per a DATE '2025-03-04' |
|---|---|---|
YEAR |
Any | 2025 |
MONTH |
Mes, 1-12 | 3 |
DAY |
Dia del mes | 4 |
QUARTER |
Trimestre, 1-4 | 1 |
WEEK |
Setmana ISO, 1-53 | 10 |
DOW / ISODOW |
Dia de la setmana: 0 = diumenge / 1 = dilluns | 2 / 2 |
DOY |
Dia de l'any, 1-366 | 63 |
EPOCH |
Segons des de 1970-01-01 | 1741046400 |
SELECT id, data_comanda,
EXTRACT(YEAR FROM data_comanda) AS any_,
EXTRACT(MONTH FROM data_comanda) AS mes,
EXTRACT(QUARTER FROM data_comanda) AS trimestre,
EXTRACT(DOW FROM data_comanda) AS dow,
EXTRACT(WEEK FROM data_comanda) AS setmana_iso
FROM comandes WHERE id IN (1, 8, 13, 17, 20) ORDER BY id;| id | data_comanda | any_ | mes | trimestre | dow | setmana_iso |
|---|---|---|---|---|---|---|
| 1 | 2025-03-04 | 2025 | 3 | 1 | 2 | 10 |
| 8 | 2025-06-28 | 2025 | 6 | 2 | 6 | 26 |
| 13 | 2025-10-22 | 2025 | 10 | 4 | 3 | 43 |
| 17 | 2026-01-13 | 2026 | 1 | 1 | 2 | 3 |
| 20 | 2026-02-21 | 2026 | 2 | 1 | 6 | 8 |
Tres avisos: DOW comença en diumenge amb el 0 (convenció de C, no europea); WEEK és la setmana ISO, de manera que els primers dies de gener poden pertànyer a la setmana 52 o 53 de l'any anterior; i EXTRACT retorna NUMERIC des de PostgreSQL 14. I el de rendiment, ja conegut: WHERE EXTRACT(YEAR FROM data_comanda) = 2025 funciona però embolcalla la columna en una funció i no aprofita l'índex; la forma indexable és la de 02-03, WHERE data_comanda >= '2025-01-01' AND data_comanda < '2026-01-01'. Ho mesuraràs a 08-03.
DATE_TRUNC: el patró de l'informe temporal
DATE_TRUNC: el patró de l'informe temporalDATE_TRUNC(unitat, data) rebaixa la data a l'inici de la unitat demanada: el 22 d'octubre truncat a mes és l'1 d'octubre. Aquesta és tota la idea, i és la base de gairebé tot informe temporal.
| Crida | Resultat per a 2025-10-22 |
|---|---|
DATE_TRUNC('day', d) |
2025-10-22 00:00:00 |
DATE_TRUNC('week', d) |
2025-10-20 00:00:00 (dilluns ISO) |
DATE_TRUNC('month', d) |
2025-10-01 00:00:00 |
DATE_TRUNC('quarter', d) |
2025-10-01 00:00:00 |
DATE_TRUNC('year', d) |
2025-01-01 00:00:00 |
Compte amb el tipus: encara que li passis un
DATE,DATE_TRUNCretorna unTIMESTAMPi per això hi veus el00:00:00. Perquè l'informe mostri una data neta, afegeix-hi::DATE.
Facturació mensual
SELECT DATE_TRUNC('month', co.data_comanda)::DATE AS mes,
COUNT(DISTINCT co.id) AS comandes,
COUNT(*) AS linies,
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 DATE_TRUNC('month', co.data_comanda) ORDER BY mes;| mes | comandes | linies | facturacio |
|---|---|---|---|
| 2025-03-01 | 2 | 5 | 68.80 |
| 2025-04-01 | 2 | 5 | 61.28 |
| 2025-05-01 | 2 | 5 | 58.85 |
| 2025-06-01 | 2 | 5 | 95.48 |
| 2025-07-01 | 1 | 3 | 44.60 |
| 2025-08-01 | 1 | 2 | 48.27 |
| 2025-09-01 | 1 | 2 | 32.76 |
| 2025-10-01 | 2 | 5 | 97.20 |
| 2025-11-01 | 1 | 3 | 31.70 |
| 2025-12-01 | 2 | 5 | 64.58 |
| 2026-01-01 | 2 | 4 | 75.10 |
| 2026-02-01 | 2 | 3 | 49.33 |
12 mesos, 20 comandes, 47 línies i 727,95 € sumant l'última columna: la xifra canònica del mòdul 4, ara desglossada. I els anys quadren: els deu primers mesos sumen 603,52 € (2025) i els dos últims, 124,43 € (2026). El millor mes és l'octubre del 2025 amb 97,20 €; el pitjor, el novembre del 2025 amb 31,70 €.
Per trimestre
SELECT DATE_TRUNC('quarter', co.data_comanda)::DATE AS trimestre,
COUNT(DISTINCT co.id) AS comandes,
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 DATE_TRUNC('quarter', co.data_comanda) ORDER BY trimestre;| trimestre | comandes | facturacio |
|---|---|---|
| 2025-01-01 | 2 | 68.80 |
| 2025-04-01 | 6 | 215.61 |
| 2025-07-01 | 3 | 125.63 |
| 2025-10-01 | 5 | 193.48 |
| 2026-01-01 | 4 | 124.43 |
DATE_TRUNC enfront d'EXTRACT per agrupar: totes dues funcionen però no són equivalents. EXTRACT(MONTH FROM d) retorna 3 per al març de qualsevol any, així que el març del 2025 i el del 2026 caurien al mateix grup; DATE_TRUNC('month', d) conserva l'any i ordena cronològicament sense trucs. EXTRACT per a estacionalitat, DATE_TRUNC per a sèries temporals.
- Formatar amb
TO_CHAR, analitzar amb TO_DATE
TO_CHAR, analitzar amb TO_DATETO_CHAR(data, patro) converteix una data en text amb el format que vulguis.
| Patró | Què produeix | Per a 2025-03-04 |
|---|---|---|
'YYYY-MM-DD' |
ISO | 2025-03-04 |
'DD/MM/YYYY' |
Format català | 04/03/2025 |
'YYYY-MM' |
Any i mes, ordenable com a text | 2025-03 |
'YYYY"-Q"Q' |
Any i trimestre (text literal entre cometes) | 2025-Q1 |
'Month' / 'FMMonth' |
Mes en anglès, amb i sense farciment a 9 caràcters | March / March |
'TMMonth' |
Mes traduït segons lc_time, sense farciment |
Març |
'TMDay' |
Dia de la setmana traduït | Dimarts |
'IW' / 'Q' |
Setmana ISO / trimestre | 10 / 1 |
Perquè la TM funcioni en català cal fixar la configuració regional de la sessió:
SET lc_time = 'ca_ES.UTF-8';
SELECT DATE_TRUNC('month', data_comanda)::DATE AS mes,
TO_CHAR(data_comanda, 'YYYY-MM') AS periode,
TO_CHAR(data_comanda, 'DD/MM/YYYY') AS data_ca,
TO_CHAR(data_comanda, 'TMMonth YYYY') AS mes_llarg,
TO_CHAR(data_comanda, 'TMDay') AS dia_setmana
FROM comandes WHERE id IN (1, 13, 20) ORDER BY id;| mes | periode | data_ca | mes_llarg | dia_setmana |
|---|---|---|---|---|
| 2025-03-01 | 2025-03 | 04/03/2025 | Març 2025 | Dimarts |
| 2025-10-01 | 2025-10 | 22/10/2025 | Octubre 2025 | Dimecres |
| 2026-02-01 | 2026-02 | 21/02/2026 | Febrer 2026 | Dissabte |
Regla d'or: formata només en presentar.
TO_CHAR(d, 'DD/MM/YYYY')produeix text, i el text s'ordena alfabèticament:'01/12/2025'aniria abans que'04/03/2025'. Si necessites ordenar o agrupar, fes-ho per la data i formata després. L'única excepció segura és'YYYY-MM', que sí que ordena bé com a text perquè és ISO.
TO_DATE(text, patro) fa el camí invers, i és on viu l'ambigüitat clàssica:
SELECT TO_DATE('03/04/2025', 'DD/MM/YYYY') AS interpretacio_europea,
TO_DATE('03/04/2025', 'MM/DD/YYYY') AS interpretacio_americana;| interpretacio_europea | interpretacio_americana |
|---|---|
| 2025-04-03 | 2025-03-04 |
El mateix text, dues dates diferents, cap de les dues incorrecta. Per això el patró és obligatori i per això el format ISO AAAA-MM-DD és l'únic que no s'interpreta malament mai. Quan importis un CSV, exigeix el format a l'especificació i no el dedueixis de les dades: '03/04/2025' no et dirà quin és.
- Informes sense buits amb
generate_series
generate_seriesAquí es cobra la promesa de 03-06 i de 06-02. Un GROUP BY només retorna els grups que existeixen a les dades: si en un mes no hi va haver cap alta, aquell mes no apareix i l'informe menteix per omissió. Les altes de clients per mes, tal com surten del GROUP BY, donen 8 files per a 13 mesos d'història. La solució és fabricar la sèrie completa de mesos i unir-la per l'esquerra:
SELECT s.mes::DATE AS mes,
COUNT(c.id) AS altes
FROM generate_series(DATE '2025-01-01', DATE '2026-01-01', INTERVAL '1 month') AS s(mes)
LEFT JOIN clients AS c
ON DATE_TRUNC('month', c.data_registre) = s.mes
GROUP BY s.mes
ORDER BY s.mes;| mes | altes |
|---|---|
| 2025-01-01 | 2 |
| 2025-02-01 | 3 |
| 2025-03-01 | 2 |
| 2025-04-01 | 2 |
| 2025-05-01 | 2 |
| 2025-06-01 | 2 |
| 2025-07-01 | 0 |
| 2025-08-01 | 0 |
| 2025-09-01 | 1 |
| 2025-10-01 | 0 |
| 2025-11-01 | 0 |
| 2025-12-01 | 0 |
| 2026-01-01 | 1 |
13 files i 15 clients. Ara es veu el que el GROUP BY amagava: BotigaVerda va captar clients amb regularitat fins al juny del 2025 i es va assecar durant el segon semestre —cinc mesos amb zero altes—. Aquesta és una conclusió de negoci que senzillament no existia a l'informe de 8 files.
Tres detalls fan que el patró funcioni: el LEFT JOIN va de la sèrie a les dades i mai a l'inrevés; es compta COUNT(c.id) i no COUNT(*), perquè COUNT(*) comptaria la fila fabricada pel LEFT JOIN i donaria 1 on ha de donar 0 (04-04); i generate_series amb INTERVAL '1 month' genera timestamps, que el ::DATE neteja i el DATE_TRUNC de l'ON fa comparables. El mateix esquema serveix per a dies de la setmana, trimestres o l'eix horari d'un tauler: és el patró d'informe més reutilitzable del curs.
- Taula comparativa per motor
| Tasca | PostgreSQL 16 | MySQL 8 | SQLite | SQL Server | Oracle |
|---|---|---|---|---|---|
| Data d'avui | CURRENT_DATE |
CURDATE() |
DATE('now') |
CAST(GETDATE() AS DATE) |
TRUNC(SYSDATE) |
| Instant actual | NOW(), CURRENT_TIMESTAMP |
NOW() |
DATETIME('now') |
SYSDATETIME() |
SYSTIMESTAMP |
| Diferència en dies | d2 - d1 |
DATEDIFF(d2, d1) |
JULIANDAY(d2) - JULIANDAY(d1) |
DATEDIFF(day, d1, d2) |
d2 - d1 |
| Sumar dies | d + 30 |
DATE_ADD(d, INTERVAL 30 DAY) |
DATE(d, '+30 days') |
DATEADD(day, 30, d) |
d + 30 |
| Extreure un component | EXTRACT(YEAR FROM d) |
EXTRACT, YEAR(d) |
STRFTIME('%Y', d) |
DATEPART(year, d) |
EXTRACT(YEAR FROM d) |
| Truncar a mes | DATE_TRUNC('month', d) |
DATE_FORMAT(d, '%Y-%m-01') |
DATE(d, 'start of month') |
DATETRUNC(month, d) (2022+) |
TRUNC(d, 'MM') |
| Formatar | TO_CHAR(d, 'DD/MM/YYYY') |
DATE_FORMAT(d, '%d/%m/%Y') |
STRFTIME('%d/%m/%Y', d) |
FORMAT(d, 'dd/MM/yyyy') |
TO_CHAR(d, 'DD/MM/YYYY') |
| Analitzar text | TO_DATE(t, 'DD/MM/YYYY') |
STR_TO_DATE(t, '%d/%m/%Y') |
— | PARSE(t AS date …) |
TO_DATE(t, 'DD/MM/YYYY') |
| Tipus amb zona horària | TIMESTAMPTZ |
TIMESTAMP (desa en UTC) |
no hi ha tipus data | DATETIMEOFFSET |
TIMESTAMP WITH TIME ZONE |
| Sèrie de dates | generate_series(d1, d2, '1 day') |
CTE recursiva | CTE recursiva | GENERATE_SERIES (2022+) |
CONNECT BY LEVEL |
Dos advertiments valen per tota la taula. SQLite no té tipus de data: desa text, números julians o segons epoch, i totes les seves funcions són STRFTIME sobre text; funciona, però res no impedeix ficar '04/03/2025' a la mateixa columna. I el DATEDIFF de MySQL i el de SQL Server porten els arguments en ordre diferent —DATEDIFF(d2, d1) enfront de DATEDIFF(day, d1, d2)—, de manera que una migració descurada canvia el signe de tots els resultats.
I l'error que cal evitar en qualsevol motor: desar dates com a text. Una columna
VARCHAR(10)amb'2025-03-04'accepta'2025-13-45', no es pot sumar ni restar, s'ordena malament tan bon punt algú escrigui'4/3/2025'i no pot fer servir un índex de rang amb eficàcia. Si la dada és una data, el tipus ésDATE.
Errors habituals i consells
- Desar dates com a text. L'error d'arrel.
DATEvalida, ordena, resta i indexa;VARCHARno fa res d'això. - Fer servir
TIMESTAMPsense zona en una aplicació amb usuaris de diversos països.TIMESTAMPTZés l'elecció per omissió. - Esperar que
date + INTERVAL '1 day'retorni unDATE. RetornaTIMESTAMP, igual queDATE_TRUNC: d'aquí els00:00:00dels informes. Afegeix-hi::DATE. - Creure que
INTERVAL '1 month'són 30 dies.2025-01-31 + 1 mesés2025-02-28. I no confonguisDOW(diumenge = 0) ambISODOW(dilluns = 1). - Agrupar per
EXTRACT(MONTH …)en una sèrie de diversos anys. El març del 2025 i el març del 2026 cauen al mateix grup. Per a sèries temporals,DATE_TRUNC. - Filtrar amb
EXTRACT(YEAR FROM d) = 2025. Correcte però no indexable. Fes servir>= '2025-01-01' AND < '2026-01-01'(02-03). - Ordenar per una data ja formatada amb
TO_CHAR. S'ordena com a text. Ordena per la data i formata després;'YYYY-MM'és l'única excepció segura. - Interpretar
'03/04/2025'sense especificar el patró. Són dues dates diferents segons el país. - Comptar amb
COUNT(*)en un informe ambgenerate_series. Compta la fila fabricada pelLEFT JOIN; compta una columna de la taula de dades. - Consell: prefereix
>= inici AND < fiaBETWEENen dates. AmbDATEfuncionen igual, però el dia que la columna passi aTIMESTAMP,BETWEENperdrà tot l'últim dia llevat de les 00:00. - Consell: desa en UTC i converteix en presentar, i per depurar fixa una data de referència (
DATE '2026-03-01') en comptes deCURRENT_DATE: els teus resultats seran reproduïbles demà.
Exercicis
Exercici 1
Direcció vol l'informe mensual del 2025 complet. Per a cadascun dels dotze mesos —encara que no hi hagi res— mostra el mes en format YYYY-MM, el nombre de comandes i la facturació de producte. Fes servir generate_series i explica per què el gener i el febrer apareixen a zero.
Exercici 2
Màrqueting vol mesurar la velocitat de conversió: quants dies passen entre el registre d'un client i la seva primera comanda. Per als clients 1 a 8, mostra el nom complet, la data_registre, la data de la primera comanda i els dies transcorreguts, ordenat per dies. (Pista: MIN(data_comanda) agrupant per client.)
Exercici 3
Un company ha escrit aquest informe d'estacionalitat i afirma que "el març és el nostre mes més fluix":
-- ⚠️ Sospitosa
SELECT EXTRACT(MONTH FROM data_comanda) AS mes, COUNT(*) AS comandes
FROM comandes GROUP BY EXTRACT(MONTH FROM data_comanda) ORDER BY mes;- Què està mesurant realment i per què la conclusió és fràgil amb les dades de BotigaVerda?
- Escriu la versió amb
DATE_TRUNCque respon a "com evoluciona el negoci mes a mes?". - En quin cas sí que seria correcta la consulta original?
Solucions
Solució 1
SELECT TO_CHAR(s.mes, 'YYYY-MM') AS mes,
COUNT(DISTINCT co.id) AS comandes,
COALESCE(ROUND(SUM(lc.quantitat * lc.preu_unitari
* (1 - lc.descompte)), 2), 0) AS facturacio
FROM generate_series(DATE '2025-01-01', DATE '2025-12-01', INTERVAL '1 month') AS s(mes)
LEFT JOIN comandes AS co ON DATE_TRUNC('month', co.data_comanda) = s.mes
LEFT JOIN linies_comanda AS lc ON lc.comanda_id = co.id
GROUP BY s.mes
ORDER BY s.mes;| mes | comandes | facturacio |
|---|---|---|
| 2025-01 | 0 | 0.00 |
| 2025-02 | 0 | 0.00 |
| 2025-03 | 2 | 68.80 |
| 2025-04 | 2 | 61.28 |
| 2025-05 | 2 | 58.85 |
| 2025-06 | 2 | 95.48 |
| 2025-07 | 1 | 44.60 |
| 2025-08 | 1 | 48.27 |
| 2025-09 | 1 | 32.76 |
| 2025-10 | 2 | 97.20 |
| 2025-11 | 1 | 31.70 |
| 2025-12 | 2 | 64.58 |
12 files, 16 comandes i 603,52 €: la facturació del 2025 del mòdul 4. El gener i el febrer surten a zero perquè la primera comanda de BotigaVerda és del 4 de març del 2025: la botiga existia però encara no venia. Sense generate_series aquests dos mesos no apareixerien i un gràfic de línies començaria al març, donant a entendre que no hi ha història anterior. El COALESCE (06-04) converteix el NULL de SUM sobre un conjunt buit en 0.00.
Solució 2
SELECT c.id, CONCAT_WS(' ', c.nom, c.cognoms) AS client, c.data_registre,
MIN(co.data_comanda) AS primera_comanda,
MIN(co.data_comanda) - c.data_registre AS dies
FROM clients AS c
JOIN comandes AS co ON co.client_id = c.id
WHERE c.id <= 8
GROUP BY c.id, c.nom, c.cognoms, c.data_registre ORDER BY dies;| id | client | data_registre | primera_comanda | dies |
|---|---|---|---|---|
| 2 | Carlos Ferrer Ibáñez | 2025-01-22 | 2025-03-12 | 49 |
| 1 | Lucía Martínez Soler | 2025-01-10 | 2025-03-04 | 53 |
| 3 | Marta Sanchis Gil | 2025-02-03 | 2025-04-02 | 58 |
| 4 | Javier Ortega Ruiz | 2025-02-14 | 2025-04-19 | 64 |
| 5 | Ana Belmonte Roca | 2025-02-27 | 2025-05-23 | 85 |
| 6 | Pau Llorens Vidal | 2025-03-09 | 2025-06-11 | 94 |
| 7 | Sofia Moreira Costa | 2025-03-21 | 2025-06-28 | 99 |
| 8 | Tiago Almeida Nunes | 2025-04-04 | 2025-07-15 | 102 |
La lectura de negoci és incòmoda: la conversió empitjora a mesura que avança l'any, de 49 a 102 dies. I hi ha un biaix que cal declarar: els clients recents han tingut menys temps per comprar. Amb JOIN normal desapareixen a més els clients 13, 14 i 15, que no han demanat mai res; amb LEFT JOIN apareixerien amb NULL a les dues últimes columnes, que és la resposta honesta.
Solució 3
1. EXTRACT(MONTH …) retorna 3 tant per al març del 2025 com per al del 2026, així que mesura estacionalitat, no evolució: agrupa tots els marços de la història. Amb les dades de BotigaVerda la conclusió és fràgil perquè només hi ha un març amb dades i dotze mesos d'història, de manera que cada "mes de l'any" té entre una i dues comandes. Amb 20 comandes en 12 mesos no hi ha estacionalitat per mesurar: hi ha soroll.
2. La sèrie temporal és la consulta de la secció 5, i la seva lectura és la contrària: mesos bons i dolents alterns, màxim a l'octubre del 2025 (97,20 €) i mínim al novembre (31,70 €), sense tendència clara.
3. Seria correcta si la pregunta fos de debò estacional —"en quin mes de l'any venem més, fent la mitjana de diversos anys?"— i hi hagués prou anys per fer la mitjana. L'incorrecte no és la funció: és fer-la servir per respondre una pregunta d'evolució.
Conclusió
Ja saps treballar amb el temps:
- Coneixes els cinc tipus temporals i per què BotigaVerda fa servir
DATE, amb la recomanació ferma que en una aplicació realTIMESTAMPTZés gairebé sempre l'elecció correcta. I distingeixesNOW()—congelada durant tota la transacció— deCLOCK_TIMESTAMP(). - Fas aritmètica de dates sabent que
date - datedona dies enters, quedate + INTERVALdonaTIMESTAMPi queINTERVAL '1 month'no són 30 dies. I fas servirAGE()quan el destinatari és una persona. - Treus components amb
EXTRACT(DOWcomença en diumenge,WEEKés ISO) i trunques ambDATE_TRUNC, que converteix 20 comandes en una sèrie mensual — el deute de 04-05, saldat. I saps quan fer servir cadascun:EXTRACTper a estacionalitat,DATE_TRUNCper a evolució. - Formates amb
TO_CHAR(inclòsTMMonthamblc_time) i analitzes ambTO_DATE, recordant que'03/04/2025'són dues dates diferents i que ordenar text no és ordenar dates. - Construeixes informes sense buits amb
generate_series+LEFT JOIN+COUNT(columna), i has vist el que amagaven: cinc mesos seguits sense ni una sola alta de client.
Queda un cap solt que has vist tres vegades en aquesta lliçó i no has pogut lligar: a l'informe d'altes per mes va caldre COUNT(c.id) en lloc de COUNT(*) perquè sortís 0; al de facturació mensual va caldre un COALESCE perquè un mes buit no mostrés NULL; i al llistat de clients sense comandes, les columnes de data apareixerien buides sense dir per què. Tots tres són el mateix problema —què fer quan no hi ha valor— i la lliçó següent, conversió de tipus i tractament dels NULL, el resol definitivament amb CAST, COALESCE i NULLIF. A més tancarà un altre compte pendent: has estat escrivint ::DATE, ::NUMERIC i ::TEXT durant tres lliçons sense que ningú t'expliqués què són.
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
