Menu

Vincoli CHECK in SQLite: validare i dati a livello di tabella

Come usare i vincoli CHECK in SQLite per imporre regole sui valori delle colonne: controlli su una colonna, su più colonne, vincoli con nome e le insidie da conoscere.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un vincolo CHECK è una regola che ogni riga deve rispettare

Un vincolo CHECK è un'espressione booleana che associ a una tabella. SQLite la valuta a ogni INSERT e UPDATE, e se l'espressione risulta falsa l'operazione fallisce. È un modo per incorporare una regola di business, "il prezzo non può essere negativo", "lo stato deve essere uno di questi tre valori", direttamente nello schema.

Le prime due righe vengono inserite. La terza solleva CHECK constraint failed e viene rifiutata: la tabella non la vede mai. Il vincolo impone la regola a chiunque scriva, che sia la tua app, uno script di migrazione o qualcuno che sta curiosando nella CLI.

A livello di colonna contro a livello di tabella

Puoi scrivere un CHECK in due punti: dopo la definizione di una colonna (a livello di colonna) o dopo tutte le colonne (a livello di tabella). Si comportano allo stesso modo; la differenza è in cosa si legge in modo più naturale.

La prima prenotazione viene inserita. La seconda fallisce: la fine è prima dell'inizio. Le regole su una sola colonna si leggono meglio a livello di colonna; tutto ciò che confronta due o più colonne si legge meglio a livello di tabella.

Limitare i valori a un elenco

Un uso comune è costringere una colonna a uno di un insieme fisso di valori. SQLite non ha un tipo enum nativo, quindi l'idioma è CHECK ... IN (...):

La terza riga fallisce: 'pending' non è nell'elenco dei valori ammessi. Se un giorno dovrai aggiungere un nuovo stato, dovrai ricostruire la tabella (ne parliamo più sotto), quindi pensaci un attimo prima di bloccare l'elenco. Ma per vocabolari davvero fissi, come nomi di ruoli o stati degli ordini, è esattamente il vincolo che ti serve.

Dare un nome ai vincoli

Di default un vincolo è anonimo. Il messaggio di errore dice solo "CHECK constraint failed" con l'espressione, che va bene quando sulla tabella c'è un solo CHECK, ma confonde quando ce ne sono cinque. Aggiungi un nome con CONSTRAINT:

Ora il messaggio di errore include il nome del vincolo, così sai subito quale regola è stata violata. Dare un nome costa qualche carattere in più e si ripaga la prima volta che qualcosa si rompe in produzione.

CHECK e NULL: l'insidia

CHECK passa quando l'espressione è vera oppure NULL. Fallisce solo con un falso esplicito. Sembra strano, finché non ricordi che quasi ogni confronto con NULL restituisce NULL, non vero o falso.

La riga con NULL viene inserita senza problemi: NULL >= 0 è NULL, non falso, quindi il CHECK non fallisce. Se vuoi davvero vietare sia i numeri negativi sia i valori mancanti, combina NOT NULL con il CHECK:

Ora l'insert fallisce sul vincolo NOT NULL prima ancora che venga eseguito il CHECK. I due vincoli lavorano insieme: NOT NULL copre l'assenza, CHECK copre la forma.

Funzioni integrate utili dentro CHECK

L'espressione può usare la maggior parte delle funzioni integrate di SQLite. Alcune capitano spesso:

Tre fallimenti: un'email con una forma sbagliata, uno username troppo corto e un codice paese in minuscolo. LIKE gestisce i pattern semplici; length(), upper(), lower() e l'aritmetica sono tutti ammessi. Tieni però l'espressione deterministica: usare qualcosa come random() o current_timestamp dentro un CHECK crea regole che possono cambiare da una riga all'altra, e raramente è quello che vuoi.

CHECK contro trigger

Sia CHECK sia i trigger possono rifiutare dati sbagliati, e chi inizia si chiede spesso quale usare. La regola pratica:

  • CHECK quando la regola dipende solo dalla riga che stai scrivendo. "Questa colonna confrontata con quella", "questo valore dentro un intervallo", "questa stringa corrisponde a un pattern".
  • Trigger (nello specifico un trigger BEFORE INSERT/UPDATE che chiama RAISE) quando la regola dipende da altre righe, da altre tabelle o deve fare qualcosa di più complesso di una singola espressione booleana.

CHECK è più veloce, più semplice e visibile nello schema: chiunque legga il CREATE TABLE vede la regola. Ricorri a un trigger solo quando CHECK non riesce a esprimere ciò che ti serve.

Non puoi eliminare un CHECK con ALTER

Questo è l'unico punto spigoloso. SQLite non ha ALTER TABLE ... DROP CONSTRAINT. Per rimuovere o cambiare un CHECK, ricostruisci la tabella:

BEGIN;

CREATE TABLE products_new (
    id    INTEGER PRIMARY KEY,
    name  TEXT NOT NULL,
    price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);

INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;

COMMIT;

Racchiudi tutto in una transazione, così un errore a metà lascia il database com'era. Se altre tabelle hanno chiavi esterne che puntano alla tabella che stai ricostruendo, il procedimento si allunga: disattivi foreign_keys, ricostruisci, riattivi, ricontrolli. Lo vedremo nella guida sulle migrazioni più avanti nel percorso.

Prossimo passo: vincoli UNIQUE

CHECK valida la forma dei valori dentro una riga. Il prossimo vincolo, UNIQUE, valida le relazioni tra righe: garantisce che due righe non abbiano lo stesso valore in una colonna o in un insieme di colonne. È il prossimo argomento.

Domande frequenti

Cos'è un vincolo CHECK in SQLite?

Un vincolo CHECK è un'espressione booleana associata a una tabella che ogni riga deve soddisfare. SQLite la valuta a ogni INSERT o UPDATE e rifiuta la modifica se l'espressione è falsa. È il modo più semplice per imporre una regola come 'il prezzo deve essere positivo' senza scrivere codice applicativo.

Un vincolo CHECK in SQLite può fare riferimento a più colonne?

Sì: scrivilo come vincolo a livello di tabella invece di associarlo a una sola colonna. Per esempio, CHECK (start_date <= end_date) dichiarato dopo l'elenco delle colonne può fare riferimento a entrambe. Tecnicamente anche i controlli a livello di colonna possono citare altre colonne, ma la forma a livello di tabella si legge meglio quando le colonne coinvolte sono più di una.

Perché il mio vincolo CHECK in SQLite non scatta con NULL?

CHECK passa quando l'espressione è vera oppure NULL: fallisce solo quando l'espressione è esplicitamente falsa. Quindi CHECK (age >= 0) accetta un'età NULL, perché NULL >= 0 è NULL, non falso. Se vuoi vietare anche NULL, aggiungi un vincolo NOT NULL insieme al CHECK.

Posso eliminare o modificare un vincolo CHECK in SQLite?

Non direttamente. SQLite non supporta ALTER TABLE ... DROP CONSTRAINT. Per cambiare un CHECK puoi modificare sqlite_schema con PRAGMA writable_schema (avanzato e rischioso) oppure ricostruire la tabella: crei una nuova tabella con i vincoli desiderati, copi i dati, elimini la vecchia tabella e rinomini la nuova. Dare un nome ai vincoli rende più leggibile lo script di ricostruzione.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA