Fins ara hem mirat les taules d'una en una. Però el poder real d'una base de dades relacional no és a les taules: és a les relacions entre elles. Aquesta lliçó respon la pregunta que tot principiant es fa en veure l'esquema de BotigaVerda: "per què nou taules i no una de sola amb tot?". Veuràs què és una clau primària i per què el curs fa servir id numèrics, com una clau forana impedeix físicament que existeixi una comanda d'un client inexistent, què passa quan intentes esborrar un registre del qual depenen d'altres, com es representen les cardinalitats 1:1, 1:N i N:M, i com la normalització descompon una taula monolítica en un conjunt de taules sanes. És la lliçó més conceptual del mòdul i també la que més rendiment et donarà quan arribis als JOIN.
Contingut
- El model relacional en 10 minuts
- Clau primària: natural davant de subrogada
- Claus candidates i claus úniques
- Clau forana i integritat referencial
- Què passa en esborrar o modificar un pare: ON DELETE i ON UPDATE
- Cardinalitats: 1:1, 1:N i N:M
- Normalització pràctica: 1FN, 2FN i 3FN
- Quan desnormalitzar a propòsit
- Errors habituals i consells
- Exercicis
- Conclusió
- El model relacional en 10 minuts
El model relacional el va proposar Edgar F. Codd el 1970 i descansa en tres idees sorprenentment simples.
Relació, tupla i domini
| Concepte formal | Nom col·loquial | Què és | Exemple a BotigaVerda |
|---|---|---|---|
| Relació | Taula | Un conjunt de tuples amb la mateixa estructura | productes |
| Tupla | Fila | Un element concret d'aquest conjunt | El producte "Mel de tarongina crua 500 g" |
| Atribut | Columna | Una propietat de la tupla | preu |
| Domini | Tipus (més restriccions) | El conjunt de valors vàlids per a un atribut | NUMERIC(10,2) ≥ 0 |
| Grau | Nombre de columnes | Quants atributs té la relació | productes té grau 9 |
| Cardinalitat | Nombre de files | Quantes tuples conté | productes té 20 files |
Nota important: "relació" no significa "relació entre taules". En la terminologia de Codd, una relació és una taula. Les connexions entre taules s'anomenen associacions o, a la pràctica, s'implementen mitjançant claus foranes. La coincidència de noms confon molta gent.
Les tres propietats que ho canvien tot
- Una relació és un conjunt, així que no hi ha ordre ni duplicats conceptuals. Si dues files fossin idèntiques en tot, serien la mateixa tupla.
- Les dades es relacionen pel seu valor, no per punters. A BotigaVerda,
comandes.client_id = 7assenyala el client 7 perquè el valor coincideix, no perquè hi hagi una adreça de memòria desada. Aquesta idea, que avui sembla òbvia, era revolucionària davant dels sistemes jeràrquics i de xarxa dels anys 60. - L'estructura és independent de l'accés. Pots reorganitzar índexs i emmagatzematge sense canviar ni una consulta.
De la propietat 2 en surt directament tot el que veuràs al mòdul 3: un JOIN no és més que aparellar files els valors de les quals coincideixen.
- Clau primària: natural davant de subrogada
Una clau primària (PK) és la columna —o combinació de columnes— que identifica de manera única cada fila d'una taula. Les seves tres propietats:
- Única: no es pot repetir.
- No nul·la: mai no pot ser
NULL. - Estable: idealment no hauria de canviar mai.
Hi ha dues filosofies per triar-la:
| Tipus | Què és | Exemple | Avantatges | Inconvenients |
|---|---|---|---|---|
| Natural | Una dada real del negoci que ja és única | email a clients, un ISBN, un NIF |
Significativa; sense columnes extra; evita duplicats per disseny | Pot canviar (una persona canvia d'email); sol ser llarga (text), cosa que encareix els índexs i les claus foranes |
| Subrogada | Un identificador artificial sense significat | id INTEGER autoincremental, UUID |
Curta, estable, uniforme, ràpida en índexs i JOIN; no canvia mai |
No significa res; obliga a afegir un UNIQUE a part per a la clau real de negoci |
Per què aquest curs fa servir id subrogat a les nou taules
-- Totes les taules de BotigaVerda segueixen el mateix patró
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEYLes raons:
- Uniformitat. Saps que qualsevol taula s'identifica per
id, i que qualsevol clau forana es diu<taula_singular>_id. Zero sorpreses. - Estabilitat. L'email d'un client pot canviar; el seu
idno. Si l'email fos la PK i canviés, caldria actualitzar en cascada totes les taules que el referencien. - Eficiència. Un
INTEGERocupa 4 bytes; un email, 30 o 40. Tots els índexs i totes les claus foranes se'n beneficien. - Llegibilitat didàctica.
WHERE client_id = 7és infinitament més còmode en un curs queWHERE client_email = '[email protected]'.
Important: fer servir
idsubrogat no eximeix de declarar la clau natural com aUNIQUE. A BotigaVerda,clients.emailésUNIQUEencara que la PK siguiid: sense aquestUNIQUEpodries registrar dues vegades el mateix client i la base de dades no protestaria.
Clau primària composta
Res no obliga que la PK sigui una sola columna. Es podria formar amb diverses:
Això significaria "un producte només pot aparèixer una vegada a cada comanda". És una decisió legítima, però BotigaVerda fa servir un id propi a linies_comanda per uniformitat i perquè permet que un mateix producte aparegui en dues línies de la mateixa comanda amb preus o descomptes diferents.
- Claus candidates i claus úniques
- Una clau candidata és qualsevol conjunt de columnes que identifica unívocament una fila. Una taula en pot tenir diverses.
- La clau primària és la candidata que tries com a identificador oficial.
- La resta de candidates es declaren com a claus úniques (
UNIQUE).
A clients tenim dues candidates:
| Candidata | Triada com a PK? | Com es declara |
|---|---|---|
id |
Sí | PRIMARY KEY |
email |
No | UNIQUE |
Diferència clau entre PRIMARY KEY i UNIQUE:
| Aspecte | PRIMARY KEY |
UNIQUE |
|---|---|---|
Admet NULL? |
No, mai | Sí (i a PostgreSQL, diversos nuls alhora) |
| Quantes per taula? | Una | Les que vulguis |
| Pot ser destí d'una FK? | Sí | Sí |
| Índex | Es crea automàticament | Es crea automàticament |
Aquest detall dels NULL a UNIQUE sorprèn: PostgreSQL considera que dos nuls no són iguals entre si, així que una columna UNIQUE pot tenir moltes files amb NULL. Si necessites el contrari, PostgreSQL 15 va introduir UNIQUE NULLS NOT DISTINCT.
- Clau forana i integritat referencial
Una clau forana (FK) és una columna que conté valors que han d'existir a la clau primària d'una altra taula. És el mecanisme que connecta les taules i, sobretot, el que garanteix que no hi hagi dades òrfenes.
A BotigaVerda:
-- Fragment conceptual (la sintaxi completa és del mòdul 5)
CREATE TABLE comandes (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
client_id INTEGER NOT NULL REFERENCES clients(id),
empleat_id INTEGER NULL REFERENCES empleats(id),
...
);Es llegeix: "client_id ha de correspondre a un id existent a clients, i és obligatori; empleat_id també ha d'existir a empleats, però pot quedar buit".
Això s'anomena integritat referencial: la base de dades garanteix que les referències apunten a alguna cosa real. I no és una recomanació, és una barrera física.
Quin error dona PostgreSQL en inserir una FK inexistent
BotigaVerda té 15 clients. Si intentes registrar una comanda del client 999:
INSERT INTO comandes (client_id, data_comanda, estat, metode_pagament, despeses_enviament)
VALUES (999, '2026-03-01', 'pendent', 'targeta', 4.95);ERROR: insert or update on table "comandes" violates foreign key constraint "comandes_client_id_fkey" DETAIL: Key (client_id)=(999) is not present in table "clients".
Llegeix-lo amb atenció, perquè el veuràs moltes vegades:
violates foreign key constraint→ has trencat una FK.- El nom
comandes_client_id_fkeysegueix el patró<taula>_<columna>_fkeyi et diu exactament quina. - El
DETAILet dona el valor culpable (999) i la taula on hauria d'existir.
Sense aquesta restricció tindries una comanda fantasma: apareixeria al total de vendes, però en intentar mostrar el nom del client no hi hauria res. Aquest tipus d'inconsistència és demolidor en un sistema real, i és impossible d'introduir aquí.
Quin error dona en esborrar un pare referenciat
La clienta 1 (Lucía Martínez Soler) té comandes. Si intentes esborrar-la:
ERROR: update or delete on table "clients" violates foreign key constraint "comandes_client_id_fkey" on table "comandes" DETAIL: Key (id)=(1) is still referenced from table "comandes".
PostgreSQL s'hi nega: esborrar aquesta clienta deixaria comandes apuntant al buit. Aquest comportament per defecte s'anomena RESTRICT (tècnicament NO ACTION, que és equivalent tret de les transaccions diferides), i és exactament el que vols la majoria de les vegades.
- Què passa en esborrar o modificar un pare: ON DELETE i ON UPDATE
En declarar una FK pots triar què ha de passar quan la fila referenciada s'esborra (ON DELETE) o canvia la seva clau (ON UPDATE).
| Acció | Comportament en esborrar el pare |
|---|---|
NO ACTION (per defecte) |
Rebutja l'operació amb error |
RESTRICT |
Rebutja immediatament, sense esperar al final de la transacció |
CASCADE |
Esborra també totes les files filles |
SET NULL |
Deixa la FK de les files filles a NULL (requereix que la columna admeti nuls) |
SET DEFAULT |
Posa el valor per defecte de la columna filla |
Aplicat a BotigaVerda, l'elecció no és arbitrària: cada relació demana una acció diferent segons el significat de negoci.
| Relació | Acció triada | Per què |
|---|---|---|
comandes.client_id → clients.id |
RESTRICT |
Una comanda no es pot quedar sense client. Abans d'esborrar un client cal decidir què fer amb el seu historial |
comandes.empleat_id → empleats.id |
SET NULL |
Si un comercial deixa l'empresa, la comanda continua sent vàlida: simplement passa a no tenir comercial assignat, igual que les comandes web |
linies_comanda.comanda_id → comandes.id |
CASCADE |
Una línia no existeix sense la seva comanda. Esborrar la comanda se n'ha de dur les línies: en formen part |
linies_comanda.producte_id → productes.id |
RESTRICT |
Mai no has d'esborrar un producte que s'ha venut: destruiries l'històric de facturació. Per retirar-lo es fa servir actiu = FALSE |
clients.referit_per_id → clients.id |
SET NULL |
Si s'esborra qui va referir, el referit continua sent client; només perd aquesta informació |
empleats.cap_id → empleats.id |
SET NULL |
Si un cap se'n va, el seu equip queda temporalment sense cap assignat, no s'esborra |
ressenyes.producte_id → productes.id |
CASCADE |
Si el producte desaparegués del catàleg, les seves ressenyes no tenen sentit |
devolucions.comanda_id → comandes.id |
CASCADE |
Una devolució és un fet associat a una comanda concreta |
Regla mental per decidir: pregunta't "la fila filla té sentit per si sola si el pare desapareix?". Si no en té, CASCADE. Si en té però perd una relació opcional, SET NULL. Si el pare no hauria de poder desaparèixer estant referenciat, RESTRICT.
Compte amb
CASCADE. És còmode i perillós: un solDELETEes pot propagar per mitja base de dades en silenci. Fes-lo servir únicament quan la relació sigui de composició real (la part no viu sense el tot), comlinies_comandarespecte acomandes.
ON UPDATE funciona igual, però es dispara quan canvia la clau primària del pare. Amb claus subrogades gairebé mai no es fa servir, perquè un id autoincremental no canvia mai. És precisament un dels avantatges de les claus subrogades davant de les naturals.
La sintaxi completa per declarar aquestes restriccions (
CONSTRAINT ... FOREIGN KEY ... REFERENCES ... ON DELETE CASCADE) s'estudia a la lliçó 05-01. Aquí el que importa és entendre el criteri de decisió.
- Cardinalitats: 1:1, 1:N i N:M
La cardinalitat descriu quantes files d'una taula es poden associar amb quantes d'una altra.
6.1. Un a molts (1:N)
És la més freqüent. Una categoria té molts productes; cada producte pertany a una sola categoria.
erDiagram
CATEGORIES ||--o{ PRODUCTES : "classifica"
CATEGORIES {
int id PK
varchar nom
text descripcio
}
PRODUCTES {
int id PK
varchar nom
int categoria_id FK
numeric preu
int stock
}
Com s'implementa: la FK va sempre al costat "molts". productes.categoria_id apunta a categories.id. Mai al revés: si posessis una columna producte_id a categories, només hi cabria un producte per categoria.
Relacions 1:N a BotigaVerda:
| Costat "un" | Costat "molts" | Columna FK |
|---|---|---|
categories |
productes |
productes.categoria_id |
proveidors |
productes |
productes.proveidor_id |
clients |
comandes |
comandes.client_id |
empleats |
comandes |
comandes.empleat_id |
comandes |
linies_comanda |
linies_comanda.comanda_id |
productes |
linies_comanda |
linies_comanda.producte_id |
productes |
ressenyes |
ressenyes.producte_id |
clients |
ressenyes |
ressenyes.client_id |
comandes |
devolucions |
devolucions.comanda_id |
6.2. Molts a molts (N:M) i la taula pont
Una comanda conté molts productes, i un producte apareix en moltes comandes. Una relació N:M no es pot implementar directament: no hi ha on posar la FK. La solució és una taula pont (o taula intermèdia, o d'associació).
erDiagram
COMANDES ||--o{ LINIES_COMANDA : "conté"
PRODUCTES ||--o{ LINIES_COMANDA : "apareix a"
COMANDES {
int id PK
int client_id FK
date data_comanda
varchar estat
}
LINIES_COMANDA {
int id PK
int comanda_id FK
int producte_id FK
int quantitat
numeric preu_unitari
numeric descompte
}
PRODUCTES {
int id PK
varchar nom
numeric preu
}
La N:M entre comandes i productes es descompon en dues relacions 1:N que convergeixen a linies_comanda.
I aquí hi ha el detall que distingeix una taula pont ben dissenyada: linies_comanda no és només un enllaç, té dades pròpies.
| Columna | Per què hi és |
|---|---|
quantitat |
Quantes unitats d'aquest producte en aquesta comanda? Només té sentit a la intersecció |
preu_unitari |
El preu en el moment de la venda. Si el producte puja de preu demà, les factures antigues no han de canviar |
descompte |
La rebaixa aplicada a aquesta línia concreta |
Aquest preu_unitari és un exemple perfecte de desnormalització deliberada (secció 8): duplica informació que també és a productes.preu, però és imprescindible perquè són coses diferents: un és el preu actual i l'altre el preu històric facturat.
6.3. Un a un (1:1)
Cada fila d'A es correspon amb com a màxim una de B. S'implementa posant una FK en una de les dues taules i declarant-la a més UNIQUE.
BotigaVerda no té relacions 1:1, perquè gairebé mai no calen: si la correspondència és exacta, el natural és fondre totes dues taules en una. Els casos legítims són tres:
- Separar columnes grans poc consultades (una taula
productes_fitxa_tecnicaamb textos enormes). - Aïllar dades sensibles amb permisos diferents (una taula
empleats_dades_bancaries). - Especialització: una taula
usuarisgeneral i taulesusuaris_admin/usuaris_clientamb camps exclusius.
6.4. Relacions reflexives
Una taula es pot referenciar a si mateixa. BotigaVerda té dos casos:
| Relació | Significat | Cardinalitat |
|---|---|---|
empleats.cap_id → empleats.id |
Jerarquia organitzativa | 1:N (un cap, molts subordinats) |
clients.referit_per_id → clients.id |
Programa de recomanació | 1:N (un client en refereix diversos) |
Totes dues columnes admeten NULL, i aquest nul té un significat precís: cap_id IS NULL identifica la direcció general (l'empleada 1, Rosa Alcázar Vives) i referit_per_id IS NULL identifica els clients que van arribar pel seu compte. Consultar aquestes relacions requereix SELF JOIN, que s'estudia a la lliçó 03-06.
6.5. Resum
| Cardinalitat | Com s'implementa | Exemple a BotigaVerda |
|---|---|---|
| 1:N | FK al costat "molts" | productes.categoria_id |
| N:M | Taula pont amb dues FK | linies_comanda |
| 1:1 | FK amb restricció UNIQUE |
(no s'aplica) |
| Reflexiva | FK a la mateixa taula, normalment nul·lable | empleats.cap_id |
- Normalització pràctica: 1FN, 2FN i 3FN
La normalització és el procés d'organitzar les columnes en taules per eliminar redundància i evitar anomalies. Sona acadèmic, però s'entén millor veient què passa quan no es fa.
El punt de partida: una taula desnormalitzada
Imagina't que BotigaVerda desés totes les seves comandes en una única taula:
| comanda_id | data | client_nom | client_email | client_ciutat | productes | categoria | preu_total |
|---|---|---|---|---|---|---|---|
| 1 | 2025-03-04 | Lucía Martínez | [email protected] | València | Oli oliva, Arròs integral, Infusió camamilla | Alimentació, Alimentació, Begudes | 43.20 |
| 5 | 2025-05-07 | Lucía Martínez | [email protected] | València | Detergent eco, Lufa, Bosses cotó | Llar sostenible | 32.10 |
| 8 | 2025-06-28 | Sofia Moreira | [email protected] | Lisboa | Oli oliva, Mel tarongina, Infusió camamilla | Alimentació | 66.87 |
Els problemes són immediats:
| Anomalia | Què passa aquí |
|---|---|
| D'inserció | No pots donar d'alta un client que encara no ha demanat res, ni un producte que encara no s'ha venut |
| D'actualització | Si la Lucía canvia d'email, cal modificar-lo a totes les seves files. Si en falles una, tindràs dos emails contradictoris |
| D'esborrat | Si esborres la comanda 8, perds tota la informació de la Sofia Moreira |
| De redundància | Les dades de la Lucía es repeteixen a cada comanda: malbaratament d'espai i font constant d'incoherències |
| De consulta | Quantes unitats d'"Oli oliva" s'han venut? Impossible: és dins d'una llista separada per comes |
Primera forma normal (1FN)
Regla: cada cel·la conté un únic valor atòmic; no hi ha grups repetits.
La columna productes viola la 1FN descaradament: conté tres valors en una cel·la. Per arreglar-ho cal treure els productes a files pròpies.
Senyals que alguna cosa viola la 1FN:
- Llistes separades per comes en una cel·la.
- Columnes numerades:
producte_1,producte_2,producte_3. - Un camp que de vegades conté una dada i de vegades diverses.
Després d'aplicar la 1FN, cada producte de cada comanda és una fila, cosa que ja ens dona el germen de linies_comanda.
Segona forma normal (2FN)
Regla: estar en 1FN i que cap atribut no clau no depengui només d'una part d'una clau primària composta.
Després de la 1FN, la clau de la nostra taula seria (comanda_id, producte_nom). Però fixa-t'hi:
datadepèn només decomanda_id, no del producte.client_nom,client_emailiclient_ciutatdepenen només decomanda_id.categoriadepèn només deproducte_nom, no de la comanda.
Són dependències parcials, i provoquen que aquestes dades es repeteixin una vegada per cada línia de la comanda. La solució és dividir:
- El que depèn de la comanda → taula
comandes. - El que depèn del producte → taula
productes. - El que depèn de la combinació (quantitat, preu de venda, descompte) → taula
linies_comanda.
Aquí tens, deduïda, la taula pont de la secció 6.2.
Tercera forma normal (3FN)
Regla: estar en 2FN i que cap atribut no clau no depengui d'un altre atribut no clau (res de dependències transitives).
A la taula comandes resultant continuaríem tenint client_nom, client_email i client_ciutat. Aquests depenen de client_email (o del client en general), no de comanda_id. És una dependència transitiva: comanda_id → client → email.
La solució: extreure una taula clients i deixar a comandes només la referència client_id. Idènticament, categoria a productes depèn de la categoria, no del producte: s'extreu categories i queda productes.categoria_id.
El resultat
graph LR
A["Taula única<br/>desnormalitzada"] -->|1FN: valors atòmics| B["comandes_linies<br/>una fila per producte"]
B -->|2FN: separar dependències parcials| C["comandes + productes<br/>+ linies_comanda"]
C -->|3FN: eliminar transitives| D["+ clients + categories<br/>+ proveidors…"]
Aplicant aquestes tres regles al cas de BotigaVerda arribes, gairebé mecànicament, a l'esquema de nou taules del curs. Aquesta és la resposta a la pregunta inicial: l'esquema no està partit per caprici, sinó perquè cada taula agrupa exactament les dades que depenen d'una mateixa cosa.
Resum memoritzable de les tres formes normals:
| Forma | Regla en una frase | Violació típica |
|---|---|---|
| 1FN | Un valor per cel·la, sense grups repetits | "Oli, Arròs, Infusió" en una columna |
| 2FN | Sense dependències parcials d'una clau composta | data_comanda repetida a cada línia |
| 3FN | Sense dependències transitives entre atributs no clau | client_email dins de comandes |
La regla mnemotècnica clàssica: "cada atribut no clau ha de dependre de la clau, de tota la clau i de res més que la clau". La primera part és 1FN/2FN, "de tota la clau" és 2FN i "de res més que la clau" és 3FN.
Hi ha formes normals superiors (BCNF, 4FN, 5FN) que resolen casos més estranys. A la pràctica professional, arribar a 3FN cobreix el 95 % dels dissenys.
- Quan desnormalitzar a propòsit
La normalització optimitza la integritat i l'escriptura. De vegades es paga un preu en velocitat de lectura, perquè reconstruir una factura obliga a combinar cinc taules. Desnormalitzar és introduir redundància conscientment a canvi de rendiment o de correcció històrica.
Casos legítims, amb exemples de BotigaVerda:
| Cas | Exemple | Per què està justificat |
|---|---|---|
| Dades històriques immutables | linies_comanda.preu_unitari |
El preu facturat no ha de canviar quan canviï productes.preu. No és redundància: són dades diferents |
| Agregats precalculats | Una columna comandes.total |
Evita recalcular la suma de línies a cada consulta. Cost: cal mantenir-la sincronitzada (amb disparadors, mòdul 10) |
| Còpia d'un atribut molt consultat | Desar client_pais a comandes |
Evita un JOIN en informes que agrupen per país. Només si el volum ho justifica |
| Taules d'informes | Una taula resum de vendes mensuals | Els magatzems de dades fan servir esquemes en estrella deliberadament desnormalitzats |
I la regla d'or:
Normalitza primer. Desnormalitza després, amb mesures a la mà, i documenta per què.
Desnormalitzar sense mesurar és la causa número u de bases de dades incoherents. Cada dada duplicada és una dada que pot quedar desincronitzada, i necessitaràs un mecanisme explícit (disparador, procés batch, lògica d'aplicació) per mantenir-la al dia. Abans de desnormalitzar, prova amb un índex (mòdul 8) o una vista materialitzada (mòdul 10): solen resoldre el problema sense cost d'integritat.
Errors habituals i consells
- Confondre "relació" amb "relació entre taules". En el model de Codd, una relació és una taula.
- Posar la FK al costat equivocat d'una 1:N. Sempre va al costat "molts". Si la poses a l'"un", limites la relació a un únic fill.
- Intentar fer una N:M sense taula pont. Desar
"3,7,12"en una columnaproductes_idsviola la 1FN, impedeix les FK i fa les consultes impossibles. - Fer servir
CASCADEper comoditat. UnDELETEes pot propagar molt més lluny del que et penses. ReservaCASCADEper a relacions de composició real. - Esborrar productes venuts. Destrueix l'històric i salta la FK. Fes servir
actiu = FALSE(esborrat lògic); per això existeix la columna. - Oblidar el
UNIQUEde la clau natural. Ambidsubrogat com a PK, res no impedeix duplicar l'email d'un client si no el declaresUNIQUE. - Sobrenormalitzar. Partir una taula en set per purisme acadèmic complica cada consulta sense aportar integritat real.
- Desnormalitzar "per si de cas". Sense un mesurament que ho justifiqui, només estàs creant incoherències futures.
- Consell: indexa les teves claus foranes. PostgreSQL crea índex automàticament per a la PK, però no per a les FK. Sense aquest índex, els
JOINi els esborrats en cascada poden ser molt lents (mòdul 8). - Consell: dibuixa el diagrama abans d'escriure DDL. Deu minuts d'esquema en paper eviten setmanes de migracions.
- Consell: anomena les FK amb el patró
<taula_singular>_id.client_id,producte_id,comanda_id. La consistència fa que les consultes s'escriguin gairebé soles.
Exercicis
Exercici 1
Per a cada parell de taules de BotigaVerda, indica la cardinalitat (1:1, 1:N o N:M), on va la clau forana i quina acció ON DELETE triaries, justificant-la:
proveidorsiproductesclientsiressenyescomandesidevolucionsclientsiproductes(a través de ressenyes)empleatsamb si mateixa
Exercici 2
Aquesta taula viola les tres formes normals. Identifica quina regla trenca en cada cas i descompon-la en taules normalitzades fins a 3FN, indicant claus primàries i foranes.
| ressenya_id | producte | preu_producte | categoria | client_email | client_ciutat | puntuacions | dates |
|---|---|---|---|---|---|---|---|
| 1 | Oli d'oliva | 12.50 | Alimentació | [email protected] | València | 5, 4 | 2025-03-15, 2025-04-02 |
| 2 | Crema àloe vera | 18.90 | Cosmètica natural | [email protected] | València | 5 | 2025-03-25 |
Exercici 3
Prediu què respon PostgreSQL a cadascuna d'aquestes operacions sobre la base de BotigaVerda ja carregada, i explica per què:
-- a)
INSERT INTO ressenyes (producte_id, client_id, puntuacio, comentari, data)
VALUES (99, 1, 5, 'Excel·lent', '2026-03-01');
-- b)
DELETE FROM productes WHERE id = 1;
-- c)
DELETE FROM comandes WHERE id = 20;
-- d)
UPDATE empleats SET cap_id = NULL WHERE id = 4;Solucions
Solució 1
| # | Parell | Cardinalitat | On va la FK | ON DELETE |
Justificació |
|---|---|---|---|---|---|
| 1 | proveidors – productes |
1:N | productes.proveidor_id |
RESTRICT |
Un proveïdor serveix molts productes. No has d'esborrar un proveïdor els productes del qual continuen al catàleg; es marca actiu = FALSE |
| 2 | clients – ressenyes |
1:N | ressenyes.client_id |
CASCADE |
Un client escriu moltes ressenyes. Si s'exerceix el dret de supressió del client, les seves ressenyes se n'han d'anar amb ell |
| 3 | comandes – devolucions |
1:N | devolucions.comanda_id |
CASCADE |
Una comanda pot tenir diverses devolucions parcials. Una devolució no existeix sense la seva comanda |
| 4 | clients – productes |
N:M | Taula pont ressenyes (amb client_id i producte_id) |
Segons cada costat | Un client ressenya molts productes i un producte rep moltes ressenyes. ressenyes és taula pont amb dades pròpies: puntuació, comentari i data |
| 5 | empleats – empleats |
1:N reflexiva | empleats.cap_id |
SET NULL |
Un cap té diversos subordinats. Si el cap deixa l'empresa, l'equip queda sense cap assignat però no s'esborra |
Solució 2
Violacions:
| Forma | Què es trenca |
|---|---|
| 1FN | puntuacions i dates contenen llistes separades per comes |
| 2FN | Després de separar cada ressenya a la seva fila, preu_producte i categoria depenen només del producte, no de la ressenya |
| 3FN | client_ciutat depèn de client_email (del client), no de ressenya_id; i categoria és una entitat pròpia, no un atribut del producte |
Descomposició en 3FN:
categories(id PK, nom)
productes(id PK, nom, preu, categoria_id FK → categories.id)
clients(id PK, email UNIQUE, ciutat)
ressenyes(id PK, producte_id FK → productes.id, client_id FK → clients.id,
puntuacio, data)I així queda:
| Taula | Files resultants |
|---|---|
categories |
Alimentació, Cosmètica natural |
productes |
Oli d'oliva (12.50, Alimentació), Crema àloe vera (18.90, Cosmètica natural) |
clients |
lucia.martinez@… (València), carlos.ferrer@… (València) |
ressenyes |
3 files: (Oli, Lucía, 5, 2025-03-15), (Oli, Lucía, 4, 2025-04-02), (Crema, Carlos, 5, 2025-03-25) |
Fixa't que la llista "5, 4" de la primera fila es converteix en dues ressenyes diferents: la 1FN ens ha obligat a descobrir que allà hi havia realment dos fets, no un.
Solució 3
a) Falla:
ERROR: insert or update on table "ressenyes" violates foreign key constraint "ressenyes_producte_id_fkey" DETAIL: Key (producte_id)=(99) is not present in table "productes".
BotigaVerda té 20 productes, així que el 99 no existeix. La integritat referencial impedeix crear una ressenya òrfena.
b) Falla:
ERROR: update or delete on table "productes" violates foreign key constraint "linies_comanda_producte_id_fkey" on table "linies_comanda" DETAIL: Key (id)=(1) is still referenced from table "linies_comanda".
El producte 1 (Oli d'oliva verge extra) apareix en diverses línies de comanda, i aquesta FK està declarada RESTRICT precisament per protegir l'històric de facturació. Per retirar-lo del catàleg es fa UPDATE productes SET actiu = FALSE WHERE id = 1;.
c) Funciona, i esborra més del que sembla:
S'elimina la comanda 20 i, en cascada, les seves dues línies de comanda (linies_comanda.comanda_id està declarada ON DELETE CASCADE). És l'exemple perfecte de per què CASCADE s'ha de fer servir amb cura: una sola sentència ha esborrat tres files en dues taules. Si la comanda tingués devolucions associades, també desapareixerien.
d) Funciona:
L'empleat 4 (Óscar Peris Blasco, comercial) passa a no tenir cap assignat. La columna cap_id admet NULL per disseny, així que no es viola cap restricció. Ara hi hauria dos empleats amb cap_id IS NULL: la directora general (que ho és per naturalesa) i aquest comercial (que ho és per un canvi organitzatiu). És un bon recordatori que NULL pot significar coses diferents en files diferents, i de per què convé documentar-ne la semàntica.
Conclusió
Aquesta lliçó explica el perquè de l'esquema que carregaràs a continuació:
- El model relacional organitza les dades en relacions (taules) de tuples (files) amb atributs (columnes) sobre dominis (tipus), i connecta la informació per valor, no per punters: d'aquí neixen els
JOIN. - La clau primària identifica cada fila de manera única, no nul·la i estable. BotigaVerda fa servir
idsubrogat a les nou taules per uniformitat, estabilitat i eficiència, sense renunciar a declararUNIQUEles claus naturals comclients.email. - La clau forana garanteix la integritat referencial: PostgreSQL rebutja amb
violates foreign key constrainttant inserir una referència inexistent com esborrar un pare referenciat. - Les accions
ON DELETE(RESTRICT,CASCADE,SET NULL) es trien segons el significat de negoci:CASCADEper alinies_comanda,SET NULLper acomandes.empleat_id,RESTRICTper a productes venuts. - Les cardinalitats 1:N (FK al costat "molts"), N:M (taula pont com
linies_comanda, amb dades pròpies) i les relacions reflexives d'empleats.cap_idiclients.referit_per_id. - La normalització fins a 3FN, deduïda a partir d'una taula de comandes monolítica, explica per què l'esquema té nou taules; i la desnormalització deliberada justifica que
linies_comanda.preu_unitariconservi el preu històric.
A la lliçó següent, La base de dades del curs: BotigaVerda, tot això es fa tangible: veuràs el diagrama entitat-relació complet, la descripció taula per taula amb les seves columnes i tipus, i l'script SQL a punt per copiar que crea les nou taules i carrega les dades que faràs servir durant els onze mòduls restants. En acabar-la tindràs la base de dades funcionant al teu equip, i a partir del mòdul 2 començaràs a consultar-la de debò.
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
