< Home
Stampa

Chiavi e normalizzazione

Sommario

Nelle precedenti lezioni abbiamo visto i principi base per creare un modello concettuale (diagramma E/R di Chen) e il relativo modello logico-relazionale. Prima di procedere oltre, bisogna però acquisire dei concetti fondamentali per capire se il nostro database funziona, ovvero se è davvero efficace nel rappresentare la realtà di riferimento del problema che vogliamo risolvere.

Come abbiamo visto, un database ci serve per suddividere l’estensionalità dei dati grezzi in entità in grado di rappresentare concetti differenti ma collegati tra loro. Ipotizziamo di avere un ecommerce che gestisce gli ordini dei clienti. Potremmo memorizzare tutti gli ordini in grande foglio di calcolo con Excel. Ogni riga rappresenta un ordine di un prodotto, il prezzo, l’anagrafica del cliente e l’indirizzo di spedizione.

Questo modello è poco efficiente. Infatti

  • per ogni riga bisogna riscrivere l’anagrafica del cliente e l’indirizzo di spedizione.
  • se il cliente cambia indirizzo bisogna modificare tutte le righe dei suoi ordini.

Una soluzione è quindi suddividere lo schema in almeno 3 entità, l’anagrafica, l’indirizzo e l’ordine vero e proprio, e poi collegare l’ordine al cliente ed il cliente all’indirizzo. In questo modo creiamo 3 tabelle, dove ogni tabella rappresenta un “pezzo” del nostro puzzle, che identifica un solo concetto che può cambiare: se cambia l’indirizzo, il cliente resta lo stesso. Se creo un nuovo ordine, riutilizzo lo stesso cliente senza riscrivere i suoi dati.

Concetti fondamentali

Un database relazionale non è però un insieme di tabelle collegate tra loro, ma è una struttura matematica che si basa sulla teoria degli insiemi e la logica del primo ordine1. Tramite questi strumenti dobbiamo capire se le tabelle che stiamo creando sono proprio tutte e sole quelle che ci servono, o bisogna applicare dei correttivi dovuti a ridondanze, e che siano in grado di evitare problemi quando andremo ad inserire o cancellare dati.

Vediamo i concetti fondamentali di un database relazionale:

  • la tabella: rappresenta l’insieme dei valori di una specifica entità. Esempio l’album, l’autore, ecc.
  • l’attributo: rappresenta una colonna della tabella. Ad esempio il titolo dell’album.
  • il record: rappresenta una riga di una tabella, cioè una istanza dell’entità contenente valori specifici per quella entità. E’ una tupla di valori, dove ogni cella viene chiamata “campo“.

Ogni attributo ha un dominio di validità, ovvero un insieme di valori possibili. Se è un intero, è l’insieme degli interi memorizzabili, se una stringa l’insieme delle combinazioni di caratteri, ecc.

Una tabella è un insieme, quindi:

  • l’ordine delle righe non conta;
  • non possono esserci due tuple identiche

Siccome non possono esserci due tuple identiche, è necessario attivare un meccanismo che ci consenta di distinguere due tuple tra loro. Questo meccanismo è la chiave primaria.

Chiave primaria

E’ detta superchiave un sottoinsieme di attributi che permette di identificare univocamente l’intera tupla. Deve essere diversa per ogni tupla e deve essere non nulla.

Ipotizziamo di avere la seguente tabella:

MatricolaCodice fiscaleNomeCognomeEmail
726265RSSMRA01A01F205ZMarioRossimrossi@cipiaceinfo.it
726266BRNBNC02B42H501WBiancaBrunobbruno@cipiaceinfo.it
726267GVNNNL00C15F205YGiovanniNerigneri@cipiaceinfo.it
726268VRDLRA03D50L219XLauraVerdilverdi@cipiaceinfo.it

Questa tabella può avere come superchiave:

  • Matricola: ogni tupla ne ha una diversa
  • Codice Fiscale: ogni tupla ne ha una diversa
  • Email: ogni tupla ne ha una diversa (stesso dominio)
  • Matricola, CodiceFiscale: una gruppo di superchiavi è superchiave
  • CodiceFiscale, Email: idem
  • Email, Matricola: idem
  • Matricola, CodiceFiscale, Email: idem

Le superchiavi possono essere più di una e possono essere anche ridondanti.

Dalla superchiave possiamo ricavare la chiave minimale, cioè quella chiave composta dal numero minimo di colonne. Qui ad esempio:

  • Matricola: ogni tupla ne ha una diversa
  • Codice Fiscale: ogni tupla ne ha una diversa
  • Email: ogni tupla ne ha una diversa (stesso dominio)

