Les dues lliçons anteriors ens han donat el vocabulari i el catàleg. Sabem escriure dependències funcionals, calcular clausures, trobar claus candidates i decidir si una relació està en una forma normal concreta. El que encara no hem fet és normalitzar alguna cosa de debò, de principi a fi.
I hi ha una diferència substancial entre les dues coses. Els exemples de la lliçó 05-02 estaven construïts per il·lustrar una forma normal aïllada: tres o quatre columnes, una dependència problemàtica, la correcció evident. La realitat no es presenta així. La realitat es presenta com el full de càlcul de préstecs de BiblioRed: tretze columnes, onze dependències funcionals, valors no atòmics, dependències parcials i transitives simultànies, vuitanta-quatre mil files amb errors tipogràfics acumulats des del 2018, i un taulell que atén socis cada dia i no es pot aturar.
Aquesta lliçó és el procediment. Sis passos, aplicats a aquell full de principi a fi, amb les dades al davant a cada etapa i el SQL que descompon i migra. Després, les dues propietats que tota descomposició ha de complir per ser correcta —i el contraexemple, amb dades, d'una que no les compleix i inventa files en reunir—. I per últim, la part que menys s'ensenya i més es necessita: com es normalitza una base de dades que ja és en producció, i l'examen taula per taula de l'esquema ampliat que vam construir al mòdul 4.
Un avís sobre l'abast: aquí no es redefineix cap forma normal. S'apliquen. Si en algun punt dubtes de què prohibeix exactament la 2FN o de per què la FNBC és més exigent que la 3FN, torna a 05-02.
Contingut
- El procediment en sis passos
- Pas 1: reunir les regles de negoci i escriure les dependències
- Pas 2: determinar les claus candidates
- Pas 3a: comprovar i assolir la 1FN
- Pas 3b i 4a: comprovar i assolir la 2FN
- Pas 3c i 4b: comprovar i assolir la 3FN
- El resultat final i el retrobament amb l'esquema del mòdul 2
- Descomposició sense pèrdua d'informació i la condició de Heath
- El contraexemple: una descomposició que inventa files
- Conservació de les dependències
- La cobertura mínima i l'algorisme de síntesi 3FN
- Pas 5: verificació amb consultes de control
- Pas 6: reposar les claus foranes
- Normalitzar a la vida real: disseny nou enfront de producció
- Examen de l'esquema ampliat del mòdul 4
- El procediment en sis passos
Aquest és el guió complet. És el mateix tant si dissenyes des de zero com si rescates un esquema existent; el que canvia és la quantitat de feina al pas 1 i el risc del pas 4.
| Pas | Què es fa | Eina |
|---|---|---|
| 1 | Reunir les regles de negoci i escriure el conjunt F de dependències funcionals |
Entrevistes, documentació, SELECT de refutació |
| 2 | Determinar les claus candidates de la relació | Classificació d'atributs + algorisme de la clausura X⁺ |
| 3 | Comprovar 1FN, després 2FN, després 3FN/FNBC — en aquest ordre i aturant-se a la primera fallada | Definicions de 05-02 |
| 4 | Descompondre: treure cada dependència infractora a la seva pròpia taula | CREATE TABLE + INSERT ... SELECT DISTINCT |
| 5 | Verificar: sense pèrdua, dependències conservades, recomptes que quadren | JOIN de reconstrucció, COUNT, EXCEPT |
| 6 | Reposar les claus foranes i les restriccions | ALTER TABLE ... ADD CONSTRAINT |
Dues observacions abans de començar.
L'ordre del pas 3 no és negociable. Es comprova de baix a dalt perquè les formes normals són acumulatives: no té sentit buscar dependències transitives en una taula que encara té llistes dins de les cel·les. I tan bon punt una comprovació falla, es descompon (pas 4) i es torna al pas 2 —perquè les taules noves tenen claus noves— abans de continuar pujant.
El pas 5 és el que la gent es salta i el que surt més car. Una descomposició pot semblar impecable sobre el paper i estar perdent informació o deixant una regla de negoci sense ningú que la vigili. Les seccions 8 a 12 són enterament sobre això.
flowchart TD
P1["1 · Regles de negoci → F"]
P2["2 · Claus candidates (clausura)"]
P3A{"3a · 1FN?"}
P3B{"3b · 2FN?"}
P3C{"3c · 3FN / FNBC?"}
P4["4 · Descompondre"]
P5["5 · Verificar"]
P6["6 · Claus foranes i restriccions"]
P1 --> P2 --> P3A
P3A -->|No| P4
P3A -->|Si| P3B
P3B -->|No| P4
P3B -->|Si| P3C
P3C -->|No| P4
P3C -->|Si| P5
P4 --> P2
P5 --> P6
- Pas 1: reunir les regles de negoci i escriure les dependències
Partim del full de càlcul tal com està. I aquí cal fer una correcció respecte de la lliçó 05-01: allà vam treballar amb una versió simplificada de la taula per poder raonar sobre l'estructura. El fitxer real, més enllà de les cinc files de mostra que vam veure a 01-01, té dues columnes més i un problema addicional.
La bibliotecària de la sucursal Nord ho explica així: "la columna de telèfons la vam anar ampliant, perquè molts socis ens donen el mòbil i el fix de casa i els apuntàvem tots dos separats per una barra. I a la columna d'autor, quan un llibre té dos autors, els posem amb coma."
Aquesta és, doncs, la taula de partida completa:
prestecs_full — el punt de partida real
| exemplar_cod | data_prestec | data_devolucio | soci_email | soci_nom | soci_telefons | isbn | titol | autors | autor_nac | sucursal_nom | sucursal_ciutat | sucursal_cp |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| EJ-3081 | 2026-03-02 | 2026-03-16 | [email protected] | Marta Alsina | 600111222 / 938880011 | 9788401339097 | El mapa del temps | Félix J. Palma | espanyola | Nord | Vallmar | 08110 |
| EJ-3081 | 2026-04-05 | (NULL) | [email protected] | M. Alsina | 600111222 | 9788401339097 | El mapa del temps | Félix J. Palma | espanyola | nord | Vallmar | 08110 |
| EJ-3090 | 2026-04-07 | 2026-04-21 | [email protected] | Ivan Pereda | 600333444 | 9788401337208 | Els pilars de la Terra | Ken Follet | britànica | Nord | Vallmar | 08110 |
| EJ-3082 | 2026-04-09 | (NULL) | [email protected] | Marta Alsina | 600111222 / 938880011 | 978840133909 | El mapa del temps | Félix J. Palma | espanyola | Nord | Vallmar | 08110 |
| EJ-3095 | 2026-04-10 | (NULL) | [email protected] | Núria Bastos | 600555666 | 9788401337208 | Els pilars de la Terra | Ken Follett | britànica | Sud | Vallmar de Mar | 08130 |
| EJ-3093 | 2026-04-12 | (NULL) | [email protected] | Núria Bastos | 600555666 | 9788432234118 | Manual d'horticultura urbana | Rosa Vinyals, Pere Coll | espanyola | Sud | Vallmar de Mar | 08130 |
Sis files. Fixa't que ara apareixen els defectes originals del diagnòstic de 01-01: M. Alsina, nord, Ken Follet, [email protected], l'ISBN truncat. Ens n'haurem d'ocupar, però més endavant i per separat: la normalització és una feina sobre l'estructura i la neteja és una feina sobre les dades. Confondre-les és la millor manera de no acabar cap de les dues.
Les entrevistes
Aquestes són les regles que BiblioRed confirma, cadascuna amb la dependència que produeix:
| # | Regla de negoci confirmada | Dependència |
|---|---|---|
| RN1 | Un exemplar físic no pot estar prestat dues vegades el mateix dia | clau {exemplar_cod, data_prestec} |
| RN2 | Cada préstec el fa un soci i es torna (o no) en una data | {exemplar_cod, data_prestec} → {soci_email, data_devolucio} |
| RN3 | Cada exemplar és una còpia d'una obra concreta i viu en una sucursal | exemplar_cod → {isbn, sucursal_nom} |
| RN4 | Un ISBN identifica una edició: un títol i una llista d'autors | isbn → titol |
| RN5 | Un autor té una nacionalitat | autor → autor_nac |
| RN6 | El correu identifica un soci, amb el seu nom | soci_email → soci_nom |
| RN7 | Cada sucursal és en una adreça amb el seu codi postal | sucursal_nom → sucursal_cp |
| RN8 | Un codi postal pertany a un sol municipi | sucursal_cp → sucursal_ciutat |
| RN9 | Un soci pot tenir diversos telèfons | multivaluat (no funcional) |
| RN10 | Una obra pot tenir diversos autors, i un autor diverses obres | multivaluat (no funcional) |
I aquestes són les no-regles, igual d'importants, que van sortir en preguntar per les excepcions:
- "Pot haver-hi dos socis amb el mateix telèfon?" → Sí, famílies que donen el fix de casa. Per tant no existeix
soci_telefon → soci_email, per molt que les dades de mostra ho semblin suggerir. - "Pot haver-hi dues edicions diferents amb el mateix títol?" → Sí, i de fet n'hi ha. Per tant no existeix
titol → isbn. - "Un exemplar canvia de sucursal alguna vegada?" → Sí, en trasllats, però l'exemplar és en una sola sucursal en cada moment. La dependència
exemplar_cod → sucursal_nomes manté, però anota que la dada és mutable.
El conjunt F
F = {
f1: {exemplar_cod, data_prestec} → soci_email
f2: {exemplar_cod, data_prestec} → data_devolucio
f3: exemplar_cod → isbn
f4: exemplar_cod → sucursal_nom
f5: isbn → titol
f6: autor → autor_nac
f7: soci_email → soci_nom
f8: sucursal_nom → sucursal_cp
f9: sucursal_cp → sucursal_ciutat
}Escrit en forma desagregada (un sol atribut a la dreta), que és com convé tenir-lo per treballar.
Comprovar que les dades no refuten les dependències
Abans de construir res sobre F, val la pena llançar una consulta de refutació per cada dependència. Recorda de 05-01: les dades no poden confirmar una dependència, però sí refutar-la, i si la refuten és que hi ha dades brutes o que la regla no és la que ens han explicat. Les dues coses cal saber-les abans de migrar.
-- Plantilla general de refutació: per a X → Y,
-- buscar valors de X amb més d'un valor de Y.
-- isbn → titol?
SELECT isbn, COUNT(DISTINCT titol) AS n
FROM prestecs_full GROUP BY isbn HAVING COUNT(DISTINCT titol) > 1;
-- soci_email → soci_nom?
SELECT soci_email, COUNT(DISTINCT soci_nom) AS n
FROM prestecs_full GROUP BY soci_email HAVING COUNT(DISTINCT soci_nom) > 1;
-- sucursal_nom → sucursal_cp?
SELECT sucursal_nom, COUNT(DISTINCT sucursal_cp) AS n
FROM prestecs_full GROUP BY sucursal_nom HAVING COUNT(DISTINCT sucursal_cp) > 1;Resultat de la segona:
soci_email | n ------------------------+--- [email protected] | 2
Aquí està: dos noms (Marta Alsina i M. Alsina) per al mateix correu. Això no refuta la regla de negoci: refuta la qualitat de les dades. La regla continua sent certa —un soci té un nom— i el que la consulta ha trobat és el defecte 3 del diagnòstic de 01-01, localitzat amb precisió quirúrgica.
Aquest és un dels beneficis menys anunciats de normalitzar: les consultes de refutació són el millor detector de dades brutes que existeix, perquè troben exactament les contradiccions que el nou esquema rebutjarà. Executa-les totes i fes la llista abans de començar a migrar; després és tard.
- Pas 2: determinar les claus candidates
Apliquem el mètode de 05-01, secció 13.
Classificació d'atributs. Només a l'esquerra: exemplar_cod, data_prestec. Només a la dreta: data_devolucio, soci_nom, titol, autor_nac, sucursal_ciutat. Als dos costats: soci_email, isbn, sucursal_nom, sucursal_cp. Sense aparèixer a F: soci_telefons, autors (són multivaluats i no produeixen dependències funcionals).
Nucli obligatori. Els "només esquerra" i els "que no apareixen" són a tota clau candidata:
Aquest resultat és en si mateix un diagnòstic. Que soci_telefons i autors hagin de formar part de la clau és absurd des del punt de vista del negoci —ningú no identifica un préstec per la llista de telèfons del soci— i és el senyal formal que aquestes columnes no encaixen en el model relacional tal com estan. És la violació de 1FN que tractarem al pas següent.
Per poder avançar, deixem aquestes dues columnes apartades de moment i calculem sobre les onze restants:
{exemplar_cod, data_prestec}⁺:
| Passada | Dependència | S'hi afegeix |
|---|---|---|
| 1 | f1 | soci_email |
| 1 | f2 | data_devolucio |
| 1 | f3 | isbn |
| 1 | f4 | sucursal_nom |
| 1 | f5 | titol |
| 1 | f7 | soci_nom |
| 1 | f8 | sucursal_cp |
| 1 | f9 | sucursal_ciutat |
| 2 | — | (res de nou) |
Conté els onze atributs considerats. És superclau. I ja vam comprovar a 05-01 que és mínima ({exemplar_cod}⁺ no hi arriba i {data_prestec}⁺ no creix). Clau candidata única: {exemplar_cod, data_prestec}.
Atributs primers: exemplar_cod, data_prestec. Tots els altres, no primers.
Nota que autor_nac no ha entrat a la clausura: f6 és autor → autor_nac, i autor (en singular) ni tan sols és una columna de la taula, perquè la columna es diu autors i conté una llista. La dependència f6 no es pot avaluar en aquesta taula. És una altra manifestació del mateix problema de 1FN.
- Pas 3a: comprovar i assolir la 1FN
Comprovació. Tots els valors són atòmics?
soci_telefons='600111222 / 938880011'→ No. Dos valors en una cel·la.autors='Rosa Vinyals, Pere Coll'→ No. Dos valors en una cel·la.
La taula no està en 1FN. No té sentit continuar comprovant res més fins que no s'arregli.
Descomposició. Cada atribut multivaluat surt a la seva pròpia taula, amb la clau de l'entitat propietària més el valor com a clau primària (regla 3 de transformació de 04-03, que ara sabem que és la correcció estàndard de la 1FN).
Aquí apareix un detall important: per treure els telèfons cal saber de quin soci són, i l'identificador del soci en aquesta taula és soci_email. Igual amb els autors: calen penjats de l'isbn. Així que la descomposició de 1FN ja produeix tres taules.
-- 1. La taula principal, sense les columnes multivaluades
CREATE TABLE p1_prestecs (
exemplar_cod VARCHAR(10) NOT NULL,
data_prestec DATE NOT NULL,
data_devolucio DATE,
soci_email VARCHAR(120) NOT NULL,
soci_nom VARCHAR(120) NOT NULL,
isbn VARCHAR(13) NOT NULL,
titol VARCHAR(200) NOT NULL,
sucursal_nom VARCHAR(60) NOT NULL,
sucursal_ciutat VARCHAR(60) NOT NULL,
sucursal_cp VARCHAR(5) NOT NULL,
CONSTRAINT pk_p1_prestecs PRIMARY KEY (exemplar_cod, data_prestec)
);
-- 2. Els telèfons, un per fila
CREATE TABLE p1_telefons (
soci_email VARCHAR(120) NOT NULL,
numero VARCHAR(20) NOT NULL,
CONSTRAINT pk_p1_telefons PRIMARY KEY (soci_email, numero)
);
-- 3. Els autors de cada obra, un per fila
CREATE TABLE p1_autors_obra (
isbn VARCHAR(13) NOT NULL,
autor VARCHAR(120) NOT NULL,
autor_nac VARCHAR(40),
CONSTRAINT pk_p1_autors_obra PRIMARY KEY (isbn, autor)
);Migració de les dades. Això és el que es fa de debò quan hi ha vuitanta-quatre mil files i no es poden teclejar a mà. PostgreSQL té funcions per partir cadenes, i són l'eina exacta per desfer una violació de 1FN:
-- La taula principal: una fila per préstec, traient les columnes de llista
INSERT INTO p1_prestecs (exemplar_cod, data_prestec, data_devolucio,
soci_email, soci_nom, isbn, titol,
sucursal_nom, sucursal_ciutat, sucursal_cp)
SELECT exemplar_cod, data_prestec, data_devolucio,
soci_email, soci_nom, isbn, titol,
sucursal_nom, sucursal_ciutat, sucursal_cp
FROM prestecs_full;
-- Els telèfons: es parteix la cadena per '/' i es genera una fila per tros.
-- unnest(string_to_array(...)) converteix una llista en files: és l'operació
-- inversa exacta de la violació de 1FN.
INSERT INTO p1_telefons (soci_email, numero)
SELECT DISTINCT
h.soci_email,
btrim(t.numero) -- btrim treu els espais sobrants
FROM prestecs_full h
CROSS JOIN LATERAL unnest(string_to_array(h.soci_telefons, '/')) AS t(numero)
WHERE btrim(t.numero) <> '';
-- Els autors: igual, partint per ','
INSERT INTO p1_autors_obra (isbn, autor, autor_nac)
SELECT DISTINCT
h.isbn,
btrim(a.autor),
h.autor_nac
FROM prestecs_full h
CROSS JOIN LATERAL unnest(string_to_array(h.autors, ',')) AS a(autor)
WHERE btrim(a.autor) <> '';El DISTINCT és imprescindible: la Marta Alsina apareix en tres préstecs i els seus dos telèfons sortirien nou vegades sense ell. És la primera aparició d'un patró que es repetirà en tot el procés: INSERT ... SELECT DISTINCT és la forma canònica de migrar dades en descompondre, perquè la taula de destinació conté cada fet una vegada i la d'origen el té repetit.
Resultat:
p1_telefons
| soci_email | numero |
|---|---|
| [email protected] | 600111222 |
| [email protected] | 938880011 |
| [email protected] | 600111222 |
| [email protected] | 938880011 |
| [email protected] | 600333444 |
| [email protected] | 600555666 |
p1_autors_obra
| isbn | autor | autor_nac |
|---|---|---|
| 9788401339097 | Félix J. Palma | espanyola |
| 9788401337208 | Ken Follet | britànica |
| 9788401337208 | Ken Follett | britànica |
| 978840133909 | Félix J. Palma | espanyola |
| 9788432234118 | Rosa Vinyals | espanyola |
| 9788432234118 | Pere Coll | espanyola |
I aquí les dades brutes salten a la vista amb una claredat que al full original no tenien: [email protected] genera un soci fantasma amb telèfons duplicats; Ken Follet i Ken Follett són dos autors diferents per al mateix ISBN; l'ISBN truncat 978840133909 crea una obra inexistent. La normalització no ha creat aquests problemes: els ha fet visibles. Abans estaven repartits entre sis files amples i no es podien comptar; ara són files de més que es poden llistar i corregir.
Anota'ls a la llista de neteja i continuem amb l'estructura.
Nota sobre la nacionalitat. En treure
autor_nacap1_autors_obrahem posat la nacionalitat al costat de cada parella obra-autor, amb la qual cosa continua repetida una vegada per obra del mateix autor. És una dependència transitiva que arrossegarem fins al pas de la 3FN. És normal: cada forma normal arregla la seva part, i les descomposicions intermèdies no són el resultat final.
- Pas 3b i 4a: comprovar i assolir la 2FN
Tornem al pas 2 amb p1_prestecs, que té clau {exemplar_cod, data_prestec}.
Comprovació de 2FN. Hi ha atributs no primers que depenguin d'una part de la clau? Calculem la clausura de cada part:
{exemplar_cod}⁺={exemplar_cod, isbn, titol, sucursal_nom, sucursal_cp, sucursal_ciutat}. Conté cinc atributs no primers. Hi ha dependències parcials.{data_prestec}⁺={data_prestec}. No aporta res.
p1_prestecs no està en 2FN. Les dependències infractores són f3 (exemplar_cod → isbn) i f4 (exemplar_cod → sucursal_nom), i amb elles s'arrossega tot el que en penja: titol, sucursal_cp, sucursal_ciutat.
Abans de la descomposició, aquesta és la redundància que eliminarem. Fixa't en la fila 1 i la 2: són el mateix exemplar prestat dues vegades, i les nou columnes de la dreta són idèntiques:
| exemplar_cod | data_prestec | soci_email | isbn | titol | sucursal_nom | sucursal_cp | sucursal_ciutat |
|---|---|---|---|---|---|---|---|
| EJ-3081 | 2026-03-02 | [email protected] | 9788401339097 | El mapa del temps | Nord | 08110 | Vallmar |
| EJ-3081 | 2026-04-05 | [email protected] | 9788401339097 | El mapa del temps | nord | 08110 | Vallmar |
| EJ-3090 | 2026-04-07 | [email protected] | 9788401337208 | Els pilars de la Terra | Nord | 08110 | Vallmar |
| EJ-3082 | 2026-04-09 | [email protected] | 978840133909 | El mapa del temps | Nord | 08110 | Vallmar |
| EJ-3095 | 2026-04-10 | [email protected] | 9788401337208 | Els pilars de la Terra | Sud | 08130 | Vallmar de Mar |
| EJ-3093 | 2026-04-12 | [email protected] | 9788432234118 | Manual d'horticultura urbana | Sud | 08130 | Vallmar de Mar |
Descomposició. Tot el que depèn d'exemplar_cod se'n va a una taula amb exemplar_cod com a clau primària; a la taula de préstecs queda exemplar_cod com a referència.
-- El que depèn de la clau COMPLETA: el préstec en si
CREATE TABLE p2_prestecs (
exemplar_cod VARCHAR(10) NOT NULL,
data_prestec DATE NOT NULL,
data_devolucio DATE,
soci_email VARCHAR(120) NOT NULL,
soci_nom VARCHAR(120) NOT NULL,
CONSTRAINT pk_p2_prestecs PRIMARY KEY (exemplar_cod, data_prestec)
);
-- El que depèn d'exemplar_cod tot sol
CREATE TABLE p2_exemplars (
exemplar_cod VARCHAR(10) NOT NULL,
isbn VARCHAR(13) NOT NULL,
titol VARCHAR(200) NOT NULL,
sucursal_nom VARCHAR(60) NOT NULL,
sucursal_ciutat VARCHAR(60) NOT NULL,
sucursal_cp VARCHAR(5) NOT NULL,
CONSTRAINT pk_p2_exemplars PRIMARY KEY (exemplar_cod)
);INSERT INTO p2_prestecs (exemplar_cod, data_prestec, data_devolucio,
soci_email, soci_nom)
SELECT exemplar_cod, data_prestec, data_devolucio, soci_email, soci_nom
FROM p1_prestecs;
-- Aquí el DISTINCT fa la feina: de les 6 files de préstecs
-- en surten només les 5 combinacions diferents d'exemplar.
INSERT INTO p2_exemplars (exemplar_cod, isbn, titol,
sucursal_nom, sucursal_ciutat, sucursal_cp)
SELECT DISTINCT exemplar_cod, isbn, titol,
sucursal_nom, sucursal_ciutat, sucursal_cp
FROM p1_prestecs;Un avís sobre aquest DISTINCT. Si les dades estiguessin brutes d'una manera concreta —el mateix exemplar_cod amb dues sucursals diferents per un error de teclejat, o amb Nord i nord— el SELECT DISTINCT retornaria dues files per al mateix exemplar i l'INSERT fallaria amb violació de clau primària. I això és exactament el que passa aquí:
ERROR: duplicate key value violates unique constraint "pk_p2_exemplars" DETAIL: Key (exemplar_cod)=(EJ-3081) already exists.
Perquè EJ-3081 apareix amb Nord en una fila i amb nord en una altra. L'error no és una fallada de la migració: és la migració funcionant. L'esquema nou està rebutjant una contradicció que el vell permetia. És el moment d'aplicar la neteja, que en aquest cas és trivial:
-- Neteja prèvia: normalitzar majúscules de la sucursal
UPDATE p1_prestecs SET sucursal_nom = initcap(lower(sucursal_nom));I tornar a llançar l'INSERT. Aquest cicle —migrar, fallar, netejar, reintentar— és el ritme normal d'una normalització sobre dades històriques, i per això es fa sempre sobre una còpia i dins d'una transacció.
Resultat després de la neteja:
p2_exemplars
| exemplar_cod | isbn | titol | sucursal_nom | sucursal_ciutat | sucursal_cp |
|---|---|---|---|---|---|
| EJ-3081 | 9788401339097 | El mapa del temps | Nord | Vallmar | 08110 |
| EJ-3082 | 978840133909 | El mapa del temps | Nord | Vallmar | 08110 |
| EJ-3090 | 9788401337208 | Els pilars de la Terra | Nord | Vallmar | 08110 |
| EJ-3093 | 9788432234118 | Manual d'horticultura urbana | Sud | Vallmar de Mar | 08130 |
| EJ-3095 | 9788401337208 | Els pilars de la Terra | Sud | Vallmar de Mar | 08130 |
p2_prestecs
| exemplar_cod | data_prestec | data_devolucio | soci_email | soci_nom |
|---|---|---|---|---|
| EJ-3081 | 2026-03-02 | 2026-03-16 | [email protected] | Marta Alsina |
| EJ-3081 | 2026-04-05 | (NULL) | [email protected] | M. Alsina |
| EJ-3090 | 2026-04-07 | 2026-04-21 | [email protected] | Ivan Pereda |
| EJ-3082 | 2026-04-09 | (NULL) | [email protected] | Marta Alsina |
| EJ-3095 | 2026-04-10 | (NULL) | [email protected] | Núria Bastos |
| EJ-3093 | 2026-04-12 | (NULL) | [email protected] | Núria Bastos |
Ja no hi ha cap fila on el títol d'"El mapa del temps" estigui repetit per culpa del préstec. Amb sis files l'estalvi és modest; amb vuitanta-quatre mil préstecs sobre quaranta mil exemplars, la columna titol passa de vuitanta-quatre mil valors a quaranta mil, i —el que importa de debò— de vuitanta-quatre mil oportunitats d'escriure'l malament a quaranta mil.
- Pas 3c i 4b: comprovar i assolir la 3FN
Ara hi ha tres taules per comprovar. Anem amb les dues que tenen candidates a dependència transitiva.
6.1 p2_prestecs
Clau: {exemplar_cod, data_prestec}. Dependències que es compleixen aquí: f1, f2 i f7 (soci_email → soci_nom).
Apliquem la comprovació mecànica de 3FN a f7:
- És
soci_emailsuperclau?{soci_email}⁺={soci_email, soci_nom}. No conté la clau. No. - És
soci_nomprimer? Els primers sónexemplar_codidata_prestec. No.
Viola la 3FN. És la dependència transitiva {exemplar_cod, data_prestec} → soci_email → soci_nom, i el seu símptoma a les dades és que Marta Alsina apareix dues vegades i Núria Bastos dues més.
CREATE TABLE p3_prestecs (
exemplar_cod VARCHAR(10) NOT NULL,
data_prestec DATE NOT NULL,
data_devolucio DATE,
soci_email VARCHAR(120) NOT NULL,
CONSTRAINT pk_p3_prestecs PRIMARY KEY (exemplar_cod, data_prestec)
);
CREATE TABLE p3_socis (
soci_email VARCHAR(120) NOT NULL,
soci_nom VARCHAR(120) NOT NULL,
CONSTRAINT pk_p3_socis PRIMARY KEY (soci_email)
);
INSERT INTO p3_prestecs SELECT exemplar_cod, data_prestec, data_devolucio, soci_email
FROM p2_prestecs;
INSERT INTO p3_socis SELECT DISTINCT soci_email, soci_nom FROM p2_prestecs;I un altre cop:
ERROR: duplicate key value violates unique constraint "pk_p3_socis" DETAIL: Key (soci_email)=([email protected]) already exists.
Marta Alsina i M. Alsina. L'esquema nou no admet que un soci tingui dos noms, que és precisament el que volíem. Neteja i reintent:
UPDATE p2_prestecs SET soci_nom = 'Marta Alsina' WHERE soci_nom = 'M. Alsina';
UPDATE p2_prestecs SET soci_email = '[email protected]'
WHERE soci_email = '[email protected]';p3_socis
| soci_email | soci_nom |
|---|---|
| [email protected] | Marta Alsina |
| [email protected] | Ivan Pereda |
| [email protected] | Núria Bastos |
Tres socis. Abans de normalitzar, un COUNT(DISTINCT soci_nom) sobre el full en donava cinc. Aquest és el defecte 3 de 01-01, resolt.
6.2 p2_exemplars
Clau: {exemplar_cod}. Dependències que es compleixen: f3, f4, f5 (isbn → titol), f8 (sucursal_nom → sucursal_cp) i f9 (sucursal_cp → sucursal_ciutat).
Tres violacions de 3FN encadenades:
| Dependència | Determinant superclau? | Determinat primer? | Veredicte |
|---|---|---|---|
isbn → titol |
No ({isbn}⁺ no inclou exemplar_cod) |
No | Viola |
sucursal_nom → sucursal_cp |
No | No | Viola |
sucursal_cp → sucursal_ciutat |
No | No | Viola |
Es treuen les tres. Nota que sucursal_nom → sucursal_cp → sucursal_ciutat és una cadena de dos salts i produeix dues taules, no una: la de sucursals i la de codis postals. És el cas que vam veure a 05-02, secció 6.
CREATE TABLE p3_exemplars (
exemplar_cod VARCHAR(10) NOT NULL,
isbn VARCHAR(13) NOT NULL,
sucursal_nom VARCHAR(60) NOT NULL,
CONSTRAINT pk_p3_exemplars PRIMARY KEY (exemplar_cod)
);
CREATE TABLE p3_obres (
isbn VARCHAR(13) NOT NULL,
titol VARCHAR(200) NOT NULL,
CONSTRAINT pk_p3_obres PRIMARY KEY (isbn)
);
CREATE TABLE p3_sucursals (
sucursal_nom VARCHAR(60) NOT NULL,
sucursal_cp VARCHAR(5) NOT NULL,
CONSTRAINT pk_p3_sucursals PRIMARY KEY (sucursal_nom)
);
CREATE TABLE p3_codis_postals (
sucursal_cp VARCHAR(5) NOT NULL,
sucursal_ciutat VARCHAR(60) NOT NULL,
CONSTRAINT pk_p3_codis_postals PRIMARY KEY (sucursal_cp)
);
INSERT INTO p3_exemplars
SELECT exemplar_cod, isbn, sucursal_nom FROM p2_exemplars;
INSERT INTO p3_obres
SELECT DISTINCT isbn, titol FROM p2_exemplars;
INSERT INTO p3_sucursals
SELECT DISTINCT sucursal_nom, sucursal_cp FROM p2_exemplars;
INSERT INTO p3_codis_postals
SELECT DISTINCT sucursal_cp, sucursal_ciutat FROM p2_exemplars;p3_obres
| isbn | titol |
|---|---|
| 9788401339097 | El mapa del temps |
| 978840133909 | El mapa del temps |
| 9788401337208 | Els pilars de la Terra |
| 9788432234118 | Manual d'horticultura urbana |
Quatre obres on n'hi ha tres: l'ISBN truncat continua allà. Aquesta vegada l'INSERT no falla, perquè tècnicament 978840133909 i 9788401339097 són claus diferents. És un avís important: la normalització només detecta automàticament els errors que produeixen contradiccions; un identificador mal escrit que no xoca amb cap altre passa desapercebut. Aquí cal una validació de domini —el dígit de control de l'ISBN, un CHECK de longitud— que és el que vam aprendre a posar a 04-04.
Després de la neteja manual de l'ISBN:
p3_obres
| isbn | titol |
|---|---|
| 9788401339097 | El mapa del temps |
| 9788401337208 | Els pilars de la Terra |
| 9788432234118 | Manual d'horticultura urbana |
p3_sucursals
| sucursal_nom | sucursal_cp |
|---|---|
| Nord | 08110 |
| Sud | 08130 |
p3_codis_postals
| sucursal_cp | sucursal_ciutat |
|---|---|
| 08110 | Vallmar |
| 08130 | Vallmar de Mar |
6.3 p1_autors_obra
Clau: {isbn, autor}. Es compleix f6: autor → autor_nac.
- És
autorsuperclau? No, és mitja clau. - I com que és mitja clau, la dependència és parcial: viola la 2FN, no només la 3FN.
Es descompon igual:
CREATE TABLE p3_autors (
autor VARCHAR(120) NOT NULL,
autor_nac VARCHAR(40),
CONSTRAINT pk_p3_autors PRIMARY KEY (autor)
);
CREATE TABLE p3_obres_autors (
isbn VARCHAR(13) NOT NULL,
autor VARCHAR(120) NOT NULL,
CONSTRAINT pk_p3_obres_autors PRIMARY KEY (isbn, autor)
);
INSERT INTO p3_autors SELECT DISTINCT autor, autor_nac FROM p1_autors_obra;
INSERT INTO p3_obres_autors SELECT DISTINCT isbn, autor FROM p1_autors_obra;Després de corregir Ken Follet → Ken Follett:
p3_autors
| autor | autor_nac |
|---|---|
| Félix J. Palma | espanyola |
| Ken Follett | britànica |
| Rosa Vinyals | espanyola |
| Pere Coll | espanyola |
6.4 p1_telefons
Clau: {soci_email, numero}. Els dos atributs són primers i no n'hi ha cap altre. Com vam veure a 05-02, tota taula de dos atributs la clau de la qual és la parella està en FNBC per construcció. Res a fer, més enllà de reemetre-la amb els correus ja corregits.
- El resultat final i el retrobament amb l'esquema del mòdul 2
Aquest és l'esquema al qual hem arribat, partint d'una taula de tretze columnes:
| Taula | Atributs | Clau primària | Forma normal |
|---|---|---|---|
p3_prestecs |
exemplar_cod, data_prestec, data_devolucio, soci_email |
{exemplar_cod, data_prestec} |
FNBC |
p3_exemplars |
exemplar_cod, isbn, sucursal_nom |
{exemplar_cod} |
FNBC |
p3_obres |
isbn, titol |
{isbn} |
FNBC |
p3_obres_autors |
isbn, autor |
{isbn, autor} |
FNBC |
p3_autors |
autor, autor_nac |
{autor} |
FNBC |
p3_socis |
soci_email, soci_nom |
{soci_email} |
FNBC |
p3_telefons |
soci_email, numero |
{soci_email, numero} |
FNBC |
p3_sucursals |
sucursal_nom, sucursal_cp |
{sucursal_nom} |
FNBC |
p3_codis_postals |
sucursal_cp, sucursal_ciutat |
{sucursal_cp} |
FNBC |
Nou taules. I totes en FNBC, no només en 3FN: com que cadascuna té una única clau candidata i està en 3FN, la regla de 05-02 s'aplica automàticament.
flowchart LR
CP["p3_codis_postals<br/>cp · ciutat"] --> SUC["p3_sucursals<br/>nom · cp"]
SUC --> EJ["p3_exemplars<br/>cod · isbn · sucursal"]
OB["p3_obres<br/>isbn · titol"] --> EJ
OB --> OA["p3_obres_autors<br/>isbn · autor"]
AU["p3_autors<br/>autor · nac"] --> OA
EJ --> PR["p3_prestecs<br/>exemplar · data · devolucio · soci"]
SO["p3_socis<br/>email · nom"] --> PR
SO --> TEL["p3_telefons<br/>email · numero"]
Ara compara'l amb el diagrama que vam dibuixar a la lliçó 01-01 —socis, prestecs, exemplars, llibres, sucursals— i amb l'esquema que vam escriure al mòdul 2 per comprensió del domini. Són el mateix esquema. La normalització formal ha arribat, per un camí completament diferent i sense consultar el resultat anterior, a la mateixa estructura a la qual va arribar el disseny intuïtiu.
Això no és una casualitat ni un parany didàctic: és el resultat esperat, i és la millor justificació possible de la teoria. Un dissenyador experimentat arriba a un esquema en 3FN sense pensar en dependències funcionals, perquè ha interioritzat les mateixes restriccions. La normalització serveix per a tres coses que la intuïció no dóna: verificar que el disseny intuïtiu és correcte, arbitrar quan dues persones discrepen, i resoldre els casos rars en què la intuïció s'equivoca.
Les diferències amb l'esquema del mòdul 2 són de detall i totes apunten cap allà:
- Les nostres taules fan servir claus naturals (
soci_email,exemplar_cod,isbn,autor). L'esquema real fa servir claus subrogades (soci_id,exemplar_id,llibre_id,autor_id), per les raons de 04-01: el correu canvia, el nom de l'autor s'escriu de diverses maneres, i una clau que canvia es propaga a totes les taules que la referencien. La normalització no obliga a fer servir claus naturals; nosaltres ho vam fer perquè al full no hi havia cap altra cosa. Substituir-les per subrogades és el pas següent i no altera la forma normal. - L'esquema real té
llibrescom a vista sobrematerials+materials_llibre, resultat de la jerarquia de generalització de 04-02 i 04-03. La nostrap3_obresés la versió sense jerarquia. - L'esquema real desa
dir_ciutatasucursalsen comptes de tenircodis_postals. És una desnormalització deliberada sobre una taula de quatre files, i la discutirem a 05-04.
- Descomposició sense pèrdua d'informació i la condició de Heath
Hem descompost vuit vegades. Com sabem que no hem perdut res pel camí?
Definició. Una descomposició d'una relació
RenR1iR2és sense pèrdua d'informació (o de reunió no additiva) si, en fer el JOIN natural deR1iR2, s'obté exactamentR: ni una fila de menys ni una fila de més.
El nom despista una mica, perquè el problema no sol ser perdre files: és guanyar-ne. Una descomposició mal feta produeix, en reunir, files que mai no van existir. I aquestes files són mentides: combinacions de dades que la base de dades afirma i que no van passar mai.
La condició que ho garanteix és senzilla i té nom propi:
Condició de Heath. Sigui
Runa relació amb atributs dividits en tres grupsX,YiZ. Si es compleix la dependència funcionalX → Y, aleshores descompondreRenR1(X, Y)iR2(X, Z)és sense pèrdua d'informació.
En llenguatge planer: descompon sempre per un determinant. Si l'atribut (o conjunt d'atributs) que queda a les dues taules —el que serveix de "cola" per al JOIN— és clau d'almenys una d'elles, no pots perdre ni inventar res.
La raó intuïtiva és aquesta: si X és clau de R1, aleshores cada valor de X apareix una sola vegada a R1. En fer el JOIN, cada fila de R2 troba exactament una parella, així que el nombre de files del resultat és el nombre de files de R2, que era l'original. No hi ha multiplicació possible.
Comprovem-ho a la nostra descomposició
Mirem el pas de la 2FN. Vam partir p1_prestecs en:
p2_prestecs(exemplar_cod, data_prestec, data_devolucio, soci_email, soci_nom)p2_exemplars(exemplar_cod, isbn, titol, sucursal_nom, sucursal_ciutat, sucursal_cp)
L'atribut compartit és exemplar_cod, i és clau primària de p2_exemplars. Condició de Heath complerta: la descomposició és sense pèrdua.
I es pot verificar empíricament, que és el que cal fer sempre:
-- Reconstruir l'original i comparar-lo fila a fila.
-- EXCEPT retorna les files de la primera consulta que NO són a la segona.
-- Si les dues direccions donen zero files, les taules són idèntiques.
WITH reconstruit AS (
SELECT p.exemplar_cod, p.data_prestec, p.data_devolucio,
p.soci_email, p.soci_nom,
e.isbn, e.titol, e.sucursal_nom, e.sucursal_ciutat, e.sucursal_cp
FROM p2_prestecs p
JOIN p2_exemplars e ON e.exemplar_cod = p.exemplar_cod
)
SELECT 'sobren al reconstruit' AS problema, * FROM (
SELECT * FROM reconstruit EXCEPT SELECT * FROM p1_prestecs
) s
UNION ALL
SELECT 'falten al reconstruit', * FROM (
SELECT * FROM p1_prestecs EXCEPT SELECT * FROM reconstruit
) f;Zero files en totes dues direccions: reconstrucció exacta.
- El contraexemple: una descomposició que inventa files
Perquè es vegi què s'està evitant, fem deliberadament una descomposició dolenta.
Prenguem tres columnes del full: soci_email, titol i sucursal_nom. Aquestes són les dades reals (una fila per préstec, traient duplicats exactes):
R original
| soci_email | titol | sucursal_nom |
|---|---|---|
| [email protected] | El mapa del temps | Nord |
| [email protected] | Els pilars de la Terra | Sud |
| [email protected] | Manual d'horticultura urbana | Sud |
| [email protected] | Els pilars de la Terra | Nord |
Ara algú decideix, raonant "un soci té els seus títols i un soci té les seves sucursals", descompondre així:
R1(soci_email, titol)
| soci_email | titol |
|---|---|
| [email protected] | El mapa del temps |
| [email protected] | Els pilars de la Terra |
| [email protected] | Manual d'horticultura urbana |
| [email protected] | Els pilars de la Terra |
R2(soci_email, sucursal_nom)
| soci_email | sucursal_nom |
|---|---|
| [email protected] | Nord |
| [email protected] | Sud |
| [email protected] | Nord |
Sembla raonable. Ara reunim:
Amb aquestes dades concretes el resultat casualment coincideix, perquè cada soci només té una sucursal. Afegim-hi un préstec més, perfectament normal: la Núria Bastos s'endú "Els pilars de la Terra" també de la sucursal Nord, un dia que passava per allà.
R original (5 files)
| soci_email | titol | sucursal_nom |
|---|---|---|
| [email protected] | El mapa del temps | Nord |
| [email protected] | Els pilars de la Terra | Sud |
| [email protected] | Manual d'horticultura urbana | Sud |
| [email protected] | Els pilars de la Terra | Nord |
| [email protected] | Els pilars de la Terra | Nord |
R2 passa a tenir dues files per a la Núria: (n.bastos, Sud) i (n.bastos, Nord). I ara el JOIN:
Resultat del JOIN (7 files)
| soci_email | titol | sucursal_nom | Existia? |
|---|---|---|---|
| [email protected] | El mapa del temps | Nord | Sí |
| [email protected] | Els pilars de la Terra | Sud | Sí |
| [email protected] | Els pilars de la Terra | Nord | Sí |
| [email protected] | Manual d'horticultura urbana | Sud | Sí |
| [email protected] | Manual d'horticultura urbana | Nord | NO |
| [email protected] | Els pilars de la Terra | Nord | Sí |
La cinquena fila és falsa. La Núria Bastos mai no va treure el "Manual d'horticultura urbana" de la sucursal Nord; el va treure de Sud. El JOIN l'ha inventada combinant els seus dos títols amb les seves dues sucursals.
I aquí hi ha el pitjor: aquesta fila és indistingible de les verdaderes. No hi ha cap marca que la senyali. Qualsevol informe sobre què es presta a cada sucursal sortirà malament, i ningú no sabrà per què.
Per què ha fallat? Perquè l'atribut compartit, soci_email, no és clau de cap de les dues taules: un soci té diversos títols i diverses sucursals. No es compleix la condició de Heath, i per tant la descomposició no està garantida.
La descomposició correcta d'aquests tres atributs seria per un determinant real. Com que el títol depèn de l'exemplar i l'exemplar de la sucursal, la ruta correcta passa per exemplar_cod, que és exactament el que vam fer a la secció 5.
Regla de butxaca: abans de partir una taula, pregunta't "la columna per la qual les uniré després és clau primària d'almenys una de les dues?". Si la resposta és no, no parteixis: estàs a punt d'inventar dades.
- Conservació de les dependències
La segona propietat. Ja la vam conèixer a 05-02 en parlar de la FNBC; aquí la formalitzem i la comprovem sobre la nostra descomposició.
Definició. Una descomposició conserva les dependències si tota dependència del conjunt original
Fes pot comprovar dins d'una sola de les taules resultants, sense necessitat de reunir-les.
Per què importa, en termes pràctics: una dependència que viu dins d'una taula es garanteix amb una PRIMARY KEY o un UNIQUE, i el SGBD la vigila a cada INSERT sense que ningú se n'hagi de recordar. Una dependència repartida entre dues taules necessita un activador, codi d'aplicació o una consulta periòdica d'auditoria: és a dir, alguna cosa que es pot oblidar, desactivar o executar tard.
Comprovació sobre el resultat de BiblioRed
| Dependència | En quina taula viu? | Com es garanteix? |
|---|---|---|
{exemplar_cod, data_prestec} → soci_email |
p3_prestecs |
Clau primària |
{exemplar_cod, data_prestec} → data_devolucio |
p3_prestecs |
Clau primària |
exemplar_cod → isbn |
p3_exemplars |
Clau primària |
exemplar_cod → sucursal_nom |
p3_exemplars |
Clau primària |
isbn → titol |
p3_obres |
Clau primària |
autor → autor_nac |
p3_autors |
Clau primària |
soci_email → soci_nom |
p3_socis |
Clau primària |
sucursal_nom → sucursal_cp |
p3_sucursals |
Clau primària |
sucursal_cp → sucursal_ciutat |
p3_codis_postals |
Clau primària |
Les nou dependències estan conservades, i les nou es garanteixen amb una clau primària. Ni un activador, ni una línia de codi d'aplicació.
Això no és casualitat. Quan la descomposició es fa traient cada dependència a una taula el determinant de la qual és la clau primària, la conservació ve de regal: la dependència X → Y es converteix literalment en "X és la clau primària de la taula que conté Y", i això és el que una clau primària significa.
El cas problemàtic és el que vam veure a 05-02, secció 8: quan hi ha claus candidates encavalcades i es força la FNBC, alguna dependència pot quedar repartida. Aquí no ha passat perquè cada taula té una sola clau candidata.
Quan una dependència es perd: què fer
Si en acabar una descomposició hi ha una dependència que no viu en cap taula, tens tres sortides, en ordre de preferència:
| Opció | Quan | Cost |
|---|---|---|
| Retrocedir a 3FN | Si la dependència perduda és una regla crítica i la redundància que s'accepta és petita | Redundància controlada, anomalies d'actualització possibles |
| Afegir un activador | Si la FNBC compensa i la regla es pot comprovar en un BEFORE INSERT/UPDATE |
Codi per mantenir; cost a cada escriptura |
| Auditoria periòdica | Si la violació és tolerable durant hores i es pot corregir després | La base de dades pot estar temporalment inconsistent |
El que no és una opció és no adonar-se'n. Fes sempre la taula de la secció anterior: una fila per dependència, una columna amb la taula on viu. Si alguna cel·la queda buida, decideix conscientment.
- La cobertura mínima i l'algorisme de síntesi 3FN
Tot el que hem fet ha estat per descomposició: partir d'una taula gran i anar-la partint. Existeix el camí contrari, que s'anomena síntesi: partir del conjunt de dependències i construir les taules directament. El presentem a nivell d'idea, perquè convé saber que existeix.
Cobertura mínima
Una cobertura mínima (o recobriment canònic) d'un conjunt de dependències
Fés un altre conjuntFcque determina exactament el mateix queF—té la mateixa clausura— però està reduït al mínim: cada dependència té un sol atribut a la dreta, cap atribut del costat esquerre no és superflu, i cap dependència sencera no és supèrflua.
Es calcula en tres passos:
- Desagregar els costats drets, fent servir la regla de descomposició de 05-01.
- Treure atributs superflus de l'esquerra: per a cada
{A, B} → C, comprovar siA → Cja es dedueix de la resta; si sí,Bhi sobrava. - Treure dependències redundants: per a cada
X → Y, treure-la del conjunt i comprovar amb la clausura siY ⊆ X⁺continua complint-se fent servir només les altres; si sí, hi sobrava.
El nostre F de BiblioRed ja està pràcticament en cobertura mínima: està desagregat, cap costat esquerre no té atributs de sobra, i cap dependència no es dedueix de les altres. Només cal vigilar les que es dedueixen per transitivitat. Per exemple, si algú hagués afegit exemplar_cod → titol a la llista, seria redundant, perquè ja es dedueix d'exemplar_cod → isbn i isbn → titol. Incloure-la portaria a crear una taula de més.
L'algorisme de síntesi 3FN
Amb la cobertura mínima calculada, l'algorisme és sorprenentment directe:
1. Calcular la cobertura mínima Fc de F. 2. Agrupar les dependències de Fc que tinguin el MATEIX costat esquerre. Crear una taula per cada grup, amb els atributs del costat esquerre (clau primària) més tots els drets del grup. 3. Si cap de les taules creades no conté una clau candidata de la relació original, afegir una taula més formada per una clau candidata. 4. Eliminar les taules els atributs de les quals estiguin continguts en una altra.
Aplicat al nostre F, agrupant per costat esquerre:
| Costat esquerre | Dependències | Taula resultant |
|---|---|---|
{exemplar_cod, data_prestec} |
f1, f2 | (exemplar_cod, data_prestec, soci_email, data_devolucio) |
exemplar_cod |
f3, f4 | (exemplar_cod, isbn, sucursal_nom) |
isbn |
f5 | (isbn, titol) |
autor |
f6 | (autor, autor_nac) |
soci_email |
f7 | (soci_email, soci_nom) |
sucursal_nom |
f8 | (sucursal_nom, sucursal_cp) |
sucursal_cp |
f9 | (sucursal_cp, sucursal_ciutat) |
Set taules, i la primera conté la clau candidata, així que el pas 3 no hi afegeix res. Són exactament les set taules a les quals vam arribar per descomposició (les altres dues, p3_telefons i p3_obres_autors, van sortir dels atributs multivaluats, que no produeixen dependències funcionals i per tant queden fora d'aquest algorisme).
Dues coses cal saber d'aquest algorisme:
- Garanteix 3FN, conservació de dependències i descomposició sense pèrdua. Les tres alhora. És un resultat fort i és la raó per la qual la 3FN es considera l'objectiu per defecte: sempre és assolible sense sacrificar res.
- No garanteix FNBC. Ja sabem per què: pot no existir una descomposició a FNBC que conservi les dependències.
A la pràctica gairebé ningú no executa aquest algorisme a mà en un projecte real —es dissenya per comprensió del domini i es verifica amb normalització—, però conèixer-lo canvia com mires un esquema: cada taula ben dissenyada correspon a un grup de dependències amb el mateix determinant, i aquest determinant és la seva clau primària. Si trobes una taula que no encaixa en aquesta descripció, tens alguna cosa per revisar.
- Pas 5: verificació amb consultes de control
Acabada la descomposició i la migració, cal demostrar que el resultat és correcte. No n'hi ha prou de mirar-lo: cal executar comprovacions. Aquestes són les quatre que no han de faltar en cap migració.
12.1 El recompte de la taula de fets
La taula principal ha de tenir exactament les mateixes files que l'original:
Si el destí en té menys, s'han perdut préstecs (probablement per duplicats exactes eliminats per un DISTINCT mal posat). Si en té més, alguna cosa s'ha multiplicat.
12.2 Els recomptes de les taules de catàleg
Cada taula nova ha de tenir tantes files com valors diferents hi havia a l'original després de la neteja:
SELECT 'socis' AS taula,
(SELECT COUNT(DISTINCT soci_email) FROM prestecs_full) AS esperat,
(SELECT COUNT(*) FROM p3_socis) AS real_
UNION ALL
SELECT 'obres',
(SELECT COUNT(DISTINCT isbn) FROM prestecs_full),
(SELECT COUNT(*) FROM p3_obres)
UNION ALL
SELECT 'exemplars',
(SELECT COUNT(DISTINCT exemplar_cod) FROM prestecs_full),
(SELECT COUNT(*) FROM p3_exemplars)
UNION ALL
SELECT 'sucursals',
(SELECT COUNT(DISTINCT sucursal_nom) FROM prestecs_full),
(SELECT COUNT(*) FROM p3_sucursals);taula | esperat | real_ ------------+---------+------- socis | 4 | 3 obres | 4 | 3 exemplars | 5 | 5 sucursals | 3 | 2
Les discrepàncies no són errors: són el registre de la neteja. Quatre correus diferents donaven tres socis (es va fusionar [email protected]); quatre ISBN donaven tres obres (es va corregir el truncat); tres noms de sucursal en donaven dos (Nord/nord). Cada diferència ha d'estar justificada i anotada. Una diferència que no sàpigues explicar és un error.
12.3 La reconstrucció completa
La prova definitiva: reunir-ho tot i comparar amb l'original.
CREATE OR REPLACE VIEW v_prestecs_reconstruit AS
SELECT p.exemplar_cod,
p.data_prestec,
p.data_devolucio,
p.soci_email,
s.soci_nom,
e.isbn,
o.titol,
e.sucursal_nom,
cp.sucursal_ciutat,
su.sucursal_cp
FROM p3_prestecs p
JOIN p3_socis s ON s.soci_email = p.soci_email
JOIN p3_exemplars e ON e.exemplar_cod = p.exemplar_cod
JOIN p3_obres o ON o.isbn = e.isbn
JOIN p3_sucursals su ON su.sucursal_nom = e.sucursal_nom
JOIN p3_codis_postals cp ON cp.sucursal_cp = su.sucursal_cp;
-- Ha de retornar el mateix nombre de files que l'original
SELECT COUNT(*) FROM v_prestecs_reconstruit;Sis files, les mateixes que hi havia. Ni una de més: no hem inventat res.
Fixa't en una cosa: fer servir JOIN i no LEFT JOIN forma part de la prova. Si algun préstec apuntés a un soci, un exemplar o una obra inexistents, el JOIN intern el deixaria fora i el recompte baixaria. Un recompte que quadra amb JOIN intern demostra alhora que no falta res i que la integritat referencial es compleix.
12.4 Les mateixes consultes donen les mateixes respostes
Per últim, i això és el que convenç qui paga: les consultes que el negoci feia sobre el full han de continuar funcionant i donar el mateix resultat.
-- "Quants préstecs va fer cada soci?" — sobre el full original
SELECT soci_email, COUNT(*) FROM prestecs_full GROUP BY soci_email;
-- La mateixa pregunta, sobre l'esquema normalitzat
SELECT s.soci_email, s.soci_nom, COUNT(*) AS prestecs
FROM p3_prestecs p
JOIN p3_socis s ON s.soci_email = p.soci_email
GROUP BY s.soci_email, s.soci_nom
ORDER BY prestecs DESC;soci_email | soci_nom | prestecs ----------------------+---------------+---------- [email protected] | Marta Alsina | 3 [email protected] | Núria Bastos | 2 [email protected] | Ivan Pereda | 1
Sobre el full original, aquella consulta donava quatre files i atribuïa a la Marta només dos préstecs, perquè el tercer estava sota el correu mal escrit. L'esquema normalitzat no només dóna la mateixa resposta: dóna la resposta correcta, que el full no donava.
- Pas 6: reposar les claus foranes
La descomposició ha deixat columnes que apunten a altres taules sense declarar-ho. Cal dir-ho al SGBD, perquè fins que no ho facis res no impedeix un préstec d'un soci inexistent.
ALTER TABLE p3_prestecs
ADD CONSTRAINT fk_prestecs_exemplar FOREIGN KEY (exemplar_cod)
REFERENCES p3_exemplars (exemplar_cod) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT fk_prestecs_soci FOREIGN KEY (soci_email)
REFERENCES p3_socis (soci_email) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT chk_prestecs_dates
CHECK (data_devolucio IS NULL OR data_devolucio >= data_prestec);
ALTER TABLE p3_exemplars
ADD CONSTRAINT fk_exemplars_obra FOREIGN KEY (isbn)
REFERENCES p3_obres (isbn) ON UPDATE CASCADE,
ADD CONSTRAINT fk_exemplars_sucursal FOREIGN KEY (sucursal_nom)
REFERENCES p3_sucursals (sucursal_nom) ON UPDATE CASCADE;
ALTER TABLE p3_sucursals
ADD CONSTRAINT fk_sucursals_cp FOREIGN KEY (sucursal_cp)
REFERENCES p3_codis_postals (sucursal_cp) ON UPDATE CASCADE;
ALTER TABLE p3_obres_autors
ADD CONSTRAINT fk_oa_obra FOREIGN KEY (isbn) REFERENCES p3_obres (isbn)
ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT fk_oa_autor FOREIGN KEY (autor) REFERENCES p3_autors (autor)
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE p3_telefons
ADD CONSTRAINT fk_telefons_soci FOREIGN KEY (soci_email)
REFERENCES p3_socis (soci_email) ON DELETE CASCADE ON UPDATE CASCADE;I amb això, la restricció del defecte 6 del diagnòstic de 01-01 —la devolució anterior al préstec de l'Ivan Pereda— queda impedida pel CHECK. Si alguna dada històrica l'incompleix, l'ALTER TABLE fallarà i hauràs de decidir: corregir la dada o afegir la restricció com a NOT VALID, com vam veure a 04-04.
Els criteris per triar CASCADE, RESTRICT o SET NULL a cada clau forana són els de la lliçó 02-06; aquí només s'apliquen.
El resultat final: d'un full amb vuit defectes catalogats, sis han quedat estructuralment impossibles (redundància de soci, redundància de llibre, inconsistència de nom, inconsistència d'autor, format incoherent de sucursal, fragmentació entre fitxers), un l'impedeix ara un CHECK (la data impossible), i un —l'ISBN truncat— requereix una validació de domini, que és feina de 04-04 i no de normalització.
- Normalitzar a la vida real: disseny nou enfront de producció
Tot l'anterior s'ha fet sobre una taula aturada. A la realitat hi ha dues situacions molt diferents.
Cas A: disseny nou
És el cas fàcil i el més freqüent en un projecte que comença. No es normalitza al final: es dissenya normalitzat des del principi. Es fa el model ER (04-02), es transforma (04-03), es trien tipus i restriccions (04-04), i la normalització es fa servir com a llista de verificació abans d'escriure la primera línia de codi d'aplicació.
La revisió és ràpida quan el disseny està ben fet: taula per taula, escrius les seves dependències, calcules la seva clau candidata i comproves la 3FN. Mitja hora per a un esquema de vint taules. I el que troba sol ser poc però valuós: una columna que s'ha colat on no tocava, una regla de negoci que ningú no havia escrit.
Cas B: base de dades en producció
Aquí el problema no és la teoria: és que hi ha vuitanta mil files, quaranta consultes escrites contra l'esquema vell i un taulell que obre demà a les nou. La normalització correcta se sap fer; el difícil és aplicar-la sense tallar el servei.
La tècnica estàndard és la migració per fases amb expansió i contracció:
Fase 1 — Expandir (sense trencar res). Es creen les taules noves al costat de la vella, buides. Res no les fa servir encara. L'aplicació continua funcionant exactament igual.
Fase 2 — Omplir en segon pla. Es migren les dades històriques amb els INSERT ... SELECT DISTINCT que hem vist, per lots, en hores de poc trànsit, i amb les consultes de refutació executades abans per saber què es trencarà.
-- Migració per lots: no bloqueja la taula durant hores
INSERT INTO socis (email, nom)
SELECT DISTINCT soci_email, soci_nom
FROM prestecs_full
WHERE data_prestec BETWEEN '2018-01-01' AND '2018-12-31'
ON CONFLICT (email) DO NOTHING;Fase 3 — Doble escriptura. Es modifica l'aplicació perquè escrigui a les dues estructures alhora, la vella i la nova, dins de la mateixa transacció. Continua llegint de la vella. Aquest és el punt de no-retorn més suau possible: si alguna cosa va malament, es desactiva l'escriptura nova i no ha passat res.
Fase 4 — Canviar les lectures. Es van movent les consultes a l'esquema nou, una a una, començant per les menys crítiques. Una vista amb el nom de la taula vella que llegeixi de l'esquema nou permet moure moltes consultes sense tocar el codi:
-- L'aplicació continua fent SELECT ... FROM prestecs_full
-- però ara llegeix de l'esquema normalitzat.
CREATE VIEW prestecs_full AS
SELECT p.exemplar_cod, p.data_prestec, p.data_devolucio,
s.email AS soci_email, s.nom AS soci_nom, ...
FROM prestecs p JOIN socis s ON ...;Fase 5 — Contraure. Quan cap consulta no fa servir ja l'estructura vella, es retira la doble escriptura i s'esborra la taula antiga. Abans d'esborrar-la, se'n desa una còpia: sempre.
Cinc regles que no s'han de saltar:
- Tot s'assaja primer en una còpia de producció. Amb el volum real, no amb sis files.
- Les consultes de refutació s'executen abans de començar, per tenir la llista de dades brutes i decidir què fer amb cada cas. No es descobreix a mitja migració.
- Cada fase és reversible per si sola. Si la fase 4 surt malament, es torna a llegir de la vella.
- Les consultes de control de la secció 12 s'executen després de cada fase, no només al final.
- La neteja de dades es documenta. Cada fila fusionada, cada valor corregit, amb el seu criteri. Algú preguntarà d'aquí a dos anys per què la Marta Alsina té tres préstecs i no dos.
- Examen de l'esquema ampliat del mòdul 4
I arribem a la prova promesa en tancar el mòdul 4: sotmetre l'esquema de l'ampliació de BiblioRed a l'instrumental formal. Anem taula per taula amb les quatre que l'enunciat del mòdul assenyalava com les més compromeses.
15.1 esdeveniments
esdeveniment_id → {titol, descripcio, tipus_esdeveniment_id, sala_id, inici, fi,
places_ofertes, estat, publicat, duracio_min}
{inici, fi} → duracio_minClau candidata única: {esdeveniment_id}, d'un sol atribut. 2FN garantida.
Comprovació de 3FN sobre la segona dependència:
- És
{inici, fi}superclau? No: dos esdeveniments diferents poden començar i acabar alhora en sales diferents. - És
duracio_minprimer? No.
Formalment, esdeveniments viola la 3FN. duracio_min és una dependència transitiva: depèn d'inici i fi, que no són clau.
I tanmateix la deixem. La raó és al CREATE TABLE de 04-04:
És una columna generada, i la paraula clau és ALWAYS: PostgreSQL calcula el valor a cada INSERT i a cada UPDATE, i no permet escriure'l a mà. La contradicció que la 3FN existeix per evitar —que duracio_min digui una cosa i les dates en diguin una altra— és físicament impossible.
Criteri general: una columna generada pel SGBD és una desnormalització amb garantia. Viola la forma normal en la lletra, no en l'esperit, perquè el risc que la forma normal prevé està eliminat per un altre mecanisme. Anota-la com a tal a la documentació de l'esquema i tira endavant.
Veredicte: 3FN a efectes pràctics. Desnormalització documentada i garantida pel SGBD.
Hi ha un segon punt més discutible: la columna estat, que pot valer 'complet'. Aquest valor es pot deduir comparant places_ofertes amb la suma de places_ocupades de les inscripcions. És informació derivada d'una altra taula, i no la garanteix res. Aquí no hi ha cap dependència funcional que ho capti —la teoria de la normalització no parla entre taules—, però és redundància igualment, i del tipus perillós: res no impedeix que estat = 'complet' amb places lliures. És un cas de manual per al tractament de la lliçó 05-04.
15.2 inscripcions
{esdeveniment_id, soci_id} → {data_inscripcio, estat, acompanyants, places_ocupades}
acompanyants → places_ocupadesClau candidata: {esdeveniment_id, soci_id}, composta. Cal comprovar la 2FN amb cura.
{esdeveniment_id}⁺={esdeveniment_id}. Cap dependència no arrenca ambesdeveniment_idtot sol. Sense dependències parcials per aquí.{soci_id}⁺={soci_id}. Igual. Sense dependències parcials.
Està en 2FN. I és un bon resultat: significa que data_inscripcio, estat i acompanyants són genuïnament fets sobre la inscripció, no sobre l'esdeveniment ni sobre el soci. Si algú hi hagués ficat soci_nom o esdeveniment_titol —la temptació de sempre— hi hauria dependències parcials immediates.
Per a la 3FN, l'únic candidat és acompanyants → places_ocupades, i és exactament el mateix cas que duracio_min: una columna generada ALWAYS AS (1 + acompanyants) STORED. Mateixa conclusió.
Veredicte: FNBC a efectes pràctics (clau candidata única + 3FN ⟹ FNBC), amb una desnormalització garantida pel SGBD.
15.3 multes
Aquí hi ha la troballa de l'examen. Les dependències:
multa_id → {soci_id, prestec_id, motiu, import, data_emissio, estat}
prestec_id → soci_id ← ATENCIÓ!La segona surt d'una regla que no havíem escrit mai però que és evident: un préstec el va fer un soci concret. Si la multa 900 està associada al préstec 5001, i el préstec 5001 el va fer el soci 14, aleshores el soci de la multa 900 ha de ser el 14. No hi ha elecció.
Comprovació de 3FN:
- És
prestec_idsuperclau demultes? No: un préstec pot generar dues multes (una per retard i una altra per deteriorament; de fet la restriccióuq_multes_prestec_motiuho contempla explícitament). - És
soci_idprimer? L'única clau candidata és{multa_id}. No.
multes viola la 3FN. És una dependència transitiva multa_id → prestec_id → soci_id, i l'anomalia és real i greu:
-- Res no impedeix això: una multa associada al préstec 5001 (del soci 14)
-- però atribuïda al soci 16.
INSERT INTO multes (soci_id, prestec_id, motiu, import)
VALUES (16, 5001, 'retard', 3.50);El SGBD l'accepta. Les dues claus foranes es compleixen —el soci 16 existeix, el préstec 5001 existeix— però la dada és falsa: acabem de multar la Núria Bastos per un retard de la Marta Alsina. I en un sistema de multes, això no és un detall acadèmic: és una reclamació.
La consulta que ho detecta:
SELECT m.multa_id, m.soci_id AS soci_multa, p.soci_id AS soci_prestec
FROM multes m
JOIN prestecs p ON p.prestec_id = m.prestec_id
WHERE m.soci_id <> p.soci_id;Quina és la correcció? Hi ha tres opcions i totes tres són defensables segons el cas:
| Opció | Com | Avantatge | Inconvenient |
|---|---|---|---|
A. Eliminar soci_id |
Treure la columna; obtenir el soci per JOIN amb prestecs |
3FN pura, impossible contradir-se | No funciona: hi ha multes sense préstec (prestec_id és opcional, decisió D6 de 04-03) — pèrdua de carnet, deteriorament d'una sala |
| B. Restricció creuada | Mantenir soci_id i afegir un activador que el compari amb el del préstec |
Conserva tots dos casos i garanteix la coherència | Un activador per mantenir; cost a cada escriptura |
| C. Clau forana composta | Afegir UNIQUE (prestec_id, soci_id) a prestecs i una FK composta (prestec_id, soci_id) des de multes |
Ho garanteix el SGBD, sense codi | Requereix un UNIQUE redundant a prestecs |
L'opció C és la més elegant i la que cal conèixer, perquè és un truc de disseny que resol molts casos d'aquest tipus:
-- 1. Una clau alternativa "redundant" a prestecs que inclogui el soci
ALTER TABLE prestecs
ADD CONSTRAINT uq_prestecs_id_soci UNIQUE (prestec_id, soci_id);
-- 2. La clau forana de multes apunta a la parella, no només al préstec
ALTER TABLE multes
DROP CONSTRAINT fk_multes_prestec,
ADD CONSTRAINT fk_multes_prestec_soci
FOREIGN KEY (prestec_id, soci_id)
REFERENCES prestecs (prestec_id, soci_id)
ON DELETE RESTRICT ON UPDATE CASCADE;Ara l'INSERT fals d'abans és impossible:
ERROR: insert or update on table "multes" violates foreign key constraint
"fk_multes_prestec_soci"
DETAIL: Key (prestec_id, soci_id)=(5001, 16) is not present
in table "prestecs".I quan prestec_id és NULL —multa sense préstec— la clau forana no es comprova (comportament MATCH SIMPLE, el predeterminat a SQL), així que aquests casos continuen funcionant. El millor de les dues opcions.
Veredicte: multes no estava en 3FN. Corregida amb una clau forana composta, la dependència queda garantida pel SGBD. Aquest és el tipus de troballa que justifica tot el mòdul: és una fallada real, amb conseqüències reals, que el disseny per intuïció del mòdul 4 no va veure i que l'anàlisi formal troba en dos minuts.
Un segon punt sobre multes, més subtil. Si BiblioRed tingués una tarifa fixa per motiu —motiu → import— seria una altra violació de 3FN i la tarifa hauria d'estar en una taula tarifes_multa. Però l'import no es dedueix del motiu: depèn dels dies de retard, i sobretot ha de quedar congelat amb el valor que va tenir el dia de l'emissió, encara que l'ordenança canviï després. Desar-lo a multes és correcte i necessari. És un cas de duplicació històrica congelada, exactament igual que el que vam veure a 03-03 per al modelatge documental, i el seu tractament és matèria de 05-04.
15.4 pagaments
Clau candidata única: {pagament_id}. 2FN garantida.
Hi ha dependències transitives? Repassem les columnes: data_pagament, import, metode i referencia són tots fets sobre el pagament concret. metode → referencia no es compleix (diversos pagaments amb targeta tenen referències diferents). import → res. multa_id → res més dins d'aquesta taula.
Veredicte: pagaments està en FNBC. Sense observacions.
Tot i que convé assenyalar el parany en què no es va caure: si algú hagués afegit soci_id a pagaments "per no haver de fer dos JOIN", tindríem multa_id → soci_id i exactament el mateix problema que a multes. I si hi hagués afegit import_multa per poder comparar, tindríem multa_id → import_multa. Totes dues són temptacions habituals i totes dues són violacions de 3FN. Que no hi siguin és mèrit del disseny de 04-03.
Resum de l'examen
| Taula | Forma normal | Observacions |
|---|---|---|
esdeveniments |
3FN* | duracio_min és columna generada ALWAYS: desnormalització garantida. estat='complet' és informació derivada d'inscripcions sense garantia: revisar a 05-04 |
inscripcions |
FNBC* | places_ocupades és columna generada ALWAYS: desnormalització garantida |
multes |
Violava 3FN | prestec_id → soci_id. Corregit amb clau forana composta (prestec_id, soci_id) |
pagaments |
FNBC | Sense observacions |
(L'asterisc marca les taules l'única desviació formal de les quals és una columna generada pel SGBD.)
Conclusió de l'examen: l'esquema del mòdul 4 aguanta. D'onze taules noves, deu eren correctes i una tenia una fallada real que ara està corregida. És un bon resultat per a un disseny fet per comprensió del domini, i alhora la demostració que la verificació formal no sobra: aquella fallada hi era i ningú no l'havia vista.
Errors Habituals i Consells
Saltar-se el pas 1 i deduir les dependències de les dades. Ja ho vam advertir a 05-01 i aquí es paga el doble, perquè una dependència inventada produeix una descomposició que rebutjarà dades legítimes en producció. Les consultes de refutació serveixen per detectar contradiccions, no per descobrir regles.
Descompondre sense comprovar la condició de Heath. És la causa del contraexemple de la secció 9, i el seu símptoma és que el JOIN de reconstrucció retorna més files que l'original. Abans de cada CREATE TABLE, pregunta't quina serà la columna d'unió i si és clau primària d'alguna de les dues taules.
Oblidar el DISTINCT a la migració. INSERT INTO socis SELECT soci_email, soci_nom FROM prestecs_full fallarà amb violació de clau primària tan bon punt un soci tingui dos préstecs. El DISTINCT no és una optimització: és part del significat de la migració.
Interpretar un error de clau duplicada com una fallada de la migració. Gairebé sempre és el contrari: és l'esquema nou rebutjant una contradicció que el vell permetia. Abans de tocar l'INSERT, mira quines files xoquen; allà tens la teva llista de dades brutes.
Barrejar neteja de dades i normalització en el mateix pas. Són dues feines amb criteris diferents i cal separar-les: primer l'estructura, i quan l'estructura rebutgi alguna cosa, s'anota, es decideix el criteri de neteja amb el negoci i s'aplica. Corregir sobre la marxa produeix decisions improvisades que ningú no documenta.
Donar per acabada la migració sense les consultes de control. El recompte de la taula de fets, els recomptes de catàleg, la reconstrucció amb JOIN intern i la comparació de les consultes del negoci. Quatre consultes, quinze minuts, i són la diferència entre "crec que està bé" i "està bé".
Normalitzar en producció de cop. Mai. Expandir, omplir, doble escriptura, canviar lectures, contraure. Cada fase reversible, cada fase verificada.
No documentar per què una taula es queda com està. El cas de duracio_min és l'exemple perfecte: algú que auditi l'esquema d'aquí a dos anys hi veurà una violació de 3FN i l'"arreglarà". Escriu al costat que és una columna generada ALWAYS i que la decisió és conscient. La documentació de l'esquema de 04-01 és el lloc.
Exercicis
Exercici 1: Normalitzar fins a 3FN
BiblioRed rep d'una biblioteca veïna que s'integra a la xarxa aquesta taula plana:
donacions(donant_nif, donant_nom, donant_ciutat, ciutat_provincia, material_isbn, material_titol, data_donacio, estat_conservacio)
Regles de negoci confirmades:
- Un donant pot donar el mateix material en dates diferents.
- El NIF identifica el donant, amb el seu nom i la seva ciutat.
- Cada ciutat pertany a una província.
- L'ISBN identifica el material i el seu títol.
- L'estat de conservació s'anota en el moment de cada donació concreta.
Es demana:
- a) Escriure el conjunt
Fde dependències funcionals. - b) Determinar la clau candidata calculant la clausura.
- c) Identificar les violacions de 2FN i de 3FN.
- d) Escriure el
CREATE TABLEde les taules resultants en 3FN i l'INSERT ... SELECT DISTINCTque migraria les dades.
Exercici 2: Detectar una descomposició amb pèrdua
Un company proposa descompondre la taula participacions(esdeveniment_id, ponent_id, rol) de BiblioRed en dues:
R1(esdeveniment_id, ponent_id)R2(esdeveniment_id, rol)
amb l'argument que "així separem qui ve de què es fa".
Amb aquestes dades:
| esdeveniment_id | ponent_id | rol |
|---|---|---|
| 210 | 7 | moderador |
| 210 | 9 | tallerista |
| 211 | 7 | tallerista |
Es demana:
- a) Construir
R1iR2i fer el JOIN natural peresdeveniment_id. - b) Dir quantes files en surten i quines són falses.
- c) Explicar amb la condició de Heath per què falla.
- d) Dir quina informació s'ha destruït irreversiblement.
Exercici 3: Examinar una taula de l'esquema real
BiblioRed vol afegir a l'esquema una taula per a les reserves anticipades de sales per part d'entitats externes:
CREATE TABLE cessions_sala (
cessio_id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
sala_id INTEGER NOT NULL REFERENCES sales (sala_id),
sala_aforament SMALLINT NOT NULL,
sucursal_id INTEGER NOT NULL REFERENCES sucursals (sucursal_id),
entitat_nif VARCHAR(9) NOT NULL,
entitat_nom VARCHAR(120) NOT NULL,
inici TIMESTAMPTZ NOT NULL,
fi TIMESTAMPTZ NOT NULL,
tarifa_hora NUMERIC(6,2) NOT NULL,
import_total NUMERIC(8,2) NOT NULL
);Sabent que cada sala té un aforament i pertany a una sucursal, que el NIF identifica l'entitat, que la tarifa per hora depèn de la sucursal, i que l'import total és la tarifa per les hores de cessió:
- a) Escriure les dependències funcionals.
- b) Determinar en quina forma normal està i quines dependències la violen.
- c) Proposar la correcció, indicant quina columna convé no eliminar encara que sigui redundant, i per què.
Solucions
Solució 1
a) Dependències funcionals:
g1: {donant_nif, material_isbn, data_donacio} → estat_conservacio
g2: donant_nif → donant_nom
g3: donant_nif → donant_ciutat
g4: donant_ciutat → ciutat_provincia
g5: material_isbn → material_titolb) Clau candidata. Atributs només a l'esquerra: data_donacio. Atributs als dos costats: donant_nif, material_isbn, donant_ciutat. Només a la dreta: la resta.
El nucli obligatori inclou data_donacio. Provem {donant_nif, material_isbn, data_donacio}:
| Passada | Dependència | S'hi afegeix |
|---|---|---|
| 1 | g1 | estat_conservacio |
| 1 | g2 | donant_nom |
| 1 | g3 | donant_ciutat |
| 1 | g5 | material_titol |
| 2 | g4 (ara donant_ciutat hi és) |
ciutat_provincia |
Els vuit atributs. És superclau. I és mínima: sense data_donacio no es determina estat_conservacio (el mateix donant pot donar el mateix material dues vegades amb estats diferents); sense donant_nif no s'arriba a les dades del donant; sense material_isbn no s'arriba al títol.
Clau candidata: {donant_nif, material_isbn, data_donacio}. Primers: aquests tres. No primers: els altres cinc.
c) Violacions.
2FN — dependències parcials (parts de la clau que determinen atributs no primers):
g2ig3:donant_nifés un terç de la clau i determinadonant_nomidonant_ciutat. Parcial.g5:material_isbnés un terç de la clau i determinamaterial_titol. Parcial.
3FN — dependència transitiva:
g4:donant_ciutat → ciutat_provincia, ambdonant_ciutatno superclau iciutat_provinciano primer. Transitiva.
d) Esquema resultant:
CREATE TABLE provincies_ciutat (
ciutat VARCHAR(60) NOT NULL,
provincia VARCHAR(60) NOT NULL,
CONSTRAINT pk_provincies_ciutat PRIMARY KEY (ciutat)
);
CREATE TABLE donants (
nif VARCHAR(9) NOT NULL,
nom VARCHAR(120) NOT NULL,
ciutat VARCHAR(60) NOT NULL,
CONSTRAINT pk_donants PRIMARY KEY (nif),
CONSTRAINT fk_donants_ciutat FOREIGN KEY (ciutat)
REFERENCES provincies_ciutat (ciutat) ON UPDATE CASCADE
);
CREATE TABLE materials_donats (
isbn VARCHAR(13) NOT NULL,
titol VARCHAR(200) NOT NULL,
CONSTRAINT pk_materials_donats PRIMARY KEY (isbn)
);
CREATE TABLE donacions (
donant_nif VARCHAR(9) NOT NULL,
material_isbn VARCHAR(13) NOT NULL,
data_donacio DATE NOT NULL,
estat_conservacio VARCHAR(20) NOT NULL,
CONSTRAINT pk_donacions PRIMARY KEY (donant_nif, material_isbn, data_donacio),
CONSTRAINT fk_donacions_donant FOREIGN KEY (donant_nif)
REFERENCES donants (nif) ON UPDATE CASCADE,
CONSTRAINT fk_donacions_material FOREIGN KEY (material_isbn)
REFERENCES materials_donats (isbn) ON UPDATE CASCADE
);Migració, en ordre de dependència (primer les taules referenciades):
INSERT INTO provincies_ciutat (ciutat, provincia)
SELECT DISTINCT donant_ciutat, ciutat_provincia FROM donacions_full;
INSERT INTO donants (nif, nom, ciutat)
SELECT DISTINCT donant_nif, donant_nom, donant_ciutat FROM donacions_full;
INSERT INTO materials_donats (isbn, titol)
SELECT DISTINCT material_isbn, material_titol FROM donacions_full;
INSERT INTO donacions (donant_nif, material_isbn, data_donacio, estat_conservacio)
SELECT donant_nif, material_isbn, data_donacio, estat_conservacio
FROM donacions_full;Cal notar que només l'última no porta DISTINCT: és la taula de fets i ha de conservar exactament les mateixes files que l'original.
Solució 2
a) Les dues projeccions:
R1(esdeveniment_id, ponent_id)
| esdeveniment_id | ponent_id |
|---|---|
| 210 | 7 |
| 210 | 9 |
| 211 | 7 |
R2(esdeveniment_id, rol)
| esdeveniment_id | rol |
|---|---|
| 210 | moderador |
| 210 | tallerista |
| 211 | tallerista |
JOIN natural per esdeveniment_id:
| esdeveniment_id | ponent_id | rol | Existia? |
|---|---|---|---|
| 210 | 7 | moderador | Sí |
| 210 | 7 | tallerista | NO |
| 210 | 9 | moderador | NO |
| 210 | 9 | tallerista | Sí |
| 211 | 7 | tallerista | Sí |
b) Cinc files on n'hi havia tres. Dues són falses: diuen que el ponent 7 va ser tallerista a l'esdeveniment 210 i que el ponent 9 va ser moderador, quan va ser just a l'inrevés.
c) Falla la condició de Heath. L'atribut compartit és esdeveniment_id, i no és clau primària de cap de les dues taules: l'esdeveniment 210 apareix dues vegades a R1 i dues vegades a R2. En reunir, aquestes dues files per costat es combinen entre si i produeixen 2 × 2 = 4 files on n'hi havia 2. És el mateix mecanisme del contraexemple de la secció 9.
Perquè la descomposició fos sense pèrdua caldria una dependència esdeveniment_id → ponent_id o esdeveniment_id → rol, i cap de les dues no es compleix: un esdeveniment té diversos ponents i diversos rols.
d) S'ha destruït l'associació entre ponent i rol. Aquest és el fet que la taula existia per desar: no "a l'esdeveniment 210 hi van participar el 7 i el 9" ni "a l'esdeveniment 210 hi va haver un moderador i un tallerista", sinó "el 7 va ser el moderador i el 9 el tallerista". Aquesta informació no és a cap de les dues projeccions i no hi ha manera de recuperar-la.
És també, de passada, la resposta a l'exercici 3b de la lliçó 05-02: participacions és una relació ternària legítima i no una violació de 4FN, precisament perquè rol depèn de la parella esdeveniment-ponent i no de l'esdeveniment tot sol.
Solució 3
a) Dependències funcionals:
c1: cessio_id → {sala_id, entitat_nif, inici, fi}
c2: sala_id → {sala_aforament, sucursal_id}
c3: entitat_nif → entitat_nom
c4: sucursal_id → tarifa_hora
c5: {tarifa_hora, inici, fi} → import_totalb) La clau candidata és {cessio_id}, d'un sol atribut, així que la 2FN està garantida. Les violacions són totes de 3FN, i hi ha quatre dependències transitives encadenades:
| Dependència | Determinant superclau? | Determinat primer? | Veredicte |
|---|---|---|---|
sala_id → sala_aforament |
No | No | Viola 3FN |
sala_id → sucursal_id |
No | No | Viola 3FN |
sucursal_id → tarifa_hora |
No | No | Viola 3FN |
entitat_nif → entitat_nom |
No | No | Viola 3FN |
La taula està en 2FN i no arriba a 3FN. La cadena completa és cessio_id → sala_id → sucursal_id → tarifa_hora, tres salts.
Les anomalies són les esperables: si es reforma una sala i canvia el seu aforament, cal actualitzar totes les seves cessions històriques; si l'ajuntament apuja la tarifa d'una sucursal, cal tocar totes les cessions de totes les seves sales; i si una entitat canvia de nom, cal buscar-la per tot arreu.
c) Correcció. S'eliminen les columnes la informació de les quals ja viu en una altra taula i s'hi arriba per clau forana:
CREATE TABLE entitats (
nif VARCHAR(9) NOT NULL,
nom VARCHAR(120) NOT NULL,
CONSTRAINT pk_entitats PRIMARY KEY (nif)
);
-- tarifa_hora s'afegeix a sucursals, que és de qui depèn
ALTER TABLE sucursals ADD COLUMN tarifa_cessio_hora NUMERIC(6,2) NOT NULL DEFAULT 0;
CREATE TABLE cessions_sala (
cessio_id INTEGER GENERATED BY DEFAULT AS IDENTITY,
sala_id INTEGER NOT NULL,
entitat_nif VARCHAR(9) NOT NULL,
inici TIMESTAMPTZ NOT NULL,
fi TIMESTAMPTZ NOT NULL,
tarifa_hora NUMERIC(6,2) NOT NULL, -- ← ES QUEDA. Vegeu justificació
import_total NUMERIC(8,2) NOT NULL, -- ← ES QUEDA. Vegeu justificació
CONSTRAINT pk_cessions_sala PRIMARY KEY (cessio_id),
CONSTRAINT chk_cessions_fi CHECK (fi > inici),
CONSTRAINT fk_cessions_sala FOREIGN KEY (sala_id)
REFERENCES sales (sala_id) ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT fk_cessions_entitat FOREIGN KEY (entitat_nif)
REFERENCES entitats (nif) ON DELETE RESTRICT ON UPDATE CASCADE
);Desapareixen sala_aforament, sucursal_id i entitat_nom: s'obtenen amb un JOIN a sales i entitats, i així no poden contradir-se.
Es queden tarifa_hora i import_total, i aquesta és la part interessant de l'exercici. Formalment són redundants: la tarifa és a sucursals i l'import es calcula. Però són dades històriques que han de quedar congelades: la cessió del 12 de març es va facturar a 18,00 €/hora, i si l'ajuntament apuja la tarifa a 22,00 €/hora a l'abril, aquella factura de març no pot canviar. Si tarifa_hora s'obtingués per JOIN a sucursals, qualsevol consulta sobre cessions passades retornaria imports que no coincideixen amb les factures emeses.
És exactament el mateix cas que l'import de multes que vam analitzar a la secció 15.3, i la mateixa "duplicació històrica congelada" que vam veure al modelatge documental de 03-03. És una desnormalització deliberada, correcta i necessària, i cal documentar-la com a tal perquè ningú no l'"arregli" després. Com es prenen i es justifiquen aquestes decisions és el contingut íntegre de la lliçó següent.
(Detall final: import_total no pot ser una columna generada ALWAYS AS, encara que ho sembli, precisament perquè ha de conservar el valor històric i no recalcular-se si la tarifa canvia. És la diferència entre duracio_min —derivada de columnes de la mateixa fila, que no canvien— i un import derivat d'una dada externa que sí que canvia.)
Conclusió
Hem fet la feina completa. D'un full de càlcul amb tretze columnes, sis files de mostra i vuit defectes catalogats des de la primera lliçó del curs, a nou taules en forma normal de Boyce-Codd, amb les seves claus foranes, les seves restriccions i les seves dades migrades i verificades.
El procediment són sis passos: reunir les regles de negoci i escriure les dependències; determinar les claus candidates amb la clausura; comprovar 1FN, 2FN i 3FN/FNBC en aquest ordre i aturant-se a la primera fallada; descompondre traient cada dependència infractora a una taula el determinant de la qual sigui la clau primària; verificar; i reposar les claus foranes. Tornar al pas 2 després de cada descomposició, perquè les taules noves tenen claus noves.
Les dues propietats que tota descomposició ha de complir són innegociables. Sense pèrdua d'informació: el JOIN ha de retornar exactament l'original, i la condició de Heath ho garanteix si descompons sempre per un determinant —si la columna d'unió és clau primària d'almenys una de les dues taules—. Vam veure amb dades què passa quan no es compleix: una descomposició que semblava raonable va inventar una fila afirmant que la Núria Bastos va treure un llibre d'una sucursal on no va estar mai, i aquella fila era indistingible de les verdaderes. Conservació de les dependències: cada regla de negoci ha de poder comprovar-se dins d'una sola taula, i quan això no és possible cal decidir conscientment entre retrocedir a 3FN, posar un activador o auditar periòdicament.
Vam aprendre també algunes coses que no surten als llibres de teoria. Que INSERT ... SELECT DISTINCT és la forma canònica de migrar en descompondre. Que un error de clau duplicada durant la migració gairebé mai no és una fallada de l'script: és l'esquema nou rebutjant una contradicció que el vell permetia, i allà tens la teva llista de dades brutes. Que la normalització no neteja les dades, les fa visibles: Marta Alsina i M. Alsina estaven amagades entre sis files amples i van aparèixer tan bon punt la clau primària de socis es va negar a admetre-les. I que en producció no es normalitza de cop: s'expandeix, s'omple, s'escriu per duplicat, es canvien les lectures i es contrau, amb les consultes de control executades després de cada fase.
I vam sotmetre a examen l'esquema del mòdul 4. Va aguantar, que era el que calia comprovar: esdeveniments i inscripcions en 3FN i FNBC respectivament, amb l'única excepció de les seves columnes generades ALWAYS, que són desnormalitzacions amb garantia del SGBD; pagaments en FNBC sense observacions. I una troballa real: multes violava la 3FN per la dependència prestec_id → soci_id, cosa que permetia atribuir a un soci la multa del retard d'un altre. La correcció —una clau forana composta (prestec_id, soci_id) recolzada en un UNIQUE redundant a prestecs— deixa la regla garantida pel SGBD sense necessitat d'activadors. Una fallada que el disseny per intuïció no va veure i que l'anàlisi formal va trobar en dos minuts: això és exactament per al que serveix aquest mòdul.
I tanmateix, tres vegades al llarg de la lliçó ens hem trobat amb el mateix, i les tres hem decidit no normalitzar: duracio_min a esdeveniments, places_ocupades a inscripcions, l'import congelat de multes i la tarifa_hora de les cessions. Totes quatre són redundàncies. Totes quatre violen la lletra de la tercera forma normal. I totes quatre són correctes.
Això no és una contradicció ni una excepció incòmoda: és l'altra meitat de l'ofici. A la lliçó 05-04, Desnormalització i els seus Usos, s'estudia la decisió inversa amb el mateix rigor amb què hem estudiat aquesta. Què es guanya i què es paga exactament en desnormalitzar; quan està justificat i quan és simple mandra; les tècniques una a una —columnes calculades, taules de resum, vistes materialitzades, l'esquema en estrella dels magatzems analítics—; com es manté la coherència del que s'ha duplicat expressament; i la regla d'or que ordena tot el mòdul: primer normalitza, després desnormalitza expressament, mesurant, i mai a l'inrevés.
Fonaments de Bases de Dades
Mòdul 1: Introducció a les Bases de Dades
- Conceptes Bàsics de Bases de Dades
- Tipus de Bases de Dades
- Història i Evolució de les Bases de Dades
- Sistemes Gestors de Bases de Dades i Arquitectura
Mòdul 2: Bases de Dades Relacionals
- Model Relacional
- Llenguatge SQL
- Operacions Bàsiques en SQL
- Consultes Multitaula: JOIN i Subconsultes
- Agregació i Agrupació de Dades
- Integritat Referencial
Mòdul 3: Bases de Dades No Relacionals
- Introducció a NoSQL
- Tipus de Bases de Dades NoSQL
- Modelatge de Dades en NoSQL
- Comparació entre Bases de Dades Relacionals i No Relacionals
Mòdul 4: Disseny d'Esquemes
- Principis de Disseny d'Esquemes
- Diagrames Entitat-Relació (ER)
- Transformació de Diagrames ER a Esquemes Relacionals
- Tipus de Dades i Restriccions
Mòdul 5: Normalització
Mòdul 6: Transaccions, Rendiment i Seguretat
- Transaccions i Propietats ACID
- Concurrència i Nivells d'Aïllament
- Índexs i Optimització de Consultes
- Seguretat, Permisos i Còpies de Seguretat
Mòdul 7: Exercicis Pràctics
- Exercicis de SQL
- Exercicis de Disseny d'Esquemes
- Exercicis de Normalització
- Exercicis de Consultes Avançades i Transaccions
Mòdul 8: Casos d'Estudi
- Cas d'Estudi: Base de Dades Relacional
- Cas d'Estudi: Base de Dades No Relacional
- Cas d'Estudi: Persistència Poliglota
