Tradeoffs between Normalization and Denormalization

TIL la differenza tra schemi normalizzati e denormalizzati per modellare i dati, e alcuni dei tradeoffs con ciascuno.

Normalizzazione

La normalizzazione è un modo di definire lo schema del database in modo da ottimizzare la velocità e l’integrità writes garantendo l’assenza di dati ridondanti tra le tabelle.

Lo standard per la normalizzazione è quello di puntare a 3rd normal form (3nf).

Un breve riassunto dei criteri per raggiungere il 3nf:

  • I record di una tabella dovrebbero contenere delle chiavi primarie che li referenzino in modo univoco (1nf)

  • Non ci dovrebbero essere colonne ripetute in una tabella (1nf)

  • Ci non dovrebbero esserci chiavi funzionalmente dipendenti (2nf)

  • Non dovrebbero esserci chiavi funzionalmente dipendenti in modo transitorio (3nf)

Poiché le tabelle non contengono dati ridondanti, il storage richiesto per i nuovi record in qualsiasi tabella è relativamente piccolo.

Siccome le scritture sono piccole, sono anche fast.

Le scritture sono anche garantite per lasciare il database in uno stato consistent, grazie alle referential integrity garanzie dei vincoli di chiave esterna tra tabelle correlate.

Denormalizzazione

La denormalizzazione è l’atto di aggiungere ridondanze o valori derivati nel vostro schema per ottimizzare per reads che altrimenti sarebbe costoso in uno schema normalizzato.

Pensate per un minuto ad un database completamente normalizzato…

Tutto è nella propria tabella…

Tutto ha la propria chiave primaria…

Le referenze tra le tabelle sono mantenute da vincoli di foreign_key…

… questo è ottimo dal punto di vista dello storage e dell’integrità, ma può portare a pezzi di una query distribuiti su molte tabelle, il che porta ad unioni lente e complesse per ottenere il quadro completo di una query.

Se siete in grado di anticipare i tipi di query che i vostri utenti potrebbero fare, potrebbe avere senso memorizzare alcuni valori in modo ridondante nel vostro sistema per accelerare le prestazioni delle query.

L’aggiunta di valori denormalizzati rende gli inserimenti e gli aggiornamenti più complicati: dovete assicurarvi che i valori denormalizzati siano mantenuti correttamente, poiché l’integrità di questi valori non è automaticamente applicata dallo schema.

La scelta di denormalizzare dovrebbe essere fatta consapevolmente. Dovrebbe essere documentata, testata, e dovrebbe essere comunicata in tutto il team in modo che tutti siano consapevoli di questa considerazione aggiuntiva quando si scrive su tabelle denormalizzate.

Esempio normalizzato

Sto costruendo un’applicazione chiamata CashBackHero, che comporta la modellazione delle relazioni tra carte di credito e utenti attraverso un’entità chiamata portafoglio. Uno schema 3nf normalized per queste entità potrebbe essere simile a questo:

CashBack

id value
1 3
2 4
3 5

Carte

id nome cash_back_id
1 Cerca 2
2 Scoperta 1
3 Amex 3

Portafogli

id name
1 Il portafoglio di Brian
2 Il portafoglio di Alex
3 Il portafoglio di Lauren

CardWallets (associazioni tra portafogli e carte)

id wallet_id card_id
1 1 1
2 1 3
3 2 2 2
4 3 2
5 3 1

Le tabelle precedenti sono normalized, poiché non contengono dati ridondanti. Le relazioni tra le tabelle sono mantenute attraverso foreign_key vincoli ad altre tabelle. Questo memorizza i dati nella loro forma più compact.

La mancanza di dati ridondanti ottimizza anche il writes del database. Le scritture più frequenti sono le aggiunte e le rimozioni alla tabella CardWallets, che comporta solo colonne id che contengono riferimenti ad altre tabelle contenenti informazioni.

La normalizzazione dei dati rende anche gli aggiornamenti un gioco da ragazzi, poiché ogni tabella è una singola fonte di verità per le informazioni contenute.

Un database normalizzato ottimizza anche alcuni tipi di lettura, come far emergere una lista di tutti i valori in una particolare tabella (come ottenere tutte le carte).

Le letture in cui i dati si trovano in più tabelle, però, diventano più difficili. Una query allo schema normalizzato di cui sopra per ottenere i nomi e i valori di cash back delle carte per il portafoglio 1 comporta l’unione delle tabelle Wallets e Cards alla tabella CardWallets, poi unendo la tabella CashBackValue alla tabella Cards. Se queste tabelle diventano grandi, le query potrebbero diventare lente rispetto ad una versione denormalizzata dove tutti i dati vivono in una sola tabella.

A proposito di denormalizzazione…

Esempio di denormalizzazione

Guarda come sarebbe lo schema normalizzato presentato sopra se fosse completamente denormalizzato:

CashBackSchema

id name_card wallet_name cash_back_value
1 Chase Brian’s Wallet 4
2 Amex Brian’s Portafoglio 5
3 Scoperta Portafoglio di Alex 3
4 Scoperta Portafoglio di Lauren 3
5 Caccia Portafoglio di Lauren 4

Questo è un tavolo mostruoso!

Ok… sono solo 5 righe… ma si capisce che una tabella denormalizzata può spesso essere più facile da leggere, dato che tutti i dati esistono in un unico posto.

Invece di dover unire 4 tabelle per ottenere le carte e il valore di cash_back per il portafoglio di un utente, ora possiamo semplicemente eseguire un’istruzione select sulla tabella CashBackSchema di cui sopra.

La ricerca di carte uniche diventa un po’ più lenta, poiché l’intera tabella deve essere letta per determinare i nomi_carta unici.

Anche gli aggiornamenti sono un po’ più complicati: dobbiamo garantire l’integrità dei nostri dati logically. Per esempio, se vogliamo aggiornare il valore di cash_back della carta Chase nel portafoglio di Brian, dobbiamo anche considerare se dobbiamo aggiornare anche il valore di cash_back della carta Chase nel portafoglio di Lauren. Gli aggiornamenti devono essere fatti in più posti, il che può essere lento per tabelle grandi.

In uno schema normalizzato, gli aggiornamenti a qualcosa come “wallet_display_name” possono essere fatti solo in un posto (la tabella Wallets), invece di dover setacciare l’intera CashBackSchema per assicurarsi che ogni riga con il nome “Brian’s Wallet” sia aggiornata adeguatamente.