Da queste ne scegliamo una, che diventa chiave primaria della nostra tabella. Essa sarà usata come identificativo nel RDBMS per identificare la tupla, e come indice per la ricerca.

La chiave primaria può essere multipla come in questo esempio, dove la chiave primaria è data dall’insieme {Matricola, Corso}.

MatricolaCorsoDataVoto
726267INF-0112/6/202628
726267INF-0217/7/202627
726267MAT-012/9/202630

La chiave primaria è usata come chiave esterna anche per collegare tra loro due entità differenti, come abbiamo visto nella precedente lezione.

Normalizzazione

La normalizzazione è una procedura che consente, in modo formale, la verifica delle tabelle e dell’assocazione degli attributi delle stesse. Ha come scopo quello di verificare non solo che esista una chiave per ogni tabella, ma che tutti gli attributi dipendano SOLO dalla chiave, in caso contrario c’è una qualche anomalia di progettazione del database che si andrà a verificare in fase di inserimento, modifica o cancellazione. Ad esempio la normalizzazione va a verificare che non si creino situazioni come quella della tabella Excel degli ordini.

La normalizzazione si basa sul concetto di dipendenza funzionale: dati X ed Y come sottoinsiemi di attributi, si diche che Y dipende da X (X -> Y) se e solo se il valore degli attributi di Y dipende SOLO dai valori degli attributi X.

Ad esempio:

  • nome e cognome Y dipendono da codice fiscale
  • nome e cognome Y dipendono da matricola
  • matricola NON dipende da nome e cognome (ci possono essere studenti omonimi)
  • email NON dipende da nome e cognome

Quando analizziamo una tabella applicando la normalizzazione, verifichiamo che siano verificate progressivamente regole via via più restrittive di normalizzazione, dette forme normali. Sono previste 4 forme normali progressive, dalla prima alla terza, ed infine la la forma più restrittiva, detta di Boyce-Codd.

Prima forma normale (1NF)

Una tabella è in prima forma normale se e solo se contiene campi atomici, cioè senza valori ripetuti.

Ad esempio:

CodiceCorsoNomeCorsoLibri
INF-01Programmazione“Python”, “Strutture dati”

Questa tabella non è in forma normale, perché la colonna Libri ha più di un valore.

Per normalizzarla è sufficiente scomporre gli attributi multipli:

CodiceCorsoNomeCorsoLibri
INF-01ProgrammazionePython
INF-01ProgrammazioneStrutture di dati

Seconda forma normale (2NF)

Una tabella è in seconda forma normale se è prima forma normale E se ogni attributo dipende interamente dalla chiave, e non da parte di essa. Riprendiamo la precedente tabella:

CodiceCorsoNomeCorsoLibro
INF-01ProgrammazionePython
INF-01ProgrammazioneStrutture di dati

Come si può vedere la chiave primaria è necessariamente {CodiceCorso, Libro}. Tuttavia NomeCorso non dipende da questa chiave composta, ma solo da parte di essa, ovvero CodiceCorso.

Per normalizzare occorre quindi creare due tabelle:

Terza forma normale (3NF)

Una tabella è in terza forma normale se è in seconda forma normale e nessun attributo non chiave dipende in modo transitivo (indiretto) dalla chiave primaria. Ovvero non deve verificarsi la situazione in cui l’attributo A dipende dall’attributo B, che dipende dall’attributo C.

Ad esempio prendiamo questa tabella:

MatricolaCorsoDiLaureaSede
726265InformaticaFesta del Perdono
726266Lettere classicheCittà studi
726267GiurisprudenzaConservatorio
726268ChimicaCittà Studi

Se osserviamo bene c’è una anomalia macroscopica: la sede dipende dal corso di laurea, e NON dalla matricola. Per normalizzare bisogna creare due tabelle:

Forma normale di Boyce-Codd (BCNF)

Una tabella è in forma normale di Boyce-Codd se è in terza forma normale, ed ogni attributo non chiave deve dipendere da un attributo che deve essere superchiave, anche se non è chiave primaria. Per capire meglio questo scenario vediamo questo esempio di tabella:

MateriaDocenteRuolo
Programmazione Iprof. Rossiteoria
Programmazione Iprof. Russolaboratorio
Reti di calcolatoriprof. Rossiteoria

La tabella ha come chiave {Materia, Docente}. E’ immediatamente visibile che è 1NF, inoltre è anche in 2NF, perché ruolo dipende dalla coppia {Materia, Docente} e non solo da uno di essi. E’ anche in 3NF, perché Ruolo non dipende da attributi non chiave. Eppure non è in forma normale, perché concettualmente nel mondo reale, il ruolo dipende dal docente, non dalla coppia docente-materia. In altri termini entrambe queste affermazioni sono entrambe vere:

  • il ruolo di Rossi è di docente di teoria per programmazione I
  • Rossi però è docente di teoria sempre, non solo per quella materia

