▦ Dati e database

NULL in SQL: le trappole che falsano i risultati delle query

Perché NULL non è uguale a NULL, come si comporta con COUNT, NOT IN e le somme, e quali regole seguire per evitare report sbagliati senza accorgersene.

In SQL, NULL non è zero, non è una stringa vuota e non è falso. Significa «valore sconosciuto» o «non applicabile». Detto così sembra semplice, ma il suo comportamento genera alcuni degli errori più insidiosi nelle query: quelli che non producono messaggi di errore, ma risultati sbagliati che sembrano giusti.

La logica a tre valori

Nella maggior parte dei linguaggi una condizione è vera o falsa. In SQL esiste un terzo esito: sconosciuto (unknown). Qualunque confronto con NULL restituisce sconosciuto:

  • 5 = NULL → sconosciuto
  • NULL = NULL → sconosciuto
  • NULL <> 3 → sconosciuto

Il ragionamento è coerente: se non conosco l'età di due persone, non posso dire che sia uguale. Ma la conseguenza pratica è che WHERE colonna = NULL non restituisce mai nessuna riga. La forma corretta è:

SELECT * FROM clienti WHERE telefono IS NULL;
SELECT * FROM clienti WHERE telefono IS NOT NULL;

La clausola WHERE conserva solo le righe per cui la condizione è vera. Le righe per cui è falsa e quelle per cui è sconosciuta vengono scartate allo stesso modo.

Trappola 1: il filtro «diverso da»

Una tabella di prodotti ha una colonna categoria, non sempre compilata. Si vogliono tutti i prodotti che non appartengono alla categoria «Outlet»:

SELECT * FROM prodotti WHERE categoria <> 'Outlet';

I prodotti senza categoria non compaiono, perché NULL <> 'Outlet' è sconosciuto. Se devono essere inclusi, va detto esplicitamente:

SELECT * FROM prodotti
WHERE categoria <> 'Outlet' OR categoria IS NULL;

Alcuni database offrono anche l'operatore IS DISTINCT FROM, che tratta NULL come un valore confrontabile.

Trappola 2: NOT IN con una sottoquery

È la trappola più pericolosa. Si cercano i clienti che non compaiono nella lista dei clienti bloccati:

SELECT * FROM clienti
WHERE id NOT IN (SELECT id_cliente FROM blocchi);

Se anche una sola riga di blocchi ha id_cliente vuoto, la query non restituisce nessun cliente. Il motivo: id NOT IN (1, 2, NULL) equivale a id <> 1 AND id <> 2 AND id <> NULL, e l'ultimo termine è sempre sconosciuto, quindi l'intera condizione non è mai vera.

La soluzione robusta è NOT EXISTS, che non ha questo problema:

SELECT * FROM clienti c
WHERE NOT EXISTS (
  SELECT 1 FROM blocchi b WHERE b.id_cliente = c.id
);

Trappola 3: i conteggi

Le funzioni di aggregazione ignorano i valori NULL, con un'eccezione importante.

Espressione Che cosa conta o calcola
COUNT(*) Tutte le righe
COUNT(telefono) Solo le righe con telefono non nullo
SUM(importo) Somma dei valori non nulli
AVG(voto) Media dei soli valori non nulli

La media è il caso più delicato. Se in una tabella di valutazioni chi non ha risposto ha NULL, AVG(voto) calcola la media solo di chi ha risposto. Se invece l'assenza di risposta andava considerata come zero, il risultato è gonfiato. Non esiste una risposta giusta in assoluto: bisogna decidere che cosa significa l'assenza e scriverlo nella query.

Un'altra particolarità: SUM su un insieme in cui tutti i valori sono NULL, o su nessuna riga, restituisce NULL e non zero. In un report questo può far comparire celle vuote dove ci si aspetta 0.

Trappola 4: i calcoli

Qualunque operazione aritmetica o concatenazione con NULL produce NULL:

SELECT prezzo + spedizione FROM ordini;

Se la spedizione non è indicata, il totale diventa sconosciuto. La funzione COALESCE restituisce il primo valore non nullo di un elenco ed è lo strumento standard per gestire questi casi:

SELECT prezzo + COALESCE(spedizione, 0) AS totale FROM ordini;

Va usata con criterio: sostituire NULL con 0 ha senso per una spedizione gratuita, ma non per una temperatura non misurata.

Trappola 5: ordinamenti e vincoli di unicità

Nell'ordinamento, i database non sono d'accordo su dove mettere i NULL: alcuni li considerano più grandi di ogni valore, altri più piccoli. Dove supportato, ORDER BY colonna NULLS LAST rende esplicita la scelta.

Nei vincoli UNIQUE, molti database permettono più righe con NULL nella stessa colonna, proprio perché due NULL non sono considerati uguali. È un comportamento utile, per esempio per un codice facoltativo che deve essere unico quando presente, ma sorprende chi non se lo aspetta.

Regole pratiche di progettazione

  1. Dichiarare NOT NULL ogni colonna che deve sempre avere un valore. È la difesa più efficace.
  2. Usare valori predefiniti con DEFAULT quando esiste un valore sensato, come 0 per una quantità.
  3. Non usare valori convenzionali come -1 o «N/D» al posto di NULL: finiscono nelle somme e nelle medie.
  4. Preferire NOT EXISTS a NOT IN con le sottoquery.
  5. Quando si scrive un report, chiedersi per ogni colonna: che cosa deve succedere se è vuota?

In sintesi

  • NULL significa sconosciuto, e ogni confronto con NULL dà sconosciuto.
  • Si filtra con IS NULL e IS NOT NULL, mai con = NULL.
  • NOT IN con una sottoquery che contiene NULL non restituisce nulla: meglio NOT EXISTS.
  • COUNT(colonna), AVG e SUM ignorano i NULL, con effetti sui report.
  • Il vincolo NOT NULL evita gran parte dei problemi alla radice.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica e in psicologia, unisce l'analisi tecnica alla comprensione del fattore umano.

Leggi anche