Tens l'encàrrec (12-01) i el contracte (12-02). Falta construir-lo, i construir-lo té un ordre: si escrius les consultes abans de tenir dades, no les pots comprovar; si crees els índexs abans de tenir les consultes, te'ls estàs inventant; i si comences pel CREATE TABLE sense haver dibuixat res, la tercera taula t'obligarà a refer les dues primeres.
Aquesta lliçó és la guia de construcció en set passos: el model complet, el DDL sencer, la tècnica per generar dades coherents, el mètode per escriure consultes sense equivocar-te i el criteri per decidir índexs i encapsulats. El que no et dona són les quinze consultes de l'enunciat fetes: això és 12-04, i arribar-hi sense haver-ho intentat és llençar el projecte.
Contingut
- Pas 1 — De l'enunciat al model
- Pas 2 — El DDL:
01-esquema.sql - Pas 3 — Dades de prova coherents:
02-dades.sql - Pas 4 — Les consultes: mètode de treball
- Pas 5 — Els índexs, després de les consultes
- Pas 6 — Vistes, procediments i disparadors
- Pas 7 — Seguretat i lliurament
- Cronograma orientatiu
- Errors habituals i consells
- Exercicis
- Conclusió
- Pas 1 — De l'enunciat al model
erDiagram
SEUS ||--o{ EXEMPLARS : "custodia"
SEUS ||--o{ BIBLIOTECARIS : "empra"
SEUS ||--o{ SOCIS : "dona d'alta"
SEUS ||--o{ RESERVES : "recull"
BIBLIOTECARIS ||--o{ BIBLIOTECARIS : "es responsable de"
BIBLIOTECARIS ||--o{ PRESTECS : "tramita"
MATERIES ||--o{ OBRES : "classifica"
EDITORIALS ||--o{ OBRES : "publica"
OBRES ||--o{ OBRES_AUTORS : "es signada en"
AUTORS ||--o{ OBRES_AUTORS : "signa"
OBRES ||--o{ EXEMPLARS : "es materialitza en"
OBRES ||--o{ RESERVES : "es reserva"
EXEMPLARS ||--o{ PRESTECS : "es presta com a"
SOCIS ||--o{ PRESTECS : "pren"
SOCIS ||--o{ RESERVES : "sollicita"
PRESTECS ||--o| MULTES : "genera"
Dotze taules i setze relacions: catorze 1:N, una N:M resolta amb la taula pont obres_autors, una reflexiva a bibliotecaris i una 1 a 0..1 entre prestecs i multes. La forma és la de BotigaVerda; el contingut, no. Les columnes són al DDL del pas 2.
Les cinc decisions difícils
1. Per què exemplars és una taula i no un comptador. Argumentat a 12-01: l'exemplar té seu, estat i història propis. La conseqüència tècnica és més contundent que l'argument conceptual: sense aquesta taula, la FK de prestecs apuntaria a obres i seria impossible saber quin dels tres va tornar. No és estil: és que el model no pot representar el fet.
2. Per què prestecs desa data_prevista en lloc de calcular-la. Es podria deduir (data_prestec + termini(tipus del soci)), i no es fa: l'argument és idèntic al de linies_comanda.preu_unitari (01-06, 11-02), és una dada històrica. Si la Marta passa de general a senior, els seus préstecs antics es recalcularien a 30 dies i un que va arribar tard passaria a estar dins de termini. A més es mou amb les renovacions (RN-03), així que ni tan sols és funció de la data inicial. Regla: si el valor depèn d'una condició del passat que pot canviar, es desa.
3. Per què les multes són una taula i no una columna. Té vida pròpia —es genera, es paga, es pot condonar—, i això són tres columnes més, nul·les per al 83 % dels préstecs; permet comptar i sumar sense recórrer tots els préstecs; i demà podria haver-hi multes que no vinguin d'un retard (un llibre malmès), i n'hi hauria prou de fer nul·lable la FK. Relació 1 a 0..1, forçada amb UNIQUE (prestec_id).
4. Com es modela la cua de reserves. Amb data_reserva i res més. Les tres opcions:
| Opció | Problema |
|---|---|
Columna posicio INTEGER |
Cancel·lar la reserva 2 obliga a renumerar les següents: concurrència, buits i errors |
Columna es_el_seguent BOOLEAN |
L'antipatró clàssic: un booleà que només pot ser cert en una fila, sense res que ho garanteixi |
data_reserva + ROW_NUMBER() |
La posició es calcula en consultar. Cancel·lar és canviar un estat; la cua es recol·loca sola |
5. Per què l'estat del préstec no s'emmagatzema i el de l'exemplar sí. Semblen simètrics i no ho són. El del préstec (actiu, vençut, retornat) és funció de dues dates i del rellotge: desar-lo obligaria a un procés nocturn que marqués els vençuts. El de l'exemplar (disponible, reparacio, extraviat, baixa) és un fet físic que algú decideix i no es dedueix de res. Per això exemplars.estat no inclou prestat: això sí que es dedueix, i tenir-ho seria desar dues vegades el mateix amb dues maneres de contradir-se.
- Pas 2 — El DDL:
01-esquema.sql
01-esquema.sqlL'ordre és el de 05-01: DROP de fills a pares, CREATE de pares a fills.
Vuit de les dotze taules no tenen cap sorpresa: són el patró exacte de categories, proveidors i devolucions a BotigaVerda, així que es resumeixen en aquesta taula —columnes, nul·lables i restriccions amb nom— i escriure'n el CREATE TABLE et durà cinc minuts:
| Taula | Columnes | Restriccions |
|---|---|---|
seus |
nom, adreca, telefon (nul·lable: pot no tenir línia pròpia), data_obertura |
pk_seus, uq_seus_nom |
materies |
nom, cdu (nul·lable: no totes estan classificades) |
pk_materies, uq_materies_nom |
editorials |
nom, pais |
pk_editorials, uq_editorials_nom |
autors |
nom, cognoms, nacionalitat i any_naixement (tots dos nul·lables: es poden desconèixer) |
pk_autors, chk_autors_any |
obres |
titol, materia_id, editorial_id (nul·lable: autoedició), any_publicacio, isbn (nul·lable: obres anteriors a l'ISBN), idioma |
pk_obres, uq_obres_isbn (RI-08), fk_obres_materia amb RESTRICT, fk_obres_editorial amb SET NULL, chk_obres_any |
multes |
prestec_id, import NUMERIC(10,2), dies_retard, data_generacio, data_pagament (nul·lable: NULL = impagada) |
pk_multes, uq_multes_prestec (RI-07, la que força l'1 a 0..1), fk_multes_prestec amb RESTRICT, chk_multes_import, chk_multes_dies, chk_multes_pagament |
socis |
nom, cognoms, document, email (nul·lable: els infantils no en tenen), data_naixement, tipus, estat, seu_id, data_alta |
pk_socis, uq_socis_document, uq_socis_email (que admet diversos NULL, 05-01), fk_socis_seu, i els dos dominis tancats de RI-10: chk_socis_tipus IN ('infantil','general','senior') i chk_socis_estat IN ('actiu','bloquejat','baixa') |
reserves |
obra_id (l'obra, no l'exemplar), soci_id, seu_id de recollida, data_reserva (d'aquí surt la posició a la cua), estat, data_avis i data_tancament (nul·lables) |
pk_reserves, fk_reserves_obra amb CASCADE i les altres dues amb RESTRICT, chk_reserves_estat IN ('en_espera','disponible','completada','cancellada','caducada'), chk_reserves_avis |
I aquestes són les quatre que sí que tenen decisions dins del mateix CREATE TABLE:
-- =====================================================================
-- Biblioteca Municipal d'Alvorada - 01-esquema.sql - PostgreSQL 16
-- =====================================================================
-- Esborrat en ordre invers a les dependències, perquè l'script sigui idempotent
DROP TABLE IF EXISTS multes CASCADE; DROP TABLE IF EXISTS reserves CASCADE;
DROP TABLE IF EXISTS prestecs CASCADE; DROP TABLE IF EXISTS exemplars CASCADE;
DROP TABLE IF EXISTS obres_autors CASCADE; DROP TABLE IF EXISTS obres CASCADE;
DROP TABLE IF EXISTS socis CASCADE; DROP TABLE IF EXISTS bibliotecaris CASCADE;
DROP TABLE IF EXISTS autors CASCADE; DROP TABLE IF EXISTS editorials CASCADE;
DROP TABLE IF EXISTS materies CASCADE; DROP TABLE IF EXISTS seus CASCADE;
-- ... i aquí van, en ordre de dependències, les set taules de la taula anterior ...
CREATE TABLE bibliotecaris (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
nom VARCHAR(60) NOT NULL,
cognoms VARCHAR(90) NOT NULL,
carrec VARCHAR(80) NOT NULL,
seu_id INTEGER NOT NULL,
responsable_id INTEGER, -- NULL: només la direcció de la xarxa
email VARCHAR(120) NOT NULL,
data_alta DATE NOT NULL,
CONSTRAINT pk_bibliotecaris PRIMARY KEY (id),
CONSTRAINT uq_bibliotecaris_email UNIQUE (email),
CONSTRAINT fk_bib_seu FOREIGN KEY (seu_id) REFERENCES seus(id) ON DELETE RESTRICT,
CONSTRAINT fk_bib_responsable FOREIGN KEY (responsable_id) -- reflexiva
REFERENCES bibliotecaris(id) ON DELETE SET NULL,
CONSTRAINT chk_bib_no_autocap CHECK (responsable_id <> id) -- RI-12
);
-- ---------------------------------------- Pont N:M i exemplars físics
CREATE TABLE obres_autors (
obra_id INTEGER NOT NULL,
autor_id INTEGER NOT NULL,
rol VARCHAR(20) NOT NULL DEFAULT 'autor',
ordre SMALLINT NOT NULL DEFAULT 1, -- ordre de signatura a la portada
CONSTRAINT pk_obres_autors PRIMARY KEY (obra_id, autor_id), -- PK composta
CONSTRAINT fk_oa_obra FOREIGN KEY (obra_id) REFERENCES obres(id) ON DELETE CASCADE,
CONSTRAINT fk_oa_autor FOREIGN KEY (autor_id) REFERENCES autors(id) ON DELETE RESTRICT,
CONSTRAINT chk_oa_rol CHECK (rol IN ('autor','coautor','traductor','illustrador')),
CONSTRAINT chk_oa_ordre CHECK (ordre > 0)
);
CREATE TABLE exemplars (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
obra_id INTEGER NOT NULL,
seu_id INTEGER NOT NULL,
codi_barres VARCHAR(20) NOT NULL,
data_adquisicio DATE NOT NULL,
estat VARCHAR(15) NOT NULL DEFAULT 'disponible',
CONSTRAINT pk_exemplars PRIMARY KEY (id),
CONSTRAINT uq_exemplars_codi UNIQUE (codi_barres), -- RI-09
CONSTRAINT fk_exemplars_obra FOREIGN KEY (obra_id) REFERENCES obres(id) ON DELETE RESTRICT,
CONSTRAINT fk_exemplars_seu FOREIGN KEY (seu_id) REFERENCES seus(id) ON DELETE RESTRICT,
-- Compte: NO existeix el valor 'prestat'. Això es dedueix de prestecs
CONSTRAINT chk_exemplars_estat CHECK (estat IN ('disponible','reparacio',
'extraviat','baixa'))
);
-- ------------------------------------------------- Operació: el préstec
CREATE TABLE prestecs (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
exemplar_id INTEGER NOT NULL,
soci_id INTEGER NOT NULL,
bibliotecari_id INTEGER, -- NULL: autopréstec a la màquina
data_prestec DATE NOT NULL,
data_prevista DATE NOT NULL, -- es desa: dada històrica
data_devolucio DATE, -- NULL = préstec ACTIU
renovacions SMALLINT NOT NULL DEFAULT 0,
CONSTRAINT pk_prestecs PRIMARY KEY (id), -- RI-14: l'
CONSTRAINT fk_pre_exemplar FOREIGN KEY (exemplar_id) -- històric
REFERENCES exemplars(id) ON DELETE RESTRICT, -- no s'esborra
CONSTRAINT fk_pre_soci FOREIGN KEY (soci_id) REFERENCES socis(id) ON DELETE RESTRICT,
CONSTRAINT fk_pre_bibliotecari FOREIGN KEY (bibliotecari_id)
REFERENCES bibliotecaris(id) ON DELETE SET NULL,
CONSTRAINT chk_pre_prevista CHECK (data_prevista > data_prestec), -- RI-01
CONSTRAINT chk_pre_devolucio CHECK (data_devolucio IS NULL
OR data_devolucio >= data_prestec),
CONSTRAINT chk_pre_renovacions CHECK (renovacions BETWEEN 0 AND 2) -- RI-04
);
-- RI-03: entre els préstecs SENSE retornar, un exemplar només pot aparèixer una vegada
CREATE UNIQUE INDEX uq_prestec_actiu_per_exemplar
ON prestecs (exemplar_id)
WHERE data_devolucio IS NULL;
-- RI-13: un soci no pot tenir dues reserves VIVES de la mateixa obra
CREATE UNIQUE INDEX uq_reserva_activa_soci_obra
ON reserves (obra_id, soci_id)
WHERE estat IN ('en_espera','disponible');L'índex parcial, a fons
És la peça tècnica del projecte. Un índex parcial (08-02) es construeix només sobre les files que compleixen una condició; quan a més és UNIQUE, la unicitat només s'exigeix entre aquestes files. Es llegeix literalment: "entre els préstecs sense retornar, l'exemplar_id és únic". Les seves dues proves:
-- ⚠️ INCORRECTA: l'exemplar 1 ja està prestat a la Sofía sense retornar
INSERT INTO prestecs (exemplar_id, soci_id, bibliotecari_id, data_prestec, data_prevista)
VALUES (1, 13, 5, DATE '2026-06-25', DATE '2026-07-16');
-- ERROR: duplicate key value violates unique constraint "uq_prestec_actiu_per_exemplar"
-- DETAIL: Key (exemplar_id)=(1) already exists.
-- ✅ CORRECTA: un altre préstec del mateix exemplar, ja retornat. És història, no un préstec viu
INSERT INTO prestecs (exemplar_id, soci_id, bibliotecari_id,
data_prestec, data_prevista, data_devolucio)
VALUES (1, 13, 5, DATE '2024-01-10', DATE '2024-01-31', DATE '2024-01-28');
-- INSERT 0 1Tres propietats el converteixen en la solució correcta i no en un truc. És declaratiu: ho fa complir el motor, no l'aplicació, així que sobreviu a scripts, a una altra aplicació i a una consola oberta a les tres de la matinada. És diminut: amb 300.000 préstecs històrics i 400 d'actius, l'índex té 400 entrades, no 300.000. I a més accelera, perquè tota consulta amb WHERE data_devolucio IS NULL el pot fer servir: una restricció que de propina és un índex útil.
I el seu límit, que cal dir a l'informe: impedeix dos préstecs actius, no dos préstecs solapats en el passat. Si algú registra avui un préstec de gener ja retornat que es trepitja amb un altre de gener, l'índex no ho veu. La solució completa és a 12-04.
- Pas 3 — Dades de prova coherents:
02-dades.sql
02-dades.sqlLes dades de prova tenen dos objectius que es destorben: ser suficients perquè les consultes signifiquin alguna cosa i petites per llegir-les amb els ulls. El repartiment: a mà, amb id explícits, tot el que aparegui a les consultes —així "el préstec 29" significa sempre el mateix—; amb generate_series, el volum per mesurar rendiment.
-- Volum sintètic per poder mesurar índexs: ~50.000 préstecs històrics
INSERT INTO prestecs (exemplar_id, soci_id, bibliotecari_id,
data_prestec, data_prevista, data_devolucio)
SELECT 1 + (random() * 19)::int, 1 + (random() * 14)::int, 1 + (random() * 7)::int,
f, f + 21, f + 21 - (random() * 10)::int -- TOTS retornats: no trenquen RI-03
FROM generate_series(DATE '2020-01-01', DATE '2025-08-31', INTERVAL '1 hour') AS g(f);El detall que decideix si això funciona: totes les files generades porten data_devolucio. Si hi deixessis nuls a l'atzar, l'índex únic parcial avortaria la càrrega tan bon punt dos préstecs del mateix exemplar quedessin oberts. Lluny de ser una molèstia, és la prova que la restricció funciona.
Els casos límit que les dades han de contenir
BotigaVerda tenia buits deliberats —clients sense comandes, productes sense vendre, comandes sense empleat— i les lliçons els necessitaven. Ara et toca posar-los a tu: sense ells, mitja consulta mal escrita retorna el mateix que una de ben escrita i no te n'assabentes.
| Cas límit | Per a què és imprescindible | Al joc de dades |
|---|---|---|
| Socis sense cap préstec | Anti-join (RC-04) i LEFT JOIN de rànquings (RC-13) |
3 |
| Obra sense exemplars i exemplars mai prestats | LEFT JOIN de disponibilitat (RC-06) i anti-join (RC-04) |
1 i 3 |
| Préstecs retornats amb retard i multes impagades | Taxa de retard, deute pendent i bloqueig (RC-08, RN-11) | 6 (de 7 a 46 dies) i 2, que sumen 13,40 € |
| Préstecs actius vençuts | Vista de vençuts i deute potencial (RC-03) | 3 |
| Tots els exemplars d'una obra prestats, i reserves en tres estats | Cua de reserves (RC-10): sense el primer la cua no pot existir | El jardí de les hores: els seus 3; i 3 reserves en espera, 1 completada, 1 caducada |
| Bibliotecari sense responsable i préstec sense bibliotecari | CTE recursiva (RC-14, és el cas base) i NULL en FK amb LEFT JOIN (04-03) |
1 i 1 (autopréstec) |
| Exemplar en reparació | Distingir habilitat de disponible (RC-06) | 1 |
| Soci bloquejat i soci de baixa | Filtres per estat; històric que sobreviu a la baixa | 1 i 1 |
| Préstecs renovats | RN-03: data_prevista ≠ data_prestec + termini |
2 |
| Mesos sense cap préstec | Sèrie temporal sense buits (RC-12) | 2 |
Verificar la càrrega
Com a 01-06, comprova abans d'escriure ni una sola consulta. El joc de dades de referència té 3 seus, 6 matèries, 5 editorials, 10 autors, 8 bibliotecaris, 12 obres, 15 files d'obres_autors, 20 exemplars, 15 socis, 36 préstecs, 5 reserves i 6 multes. I els buits, en una sola consulta:
SELECT (SELECT COUNT(*) FROM socis AS s WHERE NOT EXISTS
(SELECT 1 FROM prestecs AS p WHERE p.soci_id = s.id)) AS socis_sense_prestecs,
(SELECT COUNT(*) FROM obres AS o WHERE NOT EXISTS
(SELECT 1 FROM exemplars AS e WHERE e.obra_id = o.id)) AS obres_sense_exemplars,
(SELECT COUNT(*) FROM exemplars AS e WHERE NOT EXISTS
(SELECT 1 FROM prestecs AS p WHERE p.exemplar_id = e.id)) AS exemplars_sense_prestar,
(SELECT COUNT(*) FROM prestecs WHERE data_devolucio IS NULL) AS actius,
(SELECT COUNT(*) FROM prestecs WHERE data_devolucio IS NULL
AND data_prevista < DATE '2026-06-30') AS vencuts,
(SELECT COUNT(*) FROM prestecs WHERE bibliotecari_id IS NULL) AS sense_bibliotecari,
(SELECT COUNT(*) FROM bibliotecaris WHERE responsable_id IS NULL) AS sense_responsable,
(SELECT COUNT(*) FROM multes WHERE data_pagament IS NULL) AS multes_impagades;| socis_sense_prestecs | obres_sense_exemplars | exemplars_sense_prestar | actius | vencuts | sense_bibliotecari | sense_responsable | multes_impagades |
|---|---|---|---|---|---|---|---|
| 3 | 1 | 3 | 6 | 3 | 1 | 1 | 2 |
- Pas 4 — Les consultes: mètode de treball
- Comença pel
FROM, no pelSELECT. Decideix primer de quina taula surt una fila del resultat: una per obra? ElFROMésobres. Una per préstec? Ésprestecs. Tota la resta s'hi uneix. - Comprova el recompte després de cada
JOIN. Si en unirexemplarsambprestecspasses de 20 files a 36, has canviat de granularitat: pot ser correcte, però ho has de saber, perquè a partir d'aquíCOUNT(*)compta préstecs, no exemplars. - Decideix
JOINoLEFT JOINpreguntant pels zeros. Vull veure l'obra sense exemplars, la seu sense préstecs, el mes buit? LlavorsLEFT JOIN— i recorda queCOUNT(*)compta la fila fantasma iCOUNT(columna_de_la_dreta)no (04-04). I valida el total per dos camins: el pivot de RC-15 ha de sumar el mateix queSELECT COUNT(*) FROM prestecs; si no quadra, no discuteixis amb la consulta, està malament (11-04).
Exemple resolt A — el mapa d'una obra
"Digue'm on són els tres exemplars d'El jardí de les hores i qui els té." És la pregunta amb què començava el correu de l'Helena, i resumeix el projecte sencer.
SELECT e.codi_barres, sd.nom AS seu, e.estat,
COALESCE(so.nom || ' ' || so.cognoms, '-- a la prestatgeria --') AS qui_el_te,
p.data_prevista AS retorna_el
FROM exemplars AS e
JOIN seus AS sd ON sd.id = e.seu_id
LEFT JOIN prestecs AS p ON p.exemplar_id = e.id AND p.data_devolucio IS NULL
LEFT JOIN socis AS so ON so.id = p.soci_id
WHERE e.obra_id = 1
ORDER BY e.codi_barres;| codi_barres | seu | estat | qui_el_te | retorna_el |
|---|---|---|---|---|
| ALV-0001 | Biblioteca Central d'Alvorada | disponible | Sofía Terán | 2026-07-06 |
| ALV-0002 | Biblioteca Central d'Alvorada | disponible | Rosa Pimentel | 2026-07-01 |
| ALV-0003 | Biblioteca de Vila Nova | disponible | Lena Fuentes | 2026-05-25 |
Tres decisions que cal justificar. FROM exemplars, perquè vull una fila per exemplar, estigui prestat o no. La condició p.data_devolucio IS NULL va al ON, no al WHERE: allà desapareixerien els exemplars sense préstec actiu i el LEFT JOIN seria un JOIN disfressat (03-03). I estat continua dient disponible als tres, i està bé: és la condició física del volum; que sigui fora es veu a qui_el_te. És la separació del pas 1, feta columna.
Exemple resolt B — socis que no vénen
L'Helena va demanar "els socis que no vénen des de fa temps". Abans d'escriure res cal definir-ho (11-04): aquí, soci en estat actiu el darrer préstec del qual és anterior a fa 90 dies o que no n'ha pres cap mai. Els de baixa queden fora expressament.
SELECT s.id, s.nom || ' ' || s.cognoms AS soci, s.tipus,
MAX(p.data_prestec) AS ultim_prestec,
COUNT(p.id) AS prestecs_totals
FROM socis AS s
LEFT JOIN prestecs AS p ON p.soci_id = s.id
WHERE s.estat = 'actiu'
GROUP BY s.id, s.nom, s.cognoms, s.tipus
HAVING COALESCE(MAX(p.data_prestec), DATE '1900-01-01')
< DATE '2026-06-30' - INTERVAL '90 days'
ORDER BY ultim_prestec, s.id;| id | soci | tipus | ultim_prestec | prestecs_totals |
|---|---|---|---|---|
| 12 | Irene Sampaio | senior | (null) | 0 |
| 13 | Hugo Marques | general | (null) | 0 |
| 14 | Carla Nieto | infantil | (null) | 0 |
| 1 | Marta Coelho | general | 2026-03-09 | 4 |
Quatre socis i tres lliçons. El LEFT JOIN és obligatori: amb un JOIN normal, els tres que no han demanat mai res —els més inactius de tots— desapareixerien. El COALESCE del HAVING és el que els deixa passar, perquè NULL < data dona UNKNOWN i HAVING descarta el que no és cert (04-03). I COUNT(p.id), no COUNT(*): amb COUNT(*) els tres sortirien amb 1 préstec en lloc de 0. És l'error més repetit del projecte sencer. I una lectura que no és de SQL: els tres sense préstecs no són el mateix problema —la Carla es va donar d'alta al febrer, la Irene fa un any que té el carnet sense fer servir i la Marta té quatre préstecs i tres mesos sense venir—; ficar-los a la mateixa xifra és el que 11-04 anomenava barrejar dues preguntes en un número.
- Pas 5 — Els índexs, després de les consultes
L'ordre és innegociable: primer les quinze consultes, després els índexs. Un índex existeix per servir una consulta concreta; si no la pots anomenar, sobra i només frena les escriptures (08-02). El mètode és mecànic:
- Llista el
WHERE, elJOINi l'ORDER BYde cada consulta. Aquesta llista és la teva llista de candidats, i només aquesta (11-05). - Afegeix-hi les claus foranes que naveges: PostgreSQL indexa la PK, no la FK (08-01). És la causa número u de recorreguts seqüencials en un esquema de dotze taules.
- Descarta el que no aporta —una columna de tres valors com
socis.tipuso una taula de sis files commateriesno s'indexen: el planificador les ignorarà, i farà bé— i mesura ambEXPLAIN(08-05), no amb la intuïció.
EXPLAIN (ANALYZE, BUFFERS) SELECT p.id, p.data_prevista FROM prestecs AS p
WHERE p.soci_id = 6 AND p.data_devolucio IS NULL;Sense índex sobre prestecs(soci_id) i amb volum, el pla és un Seq Scan que llegeix la taula sencera; amb ell, un Index Scan que hi va directe. L'important no és que millori, és que ho comprovis i ho escriguis (RP-06). I amb les 36 files del joc de proves veuràs Seq Scan a tot arreu, perquè llegir 36 files és més barat que obrir un índex — cosa que és una lliçó en si mateixa: per parlar de rendiment cal tenir volum, i per a això hi ha el generate_series del pas 3. Els índexs de referència, amb el seu pla, són a 12-04.
- Pas 6 — Vistes, procediments i disparadors
La regla que evita el desastre: encapsula el que es repeteix i el que és delicat; no encapsulis per gust. Tres peces són suficients i una quarta ja seria sospitosa. El codi és a 12-04; aquí va el criteri, que és el que cal saber decidir.
| Peça | Què encapsula | Per què aquesta eina i no una altra |
|---|---|---|
Vista v_prestecs_vencuts |
La definició de "vençut" i el càlcul de la multa estimada | És una consulta que llançaran cada dia des de tres seus. Defineix una vegada la mètrica, que és la capa semàntica de 10-01 i 11-04 |
Procediment registrar_devolucio |
Tancar el préstec, generar la multa si toca i avisar la primera reserva de la cua | Són tres escriptures que van juntes o no van (mòdul 9). Un procediment les fica en una transacció i retorna un error clar si alguna cosa falla (10-04) |
Disparador trg_prestec_soci_actiu |
Impedir prestar a un soci bloquejat (RN-11) | És una regla que depèn d'una altra taula, i per tant fora de l'abast d'un CHECK (05-01). S'ha de complir passi el que passi, vingui d'on vingui l'INSERT (10-05) |
Dos detalls de la vista importen més del que sembla: la seva columna s'anomena multa_estimada, no multa, perquè encara no existeix (RN-10) —anomenar-la multa seria la primera pedra d'un informe que suma diners que ningú no deu—; i fa servir CURRENT_DATE, així que canvia sola cada nit, que és exactament el que es volia i la raó de no desar "vençut" en una columna.
I aquí es para. La temptació d'afegir un disparador per al màxim de préstecs simultanis, un altre per validar renovacions i un altre per recalcular el bloqueig és enorme, i és un error: els disparadors són lògica invisible —qui llegeix l'INSERT no veu què passa— i depurar-los és incòmode. El criteri de 10-05: disparador per al que s'ha de complir passi el que passi i no es pugui declarar; procediment per a l'operació de negoci amb diversos passos; aplicació per a la resta.
- Pas 7 — Seguretat i lliurament
Els tres rols de RS-01 a RS-03, amb el patró d'11-03: privilegis al grup, mai a l'usuari.
CREATE ROLE bib_consulta NOLOGIN; CREATE ROLE bib_taulell NOLOGIN; CREATE ROLE bib_admin NOLOGIN;
GRANT USAGE ON SCHEMA public TO bib_consulta, bib_taulell, bib_admin;
-- Consulta: només catàleg. Ni socis, ni préstecs, ni multes
GRANT SELECT ON obres, exemplars, autors, obres_autors, materies, editorials, seus
TO bib_consulta;
-- Taulell: opera, però NO esborra (RD-10, RS-04). Administració: a més, catàleg
GRANT bib_consulta TO bib_taulell;
GRANT SELECT, INSERT, UPDATE ON prestecs, reserves, multes TO bib_taulell;
GRANT SELECT, UPDATE ON socis TO bib_taulell;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO bib_taulell;
GRANT bib_taulell TO bib_admin;
GRANT INSERT, UPDATE ON obres, exemplars, autors, obres_autors, materies, editorials
TO bib_admin;
-- L'usuari real només és membre del grup que li toca: GRANT bib_taulell TO fatima;
-- I es comprova el que s'ha concedit, en lloc de suposar-ho
SELECT has_table_privilege('bib_consulta', 'socis', 'SELECT') AS consulta_veu_socis,
has_table_privilege('bib_taulell', 'prestecs', 'DELETE') AS taulell_esborra;| consulta_veu_socis | taulell_esborra |
|---|---|
| false | false |
Dos falsos, que és el que es demanava. Aquesta comprovació de dues línies és la prova de RS-01 i RS-04, i va a l'informe.
- Cronograma orientatiu
Per sessions d'unes dues hores. Desviar-se molt a la sessió 3 sol ser senyal que el model té un problema, no que vagis lent:
| Sessió | Feina | Lliurable al final |
|---|---|---|
| 1 | Glossari, regles de negoci, esbós del model en paper | Diagrama amb les 12 caixes i les seves relacions |
| 2 | 01-esquema.sql: taules, restriccions amb nom, índex parcial |
L'esquema es crea sense errors dues vegades seguides |
| 3 | 02-dades.sql amb tots els casos límit; consultes de verificació |
Els recomptes i els vuit buits quadren |
| 4 i 5 | RC-01 a RC-08 una a una; després RC-09 a RC-15: correlacionades, LATERAL, finestres, recursiva i pivot |
Les quinze, amb els totals validats |
| 6 i 7 | Índexs i EXPLAIN abans i després; vista, procediment i disparador; rols; 04-informe.md; repàs amb la rúbrica de 12-02 |
El projecte sencer, executable des de zero |
Errors habituals i consells
- Crear les taules en l'ordre en què se t'acudeixen. La integritat referencial imposa l'ordre: pares abans que fills en crear, al revés en esborrar (05-01). Si el teu script només funciona la primera vegada, no és idempotent i RE-02 no es compleix.
- Posar la condició del
LEFT JOINalWHERE. L'error més car i més silenciós del projecte: converteix elLEFT JOINenJOIN, desapareixen els zeros i el resultat continua semblant raonable. Condició sobre la taula de la dreta ⇒ va alON. COUNT(*)després d'unLEFT JOIN. Compta la fila fantasma i els socis sense préstecs surten amb 1. Fes servirCOUNT(columna_de_la_dreta).- Afegir el valor
prestataexemplars.estat, o generar dades a l'atzar sense mirar les restriccions. El primer duplica informació que acabarà contradientprestecs; el segon (devolucions anteriors al préstec, dos actius del mateix exemplar, multes sense retard) avorta l'script a mitges i deixa la base a mig carregar. - Consell: després de crear l'esquema, intenta violar-lo, i escriu el DDL amb els
-- RI-nnal costat de cada restricció. UnINSERTque ha de fallar i falla val més que tres paràgrafs d'informe, i repassar els catorze requisits amb la rúbrica et durà un minut en lloc de mitja hora. - Consell: desa un
99-comprovacions.sqlamb els recomptes, els vuit buits, elsINSERTque han de fallar i les dues consultes de privilegis. És la teva bateria de proves: executar-la després de cada canvi et dirà a l'instant si has trencat res. - Consell: escriu l'informe a mesura que decideixes. Cada decisió difícil, el seu paràgraf, en el moment. Reconstruir-lo l'última nit és impossible i es nota.
Exercicis
Exercici 1
Implementa la RN-04 completa: no es pot renovar si s'ha assolit el màxim, si el préstec està vençut o si l'obra té reserves en espera. (1) Quines de les tres condicions es poden declarar amb un CHECK i quines no, i per què? (2) Escriu un UPDATE que renovi el préstec 32 només si les tres ho permeten. (3) Què retorna amb les dades del projecte?
Exercici 2
El préstec 29 (Lena Fuentes, exemplar ALV-0003) porta 36 dies de retard i es retorna avui, 2026-06-30. (1) Enumera totes les files que canvien, en quines taules i amb quins valors. (2) Per què les tres operacions han d'anar a la mateixa transacció, i què passaria si fallés la tercera després de les dues primeres? (3) Quin efecte té la devolució sobre l'índex únic parcial?
Exercici 3
Un company proposa afegir a obres una columna num_exemplars INTEGER "per no haver de comptar cada vegada". (1) Dona tres arguments en contra. (2) Dona un escenari en què sí que seria defensable. (3) Si s'hagués de fer, com el mantindries i què et costaria?
Solucions
Solució 1 —
(1) Cap, i per motius diferents. El màxim de renovacions depèn del tipus del soci, que és en una altra taula, i un CHECK no pot consultar altres taules (05-01): l'únic declarable és el rang absolut BETWEEN 0 AND 2. Que el préstec estigui vençut depèn de CURRENT_DATE, i un CHECK amb funció no determinista està desaconsellat —una fila vàlida avui deixaria de ser-ho demà i una restauració de còpia fallaria sense motiu—. Les reserves són en una altra taula. Les tres són lògica d'operació. (2) Tot al WHERE, que és on es comprova sense llegir abans:
UPDATE prestecs AS p
SET data_prevista = p.data_prevista
+ CASE s.tipus WHEN 'infantil' THEN 14
WHEN 'general' THEN 21 ELSE 30 END,
renovacions = p.renovacions + 1
FROM socis AS s
WHERE s.id = p.soci_id AND p.id = 32 AND p.data_devolucio IS NULL
AND p.data_prevista >= DATE '2026-06-30' -- no vençut
AND p.renovacions < CASE s.tipus WHEN 'infantil' THEN 1 ELSE 2 END -- màxim
AND NOT EXISTS (SELECT 1 -- sense cua
FROM reserves AS r
JOIN exemplars AS e ON e.obra_id = r.obra_id
WHERE e.id = p.exemplar_id
AND r.estat = 'en_espera');(3) Retorna UPDATE 0. El préstec 32 és de la Sofía Terán amb l'exemplar ALV-0001, d'El jardí de les hores — l'obra que té tres socis a la cua. La tercera condició el bloqueja, i fa bé: renovar-lo deixaria en Nuno Barros esperant un mes més per un llibre els tres exemplars del qual són fora. I UPDATE 0 no és un error: cal comprovar-ho a l'aplicació i traduir-ho a un missatge.
Solució 2 — (1) Canvien tres files en tres taules:
| Taula | Operació | Valors |
|---|---|---|
prestecs |
UPDATE de la fila 29 |
data_devolucio passa de NULL a 2026-06-30 |
multes |
INSERT d'una fila nova |
prestec_id 29, dies_retard 36, import 7,20 € (0,20 × 36, per sota del topall de 20 €), sense data_pagament |
reserves |
UPDATE de la reserva més antiga de l'obra 1 |
La d'en Nuno Barros del 2026-06-10 passa d'en_espera a disponible, amb data_avis = avui |
(2) Perquè les tres són una sola operació de negoci: un exemplar retornat que genera la seva multa i activa la seva reserva. Si fallés la tercera sense transacció, quedaria un préstec tancat amb la seva multa correcta i una cua que ningú no ha avisat: en Nuno continuaria esperant un llibre que ja és al taulell, i sense rastre de la fallada. És ACID en la seva forma més simple (09-01, 09-02): o les tres, o cap. En PL/pgSQL el bloc del procediment ja és una transacció implícita, així que una excepció al pas 3 desfà els dos primers. (3) En deixar d'estar actiu, el préstec 29 surt de l'índex únic parcial —la seva fila ja no compleix WHERE data_devolucio IS NULL— i l'exemplar ALV-0003 torna a poder-se prestar. La restricció es deixa anar sola: aquesta és l'elegància de l'índex parcial enfront d'una columna actiu que algú hauria de recordar-se de canviar.
Solució 3 — (1) Primer, és redundància pura: la dada ja és a exemplars i un COUNT(*) l'obté en microsegons amb l'índex d'exemplars(obra_id). Segon, s'ha de mantenir a cada alta, baixa i trasllat d'exemplar; el dia que algú insereixi des d'un script, la xifra queda malament per sempre i ningú no se n'assabenta, perquè un número plausible no salta a la vista. Tercer, no respon a la pregunta real: ningú no pregunta quants exemplars hi ha, pregunten quants n'hi ha disponibles ara, que depèn de prestecs i canvia cada minut.
(2) Seria defensable amb milions d'obres, en una pantalla de catàleg que mostra el recompte a cada resultat de cerca i es llegeix milers de vegades per segon, si l'agregat s'hagués mesurat i fos el coll d'ampolla. És a dir: quan hi hagi un EXPLAIN que ho justifiqui, no abans (08-04). I tot i així, la primera opció seria una vista materialitzada (10-01) refrescada cada nit, que aïlla la redundància en un objecte marcat com a derivat en lloc d'amagar-la en una columna que sembla una dada.
(3) Amb un disparador AFTER INSERT OR UPDATE OR DELETE sobre exemplars que sumi o resti. El cost: cada escriptura a exemplars escriu també a obres, cosa que crea contenció sobre la fila de l'obra —dues altes simultànies d'exemplars del mateix títol es serialitzen (09-05)— i afegeix lògica invisible; més una consulta de recàlcul des de zero, per corregir-ho quan (no si) es desincronitzi. Tot això, per estalviar un COUNT.
Conclusió
Ja saps construir-lo:
- El model: dotze taules i setze relacions, amb una N:M de clau primària composta i una de reflexiva. I les cinc decisions difícils argumentades:
exemplarsés taula perquè l'objecte té seu, estat i història propis;data_previstaes desa perquè és dada històrica i a més es mou amb les renovacions, igual quepreu_unitari; les multes són taula perquè tenen cicle de vida; la cua es calcula des dedata_reservaen lloc de desar una posició; i l'estat del préstec es deriva mentre el de l'exemplar s'emmagatzema, perquè un és funció del rellotge i l'altre un fet físic. - El DDL, amb restriccions amb nom i anotades amb el seu
RI-nn, i l'índex únic parcialWHERE data_devolucio IS NULL: declaratiu, diminut i útil també com a índex, amb el seu límit reconegut —no detecta solapaments històrics—. Les dades: a mà el que apareix a les consultes,generate_seriesper al volum (amb totes les files retornades, o RI-03 avorta la càrrega) i els casos límit sense els quals una consulta mal escrita passa per bona, amb els recomptes i els vuit buits verificats abans d'escriure la primera consulta. - El mètode: començar pel
FROMdecidint què és una fila del resultat, comprovar el recompte després de cadaJOIN, triarLEFT JOINpreguntant pels zeros i validar el total per dos camins. Els dos exemples resolts ensenyen els dos paranys: la condició delLEFT JOINva alON, iCOUNT(*)compta la fila fantasma. - Els índexs després de les consultes, no al revés; tres encapsulats i ni un més —la vista de vençuts amb el seu honest
multa_estimada, el procediment atòmic de devolució i l'únic disparador necessari—; i tres rols de privilegi mínim verificats ambhas_table_privilege.
Ja l'has construït; ara toca comparar-lo, que és on de debò s'aprèn. A la lliçó següent, Solucions comentades del projecte, hi ha el solucionari: les decisions de l'esquema justificades una a una amb les alternatives que també serien correctes —inclosa la restricció EXCLUDE i el debat entre estat derivat i emmagatzemat—; les 15 consultes resoltes amb el seu resultat, la seva decisió clau i l'error típic de cadascuna; els índexs de referència amb el pla que els aprofita; el codi de la vista, el procediment i el disparador; i el catàleg del que més falla en aquest projecte en concret.
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