La prima informazione è soddisfatta dalla 3NF, ma la seconda no. Bisogna creare un’altra tabella per soddisfare il requisito:

Normalizzazione e progettazione

La normalizzazione, con le sue regole per la normalizzazione, ci da delle linee guida chiare per la progettazione, ovvero:

  • quando suddividiamo i dati in entità, la regola generale è che una entità deve essere identificabile con una superchiave (singola o multipla)
  • l‘entità è qualcosa di distinguibile dalle altre se e solo se tutti gli attributi dipendono dalla superchiave. E’ importante quindi suddividere se la regola non è soddisfatta, ma anche accorpare quando due entità inizialmente proposte dipendono dalla stessa superchiave.

La normalizzazione e quindi la progettazione corretta del database ha un impatto significativo nella fase di popolamento e gestione dei dati del database in tutti gli scenari reali:

  • inserimento: grazie alla normalizzazione si evita la ridondanza in inserimento, evitando valori duplicati: ad esempio se inseriamo un nuovo corso con il docente (esempio di BCNF) non ci dobbiamo ricordare che ruolo aveva quel docente, perché c’è un’altra tabella che già ce lo dice;
  • modifica: se si cambia il contenuto di un record, ad esempio cambia la sede del corso di laurea (vedi esempio della 3NF), è sufficiente cambiarlo in un solo punto, e tutte le tabelle collegate continueranno a funzionare;
  • cancellazione: se si elimina un record da una tabella, ad esempio un docente va in pensione, si può automatizzare una regola che cancella, in cascata, tutti i corsi che teneva quel docente, senza rischi di dati inconsistenti.

Vedremo questi concetti quando faremo esempi pratici di database nelle prossime lezioni.

Implementazione tecnica

Denormalizzazione

La normalizzazione ha come scopo ultimo l’efficienza dal punto di vista della ridondanza dei dati. E’ importante però capire che nel mondo reale la normalizzazione può portare a creare un numero eccessivo di tabelle. Ad esempio, con la forma normale di Boyce-Codd abbiamo suddiviso la tabella che associa corso, docente e ruolo in due tabelle, una che associa corso e docente e l’altra che associa docente al ruolo. Questa efficienza però ha un costo computazionale: quando andremo ad elencare l’elenco dei corsi dovremo per forza unire le due tabelle, con una operazione che come vedremo si chiama “join”, operazione che richiede un tempo computazionale maggiore rispetto ad una tabella non normalizzata.

E’ un classico problema di tradeoff: una struttura matematicamente efficiente può non esserlo nell’applicazione pratica. In questo caso bisogna includere nell’analisi gli effettivi utilizzi reali dei dati: se le due tabelle sono in effetti utilizzate sempre insieme, conviene avere un po’ di ridondanza dei dati, e quindi utilizzare la tabella in terza forma normale (con 3 colonne) ma risparmiare poi sul tempo di lettura. Questa operazione prende il nome di denormalizzazione, di norma dalla BCNF alla 3NF (non è mai conveniente invece scendere alla 2NF). E’ una scelta questa comunque che si acquisisce con l’esperienza e molta pratica.

Chiavi artificiali

Le chiavi primarie che abbiamo analizzato finora sono dette “naturali”, in quanto sono scelte dalle superchiavi di una entità. Esse possono essere singole (come le matricole per gli studenti o i codici fiscali) o multiple (come le coppie matricola-corso). La normalizzazione ci consente di progettare database dove ad ogni entità corrisponde una chiave e degli attributi che dipendono da essa.

Tuttavia le chiavi naturali, specie se multiple, sebbene corrette da un punto di vista logico e concettuale, possono essere inefficienti e/o instabili.

Un alternativa sono le chiavi artificiali. Esse sono chiavi generate automaticamente ed in modo univoco, alla creazione del record direttamente dal RDBMS. Ve ne sono principalmente di due tipi:

  • ID intero autoincrementale: valore chiave che parte da 1, e ad ogni inserimento di un nuovo record, si incrementa di 1 il valore. Il valore viene usato una volta soltanto, quindi se viene cancellato un record non viene riutilizzato un vecchio id. E’ il modello tipicamente usato sui database basati su SQL.
  • Stringa UUID (Universal Unique IDentifier), autogenerata non incrementale: non fornisce informazioni sul numero di record creati, inoltre garantisce che lo UUID sia univoco per ogni record di qualsiasi tabella: mentre con l’ID due tabelle distinte potrebbero record con lo stesso ID (anche se si riferiscono a istanze di entità differenti). E’ il modello usato sui database NoSQL per garantire iteroperabilità tra sistemi differenti (ad esempio in applicazioni distribuite). Può comunque essere usato anche nei database SQL.

