Oggi è sabato 19 settembre 2026, le lezioni del primo semestre sono appena cominciate: scrivere adesso di Basi di dati sembra in anticipo, l’esame si dà quasi ovunque nella sessione invernale di gennaio, quattro mesi da qui. Ma è proprio per questo che il post esce ora. Basi di dati è l’esame che si perde per accumulo, non per difficoltà: il modello ER che si studia a ottobre è quello che serve a dicembre per scrivere le query, e chi lo salta riparte da zero sotto esame. Molti corsi mettono in mezzo anche una prova intermedia a novembre — se il tuo corso ce l’ha, la finestra utile è già fra sei e otto settimane, non quattro mesi.
Sul blog c’è già C e puntatori, Python, React, ma niente su database e SQL — lacuna strana, perché Basi di dati è uno degli esami più trasversali: lo trovano ingegneria informatica, informatica, statistica, economia con un piano di informatica. Questo post copre l’esame per intero: modello concettuale, traduzione in schema relazionale, normalizzazione, SQL.
Il modello concettuale: entità, relazioni, cardinalità
Il modello Entità-Relazione (ER) descrive la realtà che il database deve rappresentare, prima di scrivere una sola riga di SQL. I mattoni sono pochi:
- Entità: un oggetto del dominio che ha senso trattare a sé — uno Studente, un Corso, un Libro.
- Attributi: le proprietà di un’entità (nome, email, anno di pubblicazione). Alcuni formano la chiave: il sottoinsieme minimo di attributi che identifica univocamente un’istanza.
- Relazioni: legami tra entità — uno Studente si iscrive a un Corso, un Autore scrive un Libro.
- Cardinalità: quante istanze di un’entità possono essere associate a quante istanze dell’altra. Le tre forme sono 1:1, 1:N e N:N.
La parte dove si sbaglia davvero non è la sintassi del diagramma, è leggere le specifiche in italiano e decidere la cardinalità giusta. “Un docente insegna in più corsi, un corso ha un solo docente titolare” è 1:N. “Uno studente si iscrive a più corsi, un corso ha più studenti iscritti” è N:N. Trucco pratico: per ogni lato della relazione chiedetevi “un elemento di qui può stare associato a più di uno di là?” — sì da entrambi i lati vuol dire N:N, sì da un solo lato vuol dire 1:N dal lato che risponde sì.
Altri due concetti che tornano in ogni esame:
- Generalizzazione/specializzazione: un’entità padre (per esempio Persona) con entità figlie che ne ereditano gli attributi e ne aggiungono di propri (Studente, Docente).
- Entità debole: un’entità che non ha una chiave propria e si identifica solo insieme all’entità da cui dipende (per esempio le Rate di un Pagamento, identificate dal numero di rata insieme al pagamento a cui appartengono).
Dall’ER allo schema relazionale: regole meccaniche
Tradurre un diagramma ER in tabelle non è un’arte, è l’applicazione di regole fisse. Impararle a memoria elimina metà degli errori d’esame, perché la difficoltà si sposta tutta sul modello ER a monte.
- Relazione 1:N → la chiave primaria del lato “1” diventa chiave esterna nella tabella del lato “N”. Un Corso (1) e i suoi Studenti iscritti (N) via un docente titolare: la chiave del Docente va nella tabella Corso, non il contrario.
- Relazione N:N → serve una tabella ponte con chiave composta dalle chiavi primarie delle due entità coinvolte (più eventuali attributi propri della relazione, come la data di iscrizione).
- Relazione 1:1 → o si fondono le due entità in un’unica tabella, oppure si mette la chiave dell’una come chiave esterna nell’altra con un vincolo di unicità (
UNIQUE), per impedire che diventi di fatto una 1:N. - Generalizzazione → tre strategie: accorpare tutto nel padre con una colonna che distingue il tipo; una tabella per ogni figlio con gli attributi ereditati duplicati (nessun padre); oppure padre più una tabella per figlio in 1:1. Dipende da quanto gli attributi dei figli differiscono — se molto, l’ultima strategia evita colonne piene di NULL.
Normalizzazione: eliminare le anomalie, non applicare regole a memoria
La normalizzazione ha senso solo se la si vede all’opera su una tabella che fa male. Partiamo da una tabella denormalizzata di iscrizioni universitarie:
| matricola | nome_studente | email_studente | codice_corso | nome_corso | docente | cfu | voto |
|---|
Le dipendenze funzionali (cosa determina cosa) sono:
matricola→nome_studente,email_studentecodice_corso→nome_corso,docente,cfu(matricola, codice_corso)→voto
Questa tabella è già in 1NF (valori atomici, niente liste dentro una cella), ma non in 2NF: nome_corso, docente e cfu dipendono solo da codice_corso, cioè da parte della chiave composta (matricola, codice_corso), non dalla chiave intera. Le anomalie concrete:
- Anomalia di aggiornamento: se un corso cambia docente, bisogna aggiornare la riga di ogni studente iscritto a quel corso — dimenticarne una lascia il database incoerente.
- Anomalia di inserimento: non si può registrare un nuovo corso (nome, docente, cfu) finché almeno uno studente non vi si iscrive, perché il corso non ha una riga propria.
- Anomalia di cancellazione: se l’unico studente iscritto a un corso si ritira e la sua riga viene eliminata, si perde anche l’informazione sul corso stesso.
La scomposizione in 3NF separa le entità che dipendono da chiavi diverse:
Studente(matricola PK, nome_studente, email_studente)
Corso(codice_corso PK, nome_corso, docente, cfu)
Iscrizione(matricola FK, codice_corso FK, voto, PRIMARY KEY(matricola, codice_corso))
Ogni anomalia sparisce con il passo che la riguarda: separare Studente elimina l’anomalia di aggiornamento sull’email; separare Corso elimina insieme quella di inserimento (un corso esiste anche senza iscritti) e quella di cancellazione (cancellare un’iscrizione non tocca la tabella Corso). Questo schema è anche in Boyce-Codd (BCNF), perché l’unica dipendenza residua in Iscrizione ha come determinante l’intera chiave.
SQL: dalla SELECT alle subquery
L’ordine logico di una query
Prima della sintassi, un fatto che risolve metà dei dubbi sugli alias: una query viene valutata concettualmente in quest’ordine, non nell’ordine in cui si scrive:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
Ecco perché un alias definito nella SELECT non è utilizzabile nella WHERE (la WHERE viene valutata prima che l’alias esista) ma lo è nella ORDER BY (che viene valutata dopo).
JOIN: INNER e LEFT
L’INNER JOIN restituisce solo le righe che trovano corrispondenza in entrambe le tabelle. Il LEFT JOIN restituisce tutte le righe della tabella di sinistra, con NULL al posto dei campi della tabella di destra quando non c’è corrispondenza. La differenza conta ogni volta che serve includere “anche chi non ha fatto nulla” — un utente senza prestiti, un corso senza iscritti: con l’INNER JOIN quelle righe spariscono, con il LEFT JOIN restano, con i campi mancanti a NULL.
GROUP BY, HAVING, e perché non è come WHERE
WHERE filtra le righe prima di raggrupparle; HAVING filtra i gruppi dopo l’aggregazione. “Studenti con più di 3 esami sostenuti” è una condizione su un aggregato (COUNT(*)), quindi va in HAVING, non in WHERE — mettere COUNT(*) > 3 dentro una WHERE è un errore di sintassi, perché in quel punto della valutazione l’aggregazione non esiste ancora.
Subquery, EXISTS, e la trappola di NOT IN con i NULL
Una subquery può stare nella WHERE (correlata, se fa riferimento a una colonna della query esterna, o non correlata) o nella FROM come tabella derivata. EXISTS verifica solo che la subquery restituisca almeno una riga, senza guardarne i valori — spesso più efficiente di IN su grandi tabelle. La trappola più insidiosa è NOT IN: se la subquery restituisce anche un solo valore NULL, l’intera condizione NOT IN smette di dare risultati, perché confrontare qualunque cosa con NULL produce UNKNOWN, mai TRUE. La soluzione robusta è NOT EXISTS, che non ha questo problema.
Due esercizi come quelli veri
(a) Da un caso in italiano allo schema e alle query
Traccia: una piccola biblioteca gestisce utenti, libri, autori e prestiti. Un libro può avere più autori e un autore può aver scritto più libri. Un utente può prendere in prestito più libri nel tempo, anche lo stesso libro in date diverse; ogni prestito ha una data di inizio e, se il libro è stato restituito, una data di restituzione.
Modello ER: entità Utente, Libro, Autore; relazione N:N Scrive fra Autore e Libro; l’associazione fra Utente e Libro non è una semplice relazione N:N, perché lo stesso utente può prendere lo stesso libro più volte — va modellata con un’entità propria, Prestito, con la sua chiave e i suoi attributi.
Schema relazionale:
Utente(id_utente PK, nome, email)
Libro(isbn PK, titolo, anno_pubblicazione)
Autore(id_autore PK, nome)
Scrive(id_autore FK, isbn FK, PRIMARY KEY(id_autore, isbn))
Prestito(id_prestito PK, isbn FK, id_utente FK, data_prestito, data_restituzione)
Scrive è la tabella ponte della relazione N:N; Prestito ha una chiave propria (id_prestito) perché la coppia utente-libro da sola non basterebbe a distinguere due prestiti diversi dello stesso libro.
Query 1 — titolo del libro e nome dell’utente per ogni prestito ancora aperto (non restituito):
SELECT l.titolo, u.nome
FROM Prestito p
JOIN Libro l ON p.isbn = l.isbn
JOIN Utente u ON p.id_utente = u.id_utente
WHERE p.data_restituzione IS NULL;
Query 2 — per ogni utente, il numero totale di prestiti fatti, includendo anche chi non ne ha mai fatto nessuno:
SELECT u.nome, COUNT(p.id_prestito) AS numero_prestiti
FROM Utente u
LEFT JOIN Prestito p ON u.id_utente = p.id_utente
GROUP BY u.id_utente, u.nome
ORDER BY numero_prestiti DESC;
Il LEFT JOIN è obbligatorio qui: con un JOIN semplice gli utenti senza prestiti sparirebbero dal risultato invece di comparire con 0. E COUNT(p.id_prestito), non COUNT(*): la differenza è il prossimo esercizio.
(b) Il tranello di COUNT(*) con un LEFT JOIN
Riprendiamo la query 2 sopra, ma con un obiettivo diverso: quali utenti hanno fatto almeno un prestito. Una versione che sembra corretta a prima vista:
SELECT u.nome
FROM Utente u
LEFT JOIN Prestito p ON u.id_utente = p.id_utente
GROUP BY u.id_utente, u.nome
HAVING COUNT(*) > 0;
Il risultato di questa query è sbagliato: restituisce tutti gli utenti, compresi quelli senza prestiti. Il motivo è che COUNT(*) conta le righe del gruppo, e un LEFT JOIN senza corrispondenza produce comunque una riga (con i campi di Prestito a NULL) — quindi ogni utente ha almeno una riga nel proprio gruppo, e COUNT(*) vale sempre almeno 1.
La versione corretta usa COUNT su una colonna che può essere NULL, tipicamente la chiave della tabella a destra del LEFT JOIN:
SELECT u.nome
FROM Utente u
LEFT JOIN Prestito p ON u.id_utente = p.id_utente
GROUP BY u.id_utente, u.nome
HAVING COUNT(p.id_prestito) > 0;
COUNT(colonna) ignora i NULL, quindi per un utente senza prestiti vale 0 e la riga viene esclusa dalla HAVING. È lo stesso principio della trappola di NOT IN: i NULL prodotti da un LEFT JOIN sono silenziosi, e COUNT(*) non li vede come mancanti.
Gli errori che tornano ogni sessione
- Aggregare senza
GROUP BY. Mescolare inSELECTuna colonna non aggregata con una funzione comeCOUNToAVGsenza raggruppare: alcuni motori lo permettono e restituiscono un valore arbitrario, altri (come PostgreSQL) lo rifiutano con un errore — imparate a leggerlo, non a disattivarlo. - Mettere una condizione sull’aggregato in
WHEREinvece che inHAVING. Visto sopra: al momento dellaWHEREl’aggregazione non esiste ancora. - Dimenticare
DISTINCTdopo un JOIN che duplica righe. Un JOIN su una relazione N:N moltiplica le righe della query esterna: contare o sommare senzaDISTINCT(dove serve) gonfia il risultato. - La “divisione relazionale”: quesiti come “trova gli studenti che hanno sostenuto tutti gli esami di un certo piano di studi” non si risolvono con un semplice filtro, ma con la doppia negazione — “non esiste un esame del piano che lo studente NON abbia sostenuto”:
SELECT s.matricola
FROM Studente s
WHERE NOT EXISTS (
SELECT 1 FROM Corso c
WHERE c.piano = 'Informatica'
AND NOT EXISTS (
SELECT 1 FROM Iscrizione i
WHERE i.matricola = s.matricola AND i.codice_corso = c.codice_corso
)
);
COUNT(*)controCOUNT(colonna): il primo conta le righe, il secondo conta i valori nonNULLdi quella colonna. Sono uguali solo se la colonna non ha maiNULL— altrimenti no, come nell’esercizio (b).
Un metodo di studio che funziona davvero
Basi di dati si impara facendo, non leggendo la teoria una seconda volta. Due abitudini valgono più di ogni altra:
- Progettate almeno tre schemi completi a mano prima dell’appello — dalla traccia in italiano fino allo schema relazionale con chiavi primarie ed esterne, come nell’esercizio (a). È l’unico modo per allenare la lettura delle cardinalità, che è dove si perdono più punti.
- Scrivete davvero le query su un database, non solo sulla carta. SQLite basta e si installa in due minuti: create le tabelle, inserite qualche riga finta, e verificate che la query restituisca esattamente quello che pensavate. Un errore di sintassi lo trovate subito; un errore di logica (come quello di
COUNT(*)sopra) lo trovate solo eseguendo e controllando il risultato riga per riga.
Come lavoriamo in lezione
Con chi prepara Basi di dati parto quasi sempre dal modello ER, non dall’SQL: se la lettura delle cardinalità non è solida, ogni query costruita sopra uno schema sbagliato produce un risultato che sembra plausibile e non lo è. Le lezioni sono soprattutto esercizi — una traccia, uno schema da disegnare insieme, query da scrivere ed eseguire davvero — perché è lì che emergono gli errori di lettura, non nella spiegazione della teoria.
Non esiste, per ora, una simulazione cronometrata di Basi di dati o di informatica sul sito: la simulazione universitaria che ho costruito copre matematica e fisica, non basi di dati — lo dico con onestà, senza promettere una simulazione che non c’è ancora. Vale comunque la frase che ripeto per tutte le altre: la risposta corretta è gratis, la soluzione guidata si sblocca con la prima lezione.
Se il modello ER o le query con JOIN e subquery sono il punto dove vi bloccate, o se la prova intermedia di novembre è già vicina, scrivetemi e ne parliamo: la prima lezione serve a capire se il problema è il modello concettuale, la traduzione in tabelle o la sintassi SQL — sono tre blocchi diversi e si sbloccano in modo diverso. La panoramica completa delle lezioni di informatica copre anche C, Python e web development, per chi ha più di un esame da mettere in fila questo semestre.