CREATE DATABASE
Descrizione
L'istruzione CREATE DATABASE è il fondamento di ogni progetto database in MySQL e MariaDB. Questo comando crea un nuovo database, che funziona come contenitore principale per tutti gli oggetti del database quali tabelle, viste, stored procedure e trigger. Il database rappresenta il livello più alto di organizzazione dei dati e deve essere pianificato con attenzione prima di procedere con la creazione degli oggetti contenuti.
Quando crei un database, MySQL genera automaticamente una directory nel file system del server con il nome specificato. All'interno di questa directory verranno memorizzati tutti i file relativi alle tabelle e agli indici creati successivamente. È importante scegliere un nome significativo e conforme alle convenzioni di naming del tuo progetto, poiché modificare il nome di un database esistente richiede operazioni complesse che possono influenzare le applicazioni connesse.
La clausola opzionale IF NOT EXISTS è particolarmente utile negli script di installazione e nei processi di deployment automatizzati. Questa clausola previene la generazione di errori nel caso in cui il database esista già, permettendo allo script di continuare l'esecuzione senza interruzioni. Senza questa clausola, un tentativo di creare un database già esistente genererebbe un errore che arresterebbe l'esecuzione dello script.
Sintassi
CREATE DATABASE [IF NOT EXISTS] nome_database;
Esempi Pratici
CREATE DATABASE azienda;
Spiegazione: Questo comando crea un nuovo database denominato "azienda". Dopo l'esecuzione, il database sarà visibile nell'elenco dei database disponibili e potrà essere utilizzato per creare tabelle e altri oggetti. È la forma più semplice di creazione database, adatta quando si è sicuri che il database non esista già e non si hanno requisiti specifici per il character set.
CREATE DATABASE IF NOT EXISTS ecommerce;
Spiegazione: Questo comando tenta di creare il database "ecommerce" solo se non esiste già. La clausola IF NOT EXISTS previene la generazione di un errore se il database è già presente. Questa forma è particolarmente utile negli script di setup che potrebbero essere eseguiti più volte, come negli ambienti di sviluppo, nei container Docker, o nei processi di deployment automatizzato. Senza questa clausola, l'esecuzione dello script si arresterebbe con un errore di "database already exists".
CREATE DATABASE applicazione_web CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Spiegazione: Questo comando crea un database ottimizzato per memorizzare testo in più lingue, inclusi caratteri speciali e emoji. Il character set utf8mb4 è la codifica Unicode completa che supporta tutti i caratteri esistenti, inclusi quelli delle lingue asiatiche e i simboli emoji. La collazione utf8mb4_unicode_ci garantisce confronti case-insensitive (la "A" e la "a" sono considerate uguali) e ordinamenti linguistici corretti secondo le regole Unicode. Questa configurazione è lo standard consigliato per tutte le nuove applicazioni moderne.
Nota Operativa
Per iniziare a utilizzare il database appena creato, devi selezionarlo con l'istruzione USE nome_database. Questa selezione rimane attiva per tutta la sessione corrente del client MySQL. In molte applicazioni, la stringa di connessione specifica già il database da utilizzare, rendendo il comando USE necessario solo durante le operazioni manuali di amministrazione.
Best Practice
- Convenzioni di naming: Utilizza nomi descrittivi in minuscolo con underscore per separare le parole (es.
gestione_magazzinoinvece diGMogestionemagazzino) - Character set: Definisci sempre il character set e la collazione a livello di database per garantire coerenza con le applicazioni che supportano Unicode
- Backup: Dopo la creazione, configura immediatamente le procedure di backup per proteggere i dati
- Privilegi: Assegna i privilegi appropriati agli utenti subito dopo la creazione per garantire la sicurezza dei dati
CREATE TABLE
Descrizione
Crea una nuova tabella nel database, definendo il nome delle colonne, i tipi di dati e i vincoli (constraints).
📊 Tabella Riassuntiva dei Tipi di Dati SQL (MySQL/MariaDB)
Quando crei una tabella, ogni colonna deve avere un tipo di dato. Questo dice al database che tipo di informazioni può contenere (numeri, testi, date...). Scegliere il tipo giusto è fondamentale per risparmiare spazio e velocizzare le query. Ecco la guida completa con esempi reali e spiegazioni approfondite.
🔢 Numeri Interi (senza virgola) e decimali
| Tipo | Dimensione | Intervallo (con segno) | Intervallo UNSIGNED | Esempio d'uso |
|---|---|---|---|---|
TINYINT | 1 byte | -128 a 127 | 0 a 255 | Età, voto scolastico (0-10), booleano |
SMALLINT | 2 byte | -32.768 a 32.767 | 0 a 65.535 | Quantità di prodotti in magazzino |
INT | 4 byte | -2 miliardi a +2 miliardi | 0 a 4 miliardi | ID utente, contatori normali |
BIGINT | 8 byte | -9,2×10¹⁸ a +9,2×10¹⁸ | 0 a 1,8×10¹⁹ | Visualizzazioni pagina, ID per sistemi enormi |
💰 Numeri con la Virgola (decimali)
| Tipo | Precisione | A cosa serve | Esempio |
|---|---|---|---|
DECIMAL(p,s) | Esatta (massimo 65 cifre totali) | Soldi, prezzi, voti precisi | DECIMAL(5,2) → max 999.99 |
FLOAT | Approssimata (~7 cifre) | Misure scientifiche, coordinate | 3.1415927 (pi greco) |
DOUBLE | Approssimata (~15 cifre) | Calcoli più precisi | 3.14159265358979 |
UNSIGNED?Di default i numeri interi possono essere positivi o negativi. Aggiungendo
UNSIGNED dici al database: "questo numero sarà sempre ≥ 0". In questo modo raddoppi il massimo positivo (perché non usi spazio per il segno). È perfetto per ID, quantità, età, ecc.Esempio:
eta TINYINT UNSIGNED → può andare da 0 a 255, ideale per l'età.
DECIMAL(p,s)?•
p = precisione totale (cifre totali prima + dopo la virgola)•
s = scala (cifre dopo la virgola)Esempi reali:
→
DECIMAL(4,2) → 2 cifre dopo la virgola, totale 4 cifre → max 99,99 (utile per prezzi piccoli)→
DECIMAL(2,1) → 1 cifra dopo la virgola, totale 2 cifre → max 9,9 (ottimo per i voti scolastici da 0 a 10, ad esempio 9.5)→
DECIMAL(5,2) → 2 cifre dopo la virgola, totale 5 cifre → max 999,99 ideale per prezzi inferiori a 1000€DECIMAL! FLOAT e DOUBLE introducono errori di arrotondamento (es. 0.1 + 0.2 potrebbe dare 0.30000000000000004).
| Tipo | Lunghezza massima | Caratteristiche | Quando usarlo |
|---|---|---|---|
CHAR(n) | fissa (max 255) | Occupare sempre n caratteri (riempiendo con spazi) | Codici a lunghezza fissa: codice fiscale (16 caratteri), targa (7 caratteri), sesso (1 carattere: 'M'/'F'), stato (2 caratteri: esempio IT). È più veloce di VARCHAR per ricerche esatte ma va usato solo per testi di cui si conosce il numero delle lettere altrimenti ad esempio se uso char(10) per un campo ed inserisco nel campo una parola di 2 lettere, avrò comunque occupato lo spazio di 10 lettere . |
VARCHAR(n) | variabile (max 65.535) | Nel data base occupa solo lo spazio dei caratteri effettivi + 1 o 2 byte necessari per indicare la lunghezza della stringa | Nomi, cognomi, email, indirizzi – testi di lunghezza variabile. |
TEXT | 65.535 caratteri | Testo lungo | Articoli, descrizioni lunghe, commenti. |
LONGTEXT | 4 GB | Testo lunghissimo | Libri, documenti, log. |
ENUM('val1','val2') | fino a 65.535 valori | Lista chiusa di opzioni | esempi: Stato ordine ('annullato','in lavorazione','spedito','consegnato'), Esito scrutinio ('Promosso','Rimandato','Bocciato'). |
Immagina di memorizzare un codice fiscale italiano: è sempre esattamente 16 caratteri. Se usi
VARCHAR(16), ogni codice occuperà 16 caratteri + 1 byte per indicare il numero di caratteri . Con CHAR(16) occupa esattamente 16 byte (non è necessario il byte aggiuntivo per indicare il numero di caratteri) ed è più veloce per le ricerche perché il database sa che tutti i valori hanno la stessa lunghezza. Quindi:→ CHAR: lunghezza fissa (es. codici, hash, flag).
→ VARCHAR: lunghezza variabile (es. nomi, indirizzi).
📅 Date e Orari – Formati e utilizzo
| Tipo | Formato SQL | Esempio di inserimento | Descrizione |
|---|---|---|---|
DATE | 'YYYY-MM-DD' | '2026-03-26' | Solo data (anno-mese-giorno). Usalo per date di nascita, scadenze. |
TIME | 'HH:MM:SS' | '14:30:00' | Solo ora (ore:minuti:secondi). Per durate o orari fissi. |
DATETIME | 'YYYY-MM-DD HH:MM:SS' | '2026-03-26 14:30:00' | Data e ora insieme. Non cambia con il fuso orario. Ideale per eventi. |
TIMESTAMP | 'YYYY-MM-DD HH:MM:SS' | '2026-03-26 14:30:00' | Memorizza un istante (secondi dal 1970). Viene convertito automaticamente nel fuso orario della connessione. Ottimo per "creato il" o "modificato il". |
YEAR | 'YYYY' | '2026' | Solo anno (4 cifre). Per campi come "anno di laurea". |
Le date e gli orari vanno sempre scritti tra apici singoli, nel formato standard ISO 8601 aaaa/mm/gg ove a è cifra per anno, m cifra per mese, g cifra per giorno (es.
'2026-03-26' per DATE per indicare il 26/03/2026, '2026-03-26 14:30:00' per DATETIME). Questo formato è universale e non crea ambiguità.Esempio di inserimento:
INSERT INTO eventi (nome, data_evento, ora_inizio) VALUES ('Conferenza', '2026-03-26', '14:30:00');
• DATETIME: memorizza la data/ora esatta come la vedi. Non viene modificata in base al fuso orario. Perfetta per date di nascita, orari di eventi fissi, o quando devi preservare il valore originale.
• TIMESTAMP: memorizza un numero di secondi dal 1970 (UTC). Quando leggi il dato, MySQL lo converte automaticamente nel fuso orario della connessione. È ottimo per campi come
data_creazione e ultima_modifica, perché puoi usare la funzione CURRENT_TIMESTAMP e l’opzione ON UPDATE CURRENT_TIMESTAMP per aggiornarlo automaticamente.Esempio di tabella con TIMESTAMP auto‑gestito:
CREATE TABLE articoli ( id INT PRIMARY KEY, titolo VARCHAR(200), creato TIMESTAMP DEFAULT CURRENT_TIMESTAMP, modificato TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );
⚠️ Attenzione: TIMESTAMP ha un intervallo limitato (fino al 2038). Per date oltre il 2038 usa DATETIME.
• Usare un formato sbagliato: MySQL accetta solo
'YYYY-MM-DD'. Un formato come '26/03/2026' verrà interpretato come '2026-03-26' solo se la modalità SQL lo permette, ma non è affidabile.• Dimenticare gli apici:
WHERE data = 2026-03-26 causerà un errore perché senza apici viene visto come una sottrazione.• Confondere
DATETIME con TIMESTAMP quando si lavora con fusi orari: se la tua applicazione serve utenti in diverse time zone, TIMESTAMP è più comodo perché si adatta automaticamente.
✅ Booleani e Altri Tipi Speciali
| Tipo | Descrizione | Esempio |
|---|---|---|
BOOLEAN / BOOL | Alias di TINYINT(1).Invece di usare boolean usiamo TINYINT(1) che andrà ad indicare TRUE = 1, FALSE = 0. | attivo BOOLEAN DEFAULT TRUE |
BLOB | Binary Large Object – per file binari (immagini, PDF,video) | Meglio salvare il file su disco e memorizzare in tabella solo il percorso. |
JSON | Memorizza oggetti JSON validi. Supporta funzioni di interrogazione. | {"nome": "Mario", "preferenze": {"tema": "scuro"}} |
UUID | Identificatore univoco universale a 128 bit. | 550e8400-e29b-41d4-a716-446655440000 |
SET | Insieme di valori (puoi scegliere zero o più opzioni; quindi Usa ENUM quando il campo deve assumere esattamente uno tra un insieme fisso di valori come per esitoScrutinio, statoOrdine, Sesso; Usa SET quando il campo può contenere una combinazione di valori da un insieme prefissato come permessiFile, hobby, lingue parlate ma generalmente è meglio usare una tabella che contiene i dati considerati ). | Hobby('Lettura','Musica','Sport','Collezionismo'). |
MySQL non ha un tipo booleano nativo.
BOOLEAN è solo un sinonimo di TINYINT(1). Quando scrivi TRUE in una query, viene convertito in 1; FALSE diventa 0.Esempio pratico:
CREATE TABLE utenti ( id INT PRIMARY KEY, attivo BOOLEAN DEFAULT FALSE ); INSERT INTO utenti (id, attivo) VALUES (1, TRUE); -- Inserisce 1 SELECT * FROM utenti WHERE attivo; -- Corretto, restituisce i riga con attivo=1 SELECT * FROM utenti WHERE attivo = TRUE; -- Stessa cosa SELECT * FROM utenti WHERE attivo = 1; -- Equivalente
TINYINT(1) tecnicamente può contenere qualsiasi numero da 0 a 255 (se UNSIGNED). Per coerenza, usa solo 0/1 e sfrutta la sintassi TRUE/FALSE per rendere il codice più leggibile.
🔒 Vincoli (Constraints) – Regole per proteggere i dati
I vincoli definiscono regole aggiuntive per le colonne, garantendo l'integrità dei dati. Ecco i più importanti, con esempi:
NOT NULLLa colonna non può essere vuota (NULL).
Es:
nome VARCHAR(50) NOT NULLUNIQUEImpedisce duplicati.
Es:
email VARCHAR(100) UNIQUEDEFAULTValore predefinito se non specificato.
Es:
attivo BOOLEAN DEFAULT TRUEPRIMARY KEYIdentifica univocamente ogni riga (combina NOT NULL e UNIQUE).
Es:
CF char(16)PRIMARY KEY FOREIGN KEYCollega due tabelle (integrità referenziale).
Es:
CONSTRAINT fk_ordini_utenti FOREIGN KEY (utente_id) REFERENCES utenti(id)CHECKVerifica una condizione su una colonna (vincolo di dominio) o su più colonne della stessa riga (vincolo di tupla).
Es:
CONSTRAINT chk_utenti_eta CHECK (eta >= 18), CONSTRAINT chk_prenotazioni_date CHECK (data_inizio < data_fine)-- Esempio completo di tabella con vincoli CREATE TABLE utenti ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, data_registrazione DATETIME DEFAULT CURRENT_TIMESTAMP, attivo BOOLEAN DEFAULT TRUE, eta TINYINT UNSIGNED, CONSTRAINT chk_utenti_eta CHECK (eta >= 18) );
🏷️ Dare sempre un nome ai vincoli: CONSTRAINT nomeVincolo
Ogni vincolo di dominio, di tupla e di integrità referenziale va dichiarato con la parola chiave CONSTRAINT seguita dal nome, prima della clausola:
-- vincolo di dominio / di tupla CONSTRAINT chk_ordini_sconto CHECK (sconto >= 0 AND sconto <= prezzo_totale), -- vincolo di integrità referenziale CONSTRAINT fk_ordini_clienti FOREIGN KEY (cliente_id) REFERENCES clienti (id)
Convenzioni: chk_nomeTabella_nomeRegola per dominio e tupla, fk_tabellaReferenziante_tabellaReferenziata per le chiavi esterne.
Il nome del vincolo è importante perché permette di:
- Eliminare il vincolo:
ALTER TABLE ... DROP CONSTRAINT nomeVincolo(per le FK ancheDROP FOREIGN KEY, per i CHECK ancheDROP CHECK) richiede il nome; un vincolo senza nome riceve un nome generato dal DBMS, che va prima cercato. - Ispezionare le impostazioni: i nomi si interrogano in
INFORMATION_SCHEMA.TABLE_CONSTRAINTS,INFORMATION_SCHEMA.CHECK_CONSTRAINTSeINFORMATION_SCHEMA.KEY_COLUMN_USAGE; senza nome la ricerca diventa per tentativi. - Capire i messaggi di errore: MariaDB riporta il nome del vincolo violato (es.
CONSTRAINT 'chk_ordini_sconto' failed for 'negozio'.'ordini'), quindi si capisce subito quale regola è scattata; con nomi generati il messaggio è molto meno parlante. - Documentare lo schema: un nome esplicito dice quale regola di business protegge il vincolo.
La PRIMARY KEY non ha bisogno di un nome: in MySQL/MariaDB si chiama sempre PRIMARY.
🧱 Vincoli di Dominio e di Tupla (CHECK)
Oltre ai vincoli già elencati (PRIMARY KEY, NOT NULL, UNIQUE, FOREIGN KEY, DEFAULT), la CREATE TABLE può contenere i vincoli di dominio e i vincoli di tupla. Entrambi si scrivono con CHECK (...): cambia solo a cosa si applica la condizione. Sono vincoli intra-relazionali: riguardano una sola riga della tabella, mai più righe o tabelle diverse.
Vincolo di dominio — riguarda una sola colonna e limita i valori che quella colonna può assumere, cioè il "dominio" dell'attributo. Esempi: prezzo > 0, eta >= 18, esito IN ('Promosso','Rimandato','Bocciato'), email LIKE '%@%.%'. In parte il dominio è già fissato dal tipo di dato (TINYINT UNSIGNED non accetta negativi, DATE non accetta testo, ENUM accetta solo i valori elencati): il CHECK serve per i limiti che il tipo da solo non esprime.
Vincolo di tupla — riguarda più colonne della stessa riga e confronta i loro valori. Esempi: data_check_in < data_check_out, sconto BETWEEN 0 AND prezzo_totale, quantita > 0 OR stato = 'esaurito'. Non può guardare altre righe né altre tabelle: regole del tipo "lo stipendio non supera quello del capo" non sono esprimibili con un CHECK (servono trigger o logica applicativa).
I vincoli di dominio e di tupla vanno dichiarati dentro la
CREATE TABLE, insieme alle colonne: fanno parte della definizione dello schema esattamente come la chiave primaria e le chiavi esterne. Un vincolo aggiunto dopo con ALTER TABLE è sintatticamente corretto, ma non è la risposta attesa quando l'esercizio chiede di "creare la tabella con i vincoli".
Come si scrive un CHECK con nome
Sintassi
-- 1) Vincolo di dominio: CHECK di tabella, con nome
CREATE TABLE nome_tabella (
colonna tipo_dato,
...
CONSTRAINT nome_check CHECK (condizione_sulla_colonna)
);
-- 2) Vincolo di tupla: CHECK di tabella, con nome
CREATE TABLE nome_tabella (
colonna1 tipo_dato,
colonna2 tipo_dato,
...
CONSTRAINT nome_check CHECK (condizione_su_più_colonne)
);
Il CHECK può essere scritto anche inline, subito dopo la colonna, ma in quel caso MariaDB non accetta CONSTRAINT nome dentro la definizione della colonna: per dare un nome al vincolo — cosa obbligatoria — si usa sempre la forma di tabella, con la condizione elencata dopo le colonne. Vale sia per i vincoli di dominio sia per quelli di tupla. L'inline resta ammesso solo senza nome e solo se la condizione usa esclusivamente quella colonna; se coinvolge altre colonne il DBMS lo rifiuta (in MySQL: errore 3813 Column check constraint references other column). Il nome (CONSTRAINT nome_check) va sempre scritto: senza nome il DBMS ne genera uno e ritrovarlo per ispezionarlo o eliminarlo diventa difficile (vedi il riquadro Dare sempre un nome ai vincoli). Gli esempi seguenti mostrano i quattro casi concreti: cosa funziona e cosa il DBMS rifiuta.
Inline o di tabella: i quattro casi, con esempio
-- CASO 1 — FORMA DI TABELLA CON NOME: sempre accettata, è quella da usare CREATE TABLE prodotti ( id INT UNSIGNED PRIMARY KEY, prezzo DECIMAL(10,2) NOT NULL, sconto DECIMAL(10,2) NOT NULL DEFAULT 0, -- dominio: una sola colonna CONSTRAINT chk_prodotti_prezzo CHECK (prezzo > 0), -- tupla: confronta due colonne della stessa riga CONSTRAINT chk_prodotti_sconto CHECK (sconto <= prezzo) ); -- CASO 2 — INLINE CON NOME: rifiutato (dopo una colonna non si può scrivere CONSTRAINT) CREATE TABLE prodotti ( prezzo DECIMAL(10,2) NOT NULL CONSTRAINT chk_prodotti_prezzo CHECK (prezzo > 0) ); -- ERROR 1064 (42000): You have an error in your SQL syntax ... -- ... near 'CONSTRAINT chk_prodotti_prezzo CHECK (prezzo > 0)' -- CASO 3 — INLINE SENZA NOME, condizione su una sola colonna: accettato, -- ma il nome lo sceglie il DBMS (MySQL: prodotti_chk_1, MariaDB: CONSTRAINT_1) CREATE TABLE prodotti ( prezzo DECIMAL(10,2) NOT NULL CHECK (prezzo > 0) ); -- il vincolo esiste, ma per eliminarlo bisogna prima cercare il nome generato: -- ALTER TABLE prodotti DROP CHECK prodotti_chk_1; -- nome non scelto da noi -- CASO 4 — INLINE SENZA NOME che guarda un'altra colonna: rifiutato CREATE TABLE ordini ( prezzo_totale DECIMAL(10,2) NOT NULL, sconto DECIMAL(10,2) NOT NULL DEFAULT 0 CHECK (sconto <= prezzo_totale) ); -- ERROR 3813 (HY000): Check constraint 'ordini_chk_1' references other column. -- CASO 4 (versione corretta): stessa regola, spostata a livello di tabella e con nome CREATE TABLE ordini ( prezzo_totale DECIMAL(10,2) NOT NULL, sconto DECIMAL(10,2) NOT NULL DEFAULT 0, CONSTRAINT chk_ordini_sconto CHECK (sconto >= 0 AND sconto <= prezzo_totale) );
In breve: il caso 1 è l'unico da scrivere in un compito; i casi 2 e 4 sono errori che il DBMS blocca; il caso 3 è sintatticamente lecito ma lascia il vincolo con un nome generato, quindi non si sa come citarlo in un DROP CHECK.
Esempio 1 — Vincoli di dominio
CREATE TABLE prodotti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL, prezzo DECIMAL(10,2) NOT NULL, quantita SMALLINT UNSIGNED NOT NULL DEFAULT 0, stato ENUM('disponibile','esaurito','sospeso') NOT NULL DEFAULT 'disponibile', CONSTRAINT chk_prodotti_prezzo CHECK (prezzo > 0) );
Spiegazione. Tre vincoli di dominio espressi in tre modi diversi: prezzo > 0 è un CHECK vero e proprio (il tipo DECIMAL da solo accetterebbe anche -19.99); quantita SMALLINT UNSIGNED esprime il dominio "interi non negativi" già col tipo; stato ENUM(...) ci dice che stato può avere come valore 'disponibile' o 'esaurito' o 'sospeso'. Nota che prezzo > 0 esclude anche lo zero: se lo zero è un valore lecito (prodotto omaggio) si scrive prezzo >= 0.
Esempio 2 — Dominio, NULL e validazione di formato
CREATE TABLE clienti ( id INT UNSIGNED PRIMARY KEY, email VARCHAR(100) NOT NULL, eta TINYINT UNSIGNED NOT NULL, CONSTRAINT chk_clienti_email CHECK (email LIKE '%@%.%'), CONSTRAINT chk_clienti_eta CHECK (eta BETWEEN 18 AND 120) );
Spiegazione. email LIKE '%@%.%', ovvero controlla che la email contenga una chiocciola seguita da almeno un punto: non garantisce che l'indirizzo esista, ma blocca gli errori più grossolani. eta BETWEEN 18 AND 120 è un intervallo di dominio. Attenzione al NULL: se eta fosse nullable, un NULL renderebbe la condizione UNKNOWN e il vincolo sarebbe considerato rispettato; qui il problema non si pone perché la colonna è NOT NULL. Regola pratica: se il CHECK presuppone un valore, aggiungi anche NOT NULL.
Esempio 3 — Vincolo di tupla su due colonne
CREATE TABLE prenotazioni ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, camera_id INT UNSIGNED NOT NULL, data_check_in DATE NOT NULL, data_check_out DATE NOT NULL, CONSTRAINT chk_prenotazioni_date CHECK (data_check_in < data_check_out) );
Spiegazione: il vincolo impedisce che sia commesso l'errore di inserire una data_check_in successiva alla data_check_out.
Esempio 4 — Vincolo di tupla con AND
CREATE TABLE ordini ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, prezzo_totale DECIMAL(10,2) NOT NULL, sconto DECIMAL(10,2) NOT NULL DEFAULT 0, CONSTRAINT chk_ordini_sconto CHECK (sconto >= 0 AND sconto <= prezzo_totale) );
Spiegazione. Impedisce sia gli sconti negativi (che aumenterebbero il prezzo) sia gli sconti superiori al totale (che renderebbero l'importo finale negativo). Le due condizioni sono collegate da AND: entrambe devono essere vere. Un vincolo di tupla può combinare più colonne e più operatori logici in un'unica espressione.
Esempio 5 — Vincolo di tupla con regole condizionali (AND / OR)
CREATE TABLE iscrizioni ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, tipo ENUM('universitaria','scolastica','adulti') NOT NULL, eta TINYINT UNSIGNED NOT NULL, CONSTRAINT chk_iscrizioni_eta CHECK ( (tipo = 'universitaria' AND eta >= 18) OR (tipo = 'scolastica' AND eta BETWEEN 6 AND 19) OR (tipo = 'adulti' AND eta >= 30) ) );
Spiegazione. Ogni tipo di iscrizione ha una fascia d'età diversa: il CHECK è una catena di casi, ciascuno con la propria condizione. tipo e eta sono NOT NULL, quindi nessun ramo può diventare UNKNOWN: la riga è accettata solo se almeno una delle tre alternative è vera. È il tipico esempio d'esame di vincolo di tupla "complesso".
Cosa succede quando il vincolo viene violato
-- prezzo negativo: viola chk_prodotti_prezzo (dominio) INSERT INTO prodotti (nome, prezzo) VALUES ('Prodotto errato', -19.99); -- ERROR 3819 (HY000): Check constraint 'chk_prodotti_prezzo' is violated. -- sconto maggiore del totale: viola chk_ordini_sconto (tupla) INSERT INTO ordini (prezzo_totale, sconto) VALUES (100.00, 150.00); -- ERROR 3819 (HY000): Check constraint 'chk_ordini_sconto' is violated. -- check-out prima del check-in: viola chk_prenotazioni_date (tupla) UPDATE prenotazioni SET data_check_out = '2024-02-01' WHERE id = 1; -- ERROR 3819 (HY000): Check constraint 'chk_prenotazioni_date' is violated.
Il vincolo viene valutato su ogni INSERT e ogni UPDATE della riga; se la condizione è FALSE l'intera istruzione viene rifiutata e la tabella resta invariata (non viene salvata "mezza riga"). Il CHECK agisce anche quando il vincolo è violato da un UPDATE che modifica una sola delle colonne coinvolte, come nell'ultima riga dell'esempio.
Dominio vs tupla: riepilogo
Le due colonne sono le due famiglie di CHECK, le righe rispondono alla stessa domanda per entrambe: si legge per righe. L'unica differenza vera è la prima riga (quante colonne entrano nella condizione); la sintassi, invece, è identica — sempre dopo le colonne e sempre con il nome.
| Domanda | Vincolo di dominio | Vincolo di tupla |
|---|---|---|
| Cosa controlla | i valori di una sola colonna, riga per riga | la combinazione di due o più colonne della stessa riga |
| Come si scrive | Allo stesso modo: dopo tutte le colonne, nella forma CONSTRAINT nome CHECK (condizione). La forma inline (subito dopo la colonna) non accetta il nome, quindi non si usa. |
|
| Esempio di codice | CONSTRAINT chk_prodotti_prezzo CHECK (prezzo > 0) |
CONSTRAINT chk_ordini_sconto CHECK (sconto <= prezzo_totale) |
| Condizioni tipiche | prezzo > 0, eta BETWEEN 18 AND 120, email LIKE '%@%.%' |
data_check_in < data_check_out, sconto <= prezzo_totale |
| Nome consigliato | chk_ + tabella + colonna → chk_prodotti_prezzo |
chk_ + tabella + regola → chk_ordini_sconto |
| Se il valore è NULL | Con un NULL la condizione diventa UNKNOWN (né vera né falsa) e il DBMS la considera superata: la riga entra. Es. con data_check_out nullable, un INSERT che la lascia a NULL passa anche se il confronto salta. Rimedio: dichiarare NOT NULL le colonne coinvolte. |
|
Come riconoscerli in un esercizio: conta le colonne citate nella condizione — una sola → dominio, due o più → tupla. La distinzione serve a spiegare perché il vincolo è di un tipo o dell'altro, non a cambiare il modo di scriverlo.
Operatori e funzioni utilizzabili in un CHECK
| Categoria | Esempi | Uso tipico |
|---|---|---|
| Confronto | =, <>, <, >, <=, >= |
Confronto di numeri, date, stringhe |
| Logici | AND, OR, NOT |
Combinare più condizioni |
| Insiemi e intervalli | IN (...), BETWEEN ... AND ... |
Elenchi di valori ammessi, fasce |
| Stringhe | LIKE, CHAR_LENGTH(), UPPER() |
Validazione di formato e lunghezza |
| Matematiche | ABS(), ROUND(), MOD() |
Calcoli nella condizione |
- Sottoquery (
SELECT ...) e riferimenti ad altre tabelle: un CHECK vede solo le colonne della riga corrente. - Funzioni non deterministiche:
NOW(),CURDATE(),RAND(),UUID()non sono ammesse. Per regole che dipendono dal tempo o da altre righe servono trigger o logica applicativa. - Colonne diverse da quella dichiarata, se il CHECK è inline (errore 3813): in quel caso il vincolo va spostato a livello di tabella.
Note operative
- Versioni: in MySQL i
CHECKsono applicati dalla 8.0.16 e in MariaDB dalla 10.2.1. Nelle versioni precedenti la sintassi viene accettata ma il vincolo è ignorato: il comando non dà errore e i dati non vengono controllati. - Nome univoco e sempre presente: ogni
CHECKva introdotto daCONSTRAINT nome_check; il nome deve essere univoco nello schema, con prefissochk_e un nome parlante (chk_ordini_sconto, noncheck1). Vedi il riquadro Dare sempre un nome ai vincoli. - Un vincolo per regola: se le regole sono indipendenti, meglio due
CHECKseparati (messaggi d'errore più chiari) invece di un'unica espressione con moltiOR. - Errori: violazione → 3819 in MySQL, 4025 in MariaDB; CHECK inline che usa altre colonne → 3813 in MySQL.
- Vincolo dimenticato: se non è stato scritto nella
CREATE TABLE, può essere aggiunto dopo conALTER TABLE ... ADD CONSTRAINT ... CHECK: vedi la sezione aggiunta successiva con ALTER TABLE.
🔧 Vincoli di Dominio, di Tupla e Chiavi Esterne: aggiunta successiva con ALTER TABLE
I vincoli di dominio e di tupla si dichiarano dentro la CREATE TABLE, insieme alle colonne: è lì che vanno scritti quando l'esercizio (o l'esame) chiede di definire una tabella completa di vincoli. La trattazione completa, con definizioni, sintassi ed esempi, è nella sezione Vincoli di Dominio e di Tupla qui sopra.
Questa sotto-sezione ricorda solo il caso in cui il vincolo sia stato dimenticato alla creazione della tabella: in quel caso può essere aggiunto in seguito con ALTER TABLE ... ADD CONSTRAINT ... CHECK. Il vincolo vale da quel momento per tutti gli INSERT e gli UPDATE; le righe già presenti vengono controllate al momento dell'aggiunta e, se qualcuna viola la condizione, il comando fallisce.
Sintassi
-- Vincolo di dominio (singola colonna)
ALTER TABLE nome_tabella
ADD CONSTRAINT nome_constraint
CHECK (condizione_sulla_colonna);
-- Vincolo di tupla (più colonne della stessa riga)
ALTER TABLE nome_tabella
ADD CONSTRAINT nome_constraint
CHECK (condizione_su_più_colonne);
Esempio
-- Tabella creata senza vincoli: li aggiungiamo ora ALTER TABLE ordini ADD CONSTRAINT chk_ordini_sconto CHECK (sconto >= 0 AND sconto <= prezzo_totale); ALTER TABLE prodotti ADD CONSTRAINT chk_prodotti_prezzo CHECK (prezzo > 0);
Spiegazione: il primo è un vincolo di tupla (usa sconto e prezzo_totale della stessa riga), il secondo un vincolo di dominio (solo prezzo). Se nelle righe esistenti c'è già un ordine con sconto > prezzo_totale o un prodotto con prezzo <= 0, il comando viene rifiutato con l'errore 3819 (MySQL) o 4025 (MariaDB): prima si correggono i dati, poi si aggiunge il vincolo.
Esempio: aggiunta di una chiave esterna
Sintassi
-- Chiave esterna (integrità referenziale) aggiunta dopo la creazione
ALTER TABLE tabella_referenziante
ADD CONSTRAINT nome_fk
FOREIGN KEY (colonna_referenziante)
REFERENCES tabella_referenziata (colonna_referenziata)
ON DELETE [AZIONE]
ON UPDATE [AZIONE];
-- La tabella ordini esiste già ma senza vincolo verso i clienti ALTER TABLE ordini ADD CONSTRAINT fk_ordini_clienti FOREIGN KEY (cliente_id) REFERENCES clienti (id) ON DELETE RESTRICT ON UPDATE CASCADE;
Spiegazione: il vincolo viene aggiunto alla tabella referenziante (ordini), che contiene la colonna cliente_id, e punta alla tabella referenziata (clienti). Le due colonne devono avere lo stesso tipo di dato (es. entrambe INT UNSIGNED). Con ON DELETE RESTRICT non si può eliminare un cliente che ha ancora ordini; con ON UPDATE CASCADE la modifica di clienti.id si propaga automaticamente a ordini.cliente_id. Se ordini contiene già righe con un cliente_id inesistente in clienti, il comando fallisce (errore 1452): prima si sistemano i dati orfani, poi si aggiunge il vincolo.
Prima la tabella referenziata, poi il vincolo
La tabella clienti deve esistere già quando si aggiunge la FK su ordini. Se manca, ALTER TABLE fallisce con l'errore 1215 (Cannot add foreign key constraint). Inoltre, se la colonna cliente_id contiene NULL e il vincolo è NOT NULL, l'aggiunta viene rifiutata: valgono le stesse regole viste per i vincoli di dominio e di tupla.
Il vincolo aggiunto si verifica con SHOW CREATE TABLE ordini; e si rimuove con ALTER TABLE ordini DROP FOREIGN KEY fk_ordini_clienti; (per i CHECK: DROP CONSTRAINT o DROP CHECK).
Controllare i dati già presenti
Prima di aggiungere il vincolo conviene cercare le righe che lo violerebbero:
SELECT * FROM ordini WHERE NOT (sconto >= 0 AND sconto <= prezzo_totale);
Il vincolo aggiunto compare in INFORMATION_SCHEMA.CHECK_CONSTRAINTS e si rimuove con ALTER TABLE nome_tabella DROP CONSTRAINT nome_constraint; (in alternativa DROP CHECK nome_constraint).
💡 Esempi Pratici di CREATE TABLE (commentati)
-- Memorizza gli account utente CREATE TABLE utenti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, --Chiave primaria, numeri positivi, si autoincrementa username VARCHAR(30) NOT NULL UNIQUE -- Nome utente, massimo 30 caratteri, obbligatorio e unico email VARCHAR(100) NOT NULL UNIQUE, -- Email, obbligatoria e senza duplicati password_hash VARCHAR(255) NOT NULL, -- Hash della password, lunghezza standard (es. bcrypt) data_registrazione DATETIME DEFAULT CURRENT_TIMESTAMP, -- Data di iscrizione, automatica se non fornita attivo BOOLEAN DEFAULT TRUE, -- Account attivo? 1=attivo, 0=disabilitato eta TINYINT UNSIGNED, -- Età intera positiva CONSTRAINT chk_utenti_eta CHECK (eta >= 18) -- Vincolo di dominio: maggiorenni );
-- Catalogo prodotti CREATE TABLE prodotti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL, -- Nome prodotto, massimo 100 caratteri descrizione TEXT, -- Descrizione lunga, può essere vuota prezzo DECIMAL(10,2) NOT NULL, -- Prezzo con 2 decimali, obbligatorio quantita SMALLINT UNSIGNED DEFAULT 0, -- Scorte in magazzino, positivo, default 0 categoria ENUM('elettronica','abbigliamento','casa'), -- Lista chiusa di categorie predefinite CONSTRAINT chk_prodotti_prezzo CHECK (prezzo >= 0) -- Vincolo di dominio: prezzo non negativo );
-- Ordini effettuati dagli utenti CREATE TABLE ordini ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, -- ID grande per molti ordini nel tempo utente_id INT UNSIGNED NOT NULL, -- Riferimento all'utente che ha fatto l'ordine data_ordine DATETIME DEFAULT CURRENT_TIMESTAMP, -- Data di creazione ordine totale DECIMAL(10,2) NOT NULL, -- Importo totale dell'ordine stato VARCHAR(20) DEFAULT 'pending', -- Stato (pending, shipped, delivered, cancelled) CONSTRAINT chk_ordini_totale CHECK (totale >= 0), -- Vincolo di dominio: importo non negativo CONSTRAINT fk_ordini_utenti FOREIGN KEY (utente_id) REFERENCES utenti(id) ON DELETE CASCADE -- Se elimino l'utente, vengono eliminati anche i suoi ordini );
-- Dipendenti aziendali CREATE TABLE dipendenti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(50) NOT NULL, cognome VARCHAR(50) NOT NULL, stipendio DECIMAL(10,2) NOT NULL, data_assunzione DATE NOT NULL, -- Solo data, non serve ora codice_fiscale CHAR(16) NOT NULL UNIQUE, -- Codice fiscale italiano: sempre 16 caratteri fissi → CHAR perfetto CONSTRAINT chk_dipendenti_stipendio CHECK (stipendio >= 0) -- Vincolo di dominio: stipendio non negativo );
⚠️ Errori comuni da evitare (per principianti)
- Usare VARCHAR per campi a lunghezza fissa: ad esempio codice fiscale (16), codice postale (5) → sprechi spazio e performance. Usa
CHAR. - Memorizzare i soldi in FLOAT/DOUBLE: porta a errori di arrotondamento (es. 0.1+0.2). Usa
DECIMAL. - Dimenticare NOT NULL su campi obbligatori: se non metti
NOT NULL, il database accetterà valori NULL, causando poi errori nelle applicazioni. - NON usare UNSIGNED per numeri che non possono essere negativi: ad esempio ID, età, quantità. Raddoppia il massimo valore positivo e evita controlli extra.
- Usare TEXT su colonne che devono essere indicizzate: MySQL non può indicizzare completamente un TEXT se non con prefissi. Per ricerche veloci, se il testo è breve (< 255), usa
VARCHAR. - Ignorare il set di caratteri: se hai bisogno di emoji o caratteri accentati, imposta il database/tabella su
utf8mb4(non solo utf8). - Errori con le date: usare formati sbagliati o dimenticare gli apici. Ricorda il formato
'YYYY-MM-DD'.
📌 Riepilogo: come scegliere il tipo giusto in 5 punti
- ID e contatori:
INT UNSIGNED AUTO_INCREMENT(oBIGINTse prevedi miliardi di righe). Per insiemi piccoli usaTINYINT UNSIGNED(es. l'elenco degli alunni di una classe, da 0 a 255) e per valori un po' più grandi ma sempre contenutiSMALLINT UNSIGNED(es. l'elenco dei dipendenti di una fabbrica con 2000 dipendenti, fino a 65.535). - Soldi e prezzi:
DECIMAL(p,s)con p sufficiente (es.DECIMAL(10,2)). - Testi brevi (nome, email):
VARCHAR(n)con n ragionevole (50-255). - Codici a lunghezza fissa (codice fiscale, targa):
CHAR(n). - Flag vero/falso:
BOOLEAN(alias diTINYINT(1)) – usaTRUE/FALSEnelle query.
Ricorda sempre di aggiungere NOT NULL quando il campo è obbligatorio e UNSIGNED per numeri positivi.
📌 Questa guida è basata su MySQL/MariaDB. Altri DBMS (PostgreSQL, SQLite, SQL Server) hanno piccole differenze, ma i concetti fondamentali sono gli stessi.
Sintassi
CREATE TABLE nome_tabella (
nome_colonna1 tipo_dato [vincoli],
nome_colonna2 tipo_dato [vincoli],
...
);
Esempio Pratico
CREATE TABLE dipendenti ( id INT PRIMARY KEY, nome VARCHAR(50) NOT NULL, cognome VARCHAR(50) NOT NULL, stipendio DECIMAL(10,2), data_assunzione DATE );
| id | nome | cognome | stipendio | data_assunzione |
|---|
Nota
I vincoli principali sono: PRIMARY KEY (chiave primaria univoca), NOT NULL (valore obbligatorio), UNIQUE (valore univoco), FOREIGN KEY (chiave esterna), DEFAULT (valore predefinito), CHECK (vincolo di dominio o di tupla, vedi la sezione precedente).
I vincoli di dominio e di tupla vanno dichiarati qui, nella CREATE TABLE: è la forma richiesta in sede d'esame.
La chiave primaria, anche quando è composta, va scritta come primo elemento della tabella, prima degli altri attributi; le chiavi esterne vanno per ultime.
Ogni vincolo di dominio, di tupla e di integrità referenziale va inoltre nominato con CONSTRAINT nomeVincolo: il nome serve per eliminarlo, per ritrovarlo in INFORMATION_SCHEMA e per leggere i messaggi di errore (vedi Dare sempre un nome ai vincoli).
FOREIGN KEY: integrità referenziale
Descrizione
L'istruzione FOREIGN KEY è il pilastro dell'integrità referenziale in un database. Una chiave esterna crea un legame tra due tabelle, quella referenziante (che contiene la chiave esterna) e quella referenziata (che contiene i valori puntati), garantendo che non possano esistere riferimenti a dati inesistenti. Trasforma un semplice archivio di dati in un sistema coerente dove le relazioni sono garantite dal motore del database.
La tabella referenziante contiene la chiave esterna che punta alla chiave primaria (o unica) della tabella referenziata. È fondamentale che i tipi di dati delle due colonne siano identici (es. entrambi INT UNSIGNED). Il motore di storage InnoDB è obbligatorio per far funzionare queste restrizioni in MySQL/MariaDB.
La potenza delle Foreign Key risiede nelle clausole ON DELETE e ON UPDATE. Esse determinano il destino delle righe referenzianti quando una riga referenziata viene modificata o cancellata: scegliere CASCADE, SET NULL o RESTRICT ha implicazioni drastiche sulla logica di business e sulla sicurezza dei dati.
La foreign key si dichiara dentro la
CREATE TABLE della tabella referenziante, insieme alle colonne: è la forma richiesta quando l'esercizio chiede di "creare le tabelle con i vincoli". Negli esempi che seguono ogni vincolo è quindi scritto nella CREATE TABLE; l'uso di ALTER TABLE ... ADD CONSTRAINT è riservato al caso in cui la tabella esista già e il vincolo sia stato dimenticato (sintassi riportata qui sotto, nella stessa sezione).
Ogni esempio mostra: la creazione delle due tabelle (referenziata e referenziante) con il vincolo, lo stato iniziale dei dati, il tentativo di UPDATE o DELETE sulla tabella referenziata e le tabelle dopo l'operazione, per vedere l'effetto della clausola scelta.
Sintassi
-- Forma richiesta: il vincolo nasce con la tabella referenziante
CREATE TABLE tabella_referenziante (
...,
colonna_referenziante tipo_dato [NOT NULL],
...,
CONSTRAINT nome_fk
FOREIGN KEY (colonna_referenziante)
REFERENCES tabella_referenziata (colonna_referenziata)
ON DELETE [AZIONE]
ON UPDATE [AZIONE]
);
-- Se la tabella esiste già (vincolo dimenticato)
ALTER TABLE tabella_referenziante
ADD CONSTRAINT nome_fk
FOREIGN KEY (colonna_referenziante)
REFERENCES tabella_referenziata (colonna_referenziata)
ON DELETE [AZIONE]
ON UPDATE [AZIONE];
ON DELETE e ON UPDATE sono opzionali (possono essere entrambi presenti; presenti uno dei due; assenti entrambi) e Dove [AZIONE] può essere: CASCADE, SET NULL, RESTRICT, NO ACTION.
Esempio 1:ON DELETE CASCADE (Eliminazione a cascata)
-- Tabella referenziata CREATE TABLE clienti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL ); -- Tabella referenziante: il vincolo nasce con la tabella CREATE TABLE ordini ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, cliente_id INT UNSIGNED NOT NULL, importo DECIMAL(10,2) NOT NULL, CONSTRAINT fk_ordini_clienti FOREIGN KEY (cliente_id) REFERENCES clienti (id) ON DELETE CASCADE );
Logica: L'opzione CASCADE rende il database un "domino". Se elimini un cliente (riga referenziata), il database elimina automaticamente tutti i suoi ordini (righe referenzianti). Non puoi avere un ordine senza cliente.
Tabella: clienti
| id | nome |
|---|---|
| 1 | Mario Rossi |
| 2 | Laura Bianchi |
| 3 | Giuseppe Verdi |
Tabella: ordini
| id | cliente_id | importo |
|---|---|---|
| 100 | 1 | 150.00 |
| 101 | 2 | 230.00 |
| 102 | 3 | 95.00 |
| 103 | 3 | 40.00 |
Tabella: clienti
| id | nome |
|---|---|
| 1 | Mario Rossi |
| 2 | Laura Bianchi |
Tabella: ordini
| id | cliente_id | importo |
|---|---|---|
| 100 | 1 | 150.00 |
| 101 | 2 | 230.00 |
✅ Cliente 3 eliminato → Ordini 102 e 103 eliminati automaticamente!
Esempio 2: ON DELETE SET NULL
-- Tabella referenziata CREATE TABLE reparti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL ); -- Tabella referenziante: reparto_id deve accettare NULL CREATE TABLE dipendenti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL, reparto_id INT UNSIGNED NULL, CONSTRAINT fk_dipendenti_reparti FOREIGN KEY (reparto_id) REFERENCES reparti (id) ON DELETE SET NULL );
Logica: Qui usiamo SET NULL. Se un reparto viene chiuso (riga referenziata eliminata), non vogliamo licenziare i dipendenti (righe referenzianti). Vogliamo solo che smettano di appartenere a quel reparto. Il campo reparto_id diventa NULL. Nota: La colonna nella tabella referenziante deve accettare NULL.
Tabella: reparti
| id | nome |
|---|---|
| 1 | IT |
| 2 | Marketing |
Tabella: dipendenti
| id | nome | reparto_id |
|---|---|---|
| 1 | Mario Rossi | 1 |
| 2 | Giulia Verdi | 2 |
Tabella: reparti
| id | nome |
|---|---|
| 1 | IT |
Tabella: dipendenti
| id | nome | reparto_id |
|---|---|---|
| 1 | Mario Rossi | 1 |
| 2 | Giulia Verdi | NULL |
✅ Reparto marketing eliminato, ma Giulia è ancora in azienda (senza assegnazione).
Esempio 3: ON DELETE RESTRICT
-- Tabella referenziata CREATE TABLE categorie ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL ); -- Tabella referenziante CREATE TABLE prodotti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL, categoria_id INT UNSIGNED NOT NULL, CONSTRAINT fk_prodotti_categorie FOREIGN KEY (categoria_id) REFERENCES categorie (id) ON DELETE RESTRICT );
Logica: RESTRICT (o l'equivalente predefinito: se non scriviamo nulla è come se avessimo scritto ON DELETE RESTRICT) è il "Guardiano". Se provi a eliminare una categoria che contiene ancora prodotti, il database blocca l'operazione con un errore. Ti obbliga a gestire manualmente i prodotti prima di chiudere la categoria. È l'opzione più sicura per evitare cancellazioni accidentali.
Tabella: categorie
| id | nome |
|---|---|
| 1 | Informatica |
| 2 | Libri (Attiva) |
Tabella: prodotti
| id | nome | categoria_id |
|---|---|---|
| 10 | Monitor | 1 |
| 11 | D&D Manuale | 2 |
| 12 | Harry Potter | 2 |
❌ Errore: Cannot delete or update a parent row: a foreign key constraint fails.
Non puoi eliminare la categoria "Libri" perché ci sono prodotti (ID 11, 12) collegati.
Esempio 4: Aggiornamento a cascata (ON UPDATE CASCADE)
Caso d'uso: Utilizzo di chiavi non numeriche (es. codici alfanumerici) che possono essere rinominati. Se cambia il codice nella tabella referenziata, deve cambiare ovunque sia stato usato.
-- Tabella referenziata CREATE TABLE generi ( codice CHAR(5) PRIMARY KEY, nome VARCHAR(50) NOT NULL ); -- Tabella referenziante CREATE TABLE libri ( titolo VARCHAR(200) PRIMARY KEY, codice_genere CHAR(5) NOT NULL, CONSTRAINT fk_libri_generi FOREIGN KEY (codice_genere) REFERENCES generi (codice) ON UPDATE CASCADE );
Scenario: La tabella generi usa alcuni codici brevi ('TH' per Thriller) . Decidiamo di cambiare i codice brevi in una forma più estesa (sostituiamo 'TH' con 'THRI'; 'SCIFI' lo lasciamo inalterato). Senza CASCADE, dovremmo aggiornare manualmente migliaia di libri di genere Thriller. Con CASCADE, il database propaga la modifica.
Tabella: generi (Referenziata)
| codice (PK) | nome |
|---|---|
| TH | Thriller |
| SCIFI | Science Fiction |
Tabella: libri (Referenziante)
| titolo | codice_genere (FK) |
|---|---|
| Il silenzio degli innocenti | TH |
| Shining | TH |
| Mare aperto | SCIFI |
UPDATE generi SET codice = 'THRI' WHERE codice = 'TH';Tabella: generi
| codice | nome |
|---|---|
| THRI | Thriller |
| SCIFI | Science Fiction |
Tabella: libri
| titolo | codice_genere |
|---|---|
| Il silenzio degli innocenti | THRI |
| Shining | THRI |
| Mare aperto | SCIFI |
✅ La modifica del codice nella tabella referenziata si è propagata automaticamente a tutti i libri collegati!
Esempio 5: Aggiornamento con disassociazione (ON UPDATE SET NULL)
Caso d'uso: Se l'identificativo della riga referenziata cambia, non vogliamo che le righe referenzianti seguano il nuovo identificativo, ma preferiamo "sganciarle" (NULL), magari per richiedere una riassegnazione manuale.
-- Tabella referenziata CREATE TABLE progetti ( codice VARCHAR(20) PRIMARY KEY, stato VARCHAR(20) NOT NULL ); -- Tabella referenziante: codice_progetto deve accettare NULL CREATE TABLE dipendenti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL, codice_progetto VARCHAR(20) NULL, CONSTRAINT fk_dip_progetti FOREIGN KEY (codice_progetto) REFERENCES progetti (codice) ON UPDATE SET NULL );
Scenario: Un progetto viene rinominato da 'PRJ_ALPHA' a 'PRJ_BETA'. Per policy aziendale, se un progetto cambia codice fondamentale, i dipendenti assegnati al vecchio codice vengono "sganciati" (NULL) affinché un manager possa decidere se riassegnarli al nuovo codice 'PRJ_BETA' manualmente o ad altro.
Tabella: progetti (Referenziata)
| codice | stato |
|---|---|
| PRJ_ALPHA | Attivo |
Tabella: dipendenti (Referenziante)
| nome | codice_progetto (FK) |
|---|---|
| Mario Rossi | PRJ_ALPHA |
| Luigi Verdi | PRJ_ALPHA |
UPDATE progetti SET codice = 'PRJ_BETA' WHERE codice = 'PRJ_ALPHA';Tabella: progetti
| codice | stato |
|---|---|
| PRJ_BETA | Attivo |
Tabella: dipendenti
| nome | codice_progetto |
|---|---|
| Mario Rossi | NULL |
| Luigi Verdi | NULL |
⚠️ Il codice progetto è cambiato, ma i dipendenti sono stati disassociati (NULL). Devono essere riassegnati manualmente.
Esempio 6: Aggiornamento bloccato (ON UPDATE RESTRICT)
Caso d'uso: Quando si vuole impedire la modifica della chiave primaria nella tabella referenziata se esistono righe collegate nella tabella referenziante. Questo è il comportamento predefinito in molti database e garantisce l'integrità referenziale, evitando che i riferimenti diventino orfani ovvero che righe referenzianti facciano riferimento a righe individuate tramite valori della chiave primaria non più esistenti .
-- Tabella referenziata CREATE TABLE progetti ( codice VARCHAR(20) PRIMARY KEY, stato VARCHAR(20) NOT NULL ); -- Tabella referenziante CREATE TABLE dipendenti ( id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL, codice_progetto VARCHAR(20) NOT NULL, CONSTRAINT fk_dip_progetti FOREIGN KEY (codice_progetto) REFERENCES progetti (codice) ON UPDATE RESTRICT );
Scenario: Un progetto viene rinominato da 'PRJ_ALPHA' a 'PRJ_BETA'. Tuttavia, per policy aziendale, non si permette di cambiare il codice di un progetto se ci sono dipendenti assegnati. L'update viene quindi bloccato per evitare che i dipendenti perdano il riferimento al progetto.
Tabella: progetti (Referenziata)
| codice | stato |
|---|---|
| PRJ_ALPHA | Attivo |
Tabella: dipendenti (Referenziante)
| nome | codice_progetto (FK) |
|---|---|
| Mario Rossi | PRJ_ALPHA |
| Luigi Verdi | PRJ_ALPHA |
UPDATE progetti SET codice = 'PRJ_BETA' WHERE codice = 'PRJ_ALPHA';❌ Errore di vincolo: Impossibile aggiornare la riga nella tabella 'progetti' perché esiste almeno una riga nella tabella 'dipendenti' che vi fa riferimento.
L'aggiornamento è bloccato dal vincolo RESTRICT. Per procedere, è necessario prima eliminare o riassegnare i dipendenti collegati al progetto 'PRJ_ALPHA'.
Esempio 7: Aggiornamento e cancellazione a cascata (ON UPDATE CASCADE + ON DELETE CASCADE)
Caso d'uso: Gestione di codici categoria dinamici. Quando un codice categoria viene modificato, l'aggiornamento si propaga automaticamente. Quando una categoria viene eliminata, tutti gli elementi associati vengono rimossi in sicurezza.
-- Tabella referenziata CREATE TABLE categorie ( codice VARCHAR(10) PRIMARY KEY, nome VARCHAR(50) NOT NULL ); -- Tabella referenziante: due azioni sulla stessa FK CREATE TABLE prodotti ( prodotto VARCHAR(100) PRIMARY KEY, codice_categoria VARCHAR(10) NOT NULL, CONSTRAINT fk_prodotti_categorie FOREIGN KEY (codice_categoria) REFERENCES categorie (codice) ON UPDATE CASCADE ON DELETE CASCADE );
Scenario:
ON UPDATE CASCADE: Se il codice categoria 'ELE' viene rinominato in 'ELET', tutti i prodotti aggiornano automaticamente il riferimento.ON DELETE CASCADE: Se la categoria 'ABG' (Abbigliamento) viene eliminata, tutti i prodotti associati vengono cancellati automaticamente per mantenere l'integrità referenziale.
Tabella: categorie (Referenziata)
| codice (PK) | nome |
|---|---|
| ELE | Elettronica |
| ABG | Abbigliamento |
Tabella: prodotti (Referenziante)
| prodotto | codice_categoria (FK) |
|---|---|
| Smartphone X | ELE |
| Cuffie Pro | ELE |
| Maglione Inverno | ABG |
UPDATE categorie SET codice = 'ELET' WHERE codice = 'ELE';Tabella: categorie
| codice | nome |
|---|---|
| ELET | Elettronica |
| ABG | Abbigliamento |
Tabella: prodotti
| prodotto | codice_categoria |
|---|---|
| Smartphone X | ELET |
| Cuffie Pro | ELET |
| Maglione Inverno | ABG |
✅ L'aggiornamento del codice categoria si è propagato automaticamente a tutti i prodotti!
Tabella: categorie (Referenziata)
| codice (PK) | nome |
|---|---|
| ELET | Elettronica |
| ABG | Abbigliamento |
Tabella: prodotti (Referenziante)
| prodotto | codice_categoria (FK) |
|---|---|
| Smartphone X | ELET |
| Cuffie Pro | ELET |
| Maglione Inverno | ABG |
| Sciarpa di Lana | ABG |
DELETE FROM categorie WHERE codice = 'ABG';Tabella: categorie
| codice | nome |
|---|---|
| ELET | Elettronica |
Tabella: prodotti
| prodotto | codice_categoria |
|---|---|
| Smartphone X | ELET |
| Cuffie Pro | ELET |
✅ L'eliminazione della categoria ha rimosso automaticamente tutti i prodotti associati!
Esempio 8: Chiave Esterna Composta (Composite Key)
-- Tabella referenziata: chiave primaria composta (primo elemento) CREATE TABLE inventario ( PRIMARY KEY (prodotto_id, magazzino_id), prodotto_id INT NOT NULL, magazzino_id CHAR(1) NOT NULL, quantita INT NOT NULL ); -- Tabella referenziante: la FK deve usare entrambe le colonne CREATE TABLE movimenti_magazzino ( id_mov INT PRIMARY KEY, prodotto_id INT NOT NULL, magazzino_id CHAR(1) NOT NULL, tipo ENUM('Entrata','Uscita') NOT NULL, CONSTRAINT fk_movimenti_inventario FOREIGN KEY (prodotto_id, magazzino_id) REFERENCES inventario (prodotto_id, magazzino_id) ON DELETE RESTRICT );
Logica: In questo caso la chiave primaria della tabella referenziata (inventario) è formata da due colonne. La foreign key deve riferirsi a entrambe contemporaneamente. Questo garantisce che un movimento sia valido solo per quella specifica combinazione di Prodotto e Magazzino.
| prodotto_id | magazzino_id | quantità |
|---|---|---|
| 101 | A | 50 |
| 101 | B | 20 |
Chiave primaria composta: (prodotto_id, magazzino_id)
| id_mov | prodotto_id | magazzino_id | tipo |
|---|---|---|---|
| 900 | 101 | A | Entrata |
| 901 | 101 | B | Uscita |
Foreign key composta: (prodotto_id, magazzino_id) → inventario(prodotto_id, magazzino_id)
Cosa succede se si tenta di inserire un movimento non valido?
INSERT INTO movimenti_magazzino (id_mov, prodotto_id, magazzino_id, tipo)
VALUES (902, 101, 'C', 'Uscita');
❌ Errore di vincolo: La coppia (101, 'C') non esiste nella tabella inventario!
Il database rifiuta l'inserimento perché la foreign key composta non trova una corrispondenza nella tabella referenziata.
Esempio 9: Relazione Uno-a-Uno (One-to-One)
-- Tabella 1: Persone (dati anagrafici) CREATE TABLE persone ( id INT PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(50) NOT NULL, cognome VARCHAR(50) NOT NULL ); -- Tabella 2: Passaporti (documenti) CREATE TABLE passaporti ( numero VARCHAR(20) PRIMARY KEY, persona_id INT UNIQUE, -- Vincolo UNIQUE: 1 passaporto per persona data_scadenza DATE, CONSTRAINT fk_passaporti_persone FOREIGN KEY (persona_id) REFERENCES persone(id) ON DELETE CASCADE );
Logica: Ogni persona può avere al massimo un passaporto, e ogni passaporto appartiene a una sola persona. Per garantire questo, la colonna persona_id nella tabella passaporti deve avere un vincolo UNIQUE. Se non ci fosse, potremmo inserire due passaporti per la stessa persona. L'opzione ON DELETE CASCADE fa sì che quando una persona viene eliminata, anche il suo passaporto venga rimosso automaticamente.
| id | nome | cognome |
|---|---|---|
| 1 | Mario | Rossi |
| 2 | Luigi | Verdi |
| 3 | Anna | Neri |
Stato iniziale: tre persone registrate. Anna Neri (id=3) non ha ancora un passaporto.
| numero | persona_id (FK, UNIQUE) | data_scadenza |
|---|---|---|
| AA1234567 | 1 | 2028-12-31 |
| BB7654321 | 2 | 2027-06-15 |
Ogni passaporto è collegato a una sola persona.
✅ Mario Rossi (id=1) ha il passaporto AA1234567. Luigi Verdi (id=2) ha il passaporto BB7654321. Anna Neri (id=3) è registrata ma non ha ancora un passaporto. Nessuna persona ha più di un passaporto grazie al vincolo UNIQUE.
Cosa succede se si tenta di riusare un numero di passaporto già esistente?
-- Anna Neri (id=3) presenta il passaporto di Mario Rossi
INSERT INTO passaporti (numero, persona_id, data_scadenza)
VALUES ('AA1234567', 3, '2030-01-01');
❌ Errore: Duplicate entry 'AA1234567' for key 'PRIMARY' — il numero di passaporto AA1234567 è già registrato per Mario Rossi (id=1).
Il database rifiuta la riga e la tabella passaporti resta con due righe: un passaporto può appartenere a una sola persona. Se il numero fosse nuovo, invece, l'inserimento riuscirebbe; il caso simmetrico (un secondo passaporto per la stessa persona) è bloccato dal vincolo UNIQUE su persona_id.
Comportamento con ON DELETE CASCADE
DELETE FROM persone WHERE id = 1;
✅ Mario Rossi viene eliminato dalla tabella persone. Grazie a ON DELETE CASCADE, anche il suo passaporto (AA1234567) viene rimosso automaticamente dalla tabella passaporti.
| id | nome | cognome |
|---|---|---|
| 2 | Luigi | Verdi |
| 3 | Anna | Neri |
Rimangono Luigi Verdi (id=2) e Anna Neri (id=3).
| numero | persona_id | data_scadenza |
|---|---|---|
| BB7654321 | 2 | 2027-06-15 |
Il passaporto AA1234567 (di Mario) è stato rimosso automaticamente.
Esempio 10: Foreign Key Auto-referenziale (Gerarchie)
-- Un'unica tabella: ogni riga referenzia un'altra riga della stessa tabella CREATE TABLE dipendenti ( id INT PRIMARY KEY AUTO_INCREMENT, nome VARCHAR(100) NOT NULL, manager_id INT NULL, CONSTRAINT fk_dipendenti_manager FOREIGN KEY (manager_id) REFERENCES dipendenti (id) ON DELETE SET NULL );
Logica: Una tabella può referenziare se stessa: è utile in caso di gerarchie (es. un dipendente che è responsabile di altri dipendenti). Ogni riga è quindi sia referenziante sia referenziata, e il suo manager_id punta all'id di un'altra riga nella stessa tabella.
| id (PK) | nome dipendente | manager_id(FK) | Note |
|---|---|---|---|
| 10 | CEO Azienda | NULL | Nessun manager (è il capo) |
| 20 | Resp. IT | 10 | Il suo manager è il CEO (id 10) |
| 21 | Sviluppatore Senior | 20 | Il suo manager è il Resp. IT (id 20) |
| 22 | Stagista | 21 | Il suo manager è lo Sviluppatore (id 21) |
DELETE FROM dipendenti WHERE id = 21;| id (PK) | nome dipendente | manager_id(FK) | Note |
|---|---|---|---|
| 10 | CEO Azienda | NULL | Nessun manager (è il capo) |
| 20 | Resp. IT | 10 | Il suo manager è il CEO (id 10) |
| 22 | Stagista | NULL | Il suo manager è lo Sviluppatore (id 21) |
✅ Se cancelliamo lo Sviluppatore Senior (id=21), lo stagista (id=22) non viene cancellato, ma il suo manager_id diventa NULL (perde il riferimento al capo).
Opzioni ON DELETE e ON UPDATE a confronto
| Azione | Comportamento | Quando usarlo |
|---|---|---|
CASCADE |
PROPAGA ELIMINAZIONE/MODIFICA ALLE RIGHE REFERENZIANTI: Con l'eliminazione (l'aggiornamento) di una riga referenziata si ha l'eliminazione (l'aggiornamento) delle righe referenzianti collegate | Relazioni forte dipendenza (Ordine effettuato /Righe Ordine effettuato). |
SET NULL |
IMPOSTA NULL NELLE RIGHE REFERENZIANTI: Con l'eliminazione (l'aggiornamento) di una riga referenziata si ha l'impostazione a NULL nelle righe referenzianti collegate | Dati storici o opzionali (Dipendenti/Reparto). |
RESTRICT |
BLOCCA L'OPERAZIONE SULLA RIGA REFERENZIATA: Se cancello/aggiorno la PK di una riga referenziata che è collegata a righe referenzianti, la cancellazione/aggiornamento della PK è bloccata | Sicurezza massima (Categorie/Prodotti). |
NO ACTION |
SIMILE A RESTRICT (verifica in differita). | Compatibilità SQL standard, logiche complesse. |
Convenzioni per il nome dei constraint
Una foreign key va sempre dichiarata con CONSTRAINT nome_vincolo: il nome serve per eliminarla (DROP FOREIGN KEY), per ritrovarla in INFORMATION_SCHEMA e per leggere i messaggi di errore di MariaDB, che riportano il nome del vincolo violato. Un buon nome facilita anche debugging e manutenzione:
- Sintassi consigliata:
fk_[tabella_referenziante]_[tabella_referenziata]_[colonna_opzionale] - Esempio chiaro:
fk_ordini_clientidice subito che è il legame tra ordini e clienti. - Evitare: Nomi generici come
fk1,fk_clienti(ambiguo). Usa sempre il prefissofk_.
Gestione degli errori comuni
Creare una Foreign Key a volte fallisce. Ecco come risolvere i problemi più frequenti:
- Errore 1215 (Cannot add foreign key): Solitamente causato da tipi di dati diversi (es. colonna referenziata
INT, colonna referenzianteBIGINT) o mancato supporto InnoDB. Verifica che charset e collation siano identici. - Errore 1452 (Cannot add or update a child row): Ci sono già dati "orfani" nella tabella referenziante. Prima di creare il vincolo, pulisci i dati:
-- Trova i dati sporchi che violerebbero la nuova FK SELECT referenziante.* FROM tabella_referenziante referenziante LEFT JOIN tabella_referenziata referenziata ON referenziante.colonna_fk = referenziata.id WHERE referenziata.id IS NULL AND referenziante.colonna_fk IS NOT NULL;
Verifica e rimozione dei constraint
Per identificare il nome esatto del constraint (che spesso viene generato automaticamente se non specificato):
SHOW CREATE TABLE nome_tabella;
Per rimuovere il vincolo:
ALTER TABLE nome_tabella DROP FOREIGN KEY nome_del_constraint;
ALTER TABLE ADD
Descrizione
Aggiunge una nuova colonna a una tabella esistente.
Sintassi
ALTER TABLE nome_tabella
ADD nome_colonna tipo_dato [vincoli];
Esempio Pratico
ALTER TABLE dipendenti ADD email VARCHAR(100);
| id | nome | cognome | stipendio |
|---|---|---|---|
| 1 | Mario | Rossi | 2500.00 |
| 2 | Laura | Bianchi | 2800.00 |
| id | nome | cognome | stipendio | |
|---|---|---|---|---|
| 1 | Mario | Rossi | 2500.00 | NULL |
| 2 | Laura | Bianchi | 2800.00 | NULL |
Nota
La nuova colonna viene aggiunta con valore NULL per tutte le righe esistenti, a meno che non si specifichi un valore DEFAULT.
Vincoli dimenticati alla creazione
Se un vincolo di dominio o di tupla è stato dimenticato nella CREATE TABLE, può essere aggiunto in seguito con ALTER TABLE ... ADD CONSTRAINT ... CHECK (es. ADD CONSTRAINT chk_prezzo CHECK (prezzo > 0)). La trattazione completa dei vincoli resta comunque nella sezione della CREATE TABLE; la sola sintassi per aggiungerli dopo è descritta in questa sezione.
ALTER TABLE MODIFY
Descrizione
Modifica il tipo di dato o i vincoli di una colonna esistente.
Sintassi
-- MySQL / Oracle
ALTER TABLE nome_tabella
MODIFY nome_colonna nuovo_tipo_dato;
Esempio Pratico
ALTER TABLE dipendenti MODIFY nome VARCHAR(100);
Attenzione
Modificare il tipo di una colonna può causare perdita di dati se il nuovo tipo è incompatibile o più piccolo del precedente.
ALTER TABLE DROP COLUMN
Descrizione
Rimuove una colonna esistente dalla tabella, eliminando tutti i dati contenuti in essa.
Sintassi
ALTER TABLE nome_tabella
DROP COLUMN nome_colonna;
Esempio Pratico
ALTER TABLE dipendenti DROP COLUMN email;
| id | nome | cognome | |
|---|---|---|---|
| 1 | Mario | Rossi | mario@email.it |
| 2 | Laura | Bianchi | laura@email.it |
| id | nome | cognome |
|---|---|---|
| 1 | Mario | Rossi |
| 2 | Laura | Bianchi |
Attenzione
L'operazione è irreversibile! Tutti i dati della colonna verranno persi definitivamente.
ALTER TABLE COMMENT
Descrizione
Aggiunge metadati descrittivi alla tabella o alle colonne. In MySQL/MariaDB, i commenti sono molto utili per documentare il database (es. spiegare unità di misura o codici).
Sintassi
-- Commento sull'intera tabella
ALTER TABLE nome_tabella COMMENT = 'Testo del commento';
-- Commento su una specifica colonna (richiede ridefinizione tipo)
ALTER TABLE nome_tabella
MODIFY nome_colonna tipo_dato COMMENT 'Testo del commento';
Esempio 1: Commento alla Tabella
ALTER TABLE dipendenti COMMENT = 'Registro storico del personale attivo';
Esempio 2: Commento alla Colonna
ALTER TABLE dipendenti MODIFY stipendio DECIMAL(10,2) COMMENT 'Importo mensile lordo in Euro';
Visualizzare i commenti
Per vedere i commenti inseriti puoi usare il comando:
SHOW FULL COLUMNS FROM dipendenti; oppure
SHOW CREATE TABLE dipendenti;
ALTER TABLE RENAME COLUMN
Descrizione
L'istruzione ALTER TABLE RENAME COLUMN è disponibile a partire da MySQL 8.0 e rappresenta un miglioramento significativo rispetto ai metodi precedenti per rinominare colonne. Questa operazione modifica il nome di una colonna esistente senza alterarne il tipo di dato, i vincoli, gli indici o i dati contenuti. La modifica è istantanea e non richiede la ricostruzione della tabella o la rielaborazione dei dati.
Prima di MySQL 8.0, rinominare una colonna richiedeva tecniche più complesse e rischiose, come creare una nuova colonna con il nome desiderato, copiare i dati, e poi eliminare la colonna originale. Questo approccio era lento, occupava spazio temporaneo significativo e poteva causare problemi con le applicazioni che accedevano alla tabella durante la migrazione.
La nuova sintassi RENAME COLUMN è atomica e sicura: la tabella rimane accessibile durante l'operazione, e se qualcosa va storto, la transazione può essere annullata. Inoltre, gli indici che fanno riferimento alla colonna vengono automaticamente aggiornati per riflettere il nuovo nome, eliminando la necessità di interventi manuali.
È importante notare che questa operazione non modifica il contenuto o la struttura dei dati, ma solo il nome della colonna. Tutti gli indici, le chiavi esterne, le viste e le stored procedure che facevano riferimento alla colonna continueranno a funzionare correttamente dopo la rinominazione.
Sintassi (MySQL 8.0+)
ALTER TABLE nome_tabella
RENAME COLUMN vecchio_nome TO nuovo_nome;
Esempi Pratici
ALTER TABLE dipendenti RENAME COLUMN data_assunzione TO assunzione;
Spiegazione: Questo comando rinomina la colonna data_assunzione in assunzione nella tabella dipendenti. La modifica è istantanea e tutti i dati precedentemente contenuti nella colonna rimangono intatti. Questo tipo di rinominazione è utile quando si desidera utilizzare nomi più brevi e concisi che rendano le query più leggibili. L'operazione non richiede downtime e la tabella rimane accessibile per letture e scritture durante l'esecuzione del comando.
| id | nome | cognome | data_assunzione |
|---|---|---|---|
| 1 | Mario | Rossi | 2023-03-01 |
| 2 | Laura | Bianchi | 2022-11-15 |
| 3 | Giuseppe | Verdi | 2023-01-20 |
| id | nome | cognome | assunzione |
|---|---|---|---|
| 1 | Mario | Rossi | 2023-03-01 |
| 2 | Laura | Bianchi | 2022-11-15 |
| 3 | Giuseppe | Verdi | 2023-01-20 |
ALTER TABLE clienti RENAME COLUMN ragione_sociale TO denominazione;
Spiegazione: Questo comando rinomina la colonna ragione_sociale in denominazione nella tabella clienti. In questo esempio, la colonna viene rinominata per conformarsi a uno standard di naming più conciso e moderno. Questo tipo di modifica è comune durante le fasi di refactoring o quando si consolidano database ereditati che utilizzano terminologie datate. Il nuovo nome "denominazione" è più breve e altrettanto chiaro, migliorando la leggibilità delle query SQL.
| id | ragione_sociale | P.IVA |
|---|---|---|
| 1 | Azienda Alpha S.r.l. | 12345678901 |
| 2 | Beta Industries S.p.A. | 98765432109 |
| id | denominazione | P.IVA |
|---|---|---|
| 1 | Azienda Alpha S.r.l. | 12345678901 |
| 2 | Beta Industries S.p.A. | 98765432109 |
ALTER TABLE ordini RENAME COLUMN dt_creazione TO data_creazione_ordine;
Spiegazione: Questo comando rinomina la colonna dt_creazione in data_creazione_ordine nella tabella ordini. Questo esempio mostra come rendere il nome della colonna più descrittivo aggiungendo contesto. Un nome più lungo ma chiaro migliora la manutenibilità del codice e riduce la possibilità di errori quando gli sviluppatori scrivono query, specialmente quando lavorano su tabelle con molte colonne che potrebbero avere nomi simili. La convenzione "data_" rende immediatamente chiaro che si tratta di una data, mentre "_ordine" specifica l'entità a cui si riferisce.
| id | dt_creazione | totale |
|---|---|---|
| 100 | 2024-01-15 | 150.00 |
| 101 | 2024-01-16 | 230.50 |
| id | data_creazione_ordine | totale |
|---|---|---|
| 100 | 2024-01-15 | 150.00 |
| 101 | 2024-01-16 | 230.50 |
Nota per versioni precedenti
Se stai utilizzando MySQL 5.7 o MariaDB, puoi ottenere lo stesso risultato con una procedura in tre passaggi:
-- Passo 1: Aggiungi la nuova colonna con lo stesso tipo ALTER TABLE dipendenti ADD COLUMN assunzione DATE AFTER cognome; -- Passo 2: Copia i dati dalla vecchia colonna UPDATE dipendenti SET assunzione = data_assunzione; -- Passo 3: Elimina la colonna originale ALTER TABLE dipendenti DROP COLUMN data_assunzione;
Questa procedura richiede più tempo e spazio temporaneo, specialmente per tabelle di grandi dimensioni, ed è per questo che la sintassi RENAME COLUMN è preferibile quando disponibile.
Impatto su applicazioni e codice
Prima di rinominare una colonna in un ambiente di produzione, considera:
- Query SQL: Tutte le query che fanno riferimento alla colonna devono essere aggiornate
- ORM: Se usi un Object-Relational Mapper, aggiorna i mapping delle entità
- API: Se la colonna è esposta tramite API, documenta il cambiamento
- Viste e stored procedure: Verifica e aggiorna tutti gli oggetti dipendenti
- Backup: Assicurati che i backup recenti possano essere ripristinati correttamente
Una strategia consigliata è creare prima la nuova colonna con il nome corretto, mantenere entrambe le colonne durante una fase di transizione, e poi rimuovere la vecchia colonna dopo aver verificato che tutto funzioni.
DROP TABLE
Descrizione
Elimina completamente una tabella dal database, inclusa la sua struttura e tutti i dati.
Sintassi
DROP TABLE nome_tabella;
Esempio Pratico
DROP TABLE dipendenti;
| id | nome | cognome |
|---|---|---|
| 1 | Mario | Rossi |
| 2 | Laura | Bianchi |
| Tabella e dati che verranno eliminati da DROP TABLE | ||
Attenzione
Operazione estremamente distruttiva e irreversibile! La tabella e tutti i suoi dati vengono eliminati permanentemente.
TRUNCATE TABLE
Descrizione
Svuota completamente una tabella eliminando tutte le righe, ma mantenendo la struttura intatta.
Sintassi
TRUNCATE TABLE nome_tabella;
Esempio Pratico
TRUNCATE TABLE dipendenti;
| id | nome | cognome | stipendio |
|---|---|---|---|
| 1 | Mario | Rossi | 2500.00 |
| 2 | Laura | Bianchi | 2800.00 |
| 3 | Giuseppe | Verdi | 3000.00 |
| Righe che verranno eliminate da TRUNCATE | |||
| id | nome | cognome | stipendio |
|---|
Differenza con DELETE
TRUNCATE è più veloce di DELETE perché non registra ogni singola eliminazione nel log. Inoltre, resetta i contatori AUTO_INCREMENT. Non può essere usato con clausole WHERE.
Dal testo al modello fisico: un caso completo
Perché questo esempio chiude il DDL
Le istruzioni DDL viste finora non si studiano isolate: sono l'ultima tappa di un percorso che parte da un testo in italiano e arriva alle tabelle reali. L'ordine è sempre lo stesso: testo iniziale → modello E-R → modello logico → modello fisico. Le prime due tappe e la traduzione al modello logico sono spiegate in modelloErLogico.html; il modello fisico è ciò che il DDL costruisce. Il caso seguente percorre tutte e quattro le tappe sullo stesso enunciato, così ogni tabella creata ha una giustificazione visibile e nessuna istruzione resta fine a sé stessa.
Tappa 1Testo iniziale (requisiti)
Una scuola di musica vuole gestire istruttori, corsi e iscrizioni. Di ogni istruttore si conoscono codice fiscale, nome, cognome e data di nascita. Ogni corso ha un codice, un nome (per esempio «Chitarra classica»), un livello (Base, Intermedio, Avanzato) e un numero massimo di partecipanti; è tenuto da un solo istruttore, mentre un istruttore può tenere più corsi. Gli allievi (codice fiscale, nome, cognome, telefono) si iscrivono ai corsi: un allievo può iscriversi a più corsi e un corso può avere molti allievi. Di ogni iscrizione si registrano la data e il voto finale, che manca finché il corso non è terminato.
Le informazioni da conservare sono quattro gruppi (istruttori, corsi, allievi, iscrizioni) e due legami (chi tiene un corso, chi si iscrive a un corso). Il testo dice anche quante volte un'entità può comparire nel legame: è da lì che nascono le cardinalità.
Tappa 2Modello E-R
Le scelte che rendono il modello E-R corretto rispetto al testo:
- Identificatori:
CFper istruttore e allievo,CodCorsoper il corso. Ogni entità ha un identificatore proprio, quindi nessuna entità è debole. - TIENE è 1:N: il testo dice «un solo istruttore per corso, più corsi per istruttore». La coppia accanto a un'entità parla dell'altra: accanto a ISTRUTTORE c'è (0,n) (un istruttore tiene più corsi), accanto a CORSO c'è (1,1) (un corso ha un solo istruttore). Il massimo 1 sta accanto a CORSO, ed è in CORSO che andrà la chiave esterna.
- ISCRIZIONE è N:M con attributi: data e voto non appartengono né all'allievo né al corso, ma alla coppia allievo-corso. Per questo sono attributi dell'associazione e non delle entità.
- MaxPartecipanti resta attributo di CORSO: è un limite dichiarato dal corso, non un dato dell'iscrizione. Che il limite venga poi rispettato è una regola applicativa (la controlla la pagina PHP, oppure un trigger), non un vincolo esprimibile con una chiave esterna.
Tappa 3Modello logico
La traduzione segue due regole già viste in modelloErLogico.html: ogni entità diventa una tabella; ogni associazione 1:N diventa una chiave esterna nella tabella che ha il massimo 1 accanto a sé, mentre un'associazione N:M diventa una tabella autonoma. Le entità sono tre (ISTRUTTORE, CORSO, ALLIEVO); l'associazione N:M ISCRIZIONE diventa una tabella, quindi le tabelle sono quattro.
Schema relazionale
ISTRUTTORE (CF(PK), Nome, Cognome, DataNascita)
CORSO (CodCorso(PK), Nome, Livello, MaxPartecipanti, CFIstruttore(FK))
ALLIEVO (CF(PK), Nome, Cognome, Telefono)
ISCRIZIONE (PK(CFAllievo, CodCorso), DataIscrizione, VotoFinale,
CFAllievo(FK), CodCorso(FK))
Notazione: PK(...) e FK(...); con la chiave composta i nomi stanno dentro la parentesi. L'ordine è quello del manuale: prima la PK, poi gli attributi propri, per ultime le FK — per questo in ISCRIZIONE CFAllievo e CodCorso compaiono due volte, dentro PK(...) e in coda come FK.
- Da TIENE (1:N) nasce la colonna
CFIstruttoreinCORSO: la FK va nell'entità che ha il massimo 1 accanto a sé — qui CORSO — e punta aISTRUTTORE.CF. Se la mettessimo in ISTRUTTORE avremmo bisogno di più righe per lo stesso istruttore, cioè di un errore di progetto. - Da ISCRIZIONE (N:M) nasce una tabella con PK composta
PK(CFAllievo, CodCorso): le due colonne sono anche FK, scritte in coda. La coppia impedisce di iscrivere due volte lo stesso allievo allo stesso corso, mentre gli attributi dell'associazione restano qui. - Il voto può mancare: nel modello logico
VotoFinaleammette NULL, a differenza diDataIscrizioneche è obbligatoria. Questa distinzione si trasferisce direttamente nel DDL.
Tappa 4Modello fisico (DDL)
Il modello fisico è la trascrizione dello schema logico in istruzioni DDL per uno specifico DBMS: tipi di dato concreti, vincoli di chiave e regole di integrità referenziale. L'ordine conta: prima le tabelle che non hanno FK, poi quelle che le referenziano.
-- Tappa 4: modello fisico (MySQL / MariaDB) CREATE DATABASE scuola_musica; USE scuola_musica; CREATE TABLE istruttore ( CF CHAR(16) PRIMARY KEY, Nome VARCHAR(50) NOT NULL, Cognome VARCHAR(50) NOT NULL, DataNascita DATE NOT NULL ); CREATE TABLE allievo ( CF CHAR(16) PRIMARY KEY, Nome VARCHAR(50) NOT NULL, Cognome VARCHAR(50) NOT NULL, Telefono VARCHAR(20) ); CREATE TABLE corso ( CodCorso INT PRIMARY KEY AUTO_INCREMENT, Nome VARCHAR(60) NOT NULL, Livello ENUM('Base', 'Intermedio', 'Avanzato') NOT NULL DEFAULT 'Base', MaxPartecipanti SMALLINT NOT NULL, CFIstruttore CHAR(16) NOT NULL, CONSTRAINT chk_corso_maxpartecipanti CHECK (MaxPartecipanti > 0), CONSTRAINT fk_corso_istruttore FOREIGN KEY (CFIstruttore) REFERENCES istruttore(CF) ON UPDATE CASCADE ON DELETE RESTRICT ); CREATE TABLE iscrizione ( -- chiave primaria composta: primo elemento della tabella PRIMARY KEY (CFAllievo, CodCorso), CFAllievo CHAR(16) NOT NULL, CodCorso INT NOT NULL, DataIscrizione DATE NOT NULL, VotoFinale DECIMAL(4,2) NULL, CONSTRAINT chk_iscrizione_voto CHECK (VotoFinale BETWEEN 0 AND 10), CONSTRAINT fk_iscrizione_allievo FOREIGN KEY (CFAllievo) REFERENCES allievo(CF) ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT fk_iscrizione_corso FOREIGN KEY (CodCorso) REFERENCES corso(CodCorso) ON UPDATE CASCADE ON DELETE CASCADE );
Dal modello logico al DDL, riga per riga
| Modello logico | Nel modello fisico |
|---|---|
| Quattro tabelle | Quattro CREATE TABLE, dopo CREATE DATABASE e USE |
CF, CodCorso, coppia (CFAllievo, CodCorso) |
PRIMARY KEY di colonna per le prime due, PRIMARY KEY composta per ISCRIZIONE |
CFIstruttore(FK) in CORSO |
FOREIGN KEY ... REFERENCES istruttore(CF) con ON DELETE RESTRICT: non si cancella un istruttore che ha ancora corsi |
| FK verso allievo e corso in ISCRIZIONE | Due FOREIGN KEY con ON DELETE CASCADE: cancellando un allievo o un corso spariscono le iscrizioni collegate |
VotoFinale opzionale; livello tra tre valori; massimo > 0 |
NULL ammesso, ENUM('Base','Intermedio','Avanzato'), CHECK su VotoFinale e MaxPartecipanti |
Perché le FK si scrivono dopo le tabelle che referenziano
Se esegui le istruzioni in ordine, istruttore e allievo esistono già quando corso e iscrizione le citano.
Con questo schema il database esiste ma è vuoto: le tabelle sono «scatole» con la loro struttura, senza righe. Riempirle è compito del DML (INSERT, UPDATE, DELETE), che è la sezione successiva. Da qui in poi è anche lo stesso schema che la pagina web in PHP userà per leggere e scrivere i dati: se il modello fisico è corretto, la pagina non deve «aggiustare» nulla nel database.
📋 Tabella riassuntiva dei comandi DDL
Un comando per riga, con un esempio essenziale e la spiegazione di ciò che l'esempio produce sul database. Gli esempi usano lo schema dipendenti / ordini / clienti delle sezioni precedenti.
| Comando | Esempio | Spiegazione dell'esempio |
|---|---|---|
CREATE DATABASE |
CREATE DATABASE IF NOT EXISTS azienda; |
Crea il database azienda solo se non esiste già, così lo script di setup può essere rieseguito senza errori. |
CREATE TABLE |
CREATE TABLE dipendenti (
id INT PRIMARY KEY,
nome VARCHAR(50) NOT NULL,
stipendio DECIMAL(10,2)
); |
Crea la tabella con tre colonne: id chiave primaria (univoca e non NULL), nome obbligatorio, stipendio decimale a due cifre. |
CONSTRAINT ... CHECK |
CREATE TABLE prodotti (
prezzo DECIMAL(10,2) NOT NULL,
sconto DECIMAL(10,2) NOT NULL DEFAULT 0,
CONSTRAINT chk_prodotti_sconto
CHECK (sconto >= 0 AND sconto <= prezzo)
); |
Vincolo di tupla: rifiuta ogni riga in cui lo sconto esce dall'intervallo 0 … prezzo. Va scritto a livello di tabella e con un nome, altrimenti il DBMS ne genera uno non riutilizzabile in un DROP CHECK. |
FOREIGN KEY |
CONSTRAINT fk_ordini_cliente
FOREIGN KEY (cliente_id)
REFERENCES clienti(id)
ON DELETE CASCADE
ON UPDATE CASCADE |
Integrità referenziale: un ordine può puntare solo a un cliente esistente; eliminando o modificando quel cliente, la stessa azione si propaga agli ordini. |
ALTER TABLE ... ADD |
ALTER TABLE dipendenti
ADD email VARCHAR(100); |
Aggiunge la colonna email a tabella già popolata: le righe esistenti ricevono NULL, nessun dato viene perso. |
ALTER TABLE ... MODIFY |
ALTER TABLE dipendenti
MODIFY nome VARCHAR(100); |
Cambia tipo e lunghezza della colonna nome conservando i valori già memorizzati. |
ALTER TABLE ... DROP COLUMN |
ALTER TABLE dipendenti
DROP COLUMN email; |
Rimuove la colonna email insieme a tutti i valori che conteneva: è irreversibile, va verificato prima cosa si perde. |
ALTER TABLE ... COMMENT |
ALTER TABLE dipendenti
COMMENT = 'Registro storico del personale attivo'; |
Salva un commento nei metadati della tabella (compare in SHOW CREATE TABLE): documentazione che resta dentro il database. |
ALTER TABLE ... ADD CONSTRAINT |
ALTER TABLE ordini
ADD CONSTRAINT fk_ordini_cliente
FOREIGN KEY (cliente_id) REFERENCES clienti(id); |
Aggiunge una chiave esterna a una tabella che esiste già, senza ricrearla: utile quando il vincolo è stato dimenticato. |
ALTER TABLE ... RENAME COLUMN |
ALTER TABLE dipendenti
RENAME COLUMN data_assunzione TO assunzione; |
Rinomina la colonna: i dati e gli eventuali indici restano, cambia solo il nome usato nelle query. |
CREATE INDEX |
CREATE INDEX idx_cognome
ON dipendenti (cognome); |
Crea un indice su cognome: le ricerche e gli ordinamenti su quella colonna non richiedono più la scansione completa della tabella. |
DROP TABLE |
DROP TABLE dipendenti; |
Elimina la tabella con struttura e dati. In MySQL/MariaDB è un comando DDL, quindi esegue un commit implicito: non è annullabile con ROLLBACK. |
TRUNCATE TABLE |
TRUNCATE TABLE dipendenti; |
Svuota la tabella conservandone la struttura: più rapido di DELETE senza WHERE e di norma azzera il contatore AUTO_INCREMENT. |