Progettare un database da zero somiglia più al lavoro di un urbanista che a quello di un semplice costruttore. È facile tirare su tabelle e colonne, così come è facile tracciare strade. La vera sfida è far sì che la città (il database) funzioni tra dieci anni, gestendo il traffico (i dati) senza crollare e, soprattutto, senza far sparire interi quartieri (i dati storici).
Molti tutorial si fermano alle definizioni: chiave primaria, chiave esterna, terza forma normale. Questo articolo fa un passo in più. Non ci concentriamo solo su come creare le tabelle, ma su perché certe decisioni apparentemente innocue, come un ON DELETE CASCADE, possono distruggere l’integrità del tuo sistema.
Partiremo da un foglio bianco e arriveremo a uno schema SQL per un e-commerce, ma l’obiettivo reale è imparare a costruire sistemi che rispettino il loro bene più prezioso: i dati.
1. Panoramica rapida: cos’è un database relazionale
Un database relazionale organizza i dati in tabelle (relation = tabella) dove ogni riga è una istanza e ogni colonna è un attributo. Le relazioni fra tabelle sono implementate tramite chiavi (PK, FK). Lo scopo della modellazione è rappresentare il dominio reale in modo coerente, minimale (senza ridondanze inutili) e efficiente.
2. Concetti fondamentali
Entità
Un’entità è una cosa del mondo reale che vogliamo rappresentare (es. Cliente, Prodotto, Ordine). In termini pratici: ogni entità diventa una tabella.
-
Ogni tabella rappresenta un tipo di entità.
-
Gli attributi dell’entità diventano colonne della tabella.
Relazioni
Una relazione descrive come due (o più) entità sono correlate.
-
1:1 (uno-a-uno): raro, spesso si può unire in una sola tabella; utile se si vogliono separare dati opzionali o sensibili.
-
1:N (uno-a-molti): la relazione più comune. Lato N contiene la FK verso la PK del lato 1.
-
N:M (molti-a-molti): si risolve con una tabella di associazione (junction table) che contiene due FK — spesso con una propria PK surrogata.
Attributi e tipi
Definisci tipi adeguati (VARCHAR(n), TEXT, INTEGER, DATE, TIMESTAMP, DECIMAL(p,s), BOOLEAN, UUID, ecc.) e limiti di lunghezza.
Evita colonne generiche tipo TEXT se puoi essere più specifico.
3. Regole per le chiavi
Chiave primaria (PK)
-
Deve essere unica e non nulla per ogni riga.
-
Consiglio: usa sempre surrogate key (ID) — cioè una colonna
idgenerata (AUTO_INCREMENT INT o BIGINT, oppure UUID).
Vantaggi delle surrogate key:-
Stabilità (non cambia se cambiano gli attributi naturali).
-
Performance sugli indici (numerici, brevi).
-
Semplicità quando ci sono più attributi naturali.
Svantaggi (da conoscere): -
Perdita di “significato umano” nella chiave — ma è proprio lo scopo: separare identificatore da contenuto.
-
-
Regola pratica: definisci
idcome PK su tutte le tabelle, anche quando esiste una chiave naturale.
Chiave esterna (FK)
-
Una FK è un vincolo che fa riferimento a una PK (o a unicità) di un’altra tabella.
-
Deve rispettare integrità referenziale: non puoi inserire una FK che punti a una PK inesistente (a meno che non sia
NULLse permesso). -
Scegli il comportamento in caso di delete/update (NO ACTION / RESTRICT, CASCADE, SET NULL, SET DEFAULT) in base alle regole di business.
Regola esplicita:
Usa sempre surrogate key (ID) anche se esiste una chiave naturale.
— quindi ogni tabella avràidcome PK.
4. Passi di progettazione (workflow pratico)
-
Raccogli i requisiti: entità, attributi, processi, regole del dominio.
-
Identifica le entità principali e le relazioni fra loro.
-
Disegna un ERD (Entity–Relationship Diagram) con entità/relazioni e cardinalità.
-
Scegli le PK surrogate (
id) per tutte le tabelle. -
Spezza le relazioni N:M con tabelle di associazione.
-
Applica normalizzazione fino alla 3NF (o oltre se occorre). Valuta denormalizzazione solo per performance ben motivate.
-
Definisci vincoli e indici (FK, UNIQUE, NOT NULL, CHECK, indici su colonne usate nelle query).
-
Scrivi la DDL (CREATE TABLE) e prova con dati reali.
-
Testa casi limite: cancellazioni, aggiornamenti, transazioni concorrenti.
-
Documenta lo schema e le convenzioni di naming.
5. Normalizzazione
Breve riassunto pratico:
1) Prima Forma Normale — 1NF
Regola: ogni campo contiene un singolo valore atomico (no liste, no colonne ripetute, niente colonne multiple come phone1, phone2 non generalizzate).
Esempio non 1NF (tabella customers)
| customer_id | name | phones |
|---|---|---|
| 1 | Maria Rossi | 333-1111111, 333-2222222 |
| 2 | Luca Bianchi | 333-3333333 |
Problema: la colonna phones contiene una lista di valori separati da virgola. Impossibile fare ricerche efficaci, indicizzazione, vincoli di unicità, e aggiornamenti atomici.
Come normalizzare (portare in 1NF)
Separare i telefoni in una tabella customer_phones (una riga per numero).
Risultato: ogni cella è atomica; si possono aggiungere, rimuovere e indicizzare numeri singoli.
2) Seconda Forma Normale — 2NF
Regola: essere in 1NF e nessun attributo non-primo (cioè non parte della chiave primaria) deve dipendere solo da una parte di una PK composita. In pratica: con PK composte, evita attributi che dipendono solo su una parte della PK.
Nota pratica: se usi surrogate key (
id) quasi sempre eviti PK composite e molti problemi di 2NF spariscono.
Esempio non 2NF (tabella order_items_products)
Supponiamo uno schema con PK composita (order_id, product_id):
| order_id | product_id | product_name | quantity | unit_price |
|---|---|---|---|---|
| 1001 | P01 | Penna Blu | 2 | 1.50 |
| 1001 | P02 | Quaderno A4 | 1 | 3.00 |
| 1002 | P01 | Penna Blu | 5 | 1.40 |
Problema: product_name dipende solo da product_id, non dall’intera PK (order_id, product_id). Questo significa duplicazione di product_name e inconsistenza se viene aggiornato solo in alcune righe.
Dipendenza funzionale rilevante: product_id → product_name
Come normalizzare (portare in 2NF)
Separare l’informazione del prodotto in products e tenere in order_items solo ciò che riguarda l’ordine.
Nota sul unit_price: unit_price è intenzionalmente duplicato rispetto a products.current_price per congelare il prezzo storico (denormalizzazione voluta). Questo non viola la 2NF se consideriamo unit_price come proprietà dell’item nell’ordine (dipende dall’intera PK (order_id,product_id)), non solo da product_id.
3) Terza Forma Normale — 3NF
Regola: essere in 2NF e nessuna dipendenza transitiva: cioè, nessun attributo non PK deve dipendere da un altro attributo non PK.
Formalmente: se A → B e B → C, allora non vogliamo che A → C via B quando B è un attributo non-primo.
Esempio non 3NF (tabella employees)
| emp_id | name | dept_id | dept_name | dept_location |
|---|---|---|---|---|
| 10 | Anna | D01 | Contabilità | Milano |
| 11 | Marco | D02 | Vendite | Roma |
| 12 | Giulia | D01 | Contabilità | Milano |
Problema: dept_name e dept_location dipendono da dept_id, che è un attributo non-primo (non fa parte della PK emp_id). Questo è una dipendenza transitiva:
emp_id → dept_id e dept_id → dept_name, quindi emp_id → dept_name transitivamente. Questo causa ridondanza e possibili inconsistenze se i dati del dipartimento cambiano.
Come normalizzare (portare in 3NF)
Estrarre la tabella dei dipartimenti:
Risultato: le informazioni relative al dipartimento sono mantenute in un’unica tabella departments; aggiornamenti al nome o alla sede del dipartimento sono fatti in un solo posto.
Riassunto pratico di come verificare le 3 forme normali
-
1NF: rimuovi colonne che contengono collezioni/array/CSV; crea tabelle separate per elementi ripetuti.
-
2NF: se hai una PK composta, verifica che nessun attributo non-primo dipenda solo da una parte della PK. Se succede, estrai la dipendenza in una nuova tabella.
-
3NF: verifica che NON esistano dipendenze del tipo
A → BeB → CdoveAè la PK eCè un attributo non-primo; se ci sono, estraiBeCin una nuova tabella.
Considerazioni pratiche / eccezioni utili
-
Surrogate key (id): usare sempre
idsemplifica molto 2NF perché riduce l’uso di PK composite. Tuttavia attenzione: anche coniddevi rispettare 3NF (non lasciare attributi non-primo dipendenti da altri non-primo). -
Denormalizzazione controllata: campi come
unit_priceinorder_itemssono accettabili e spesso necessari per congelare informazioni storiche. Documenta queste eccezioni. -
Performance vs Purezza: la normalizzazione previene ridondanza e inconsistenza; denormalizzare (es. duplicare alcuni campi) può aiutare performance e reporting. Valuta trade-off e aggiungi indici/materialized views dove serve.
-
3NF è spesso un buon punto di equilibrio per sistemi OLTP; per data warehousing si usano modelli differenti (star schema, denormalizzazione più spinta).
Esempio completo (mini-flow): da non-normalizzato → 1NF → 2NF → 3NF
Immagina una tabella iniziale sales_raw con colonne: sale_id, customer_name, customer_phones, product_id, product_name, product_price, quantity, dept_name.
-
Portare in 1NF: separare telefoni in
customer_phones. -
Portare in 2NF: estrarre
products(product_id → product_name, price) e lasciareorder_itemsconunit_price. -
Portare in 3NF: estrarre
departmentssedept_namedipende dadept_id.
6. Esempio reale completo: E-commerce (semplificato)
Domini / entità principali
-
customers(clienti) -
products(prodotti) -
orders(ordini) -
order_items(righe ordine) — gestisce N:M traorderseproducts -
categories(categorie prodotto) -
product_categories(associazione prodotto-categoria) — N:M
Regole:
-
Tutte le tabelle hanno
idsurrogate (BIGINT AUTO_INCREMENT o UUID). -
ordersha FK sucustomers. -
order_itemsha FK suorderseproducts. -
Quando un cliente viene cancellato, business decision: generalmente NON fare cascade delete sugli ordini (meglio
RESTRICToSET NULLper tracciare storico).
Principio generale: i dati “storici” non si cancellano mai!
Quando un’entità è coinvolta in transazioni o registrazioni storiche (es. ordini, fatture, iscrizioni, ticket, log), non deve mai essere cancellata fisicamente dal database.
Questo vale, per esempio, per:
-
Prodotti già venduti (anche se non più disponibili),
-
Clienti che hanno effettuato ordini (anche se non più attivi),
-
Corsi in cui uno studente si è iscritto,
-
Dipendenti che hanno partecipato a progetti chiusi.
Il motivo è semplice:
-
Gli order_items rappresentano fatti storici.
-
Se elimini la riga di
products, la FK diorder_itemsdiventerebbe “orfana” o costringerebbe il database a cancellare anche le righe ordine (in caso diON DELETE CASCADE), distruggendo la tracciabilità.
👉 In un database progettato per integrità e auditabilità, i dati storici non si cancellano, si “disattivano”.
Soluzione corretta: Soft Delete o flag di stato
Invece di cancellare una riga da products, si gestisce la disattivazione con un campo, per esempio:
Quando un prodotto non è più disponibile, imposti:
Questo approccio ha diversi vantaggi:
| Vantaggio | Descrizione |
|---|---|
| Integrità storica | Gli ordini storici rimangono coerenti e interrogabili. |
| Audit e tracciabilità | Puoi sempre sapere cosa è stato venduto, anche se il prodotto non è più in vendita. |
| Sicurezza dati | Nessuna cancellazione fisica involontaria. |
| Compatibilità con FK | Nessun problema di ON DELETE o di chiavi orfane. |
Opzionale: Archiviazione o Versionamento
In sistemi più complessi (ERP, e-commerce enterprise, gestionali) si adotta un approccio di versionamento o archiviazione storica:
-
Versionamento
Quando cambia un prodotto (es. prezzo, nome, descrizione), non si aggiornano direttamente i valori, ma si crea una nuova versione con un timestamp valido:Così puoi sempre risalire alle condizioni del prodotto al momento della vendita.
-
Archiviazione logica
Per alleggerire la tabella principale, si può spostare il record in una tabellaarchived_products, mantenendo la FK valida:Ma questo si fa solo se si garantisce che
order_items.product_idpunti comunque a una copia coerente dei dati.
Quindi, nel nostro schema (aggiornamento alla sezione “products”):
products
order_items
🔒
ON DELETE RESTRICTimpedisce la cancellazione di un prodotto se esistono righeorder_itemscollegate, evitando inconsistenze storiche.
Best practice: mai cancellare “fatti” o “istanze storiche”
Puoi memorizzarlo come regola di modellazione:
Regola aurea:
Le entità che rappresentano “stati correnti” (es. un carrello temporaneo) possono essere cancellate.
Le entità che rappresentano “fatti avvenuti” (ordini, vendite, fatture, partecipazioni) non vanno mai cancellate.
Le entità collegate a “fatti avvenuti” (prodotti, clienti, account) non vanno cancellate, ma eventualmente disattivate.
Alternative avanzate: Soft Delete con timestamp
In sistemi enterprise o GDPR-friendly si preferisce usare:
Quando il prodotto viene disattivato:
E le query operative filtrano con:
Esempio completo: comportamento previsto
| Azione | Effetto |
|---|---|
| Cliente acquista un prodotto | Viene creato order_items con product_id, unit_price e total_line salvati. |
| Prodotto viene ritirato dal catalogo | is_active → FALSE (non appare più nelle ricerche), ma resta nel DB. |
| Query storica degli ordini | Tutti gli ordini mostrano ancora il nome e il prezzo corretto (possiamo anche storicizzare unit_price). |
Tentativo di DELETE FROM products |
Fallisce se ci sono order_items → grazie a ON DELETE RESTRICT. |
Peculiarità e Spunti Applicativi
Questi due blocchi di CREATE TABLE sono un caso di studio perfetto per comprendere come le scelte di progettazione influenzano il comportamento e l’affidabilità di un database relazionale. Analizziamo i tre motivi principali per cui questo schema è interessante e realistico.
1️⃣ Il contrasto tra CASCADE e RESTRICT
Questa coppia di vincoli mostra due filosofie opposte ma complementari:
-
CASCADE (per
order_id):
Se un ordine viene cancellato, tutte le sue righe inorder_itemsvengono eliminate automaticamente.
Ha perfettamente senso: le righe dell’ordine non hanno significato senza l’ordine a cui appartengono. Sono parte dello stesso “fatto” e condividono la stessa vita nel database. -
RESTRICT (per
product_id):
Qui la logica cambia completamente. Se provi a eliminare un prodotto che compare in uno qualunque degliorder_items, il database rifiuterà l’operazione.
Questa è una protezione attiva dell’integrità storica. A livello applicativo, puoi marcare il prodotto come non disponibile (is_active = false), ma non devi mai cancellarlo fisicamente.🔒 È la regola del “soft delete” applicato a livello logico, rinforzato dal database.
L’applicazione decide cosa mostrare, il DB decide cosa non si può distruggere.
2️⃣ La Denormalizzazione Intelligente (unit_price)
Un principiante potrebbe chiedere:
“Perché mettere
unit_priceinorder_itemsse il prezzo è già nella tabellaproducts?”
Domanda giustissima — ma la risposta è illuminante:
Questa non è una violazione della normalizzazione, è una denormalizzazione consapevole.
Il prezzo di un prodotto può cambiare nel tempo (sconti, saldi, variazioni stagionali).
Quando un cliente effettua un acquisto, bisogna “fotografare” il prezzo di quel momento.
-
products.price→ rappresenta il prezzo attuale. -
order_items.unit_price→ rappresenta il prezzo storico al momento della vendita.
Senza questo campo, ogni modifica ai prezzi renderebbe incoerenti le vecchie fatture:
tutti gli ordini cambierebbero prezzo magicamente ogni volta che aggiorni il catalogo.
Un errore concettuale e contabile gravissimo.
💡 Denormalizzare consapevolmente significa sapere quando vale la pena sacrificare un po’ di purezza teorica per garantire coerenza storica e correttezza dei dati.
3️⃣ La gestione del Soft Delete (is_active)
L’uso combinato di is_active e ON DELETE RESTRICT crea una doppia protezione:
-
is_active(livello applicativo):
L’applicazione filtra e mostra solo i prodotti attivi.
Nessun utente vedrà prodotti obsoleti o ritirati. -
ON DELETE RESTRICT(livello database):
Impedisce che qualcuno, magari per errore o tramite uno script, cancelli fisicamente il prodotto.
Così il database rimane coerente e lo storico degli ordini intatto.
È una strategia “cintura e bretelle”:
anche se il livello applicativo fallisce, il DB garantisce che la cronologia delle transazioni resti integra.
In sintesi
Questo mini-scenario orders / order_items / products riassume le regole auree di un buon schema relazionale:
| Principio | Applicazione pratica |
|---|---|
| Integrità referenziale | Vincoli CASCADE e RESTRICT usati con criterio |
| Storicitazione dei dati | Denormalizzazione consapevole (unit_price) |
| Protezione logica e fisica | Combinazione is_active + ON DELETE RESTRICT |
| Evolvibilità | Tutte le entità hanno ID surrogato come PK |
| Coerenza applicativa | Nessuna cancellazione di dati storici |
Errori Comuni nella Progettazione di un Database Relazionale
Ogni database nasce con le migliori intenzioni… ma basta una piccola disattenzione nella progettazione per creare problemi enormi nel tempo: dati incoerenti, query lente, cancellazioni accidentali o difficoltà di manutenzione.
Ecco gli errori più frequenti — e come evitarli.
1️⃣ Usare chiavi naturali invece di chiavi surrogate
❌ Errore: usare campi “logici” come chiave primaria — ad esempio
codice_fiscale,numero_ordine.
All’inizio sembra comodo (“tanto è univoco!”), ma presto diventa un incubo:
-
L’utente può cambiare email.
-
Un ordine può essere importato con un nuovo numero.
-
Un codice fiscale può essere errato o duplicato per test.
✅ Soluzione:
Usa sempre chiavi surrogate, di solito un campo id auto-increment (INT AUTO_INCREMENT o SERIAL).
Le chiavi naturali restano comunque uniche, ma diventano vincoli secondari (UNIQUE), non la PK.
2️⃣ Usare DELETE CASCADE ovunque
❌ Errore: impostare ON DELETE CASCADE in tutte le relazioni “per comodità”.
Questo è pericoloso: potresti cancellare un ordine e perdere anche i dati storici collegati a prodotti o utenti.
Il CASCADE va usato solo dove una relazione è di dipendenza forte (es. order_items dipende da orders).
✅ Soluzione:
Usa CASCADE solo per tabelle che non hanno significato autonomo.
Per tutto il resto, preferisci RESTRICT o SET NULL, e usa soft delete (is_active, deleted_at, ecc.) a livello applicativo.
3️⃣ Ignorare la normalizzazione (o applicarla in modo cieco)
❌ Errore 1: mettere tutti i dati in un’unica tabella (es. utenti e ordini insieme).
❌ Errore 2: spingersi alla 5ª forma normale “a tutti i costi”.
Entrambi gli estremi sono dannosi:
-
Se non normalizzi, i dati si duplicano e diventano incoerenti.
-
Se normalizzi troppo, il modello diventa inutilmente complesso e le query rallentano.
✅ Soluzione:
Normalizza fino alla 3ª forma normale (3NF).
Dopo di che valuta caso per caso: a volte una denormalizzazione controllata (come unit_price) è la scelta migliore.
4️⃣ Dimenticare la cronologia dei dati
❌ Errore: aggiornare direttamente i valori “vivi” (es. cambiare il prezzo di un prodotto senza storicizzare).
Così si perde la traccia temporale dei cambiamenti, e le analisi diventano impossibili.
✅ Soluzione:
Aggiungi sempre almeno uno di questi elementi:
-
created_at,updated_at,deleted_at -
Campi storici (es.
unit_price,status_at_order_time) -
Tabelle di audit o log (
order_history,price_history)
5️⃣ Non distinguere tra dati attivi e dati storici
❌ Errore: cancellare fisicamente record “vecchi”.
Mai cancellare i dati legati a eventi passati: ordini, transazioni, contratti.
La storia è parte del valore informativo del database.
✅ Soluzione:
Implementa un soft delete o un flag is_active.
Il record resta nel database ma è escluso dalle query operative.
6️⃣ Non documentare le regole di business nel modello
❌ Errore: pensare che lo schema si spieghi da solo.
Dopo pochi mesi, nessuno ricorderà perché hai usato RESTRICT invece di CASCADE, o cosa rappresenta davvero unit_price.
✅ Soluzione:
Documenta sempre nel codice SQL o nel manuale tecnico:
-
I motivi delle scelte di chiavi e vincoli
-
Le logiche di aggiornamento e cancellazione
-
Le relazioni forti e deboli tra entità
Un buon schema documentato vale quanto un buon codice commentato.
Esercizio Pratico Finale: “Progetta il Database di una Libreria Online”
Obiettivo
Creare da zero un modello relazionale per una libreria online, capace di gestire:
-
Catalogo libri e autori
-
Clienti e ordini
-
Prezzi storici e disponibilità
-
Conservazione dello storico (nessuna cancellazione reale)
1. Identificazione delle Entità
Entità principali (forti):
-
books— rappresenta ogni libro nel catalogo -
authors— rappresenta gli autori -
customers— rappresenta i clienti registrati -
orders— rappresenta gli ordini effettuati
Entità derivate (deboli o di relazione):
-
order_items— connette un ordine ai libri acquistati -
book_authors— connette i libri ai loro autori (relazione molti-a-molti)
2. Scelta delle Chiavi
| Tipo di chiave | Regola | Esempio |
|---|---|---|
| Primaria (PK) | Sempre surrogata (id auto-increment) |
book_id, author_id, order_id, customer_id |
| Esterna (FK) | Sempre referenzia PK di un’altra tabella | order_items.order_id → orders.order_id |
| Univoca (UNIQUE) | Per chiavi naturali (es. ISBN, email) | books.isbn o customers.email |
| Soft delete | Campo booleano o timestamp | is_active o deleted_at |
3. Modellazione delle Tabelle (Schema SQL)
🔸 Tabella authors
🔸 Tabella books
🔸 Tabella di relazione book_authors (molti-a-molti)
✳️ Il libro può essere cancellato (insieme alle relazioni),
ma l’autore rimane: ON DELETE RESTRICT protegge l’integrità biografica.
🔸 Tabella customers
🔸 Tabella orders
✅ CASCADE ha senso: se un cliente viene cancellato (evento raro, ma possibile),
i suoi ordini vengono rimossi insieme. Gli ordini dipendono dall’esistenza del cliente.
🔸 Tabella order_items
📘 Come nel caso “products / order_items”:
order_id→ CASCADE (dipendenza forte)
book_id→ RESTRICT (storico protetto)
unit_price→ denormalizzazione intelligente per preservare il prezzo dell’epoca
4. Analisi delle Scelte
| Aspetto | Decisione progettuale | Motivazione |
|---|---|---|
| Chiavi | Tutte surrogate (ID) | Uniformità, semplicità, indipendenza logica |
| Integrità referenziale | CASCADE per ordini, RESTRICT per libri | Coerenza storica |
| Soft delete | is_active in tabelle principali |
Non si perde mai lo storico |
| Denormalizzazione | unit_price in order_items |
Congela i prezzi |
| Molti-a-molti | book_authors |
Correttezza semantica |
| Audit logico | created_at |
Tracciabilità temporale |
Questo esercizio racchiude in miniatura tutti i principi fondamentali della modellazione relazionale:
-
Entità forti e deboli ben distinte
-
Chiavi surrogate e FK coerenti
-
Denormalizzazione ragionata dove serve
-
Integrità storica garantita da RESTRICT e soft delete
-
Scalabilità e leggibilità semantica del modello
💬 “Il database non è solo un contenitore di dati, ma la mappa logica di un sistema reale.”
Quando lo progetti bene, non serve ricordare le regole: le applica da solo.
In Conclusione
Progettare un database relazionale non è solo disegnare tabelle:
è tradurre regole del mondo reale in vincoli e comportamenti coerenti nel tempo.
Ogni scelta — chiavi, relazioni, cancellazioni, cronologia — è una decisione semantica, non solo tecnica.
“Un database ben progettato non è quello che funziona subito,
ma quello che continua a funzionare dopo cinque anni di cambiamenti.”





