Teoria e pratica per la normalizzazione delle basi di dati (Parte seconda)
a cura di Marco Tonti (requisiti: conoscenza dei DB relazionali)
Terza forma normale (3NF)
Riprendiamo (dalla prima parte) la tabella di esempio fatta per la 2NF:
cod_articolo magazzino quantità indirizzo 1 BO 1500 Via Vieni 15 2 BO 2300 Via Vieni 15 1 MI 850 Via Vai 47 2 MI 1700 Via Vai 47 Esiste una maniera per rimaneggiare questa tabella in modo da renderla 2NF senza in sostanza cambiarla. (ATTENZIONE: Questo è un esperimento pericoloso e potrebbe provocare dei danni. Non provateci a casa vostra). ;)
Pensiamo alla definizione di 2NF: ogni attributo non primo deve dipendere interamente dalla chiave. Questa dipendenza viene stabilita con l'uso delle dipendenze funzionali. In questo caso:
Articolo, Magazzino -> Quantità, Indirizzo
Magazzino -> IndirizzoIl fatto che alla sinistra delle frecce ci sia il conflitto dell'attributo Magazzino impedisce che quello schema sia in 2NF. Proviamo a pasticciare la tabella:
cod_articolo magazzino quantità magico indirizzo 1 BO 1500 BO1 Via Vieni 15 2 BO 2300 BO2 Via Vieni 15 1 MI 850 MI1 Via Vai 47 2 MI 1700 MI2 Via Vai 47 L'attributo magico che abbiamo inserito non è altro che l'unione dei due attributi della chiave, dunque dipende dalla chiave ma non vi appartiene!
Vediamo se questo artificio ci aiuta a passare il controllo della 2NF, rimaneggiando abilmente le dipendenze funzionali:Articolo, Magazzino -> Quantità, Magico, Indirizzo
Magico -> IndirizzoEt voilà! Il gioco è fatto. Questa tabella è diventata coerente con la 2NF. Infatti ogni attributo non primo dipende interamente dalla chiave. Certo, esiste un'altra dipendenza funzionale che non si capisce bene come interpretare, ma l'importante è la forma.
O no?
Come i piú acuti di voi avranno intuito, questa non è una buona soluzione per fare in modo che una tabella sia in 2NF. Infatti la tabella presenta esattamente gli stessi problemi, se non addirittura qualcuno in piú, che avevamo inteso eliminare con la definizione della 2NF. (Devo sottolineare che questo è un artificio, un trucchetto di esempio. In realtà questa struttura non avrebbe passato neppure il test sulla 1NF, per via dell'eterogeneità dell'attributo magico. Per aggirare quel problema avrei dovuto complicare di molto l'esempio, e non mi è sembrato il caso).
Mi sentirei quasi un cretino a ripetere gli stessi identici ragionamenti già fatti per i problemi di questo schema legati alla seconda forma normale. La cosa che dovrebbe piú interessare è che abbiamo una tabella che, pur essendo in 2NF, presenta gli stessi problemi di una che non lo è.
Cerchiamo di capire cosa ha permesso che gli ostacoli buttati fuori dalla porta rientrassero dalla finestra. Ed è questo il problema che dobbiamo isolare ed eliminare adesso, non piú quindi "perché ci sono problemi", ma "perché continuano ad essercene?". Tecnicamente potremmo definirlo un meta-problema.
Non mi si venga a dire che il motivo del nostro fallimento è "morale", perché abbiamo cercato alchemicamente di aggirare il problema. Non lo si può dire perché ci sono tantissime situazioni nelle quali una tabella, pur non essendo stata rimaneggiata, può soffrire degli stessi problemi di questa, e ne vedremo una piú avanti.
Intuitivamente il problema è chiaro e può essere visto un po' come quello che esprimevo alla fine del paragrafo sull'uso della 2NF: esiste un'altra chiave in questa tabella. Una chiave nascosta, infida, strisciante, vigliacca. Ma c'è. Nei fatti, l'attributo indirizzo ha una sua "chiave personale", che è l'attributo magico. E come abbiamo già visto l'esistenza contemporanea di due chiavi nella stessa tabella è deprecabile.
Forse ora siamo in grado di capire meglio cosa c'è che non va nel fatto di avere piú di una chiave, cioè una chiave "ufficiale" e una "secondaria". Il fatto è che il valore della chiave secondaria varia al variare di quella ufficiale, e fin qui non c'è niente di male, ma questa chiave umbratile a sua volta definisce il valore di un altro attributo, e questo comporta una ripetizione strutturale.
Ripetiamo la definizione di dipendenza funzionale, perché ci aiuta a capire meglio cosa sono le chiavi, essendone un'astrazione. L'attributo A determina funzionalmente l'attributo B se e solo se quando il valore di A rimane costante, rimane costante anche il valore di B.
E ora facciamo qualche capriola mentale. Diciamo di avere una tabella composta da K, la chiave, che determina gli attributi A, B, K2 (la chiave umbratile), Z (l'attributo determinato da K2). Seguite il mio ragionamento:
- Se K varia il suo valore, A, B, K2, Z potrebbero cambiare il loro valore.
- Se K2 non varia il suo valore, come lecito, Z non varia il proprio valore.
Un attributo non primo non ha la necessità di variare al variare della chiave, non rientra infatti in quelle che abbiamo chiamato "ripetizioni strutturali". Ma se quest'attributo determina funzionalmente un altro attributo, be';, questo secondo attributo è tenuto a non cambiare per via del vincolo imposto. E questo sí che si configura come ripetizione strutturale. Vediamone un esempio piú concreto:
Impiegato Nome Reparto Caporeparto 1 Rossi Vendite Marchi 2 Verdi Acquisti Giorgi 3 Bianchi Magazzino Stefanini 4 Neri Vendite Marchi Osserviamo le dipendenze funzionali:
Impiegato -> Nome, Reparto, Caporeparto
Reparto -> CaporepartoE vediamo come questo vincolo tra due attributi non primi determina il problema:
- Impiegato varia
- Ma reparto non è tenuto a variare in base alla variazione di impiegato, infatti gli impiegati 1 e 4 lavorano entrambi nel reparto vendite, com'è lecito.
- Ma la non-variazione di reparto in quei due casi, implica la non-variazione del caporeparto, essendo vincolati, e provoca una ripetizione strutturale.
È proprio questa ripetizione strutturale che ci crea dei problemi. Possiamo intravvedere una cosa che potremmo chiamare una "dipendenza transitiva", infatti la chiave determina tra l'altro il reparto, e il reparto a sua volta determina il caporeparto, dunque la chiave determina caporeparto attraverso due diverse strade.
Guarda caso, questa è proprio la definizione della Terza Forma Normale: Uno schema T è in 3NF se e solo se ogni attributo non primo non dipende transitivamente dalla chiave.
Come risolvere questa difficoltà? Dando alla chiave che abbiamo chiamato umbratile, in questo caso reparto, lo status di chiave vera e propria creando una tabella tutta sua, nella quale possa espletare liberamente il suo bisogno di definire l'attributo caporeparto. Vale a dire:
A
Impiegato Nome Reparto 1 Rossi Vendite 2 Verdi Acquisti 3 Bianchi Magazzino 4 Neri Vendite B
Reparto Caporeparto Vendite Marchi Acquisti Giorgi Magazzino Stefanini Incidentalmente, notate che in questo modo si chiarisce bene anche la necessità di definire il concetto di foreign key, cioè di chiave esterna. Reparto nella sua tabella B è una chiave vera e propria (primary key o chiave primaria), ma quando viene usata nella tabella A è un attributo esattamente come tutti gli altri. Un attributo un po' speciale, perché fa da chiave in un'altra tabella. E questa è esattamente la definizione di chiave esterna. Su questi concetti si costruiscono le definizioni e i vincoli di integrità referenziale, che però esulano dall'oggetto dell'articolo.
Con la 3NF si incomincia a intravvedere un significato profondo dell'analisi formale. Quante volte abbiamo avuto la tentazione di costruire tabelle fatte come quella che abbiamo disaggregato? Probabilmente qualche volta sí. La formalizzazione ci fa capire in primo luogo che non va fatto, e poi ci fa anche scoprire il perché. Be', forse queste forme normali incominciano a starvi un po' piú simpatiche, no?
Forma normale di Boyce-Codd (BCNF)
(Quando il gioco si fa duro, i duri cominciano a giocare). Questa forma normale agisce sul punto cardine delle nostre strutture: le chiavi. Come vedremo è un passaggio molto delicato e va considerato con grande attenzione. Per la nostra serenità sarebbe quasi sconsigliabile normalizzare fino a questo punto, ma sono concetti che vanno trattati per necessità di completezza.Mettiamo il caso di dover mantenere le informazioni degli abbonati al telefono. Visto che siamo ormai pratici stabiliamo la struttura che vogliamo dare alla tabella e poi le dipendenze funzionali tra le varie entità.
Prefisso Numero Località Abbonato 051 123456 Bologna Rossi 059 123456 Modena Bianchi 051 134652 Bologna Verdi 051 316452 Castenaso Neri 059 625143 Vignola Grigi Prefisso, Numero -> Località, Abbonato
E fin qui tutto sembra a posto, quasi semplice perfino. Ma la nostra conoscenza ci suggerisce che esiste un altro vincolo che possiamo applicare, vale a dire che la località di un abbonato ne stabilisce il prefisso:
Località -> Prefisso
Anche aggiungendo questo vincolo la struttura proposta è in 3NF, infatti non ci sono attributi non primi che dipendono transitivamente dalla chiave. Però notiamo che c'è invece un attributo primo che ne dipende, e cioè precisamente il prefisso. In un certo senso c'è una dipendenza transitiva tra la chiave e una parte della chiave stessa. L'obiettivo della BCNF è eliminare queste dipendenze: Uno schema T è in BCNF se e solo se ogni attributo (primo o non primo) non dipende transitivamente dalla chiave. Bisogna stare attenti: questo vale solamente per le dipendenze transitive, perché è ovvio che una parte della chiave è determinata funzionalmente dalla chiave stessa: A, B, C -> A, B, C... si chiamano dipendenze banali, e bisogna evitare di dare loro uno status di dipendenza effettiva.
Con l'aggiunta del secondo vincolo abbiamo scoperto che esiste una ripetizione strutturale sul campo prefisso. Infatti che bisogno c'è di indicare esplicitamente il prefisso quando già dalla località possiamo risalirvi? Inoltre la coppia Località e Numero è un'ottima candidata al ruolo di chiave, visto che Località addirittura varia piú spesso di prefisso. Quindi se è chiave la coppia (Prefisso, Numero) deve esserlo a maggior ragione anche la coppia (Località, Numero). Andiamo allora a operare queste trasformazioni sulla tabella:
Località Numero Abbonato Bologna 123456 Rossi Modena 123456 Bianchi Bologna 134652 Verdi Castenaso 316452 Neri Vignola 625143 Grigi
Località Prefisso Bologna 051 Modena 059 Castenaso 051 Vignola 059 Sembra perfetto vero? Tutto bello ordinato, ben diviso, la completa assenza di ripetizioni strutturali... Ma c'è la fregatura, anzi, le fregature trasudano.
In primo luogo, la ricerca di un abbonato diventa piú difficoltosa. Per sapere che il numero 051-123456 corrisponde al signor Rossi, prima dobbiamo cercare nella tabella dei prefissi a quale località corrisponde quel prefisso, e poi usare l'informazione della località per determinare l'abbonato. Certo, esistono i join, ma non è la stessa cosa. E si tenga conto che questo è un esempio estremamente semplice.
Ma esiste anche un problema piú sotterraneo, e molto piú grosso. Per esempio, cosa succederebbe nella prima tabella del paragrafo se volessimo inserire un record di questo tipo: Prefisso 051, Numero 123456, Località Castenaso, Abbonato Rossini? Il motore DB ci restituirebbe un errore dicendo "Ripetizione dei valori della chiave primaria" e noi ci accorgeremmo dell'errore. Perché si sarebbe trattato di un errore logico oltre che materiale (non possono esistere due abbonati con lo stesso numero).
Ma se avessimo fatto l'operazione corrispondente nello schema decomposto in BCNF? Avremmo aggiunto da una parte ("Castenaso", 123456, Rossini) e niente dall'altra perché la località era già presente. Nessun errore! Non ci saremmo accorti dell'esistenza di problemi, se non scrivendo del codice che controllasse prima di tutto la correttezza dell'inserimento. Provate a pensare di dover fare una cosa del genere con qualche decina di tabelle...
Questo problema ha un nome preciso: legalità globale. Piú precisamente osservate che ricomponendo singolarmente (localmente) ogni informazione, questa la vediamo "completa", legale, coerente, che non dà adito a dubbi. Scopriamo i problemi solo quando prendiamo in esame la ricomposizione globale delle informazioni. In questo caso la legalità formale viene a cadere, ma solo molto dopo che era scomparsa la legalità materiale.
Per riassumere:
- Non sempre conviene compromettere una struttura piú comprensibile (nel nostro caso, la chiave (Prefisso, Numero)) e piú "logica" in nome della piú elevata normalizzazione che la BCNF ci offre.
- Tale normalizzazione inoltre, rompendo i legami delle chiavi, rischia di rendere difficoltoso il riconoscimento delle situazioni di errore.
Bisogna però notare che anche non normalizzando a BCNF questo schema i problemi persistono: infatti nella tabella inziale avremmo potuto aggiungere anche un record tipo (051, 134679, "Caltanissetta", "Ferrari") che avrebbe compromesso la coerenza fra prefisso e località, ma questo genere di errori è meno grave, perché agisce su attributi non primi. In altre parole non ci impedisce di distinguere fra due differenti istanze dello schema, che tende a rimanere integro malgrado questa eccezione.
A questo punto si comincia a percepire fastidiosamente che tutto questo lavoro di "compattazione" delle informazioni diventa pesante e quasi inutile... della serie "Vabbe', ripeto una parte della chiave primaria... ma quella chiave primaria aveva un senso fatta cosí, veniva 'naturale' usarla cosí." Infatti come dicevo prima di solito ci si ferma alla 3NF perché è ben piú che accettabile, e solo per alcuni casi si osa scomporre ulteriormente. Direi che vale per quei casi in cui, a differenza dell'esempio, la chiave non ha un significato concettuale preciso, e può essere ulteriormente suddivisa senza introdurre complicazioni inutili.
Considerazioni e conclusioni
Abbiamo visto, usando la notazione delle dipendenze funzionali, diversi tipi di normalizzazione:
- 2NF (Dipendenza di un attributo non primo da una parte della chiave)
- A, B, C -> D, E, F
- C -> D
- 3NF (Dipendenza transitiva di un attributo non primo dalla chiave)
- A, B, C -> D, E, F
- D -> E
- (Che produce, per via della transitività delle dipendenze, A, B, C -> D -> E)
- BCNF (Dipendenza transitiva di un attributo, primo o non primo, dalla chiave)
- A, B, C -> D, E, F
- D -> C
- (Come prima, questo insieme di vincoli produce A, B, C -> D -> C)
Riassumendo potremmo dire che quegli schemi che creano problemi sono quelli che posseggono piú di una dipendenza funzionale che fa riferimento agli stessi attributi. Ma c'è dell'altro: esistono dipendenze funzionali esplicite e implicite. Tecnicamente, queste considerazioni non devono valere solo per le dipendenze esplicite (chiamiamo F questo insieme), ma anche per tutte quelle dipendenze implicite generabili (si chiama chiusura dell'insieme dei vincoli, e si scrive F+). Alcuni esempi di dipendenze implicite li abbiamo visti trattando le dipendenze transitive.
Esistono degli algoritmi che a partire da un insieme di vincoli producono schemi in 3NF, ma è quasi sempre piú facile ragionare su tabelle che su vincoli formali, per non parlare della complessità degli algoritmi stessi.
Ragionare a oggetti aiuta. Aiuta moltissimo. Se infatti consideriamo le tabelle come le classi che definiscono gli oggetti, e i singoli record come le istanze di quelle classi, otteniamo, in modo quasi automatico e con pochi aggiustamenti, uno schema in 3NF. Esistono ovviamente alcune differenze tra i due approcci. Per esempio, nella struttura a oggetti abbiamo le Collection, che tradurre pari pari in un DB è poco furbo perché dovremmo anche crearci una tabella che rappresenti la classe Collection, una per ogni tipo di dato contenuto. O anche il fatto che nel paradigma relazionale dei dati le relazioni n-n (molti a molti) sono molto diffuse, cosa che invece va contro la strutturazione a oggetti che è prevalentemente gerarchica. Ma a parte queste difficoltà, che tutto sommato si risolvono facilmente e meccanicamente, concepire una struttura pensando agli oggetti è un grosso vantaggio. Creare le tabelle modellandole pari pari sugli oggetti potrebbe generare una struttura dati poco efficiente, ma che sarebbe sicuramente corretta. Solo dopo una certa esperienza si può avere la pretesa di saper "pensare relazionale", fin dal principio.
Tutte queste considerazioni devono poi mettere in guardia dalla tendenza che si ha sempre di semplificare, rendere piú efficiente, uniformare una base di dati, fare meno tabelle possibile. Bisogna stare attenti perché alcune di queste iniziative possono portare, magari nascostamente, a basi di dati piú facilmente corruttibili, meno scalabili, e forse in qualche caso anche meno efficienti di quanto si potrebbe pensare. Non so come la vedete voi, ma se devo scegliere fra maggiore sicurezza e maggiore velocità, mi butto sulla sicurezza. Anche perché, banalmente, velocità ed efficienza non sono la stessa cosa!
È giusto cercare di normalizzare nel modo migliore i propri dati, ma non sempre "nel modo migliore" corrisponde a "il piú possibile". Bisogna fare molta attenzione a non forzare quella che in qualche modo potremmo definire la "naturale strutturazione delle informazioni". Una normalizzazione di livello 3 è accettabile e diffusamente accettata, perché rimuove gli errori piú marchiani senza complicare eccessivamente la struttura sottostante.
Inoltre per alcuni attributi (quelli che abbiamo chiamato passivi) abbiamo visto che si può trascurare perfino il primo livello di normalizzazione, registrandovi anche informazioni composte o strutturate. Ma occorre ribadire che va fatto con estremo criterio e con un occhio puntato non solo alle esigenze, ma anche ai possibili usi sia presenti che futuri dei dati che andiamo a modellare.
Questi principi di normalizzazione, come dice il nome, sono normativi, cioè esprimono la norma per agire, per creare strutture funzionanti, non dicono come trasformare una cattiva strutturazione in una buona, anche perché pure il solo stabilire se uno schema è in 3NF è un problema NP-Completo, che cioè non ha algoritmi efficienti che lo risolvano. I principi esposti dovrebbero essere intrinseci nel modo di pensare del progettista, obiettivo che si raggiunge un po' con lo studio, un po' di piú col ragionamento, ma soprattutto con la pratica.
Eventuali chiarimenti in merito a questo articolo possono essere richiesti all'autore Marco Tonti, del quale segue il curriculum tecnico.
6 Aprile '75 Nasce nella soleggiata Rimini. Maggio '95 È tra i vincitori del "Concorso italiano per giovani ricercatori" indetto dalla FAST (Federazione delle Associazioni Scientifiche e Tecniche), con una ricerca su un algoritmo di ordinamento basato sull'analisi di Fourier. Giugno '95 Si diploma con 56/60 all'Istituto Tecnico Industriale Blaise Pascal di Cesena (FC), specializzandosi in informatica. Settembre '95 Partecipa a Newcastle-upon-Tyne (UK) all'"European Union Contest for Young Scientists" in rappresentanza dell'Italia.
Contemporaneamente si trasferisce a Bologna per proseguire i suoi studi, presso il corso di informatica della Facoltà di scienze matematiche fisiche e naturali. (La sua homepage) '97 Insieme col suo gruppo universitario partecipa al progetto di realizzazione del sistema operativo YAOS, per il corso di Sistemi Operativi. '98 Per il corso di Basi di Dati, partecipa alla creazione di un supermercato virtuale per gli acquisti via web. Luglio-Dicembre '98 Progetta, sviluppa e tuttora amministra il sistema informativo nazionale del circuito UNOCLUB che, fino a tutto il 2000, conta oltre 60 postazioni sparse su tutto il territorio italiano e 120.000 iscritti. '99 Sempre col suo gruppo sviluppa Yahum, un motore di ricerca gerarchico, per il corso di Interazione Uomo-Macchina.
Inoltre collabora con Polidata Italia SpA (MO) per l'interfacciamento tra il loro sistema informativo aziendale e il web. '00 Per il corso di Intelligenza Artificiale crea un motore di analisi lessicale, che ha il compito di ricondurre le forme flesse di un termine al suo lemma. ... a oggi. Ha sviluppato numerosi programmi gestionali collegati (tramite le tecnologie COM) ai client del sistema nazionale già illustrato. Prevalentemente sono programmi di drinkcard elettronica su computer in rete locale, ma in generale si tratta di programmi creati per mantenere le informazioni sull'afflusso di persone, dei movimenti di magazzino, dell'amministrazione dei circoli e dei soci. Ha sviluppato anche un sistema informativo distribuito per una catena di locali del Norditalia. I suoi programmi operano in città quali Milano, Padova, Mestre, Piacenza, Ancona e Roma. E domani... È votato alla ricerca scientifica astratta. Nell'attesa della laurea scrive programmi, ma come dice di sé: "Non sono un programmatore, emulo un programmatore".
I campi di ricerca che piú lo affascinano sono l'intelligenza artificiale, vista dalla prospettiva delle scienze cognitive, e l'informatica teorica.
Usa comunemente Microsoft Visual Basic, ma ha conoscenze di Java, C, ASP, Assemby (Z80, 8086, 68000), di linguaggi logici (Prolog) e funzionali (OCAML).