Una vista dona nom a una consulta per sempre i per a tothom. Moltes vegades el que necessites és el contrari: donar nom a un pas intermedi aquí, ara i només dins d'aquesta consulta, sense crear cap objecte ni demanar permís a ningú. Això és una expressió de taula comuna —Common Table Expression, CTE—, i s'escriu posant WITH davant del SELECT.
Amb elles tancaràs dues promeses del curs. La primera és de 07-04 i 07-05: la llegibilitat es trenca a partir de tres nivells de taules derivades, i aquí veuràs la mateixa consulta escrita de les dues maneres, l'una al costat de l'altra. La segona és de 03-06: un SELF JOIN recorre un nivell de la jerarquia, i per recórrer un arbre de profunditat desconeguda cal WITH RECURSIVE — amb el qual trauràs per fi l'organigrama complet de BotigaVerda amb el seu nivell i la seva ruta, la cadena de referits de tres salts que va de la Lucía a la Núria, i una sèrie de dotze mesos generada del no-res.
Contingut
WITH: la subconsulta amb nom, posada al davant- Llegibilitat: tres nivells de taules derivades enfront de tres CTE
- Diverses CTE encadenades: la consulta per passos
- CTE, vista i taula derivada: quina fer servir
- Materialització: el que va canviar a PostgreSQL 12
- CTE a
INSERT,UPDATEiDELETE; el patró "moure files" WITH RECURSIVE: l'anatomia- Els tres casos de BotigaVerda
- Bucles infinits i com protegir-se'n
- Errors habituals i consells
- Exercicis
- Conclusió
WITH: la subconsulta amb nom, posada al davant
WITH: la subconsulta amb nom, posada al davantUna CTE és una subconsulta a la qual es dona un nom abans de la consulta principal. Viu només durant aquella sentència i es fa servir després com si fos una taula.
WITH totals_comanda AS (
SELECT co.id AS comanda_id,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS total
FROM comandes AS co JOIN linies_comanda AS lc ON lc.comanda_id = co.id
GROUP BY co.id
)
SELECT COUNT(*) AS comandes, ROUND(AVG(t.total), 2) AS tiquet_mitja,
ROUND(MIN(t.total), 2) AS tiquet_minim, ROUND(MAX(t.total), 2) AS tiquet_maxim,
ROUND(SUM(t.total), 2) AS facturacio
FROM totals_comanda AS t;| comandes | tiquet_mitja | tiquet_minim | tiquet_maxim | facturacio |
|---|---|---|---|---|
| 20 | 36.40 | 22.60 | 66.90 | 727.95 |
És exactament la taula derivada canònica de 07-04, amb les mateixes xifres, però llegida de dalt a baix: primer es defineix totals_comanda, després es fa servir. La sintaxi mínima és WITH nom1 AS ( SELECT ... ), nom2 AS ( SELECT ... ) SELECT ... FROM nom1 JOIN nom2 ..., on nom2 pot fer servir nom1.
Quatre regles que convé fixar des del principi: cada CTE necessita nom (i opcionalment en pot reanomenar les columnes, WITH t (comanda_id, total) AS (...)); se separen per comes, i WITH s'escriu una sola vegada encara que n'hi hagi cinc; una CTE pot referir-se a les anteriors, mai a les posteriors (tret que hi hagi RECURSIVE, apartat 7); i una CTE es pot fer servir diverses vegades a la mateixa consulta, cosa que una taula derivada no permet.
- Llegibilitat: tres nivells de taules derivades enfront de tres CTE
Aquí es tanca la promesa de 07-04. La pregunta: dels clients que han comprat, quins superen la facturació mitjana per client, i per quant? Són tres passos —total per comanda, total per client, mitjana d'aquests totals— i amb taules derivades queden imbricats:
-- ⚠️ Correcta, però il·legible: tres nivells d'imbricació
SELECT c.id, c.nom || ' ' || c.cognoms AS client, x.comandes, x.facturacio,
ROUND(x.facturacio - x.mitjana_clients, 2) AS dif
FROM (SELECT tc.client_id, tc.comandes, tc.facturacio,
(SELECT AVG(tc2.facturacio)
FROM (SELECT tp2.client_id, SUM(tp2.total) AS facturacio
FROM (SELECT co2.id, co2.client_id,
ROUND(SUM(lc2.quantitat * lc2.preu_unitari * (1 - lc2.descompte)), 2) AS total
FROM comandes co2 JOIN linies_comanda lc2 ON lc2.comanda_id = co2.id
GROUP BY co2.id, co2.client_id) AS tp2
GROUP BY tp2.client_id) AS tc2) AS mitjana_clients
FROM (SELECT tp.client_id, COUNT(*) AS comandes, SUM(tp.total) AS facturacio
FROM (/* … i aquí, un altre cop sencer, el mateix càlcul de tp2 … */) AS tp
GROUP BY tp.client_id) AS tc) AS x
JOIN clients AS c ON c.id = x.client_id
WHERE x.facturacio > x.mitjana_clients ORDER BY x.facturacio DESC;Compta els parèntesis, i fixa't que el mateix càlcul de totals per comanda hi apareix dues vegades, copiat, perquè una taula derivada no es pot reutilitzar. Ara el mateix amb CTE:
-- ✅ La mateixa consulta, per passos
WITH totals_comanda AS (
SELECT co.id AS comanda_id, co.client_id,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS total
FROM comandes AS co JOIN linies_comanda AS lc ON lc.comanda_id = co.id
GROUP BY co.id, co.client_id
),
totals_client AS (
SELECT client_id, COUNT(*) AS comandes, SUM(total) AS facturacio
FROM totals_comanda GROUP BY client_id
),
mitjana AS (
SELECT ROUND(AVG(facturacio), 2) AS mitjana_clients FROM totals_client
)
SELECT c.id, c.nom || ' ' || c.cognoms AS client,
tc.comandes, tc.facturacio, m.mitjana_clients,
ROUND(tc.facturacio - m.mitjana_clients, 2) AS dif
FROM totals_client AS tc
JOIN clients AS c ON c.id = tc.client_id
CROSS JOIN mitjana AS m
WHERE tc.facturacio > m.mitjana_clients ORDER BY tc.facturacio DESC;| id | client | comandes | facturacio | mitjana_clients | dif |
|---|---|---|---|---|---|
| 7 | Sofia Moreira Costa | 2 | 111.88 | 60.66 | 51.22 |
| 1 | Lucía Martínez Soler | 3 | 107.60 | 60.66 | 46.94 |
| 9 | Camille Dubois | 2 | 70.87 | 60.66 | 10.21 |
| 10 | Julien Moreau | 1 | 66.90 | 60.66 | 6.24 |
| 4 | Javier Ortega Ruiz | 2 | 62.93 | 60.66 | 2.27 |
Cinc dels dotze clients amb compres superen la mitjana de 60,66 € (727,95 € entre 12). El resultat és idèntic al de la versió anterior; el que ha canviat és que ara es pot llegir, revisar en un pull request i depurar pas a pas: n'hi ha prou amb canviar el SELECT final per SELECT * FROM totals_client per veure el resultat intermedi sense tocar res més.
I hi ha un guany que no es veu: totals_comanda s'escriu una vegada i es fa servir dues. A la versió imbricada estava duplicat, amb tot el que això significa el dia que canviï la fórmula de l'import.
- Diverses CTE encadenades: la consulta per passos
Aquest és el patró natural d'escriure anàlisis complexes: no es pensa en una consulta, sinó en una seqüència de transformacions.
flowchart LR
A["linies_comanda<br/>47 files"] --> B["totals_comanda<br/>20 files"] --> C["totals_client<br/>12 files"] --> D["mitjana<br/>1 fila"] --> E["final<br/>5 files"]
C --> E
Cada CTE redueix o transforma la granularitat i té un nom que diu què conté. Si el resultat final no quadra, el diagnòstic és mecànic: executa SELECT COUNT(*) FROM totals_comanda (n'han de sortir 20), després totals_client (12), i el nivell on el número no sigui l'esperat és on és l'error. És el consell de 07-04 —"compta les files de cada nivell"—, ara amb els nivells batejats. I anomena-les pel que contenen, no c1, c2, c3: una consulta amb WITH vendes_2025 AS ..., devolucions_2025 AS ..., net AS ... s'entén sense llegir el SQL.
- CTE, vista i taula derivada: quina fer servir
| Taula derivada | CTE | Vista | |
|---|---|---|---|
| On s'escriu | Al FROM |
Al davant, amb WITH |
A l'esquema, amb CREATE VIEW |
| Quant viu / la veuen altres sessions? | Aquella consulta / no | Aquella consulta / no | Fins al DROP / sí |
| Reutilitzable dins de la consulta | No: cal copiar-la | Sí, tantes vegades com vulguis | Sí |
| Llegible amb 3+ nivells / recursivitat | Malament / no | Bé / sí | Bé / només amb WITH RECURSIVE a dins |
| Requereix permisos per crear-la | No | No | Sí (CREATE sobre l'esquema) |
La regla de decisió cap en tres frases: un sol nivell i d'usar i llençar → taula derivada; dos passos o més, o el mateix pas fet servir dues vegades → CTE; el que faran servir altres consultes, altres persones o l'equip de BI → vista. I no són excloents: una vista pot estar definida amb WITH a dins.
- Materialització: el que va canviar a PostgreSQL 12
Aquest apartat corregeix el que diuen gairebé tots els tutorials antics. Fins a PostgreSQL 11, una CTE era una barrera d'optimització: el motor l'executava sencera, desava el seu resultat en memòria i només després continuava, de manera que un filtre de la consulta exterior no es podia empènyer cap a dins. Escriure WITH t AS (SELECT * FROM linies_comanda) SELECT * FROM t WHERE producte_id = 15 calculava les 47 files per descartar-ne gairebé totes.
Des de PostgreSQL 12 ja no és així. Una CTE que es fa servir una sola vegada i no té efectes secundaris s'integra (inlining) a la consulta principal, exactament igual que una vista o una taula derivada, i el planificador pot empènyer els filtres cap a dins. S'ha acabat la penalització per escriure consultes llegibles. Les dues paraules clau per forçar cada comportament:
| Clàusula | Què fa | Quan fer-la servir |
|---|---|---|
| (res) | S'integra si es fa servir una vegada; es materialitza si es fa servir dues o més | El 95 % dels casos |
AS MATERIALIZED |
Força a calcular-la una vegada i desar-ne el resultat | La CTE és cara i es fa servir diverses vegades; o vols una barrera deliberada (per exemple, perquè una funció volàtil s'avaluï una sola vegada) |
AS NOT MATERIALIZED |
Força la integració encara que es faci servir diverses vegades | La CTE és trivial i materialitzar-la fa nosa als índexs |
Escrit com a WITH totals_comanda AS MATERIALIZED ( ... ), el pas es calcula una sola vegada per moltes vegades que el consulti la sentència principal — que és el que vols quan aquest pas recorre 47 línies i el fas servir en dues subconsultes diferents.
La manera de saber què està passant és mirar-ho, no suposar-ho: si a l'EXPLAIN apareix un node CTE Scan sobre un CTE nom, s'ha materialitzat; si no apareix enlloc i veus directament els escanejos de les taules, s'ha integrat. La tècnica és la de 08-05.
L'error heretat. Durant anys es va ensenyar "fes servir una CTE per forçar l'ordre d'execució" i "no facis servir CTE, que són lentes". Les dues afirmacions van caducar a PostgreSQL 12. Si necessites la barrera, demana-la explícitament amb
MATERIALIZED; si no, escriu la CTE perquè es llegeix millor i no en paguis res. A MySQL 8 i SQL Server les CTE sempre s'integren i la paraula clau no existeix; a Oracle hi ha pistes equivalents (/*+ MATERIALIZE */).
- CTE a
INSERT, UPDATE i DELETE; el patró "moure files"
INSERT, UPDATE i DELETE; el patró "moure files"Un WITH pot precedir qualsevol sentència, no només un SELECT. I encara hi ha més: una CTE pot ser ella mateixa un INSERT, UPDATE o DELETE amb RETURNING, i el seu resultat alimentar el pas següent. Aquest és el patró que resol una cosa que a 05-04 era impossible en una sola sentència: moure files d'una taula a una altra.
BotigaVerda vol arxivar les comandes cancel·lades —avui, només la 6— en una taula històrica:
CREATE TABLE comandes_arxiu (LIKE comandes INCLUDING DEFAULTS); -- ⚠️ NO canònica
WITH esborrades AS (
DELETE FROM comandes WHERE estat = 'cancellat' RETURNING *
)
INSERT INTO comandes_arxiu SELECT * FROM esborrades; -- INSERT 0 1Una sola sentència, una sola transacció, cap finestra en què la fila no existeixi enlloc. Les tres propietats que ho fan possible: totes les subsentències veuen la mateixa instantània de la base de dades, la de l'inici de la sentència: la CTE esborrades no veu l'efecte de l'INSERT, i l'INSERT no veu l'efecte del DELETE sobre comandes. L'ordre d'execució no està garantit, així que escriure a la mateixa taula des de dues branques de la mateixa sentència dona resultats imprevisibles. I una CTE que escriu s'executa sempre, encara que la consulta principal no la faci servir: és l'única excepció a la integració de l'apartat 5.
Un segon ús, més quotidià: calcular amb WITH i actualitzar amb el resultat. Alinear el preu de catàleg amb el preu mitjà realment venut:
WITH preu_real AS (
SELECT lc.producte_id, ROUND(AVG(lc.preu_unitari), 2) AS mitjana
FROM linies_comanda AS lc GROUP BY lc.producte_id
)
UPDATE productes AS p SET preu = pr.mitjana
FROM preu_real AS pr WHERE pr.producte_id = p.id; -- UPDATE 17Disset productes, els que s'han venut alguna vegada. I fixa't en l'avantatge sobre la versió de 07-04: allà calia un WHERE EXISTS perquè els tres productes mai venuts no rebessin NULL; aquí el JOIN implícit amb la CTE ja els deixa fora. (Recarrega l'script després de provar-ho.)
WITH RECURSIVE: l'anatomia
WITH RECURSIVE: l'anatomiaAquí es tanca la promesa de 03-06. Una CTE recursiva és una CTE que es referencia a si mateixa, i té sempre la mateixa forma:
WITH RECURSIVE nom AS (
SELECT ... -- TERME BASE: d'on es parteix. No es referencia a si mateix
UNION ALL
SELECT ... FROM taula JOIN nom ON ... -- TERME RECURSIU: fa servir el resultat anterior
)
SELECT * FROM nom;Com s'executa, que és l'única cosa que cal entendre de debò:
flowchart TD
A["<b>Terme base</b><br/>Rosa (nivell 1)"] --> B["taula de treball: 1 fila"] --> C{"taula de treball<br/>buida?"}
C -->|no| D["<b>Terme recursiu</b>: fills de<br/>les files de la taula de treball"]
D --> E["s'acumulen al resultat i passen<br/>a ser la nova taula de treball"] --> C
C -->|sí| F["<b>fi</b>: es retorna<br/>tot l'acumulat"]
Iteració a iteració, amb l'organigrama de BotigaVerda: la base produeix Rosa; la primera iteració busca els fills de Rosa i produeix Andrés, Beatriz i Daniel; la segona busca els fills d'aquests tres i produeix Óscar, Laia, Marc i Irene; la tercera busca els fills d'aquests quatre, no en troba cap, la taula de treball queda buida i el procés acaba. Total: 1 + 3 + 4 = 8 files, els vuit empleats.
Tres detalls de sintaxi amb parany: RECURSIVE va immediatament després de WITH, una sola vegada, encara que hi hagi diverses CTE i només una sigui recursiva; UNION ALL no elimina duplicats i és el normal, mentre que UNION tot sol els elimina a cada pas —protecció rudimentària contra cicles, però més cara—; i el terme recursiu només pot referenciar la CTE una vegada, sense agregats, sense ORDER BY i sense LIMIT a dins.
- Els tres casos de BotigaVerda
8.1. La jerarquia d'empleats completa
WITH RECURSIVE arbre AS (
-- Base: l'arrel, qui no té cap
SELECT e.id, e.nom || ' ' || e.cognoms AS empleat, e.carrec,
1 AS nivell, e.nom AS ruta
FROM empleats AS e WHERE e.cap_id IS NULL
UNION ALL
-- Recursiu: els subordinats de qui ja és a l'arbre
SELECT e.id, e.nom || ' ' || e.cognoms, e.carrec,
a.nivell + 1, a.ruta || ' > ' || e.nom
FROM empleats AS e JOIN arbre AS a ON e.cap_id = a.id
)
SELECT nivell, id, empleat, carrec, ruta FROM arbre ORDER BY ruta;| nivell | id | empleat | carrec | ruta |
|---|---|---|---|---|
| 1 | 1 | Rosa Alcázar Vives | Directora general | Rosa |
| 2 | 2 | Andrés Company Talens | Responsable de vendes | Rosa > Andrés |
| 3 | 5 | Laia Puig Sanchis | Comercial | Rosa > Andrés > Laia |
| 3 | 6 | Marc Estévez Roig | Atenció al client | Rosa > Andrés > Marc |
| 3 | 4 | Óscar Peris Blasco | Comercial | Rosa > Andrés > Óscar |
| 2 | 3 | Beatriz Nadal Ripoll | Responsable de logística | Rosa > Beatriz |
| 3 | 7 | Irene Salvador Mira | Operària de magatzem | Rosa > Beatriz > Irene |
| 2 | 8 | Daniel Vercher Lluch | Analista de dades | Rosa > Daniel |
Els vuit empleats, amb la seva profunditat i la seva cadena de comandament, i presentats com un arbre gràcies a ORDER BY ruta. Compara-ho amb 03-06: allà calien dos LEFT JOIN encadenats, el nombre de nivells estava escrit a la consulta i tot i així només s'arribava fins a l'"avi". Aquí la consulta no sap quants nivells hi ha, i funcionaria igual amb quinze.
Dues variants d'una línia que valen molt: el subarbre d'una persona —canvia el WHERE e.cap_id IS NULL del terme base per WHERE e.id = 2 i obtens l'Andrés i els seus tres subordinats, 4 files—; i un ordre estable, perquè ruta amb noms depèn de com ordeni els accents l'idioma: acumula una segona columna amb els ids emplenats, a.ruta_id || '.' || lpad(e.id::text, 5, '0'), i ordena-hi.
8.2. La cadena de referits, cap amunt
La recursivitat també funciona en sentit contrari: en lloc de baixar de pares a fills, pujar de fill a pare. La pregunta de màrqueting és "de qui ve, en última instància, la Núria Bosch Ferrer?".
WITH RECURSIVE cadena AS (
SELECT c.id, c.nom || ' ' || c.cognoms AS client, c.referit_per_id,
0 AS salt, c.nom AS ruta
FROM clients AS c WHERE c.id = 13
UNION ALL
SELECT r.id, r.nom || ' ' || r.cognoms, r.referit_per_id,
ca.salt + 1, r.nom || ' > ' || ca.ruta
FROM clients AS r JOIN cadena AS ca ON ca.referit_per_id = r.id
)
SELECT salt, id, client, ruta FROM cadena ORDER BY salt;| salt | id | client | ruta |
|---|---|---|---|
| 0 | 13 | Núria Bosch Ferrer | Núria |
| 1 | 5 | Ana Belmonte Roca | Ana > Núria |
| 2 | 2 | Carlos Ferrer Ibáñez | Carlos > Ana > Núria |
| 3 | 1 | Lucía Martínez Soler | Lucía > Carlos > Ana > Núria |
La cadena de tres salts que 03-06 no podia recórrer: 1 → 2 → 5 → 13. La fila amb salt = 3 és l'origen de la branca, i es detecta perquè el seu referit_per_id és NULL. Girant el JOIN —ON h.referit_per_id = d.id— i partint del client 1, s'obté el contrari: tots els descendents de la Lucía, que són 5 (Carlos, Marta i Inés al nivell 1; Ana al 2; Núria al 3).
8.3. Generar una sèrie sense generate_series
La recursivitat no és només per a arbres: serveix per produir files del no-res — els dotze mesos de l'informe, sense cap taula pel mig:
WITH RECURSIVE mesos AS (
SELECT DATE '2025-03-01' AS mes
UNION ALL
SELECT (mes + INTERVAL '1 month')::date FROM mesos WHERE mes < DATE '2026-02-01'
)
SELECT to_char(mes, 'YYYY-MM') AS mes FROM mesos;Retorna dotze files, de 2025-03 a 2026-02: l'esquelet exacte de l'informe mensual. A PostgreSQL això s'escriu molt millor amb generate_series (03-06), però generate_series no existeix a MySQL ni a SQLite, i aquesta és la manera portable d'aconseguir el mateix. Fixa't en on és la condició de parada: dins del terme recursiu, al seu propi WHERE. Si la treus, la consulta no acaba mai.
- Bucles infinits i com protegir-se'n
El perill de la recursivitat és el cicle a les dades: si per un error de captura el client 1 aparegués com a referit pel 13, la consulta 8.2 donaria voltes per sempre generant files fins a esgotar el disc temporal. Quatre defenses, de la més artesanal a la més neta:
1. La columna de ruta amb = ANY(...). S'acumula el camí recorregut en un array i es rebutja qui ja hi sigui:
WITH RECURSIVE cadena AS (
SELECT c.id, c.referit_per_id, ARRAY[c.id] AS ruta
FROM clients AS c WHERE c.id = 13
UNION ALL
SELECT r.id, r.referit_per_id, ca.ruta || r.id
FROM clients AS r JOIN cadena AS ca ON ca.referit_per_id = r.id
WHERE NOT r.id = ANY(ca.ruta) -- ← la defensa
)
SELECT id, ruta FROM cadena;2. CYCLE, des de PostgreSQL 14, que és el mateix escrit pel motor. S'afegeix després del parèntesi de tancament de la CTE:
Significa: vigila la columna id, marca amb TRUE a es_cicle la fila en què es detecti repetició i deixa d'expandir per aquí; a cami queda el recorregut. És més curt, més ràpid i més difícil d'equivocar.
3. Un comptador de profunditat, útil quan a més vols limitar el nivell: WHERE a.nivell < 10 al terme recursiu. 4. El LIMIT d'emergència: SELECT * FROM cadena LIMIT 1000 atura l'execució en arribar a mil files, perquè PostgreSQL avalua la recursió de manera mandrosa. És una xarxa de seguretat per experimentar, no una solució: fes-la servir mentre desenvolupes una recursiva sobre dades que no coneixes.
Nota de dialecte:
Motor Paraula clau Detall PostgreSQL WITH RECURSIVEobligatoriCYCLEiSEARCHdes de la 14. SenseRECURSIVE, error de "la relació no existeix"SQL Server WITHtot solRECURSIVEno existeix;MAXRECURSIONlimita a 100 nivells per omissióMySQL 8 / MariaDB WITH RECURSIVELímit per cte_max_recursion_depth(1000 per omissió)SQLite WITH RECURSIVE(la paraula és opcional)Suport complet, inclòs UNIONOracle WITH RECURSIVE, o el clàssicCONNECT BYCONNECT BY PRIOR ... START WITH ...és anterior a l'estàndard i continua molt viu, ambLEVEL,SYS_CONNECT_BY_PATHiNOCYCLE
Errors habituals i consells
- Oblidar
RECURSIVE. Sense ell, la CTE no pot referir-se a si mateixa:ERROR: relation "arbre" does not exist. I va darrere deWITH, no davant del nom de la CTE recursiva. - Repetir
WITHa cada CTE (s'escriu una sola vegada; les altres se separen per comes) o referir-se a una CTE definida més avall (només es veuen les anteriors, tret de l'autoreferència deRECURSIVE). - Escriure un terme recursiu sense condició de parada. La consulta no acaba. En un arbre, la parada és implícita (s'acaben els fills); en una sèrie generada, l'has d'escriure tu al
WHERE. - Ignorar els cicles a les dades. Una jerarquia amb un bucle penja la consulta.
CYCLE(PG 14+) o la columna de ruta amb= ANY(...). - Creure que una CTE és sempre una barrera d'optimització. Ho era fins a PostgreSQL 11. Des de la 12 s'integra si es fa servir una vegada; si vols la barrera, demana-la amb
MATERIALIZED. - Fer servir
UNIONen lloc d'UNION ALL"per si de cas". Elimina duplicats a cada pas i costa bastant més. I escriure a la mateixa taula des de dues branques de la mateixa sentència: l'ordre no està garantit i el resultat és imprevisible. - Consell: escriu la consulta per passos i executa-la per passos. Substitueix el
SELECTfinal perSELECT * FROM pas_intermedii comprova les files de cada nivell: 47 → 20 → 12 → 1. - Consell: en una recursiva, comença sempre pel terme base sol, comprova que retorna exactament les arrels que esperes i només llavors afegeix l'
UNION ALL. I acumula semprenivelliruta: no costen res i són la meitat del diagnòstic quan alguna cosa surt malament.
Exercicis
Exercici 1
Reescriu amb CTE l'informe de "dos agregats de granularitat diferent" de 07-04: per client, nombre de comandes, facturació de producte, ports i total. Ha de quadrar en 727,95 € + 118,25 € = 846,20 €. (1) Escriu-lo amb dues CTE (per_comanda i per_client) en lloc de dues taules derivades. (2) Afegeix-ne una tercera que calculi els totals generals i mostra'ls al costat dels de cada client. (3) S'integraran les CTE o es materialitzaran? Com ho comprovaries?
Exercici 2
RH vol, per a cada empleat, quantes persones té per sota en total (directes i indirectes). (1) Escriu una CTE recursiva que retorni tots els parells (cap, subordinat a qualsevol profunditat). (2) Agrega per obtenir el recompte per cap, incloent-hi amb 0 els qui no tenen ningú. (3) Comprova que la Rosa surt amb 7 i l'Andrés amb 3.
Exercici 3
Un company ha escrit això per arxivar les ressenyes de productes descatalogats i no entén el resultat:
WITH mogudes AS (
DELETE FROM ressenyes AS r USING productes AS p
WHERE p.id = r.producte_id AND p.actiu = FALSE RETURNING r.*
)
SELECT COUNT(*) FROM ressenyes;- Què retorna el
COUNT, 12 o un altre número? Per què? - S'ha esborrat alguna cosa realment? Quantes files?
- Reescriu-ho perquè arxivi de debò en una taula
ressenyes_arxiui retorni quantes n'ha mogut.
Solucions
Solució 1
1 i 2:
WITH per_comanda AS ( -- 20 files: una comanda, els seus productes i els seus ports
SELECT co.id AS comanda_id, co.client_id, co.despeses_enviament,
ROUND(SUM(lc.quantitat * lc.preu_unitari * (1 - lc.descompte)), 2) AS productes
FROM comandes AS co JOIN linies_comanda AS lc ON lc.comanda_id = co.id
GROUP BY co.id, co.client_id, co.despeses_enviament
),
per_client AS ( -- 12 files
SELECT client_id, COUNT(*) AS comandes,
SUM(productes) AS productes, SUM(despeses_enviament) AS ports
FROM per_comanda GROUP BY client_id
),
total_general AS ( -- 1 fila
SELECT SUM(productes) AS productes_tot, SUM(ports) AS ports_tot FROM per_client)
SELECT c.nom || ' ' || c.cognoms AS client, pc.comandes, pc.productes, pc.ports,
ROUND(pc.productes + pc.ports, 2) AS total, tg.productes_tot, tg.ports_tot
FROM per_client AS pc JOIN clients AS c ON c.id = pc.client_id
CROSS JOIN total_general AS tg ORDER BY total DESC;| client | comandes | productes | ports | total | productes_tot | ports_tot |
|---|---|---|---|---|---|---|
| Sofia Moreira Costa | 2 | 111.88 | 19.80 | 131.68 | 727.95 | 118.25 |
| Lucía Martínez Soler | 3 | 107.60 | 4.95 | 112.55 | 727.95 | 118.25 |
(2 primeres de 12 files.) Les columnes quadren: 727,95 + 118,25 = 846,20 €. El que resol el problema és el mateix que a 07-04 —agregar cada cosa a la seva granularitat abans d'unir—, però ara els passos tenen nom i per_comanda es defineix una vegada i es fa servir dues vegades (des de per_client i, a través d'ella, des de total_general).
3. per_comanda es fa servir una sola vegada (des de per_client), així que s'integrarà; per_client es fa servir dues vegades, així que PostgreSQL la materialitzarà — i aquí és el desitjable, perquè calcular-la dues vegades seria recórrer 47 línies dues vegades. Es comprova amb EXPLAIN (ANALYZE, COSTS OFF): si apareix un node CTE Scan on per_client, s'ha materialitzat.
Solució 2
WITH RECURSIVE descendencia AS (
SELECT e.id AS cap_id, e.id AS emp_id FROM empleats AS e
UNION ALL
SELECT d.cap_id, e.id
FROM empleats AS e JOIN descendencia AS d ON e.cap_id = d.emp_id
)
SELECT e.id, e.nom || ' ' || e.cognoms AS empleat, e.carrec,
COUNT(*) - 1 AS subordinats_totals
FROM descendencia AS d JOIN empleats AS e ON e.id = d.cap_id
GROUP BY e.id, e.nom, e.cognoms, e.carrec
ORDER BY subordinats_totals DESC, e.id;| id | empleat | carrec | subordinats_totals |
|---|---|---|---|
| 1 | Rosa Alcázar Vives | Directora general | 7 |
| 2 | Andrés Company Talens | Responsable de vendes | 3 |
| 3 | Beatriz Nadal Ripoll | Responsable de logística | 1 |
| 4 | Óscar Peris Blasco | Comercial | 0 |
(4 primeres de 8 files; els empleats 5, 6, 7 i 8 tanquen també amb 0.) La Rosa amb 7 i l'Andrés amb 3, com demanava l'enunciat. Dues idees fan que funcioni: el terme base arrenca des de tots els empleats alhora, no només des de l'arrel, de manera que cadascun construeix el seu propi subarbre; i el COUNT(*) - 1 descompta la fila (x, x) en què cada empleat es compta a si mateix — que és justament el que permet que els quatre sense subordinats apareguin amb 0 en lloc de desaparèixer.
Solució 3
1. Retorna 12, és a dir, el recompte d'abans de l'esborrat. És la propietat 1 de l'apartat 6: totes les parts de la sentència veuen la mateixa instantània, la de l'instant inicial. El SELECT principal no veu l'efecte del DELETE de la CTE.
2. Sí que s'ha esborrat, però zero files. L'únic producte amb actiu = FALSE és el 20 (Càpsules d'espirulina) i no té ressenyes, així que la CTE retorna el conjunt buit. Si el producte descatalogat fos l'1, se n'haurien esborrat les 2 ressenyes i el COUNT hauria continuat dient 12 — que és on és el parany real de l'exercici. 3. La versió correcta:
CREATE TABLE ressenyes_arxiu (LIKE ressenyes); -- ⚠️ NO canònica
WITH mogudes AS (
DELETE FROM ressenyes AS r USING productes AS p
WHERE p.id = r.producte_id AND p.actiu = FALSE RETURNING r.*
),
arxivades AS (
INSERT INTO ressenyes_arxiu SELECT * FROM mogudes RETURNING id
)
SELECT COUNT(*) AS ressenyes_arxivades FROM arxivades;Ara sí: el DELETE alimenta l'INSERT, l'INSERT retorna el que ha inserit i el SELECT final compta el resultat de l'operació, no l'estat d'una taula. És el patró "moure files" complet, en una sentència i en una transacció.
Conclusió
WITH és l'eina que converteix SQL en una cosa que es pot llegir:
- Una CTE és una subconsulta amb nom posada al davant. Viu només durant la sentència, es pot reutilitzar dins d'ella —cosa que una taula derivada no permet— i no requereix permisos ni deixa rastre. Enfront de tres nivells de taules derivades imbricades, tres CTE encadenades diuen el mateix amb la meitat de parèntesis i es depuren pas a pas: 47 línies → 20 comandes → 12 clients → 1 mitjana, amb 5 clients per sobre dels 60,66 € de mitjana.
- CTE, vista o taula derivada: un pas d'usar i llençar, derivada; dos passos o més o reutilització, CTE; una cosa que faran servir altres consultes i altres persones, vista.
- Des de PostgreSQL 12 una CTE ja no és una barrera d'optimització: s'integra si es fa servir una vegada i es materialitza si es fa servir diverses.
AS MATERIALIZEDiAS NOT MATERIALIZEDforcen cada comportament, i l'EXPLAINdiu quin està passant. El que diguin els tutorials anteriors al 2019 sobre això ja no val. - Un
WITHpot precedir unINSERT,UPDATEoDELETE, i una CTE pot ser ella mateixa una escriptura ambRETURNING: d'aquí el patró "moure files", que esborra d'una taula i insereix el que ha esborrat en una altra en una sola sentència atòmica. Totes les branques veuen la mateixa instantània. WITH RECURSIVE= terme baseUNION ALLterme recursiu, iterant fins que no surten files noves. Amb ell: l'organigrama complet amb nivell i ruta (8 empleats, 3 nivells), la cadena de referits de tres salts1 → 2 → 5 → 13, i una sèrie de 12 mesos generada sensegenerate_series. I els cicles a les dades, que la pengen, s'eviten amb una columna de ruta i= ANY(...), ambCYCLE ... SET ... USING ...des de PostgreSQL 14, amb un límit de profunditat o, mentre s'experimenta, amb unLIMITd'emergència.
Amb les CTE ja saps descompondre una consulta i recórrer una estructura. Queda el buit que 04-05 va deixar obert amb totes les lletres: agregar sense col·lapsar les files. Quan vols el total de la comanda al costat de cadascuna de les seves línies, el percentatge que representa cada producte sobre la facturació global, la posició de cada client en un rànquing, quant ha variat un mes respecte de l'anterior o la mitjana mòbil d'un trimestre, un GROUP BY no serveix: col·lapsa exactament el que vols conservar. A la lliçó següent, funcions de finestra, veuràs la clàusula OVER que resol tot això d'una vegada, l'anatomia de PARTITION BY / ORDER BY / marc, per què no es pot filtrar per una funció de finestra al WHERE —l'error més freqüent de tots— i els rànquings, acumulats i mitjanes mòbils de BotigaVerda calculats sense perdre ni una de les 47 línies.
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
