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

  1. El procediment en sis passos
  2. Pas 1: reunir les regles de negoci i escriure les dependències
  3. Pas 2: determinar les claus candidates
  4. Pas 3a: comprovar i assolir la 1FN
  5. Pas 3b i 4a: comprovar i assolir la 2FN
  6. Pas 3c i 4b: comprovar i assolir la 3FN
  7. El resultat final i el retrobament amb l'esquema del mòdul 2
  8. Descomposició sense pèrdua d'informació i la condició de Heath
  9. El contraexemple: una descomposició que inventa files
  10. Conservació de les dependències
  11. La cobertura mínima i l'algorisme de síntesi 3FN
  12. Pas 5: verificació amb consultes de control
  13. Pas 6: reposar les claus foranes
  14. Normalitzar a la vida real: disseny nou enfront de producció
  15. Examen de l'esquema ampliat del mòdul 4

  1. 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

  1. 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?", 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?", 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_nom es 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.

  1. 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:

nucli = {exemplar_cod, data_prestec, soci_telefons, autors}

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.

  1. 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_nac a p1_autors_obra hem 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.

  1. 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.

  1. 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_email superclau? {soci_email}⁺ = {soci_email, soci_nom}. No conté la clau. No.
  • És soci_nom primer? Els primers són exemplar_cod i data_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 autor superclau? 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 FolletKen 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.

  1. 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é llibres com a vista sobre materials + materials_llibre, resultat de la jerarquia de generalització de 04-02 i 04-03. La nostra p3_obres és la versió sense jerarquia.
  • L'esquema real desa dir_ciutat a sucursals en comptes de tenir codis_postals. És una desnormalització deliberada sobre una taula de quatre files, i la discutirem a 05-04.

  1. 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ó R en R1 i R2 és sense pèrdua d'informació (o de reunió no additiva) si, en fer el JOIN natural de R1 i R2, s'obté exactament R: 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 R una relació amb atributs dividits en tres grups X, Y i Z. Si es compleix la dependència funcional X → Y, aleshores descompondre R en R1(X, Y) i R2(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;
 problema | exemplar_cod | ...
----------+--------------+-----
(0 rows)

Zero files en totes dues direccions: reconstrucció exacta.

  1. 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:

SELECT r1.soci_email, r1.titol, r2.sucursal_nom
FROM r1 JOIN r2 ON r2.soci_email = r1.soci_email;

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
[email protected] Els pilars de la Terra Sud
[email protected] Els pilars de la Terra Nord
[email protected] Manual d'horticultura urbana Sud
[email protected] Manual d'horticultura urbana Nord NO
[email protected] Els pilars de la Terra Nord

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.

  1. 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 F es 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.

  1. 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 conjunt Fc que determina exactament el mateix que F —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:

  1. Desagregar els costats drets, fent servir la regla de descomposició de 05-01.
  2. Treure atributs superflus de l'esquerra: per a cada {A, B} → C, comprovar si A → C ja es dedueix de la resta; si sí, B hi sobrava.
  3. Treure dependències redundants: per a cada X → Y, treure-la del conjunt i comprovar amb la clausura si Y ⊆ 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.

  1. 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:

SELECT (SELECT COUNT(*) FROM prestecs_full) AS origen,
       (SELECT COUNT(*) FROM p3_prestecs)   AS desti;
 origen | desti
--------+-------
      6 |     6

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;
 count
-------
     6

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.

  1. 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ó.

  1. 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.

CREATE TABLE socis (...);
CREATE TABLE exemplars (...);
-- La taula vella continua intacta i en ús.

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:

  1. Tot s'assaja primer en una còpia de producció. Amb el volum real, no amb sis files.
  2. 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ó.
  3. Cada fase és reversible per si sola. Si la fase 4 surt malament, es torna a llegir de la vella.
  4. Les consultes de control de la secció 12 s'executen després de cada fase, no només al final.
  5. 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.

  1. 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_min

Clau 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_min primer? 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:

duracio_min INTEGER GENERATED ALWAYS AS
                (EXTRACT(EPOCH FROM (fi - inici)) / 60) STORED

É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_ocupades

Clau candidata: {esdeveniment_id, soci_id}, composta. Cal comprovar la 2FN amb cura.

  • {esdeveniment_id}⁺ = {esdeveniment_id}. Cap dependència no arrenca amb esdeveniment_id tot 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_id superclau de multes? No: un préstec pot generar dues multes (una per retard i una altra per deteriorament; de fet la restricció uq_multes_prestec_motiu ho contempla explícitament).
  • És soci_id primer? 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);
INSERT 0 1

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

pagament_id → {multa_id, data_pagament, import, metode, referencia}

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 F de dependències funcionals.
  • b) Determinar la clau candidata calculant la clausura.
  • c) Identificar les violacions de 2FN i de 3FN.
  • d) Escriure el CREATE TABLE de les taules resultants en 3FN i l'INSERT ... SELECT DISTINCT que 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 R1 i R2 i fer el JOIN natural per esdeveniment_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_titol

b) 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):

  • g2 i g3: donant_nif és un terç de la clau i determina donant_nom i donant_ciutat. Parcial.
  • g5: material_isbn és un terç de la clau i determina material_titol. Parcial.

3FN — dependència transitiva:

  • g4: donant_ciutat → ciutat_provincia, amb donant_ciutat no superclau i ciutat_provincia no 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
210 7 tallerista NO
210 9 moderador NO
210 9 tallerista
211 7 tallerista

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_total

b) 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.

© Copyright 2026. Tots els drets reservats