Qui un esempio di tabella con ID autoincrementale:

IdMatricolaCodice fiscaleNomeCognomeEmail
1726265RSSMRA01A01F205ZMarioRossimrossi@cipiaceinfo.it
2726266BRNBNC02B42H501WBiancaBrunobbruno@cipiaceinfo.it
3726267GVNNNL00C15F205YGiovanniNerigneri@cipiaceinfo.it
4726268VRDLRA03D50L219XLauraVerdilverdi@cipiaceinfo.it

Le chiavi artificiali risolvono due tipologie di problemi delle chiavi naturali:

  • il dato artificiale identifica il dato-informazione, ma NON è parte dell’informazione stessa. La sua artificialità ne garantisce cioè la stabilità, quindi anche se cambia il valore della chiave naturale la sua identità rimane la stessa. Ipotizziamo ad esempio che l’università decida di cambiare l’intera valorizzazione delle matricole degli studenti per usare un codice a 7 cifre. Siccome quell’attributo è chiave primaria utilizzata da tutte le tabelle collegate come chiave esterna, bisogna in cascata modificare anche tutte le tabelle collegate, col rischio di forti inefficienze. Con una chiave artificiale questo non succede, perché l’Id non dipende da dati provenienti dal mondo reale.
  • Garantisce prestazioni ottimali: la chiave primaria è indicizzata in un albero di ricerca bilanciato che come noto fornisce le migliori prestazioni O(log n) nella ricerca della chiave. Le chiavi intere offrono sempre prestazioni migliori delle chiavi stringa, ed ancora di più sulle chiavi multiple.

Le chiavi artificiali però non eliminano il problema della normalizzazione. Esse danno un “falso senso di sicurezza” perché garantiscono come minimo una 2NF, almeno formalmente. Si tratta però di un errore concettuale. Va sempre tenuto presente infatti che il modello logico-relazionale è qualcosa che è del tutto indipendente dalla sua implementazione fisica in un sistema reale. Metterli sullo stesso piano significa rischiare di confondere uno strumento tecnico (l’id artificiale) con un concetto logico (la dipendenza degli attributi da una chiave) col rischio di mantenere tutte le ridondanze e quindi le inconsistenze, con relative anomalie di inserimento, modifica e cancellazione.

Il modo corretto di operare è quindi quello di procedere alla creazione di chiavi primarie naturali, verificare la normalizzazione, e solo quando si è costruito un database normalizzato, aggiungere, solo nei casi in cui è prevista instabilità e/o inefficienza, un campo chiave artificiale aggiuntivo. Ma anche in questo caso, occorre nella fase di creazione tecnica, creare un vincolo di tipo UNIQUE (permesso dai moderni RDBMS), che garantisce che la chiave naturale (singola o composta) contenga comunque valori unici, indipendentemente dal valore dell’ID artificiale.

Infine, occorre ricordare di nuovo che l’ID artificiale è un dato tecnico dipendente dal sistema fisico, e quindi non dipendente dall’informazione trasportata. Questo significa che se per esempio si cancella il database e lo si ricrea, esso va a rigenerare un nuovo id per ogni istanza di ogni tabella, che può essere diverso da quello del database precedentemente utilizzato. Non è informazione significativa, ma solo dati tecnici per il funzionamento del database.

Nel caso degli ID autoincrementali va quindi trattato come tale: quando si espongono dati all’esterno (come nelle applicazioni distribuite) il dato dell’Id non va mai esposto ad altri sistemi. Discorso diverso lo merita lo UUID, che invece genera un ID globale, quindi condiviso tra più sistemi, e diventa parte dell’informazione. In quel caso il valore va preservato in caso di cancellazione e ripristino di un database.

Conclusioni

In questa lezione abbiamo visto i seguenti concetti fondamentali:

  • un database non è semplice agglomerato di tabelle, ma è una struttura matematica che prevede insiemi e vincoli dentro le tabelle (chiavi primarie) e tra tabelle (chiavi esterne);
  • le chiavi rappresentano attributi di una tabella da cui dipendono tutti gli altri attributi, in modo diretto
  • la normalizzazione è una tecnica di analisi che serve per garantire il raggiungimento delle 3 forme normali, più la forma normale di Boyce-Codd
  • è possibile usare chiavi artificiali per ragioni di stabilità ed efficienza, purché sia sempre chiaro che non sono parte dell’informazione, ma dati tecnici

  1. ovvero quella parte della logica che si applica agli elementi di un insieme, usando quantificatori (“per ogni”, “esiste almeno”) connettivi (and, or, not) e predicati (“se vero A, allora vero B” e “A è vero se e solo se è vero B”). ↩︎