Database
 sql >> Database >  >> RDS >> Database

Complessità NULL – Parte 2

Questo articolo è il secondo di una serie sulle complessità NULL. Il mese scorso ho introdotto NULL come indicatore di SQL per qualsiasi tipo di valore mancante. Ho spiegato che SQL non ti offre la possibilità di distinguere tra mancante e applicabile (valori A) e mancante e non applicabile (valori I) marker. Ho anche spiegato come funzionano i confronti che coinvolgono NULL con costanti, variabili, parametri e colonne. Questo mese continuo la discussione coprendo le incongruenze del trattamento NULL in diversi elementi T-SQL.

Continuerò a utilizzare il database di esempio TSQLV5 come il mese scorso in alcuni dei miei esempi. Puoi trovare lo script che crea e popola questo database qui e il suo diagramma ER qui.

NULLA incoerenze di trattamento

Come hai già capito, il trattamento NULL non è banale. Parte della confusione e della complessità ha a che fare con il fatto che il trattamento dei NULL può essere incoerente tra i diversi elementi di T-SQL per operazioni simili. Nelle prossime sezioni descrivo la gestione di NULL nei calcoli lineari rispetto a quelli aggregati, le clausole ON/WHERE/HAVING, il vincolo CHECK rispetto all'opzione CHECK, gli elementi IF/WHILE/CASE, l'istruzione MERGE, la distinzione e il raggruppamento, nonché l'ordinamento e l'unicità.

Calcoli lineari rispetto a quelli aggregati

T-SQL, e lo stesso vale per SQL standard, utilizza una logica di gestione NULL diversa quando si applica una funzione aggregata effettiva come SUM, MIN e MAX su righe rispetto a quando si applica lo stesso calcolo di uno lineare su colonne. Per dimostrare questa differenza, userò due tabelle di esempio denominate #T1 e #T2 che crei e popola eseguendo il codice seguente:

DROP TABLE IF EXISTS #T1, #T2;
 
SELECT * INTO #T1 FROM ( VALUES(10, 5, NULL) ) AS D(col1, col2, col3);
 
SELECT * INTO #T2 FROM ( VALUES(10),(5),(NULL) ) AS D(col1);

La tabella #T1 ha tre colonne denominate col1, col2 e col3. Attualmente ha una riga con i valori di colonna 10, 5 e NULL, rispettivamente:

SELECT * FROM #T1;
col1        col2        col3
----------- ----------- -----------
10          5           NULL

La tabella #T2 ha una colonna chiamata col1. Attualmente ha tre righe con i valori 10, 5 e NULL in col1:

SELECT * FROM #T2;
col1
-----------
10
5
NULL

Quando si applica quello che in definitiva è un calcolo aggregato come l'addizione lineare tra colonne, la presenza di qualsiasi input NULL produce un risultato NULL. La query seguente mostra questo comportamento:

SELECT col1 + col2 + col3 AS total
FROM #T1;

Questa query genera il seguente output:

total
-----------
NULL

Al contrario, le effettive funzioni aggregate, che vengono applicate su più righe, sono progettate per ignorare gli input NULL. La query seguente mostra questo comportamento utilizzando la funzione SOMMA:

SELECT SUM(col1) AS total
FROM #T2;

Questa query genera il seguente output:

total
-----------
15

Warning: Null value is eliminated by an aggregate or other SET operation.

Si noti l'avviso imposto dallo standard SQL che indica la presenza di input NULL che sono stati ignorati. Puoi eliminare tali avvisi disattivando l'opzione di sessione ANSI_WARNINGS.

Allo stesso modo, quando applicata a un'espressione di input, la funzione COUNT conta il numero di righe con valori di input non NULL (al contrario di COUNT(*) che conta semplicemente il numero di righe). Ad esempio, la sostituzione di SUM(col1) con COUNT(col1) nella query precedente restituisce il conteggio di 2.

Curiosamente, se si applica un'aggregazione COUNT a una colonna definita come non consentita NULL, l'ottimizzatore converte l'espressione COUNT() in COUNT(*). Ciò consente l'utilizzo di qualsiasi indice ai fini del conteggio anziché richiedere l'utilizzo di un indice che contiene la colonna in questione. Questo è un motivo in più oltre a garantire la coerenza e l'integrità dei tuoi dati che dovrebbe incoraggiarti a imporre vincoli come NOT NULL e altri. Tali vincoli consentono all'ottimizzatore una maggiore flessibilità nel considerare alternative più ottimali ed evitare lavori non necessari.

In base a questa logica, la funzione AVG divide la somma dei valori non NULL per il conteggio dei valori non NULL. Considera la seguente query come esempio:

SELECT AVG(1.0 * col1) AS avgall
FROM #T2;

Qui la somma dei valori col1 non NULL 15 è divisa per il conteggio dei valori non NULL 2. Moltiplichi col1 per il valore letterale numerico 1.0 per forzare la conversione implicita dei valori di input interi in valori numerici per ottenere una divisione numerica e non un intero divisione. Questa query genera il seguente output:

avgall
---------
7.500000

Allo stesso modo, gli aggregati MIN e MAX ignorano gli input NULL. Considera la seguente query:

SELECT MIN(col1) AS mincol1, MAX(col1) AS maxcol1
FROM #T2;

Questa query genera il seguente output:

mincol1     maxcol1
----------- -----------
5           10

Tentare di applicare calcoli lineari ma emulare la semantica della funzione aggregata (ignorare i NULL) non è carino. L'emulazione di SUM, COUNT e AVG non è troppo complessa, ma richiede di controllare ogni input per NULL, in questo modo:

SELECT col1, col2, col3,
  CASE
    WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
    ELSE COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0)
  END AS sumall,
  CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
    + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
    + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END AS cntall,
  CASE
    WHEN COALESCE(col1, col2, col3) IS NULL THEN NULL
    ELSE 1.0 * (COALESCE(col1, 0) + COALESCE(col2, 0) + COALESCE(col3, 0))
           / (CASE WHEN col1 IS NOT NULL THEN 1 ELSE 0 END
                + CASE WHEN col2 IS NOT NULL THEN 1 ELSE 0 END
                + CASE WHEN col3 IS NOT NULL THEN 1 ELSE 0 END)
  END AS avgall
FROM #T1;

Questa query genera il seguente output:

col1        col2        col3        sumall      cntall      avgall
----------- ----------- ----------- ----------- ----------- ---------------
10          5           NULL        15          2           7.500000000000

Il tentativo di applicare un minimo o un massimo come calcolo lineare a più di due colonne di input è piuttosto complicato anche prima di aggiungere la logica per ignorare i NULL poiché implica l'annidamento di più espressioni CASE direttamente o indirettamente (quando si riutilizzano gli alias di colonna). Ad esempio, ecco una query che calcola il massimo tra col1, col2 e col3 in #T1, senza la parte che ignora i NULL:

SELECT col1, col2, col3, 
  CASE WHEN col1 IS NULL OR col2 IS NULL OR col3 IS NULL THEN NULL ELSE max2 END AS maxall
FROM #T1
  CROSS APPLY (VALUES(CASE WHEN col1 >= col2 THEN col1 ELSE col2 END)) AS A1(max1)
  CROSS APPLY (VALUES(CASE WHEN max1 >= col3 THEN max1 ELSE col3 END)) AS A2(max2);

Questa query genera il seguente output:

col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        NULL

Se esamini il piano di query, troverai la seguente espressione espansa che calcola il risultato finale:

[Expr1005] = Scalar Operator(CASE WHEN CASE WHEN [#T1].[col1] IS NOT NULL THEN [#T1].[col1] ELSE 
  CASE WHEN [#T1].[col2] IS NOT NULL THEN [#T1].[col2] 
    ELSE [#T1].[col3] END END IS NULL THEN NULL ELSE 
  CASE WHEN CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
    ELSE [#T1].[col2] END>=[#T1].[col3] THEN 
  CASE WHEN [#T1].[col1]>=[#T1].[col2] THEN [#T1].[col1] 
    ELSE [#T1].[col2] END ELSE [#T1].[col3] END END)

Ed è allora che sono coinvolte solo tre colonne. Immagina di avere una dozzina di colonne coinvolte!

Ora aggiungi a questo la logica per ignorare i NULL:

SELECT col1, col2, col3, max2 AS maxall
FROM #T1
  CROSS APPLY (VALUES(CASE WHEN col1 >= col2 OR col2 IS NULL THEN col1 ELSE col2 END)) AS A1(max1)
  CROSS APPLY (VALUES(CASE WHEN max1 >= col3 OR col3 IS NULL THEN max1 ELSE col3 END)) AS A2(max2);

Questa query genera il seguente output:

col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        10

Oracle ha una coppia di funzioni chiamate GREATEST e LEAST che applicano i calcoli minimi e massimi, rispettivamente, come quelli lineari ai valori di input. Queste funzioni restituiscono un NULL dato qualsiasi input NULL come fanno la maggior parte dei calcoli lineari. C'era un elemento di feedback aperto che chiedeva di ottenere funzioni simili in T-SQL, ma questa richiesta non è stata trasferita nell'ultima modifica del sito di feedback. Se Microsoft aggiunge tali funzioni a T-SQL, sarebbe fantastico avere un'opzione per controllare se ignorare o meno i NULL.

Nel frattempo, esiste una tecnica molto più elegante rispetto a quelle sopra menzionate che calcola qualsiasi tipo di aggregato come lineare su colonne utilizzando la semantica della funzione di aggregazione effettiva ignorando i NULL. Si utilizza una combinazione dell'operatore CROSS APPLY e una query di tabella derivata rispetto a un costruttore di valori di tabella che ruota le colonne in righe e applica l'aggregazione come una funzione di aggregazione effettiva. Ecco un esempio che dimostra i calcoli MIN e MAX, ma puoi usare questa tecnica con qualsiasi funzione di aggregazione che ti piace:

SELECT col1, col2, col3, maxall, minall
FROM #T1 CROSS APPLY
  (SELECT MAX(mycol), MIN(mycol)
   FROM (VALUES(col1),(col2),(col3)) AS D1(mycol)) AS D2(maxall, minall);

Questa query genera il seguente output:

col1        col2        col3        maxall      minall
----------- ----------- ----------- ----------- -----------
10          5           NULL        10          5

E se volessi il contrario? Cosa succede se è necessario calcolare un aggregato su righe, ma produrre un NULL se è presente un input NULL? Ad esempio, supponiamo di dover sommare tutti i valori col1 da #T1, ma di restituire NULL se uno qualsiasi degli input è NULL. Questo può essere ottenuto con la seguente tecnica:

SELECT SUM(col1) * NULLIF(MIN(CASE WHEN col1 IS NULL THEN 0 ELSE 1 END), 0) AS sumall
FROM #T2;

Si applica un'aggregazione MIN a un'espressione CASE che restituisce zeri per input NULL e uno per input non NULL. Se è presente un input NULL, il risultato della funzione MIN è 0, altrimenti è 1. Quindi, utilizzando la funzione NULLIF, converti un risultato 0 in un NULL. Quindi moltiplichi il risultato della funzione NULLIF per la somma originale. Se è presente un input NULL, moltiplichi la somma originale per un NULL ottenendo un NULL. Se non è presente alcun input NULL, moltiplichi il risultato della somma originale per 1, ottenendo la somma originale.

Tornando ai calcoli lineari che producono un NULL per qualsiasi input NULL, la stessa logica si applica alla concatenazione di stringhe utilizzando l'operatore +, come dimostra la query seguente:

USE TSQLV5;
 
SELECT empid, country, region, city,
  country + N',' + region + N',' + city AS emplocation
FROM HR.Employees;

Questa query genera il seguente output:

empid       country         region          city            emplocation
----------- --------------- --------------- --------------- ----------------
1           USA             WA              Seattle         USA,WA,Seattle
2           USA             WA              Tacoma          USA,WA,Tacoma
3           USA             WA              Kirkland        USA,WA,Kirkland
4           USA             WA              Redmond         USA,WA,Redmond
5           UK              NULL            London          NULL
6           UK              NULL            London          NULL
7           UK              NULL            London          NULL
8           USA             WA              Seattle         USA,WA,Seattle
9           UK              NULL            London          NULL

Si desidera concatenare le parti di posizione dei dipendenti in una stringa, utilizzando una virgola come separatore. Ma vuoi ignorare gli input NULL. Invece, quando uno qualsiasi degli input è un NULL, ottieni un NULL come risultato. Alcuni disattivano l'opzione di sessione CONCAT_NULL_YIELDS_NULL, che fa sì che un input NULL venga convertito in una stringa vuota per scopi di concatenazione, ma questa opzione non è consigliata poiché applica un comportamento non standard. Inoltre, rimarrai con più separatori consecutivi quando sono presenti input NULL, che in genere non è il comportamento desiderato. Un'altra opzione consiste nel sostituire esplicitamente gli input NULL con una stringa vuota usando le funzioni ISNULL o COALESCE, ma questo di solito si traduce in un codice lungo e dettagliato. Un'opzione molto più elegante consiste nell'usare la funzione CONCAT_WS, introdotta in SQL Server 2017. Questa funzione concatena gli input, ignorando i valori NULL, usando il separatore fornito come primo input. Ecco la query di soluzione utilizzando questa funzione:

SELECT empid, country, region, city,
  CONCAT_WS(N',', country, region, city) AS emplocation
FROM HR.Employees;

Questa query genera il seguente output:

empid       country         region          city            emplocation
----------- --------------- --------------- --------------- ----------------
1           USA             WA              Seattle         USA,WA,Seattle
2           USA             WA              Tacoma          USA,WA,Tacoma
3           USA             WA              Kirkland        USA,WA,Kirkland
4           USA             WA              Redmond         USA,WA,Redmond
5           UK              NULL            London          UK,London
6           UK              NULL            London          UK,London
7           UK              NULL            London          UK,London
8           USA             WA              Seattle         USA,WA,Seattle
9           UK              NULL            London          UK,London

ON/DOVE/AVERE

Quando si utilizzano le clausole di query WHERE, HAVING e ON per scopi di filtro/corrispondenza, è importante ricordare che utilizzano la logica dei predicati a tre valori. Quando è coinvolta la logica a tre valori, si desidera identificare accuratamente il modo in cui la clausola gestisce i casi VERO, FALSO e SCONOSCIUTO. Queste tre clausole sono progettate per accettare casi TRUE e rifiutare casi FALSE e SCONOSCIUTI.

Per dimostrare questo comportamento userò una tabella chiamata Contatti che crei e popola eseguendo il codice seguente:.

DROP TABLE IF EXISTS dbo.Contacts;
GO
 
CREATE TABLE dbo.Contacts
(
  id INT NOT NULL 
    CONSTRAINT PK_Contacts PRIMARY KEY,
  name VARCHAR(10) NOT NULL,
  hourlyrate NUMERIC(12, 2) NULL
    CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)
);
 
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES
  (1, 'A', 100.00),(2, 'B', 200.00),(3, 'C', NULL);

Si noti che i contatti 1 e 2 hanno tariffe orarie applicabili e il contatto 3 no, quindi la sua tariffa oraria è impostata su NULL. Considera la seguente query alla ricerca di contatti con una tariffa oraria positiva:

SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00;

Questo predicato restituisce VERO per i contatti 1 e 2 e SCONOSCIUTO per il contatto 3, quindi l'output contiene solo i contatti 1 e 2:

id          name       hourlyrate
----------- ---------- -----------
1           A          100.00
2           B          200.00

Il pensiero qui è che quando sei certo che il predicato sia vero, vuoi restituire la riga, altrimenti vuoi scartarlo. All'inizio potrebbe sembrare banale, finché non ti rendi conto che alcuni elementi del linguaggio che utilizzano anche i predicati funzionano in modo diverso.

vincolo CHECK rispetto all'opzione CHECK

Un vincolo CHECK è uno strumento utilizzato per imporre l'integrità in una tabella basata su un predicato. Il predicato viene valutato quando si tenta di inserire o aggiornare righe nella tabella. A differenza delle clausole di filtro e corrispondenza delle query che accettano casi TRUE e rifiutano casi FALSE e UNKNOWN, un vincolo CHECK è progettato per accettare casi TRUE e UNKNOWN e rifiutare casi FALSE. Il pensiero qui è che quando sei certo che il predicato è falso, vuoi rifiutare il tentativo di modifica, altrimenti vuoi consentirlo.

Se esamini la definizione della nostra tabella Contatti, noterai che ha il seguente vincolo CHECK, rifiutando i contatti con tariffe orarie non positive:

CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

Si noti che il vincolo utilizza lo stesso predicato come quello utilizzato nel filtro di query precedente.

Prova ad aggiungere un contatto con una tariffa oraria positiva:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (4, 'D', 150.00);

Questo tentativo riesce.

Prova ad aggiungere un contatto con tariffa oraria NULL:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (5, 'E', NULL);

Anche questo tentativo riesce, poiché un vincolo CHECK è progettato per accettare casi TRUE e UNKNOWN. Questo è il caso in cui un filtro di query e un vincolo CHECK sono progettati per funzionare in modo diverso.

Prova ad aggiungere un contatto con una tariffa oraria non positiva:

INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (6, 'F', -100.00);

Questo tentativo non riesce con il seguente errore:

Msg 547, livello 16, stato 0, riga 454
L'istruzione INSERT era in conflitto con il vincolo CHECK "CHK_Contacts_hourlyrate". Il conflitto si è verificato nel database "TSQLV5", tabella "dbo.Contacts", colonna 'hourlyrate'.

T-SQL consente inoltre di imporre l'integrità delle modifiche tramite le viste utilizzando un'opzione CHECK. Alcuni pensano che questa opzione serva a uno scopo simile a un vincolo CHECK purché si applichi la modifica attraverso la vista. Si consideri, ad esempio, la visualizzazione seguente, che utilizza un filtro basato sulla tariffa oraria predicata> 0,00 ed è definita con l'opzione VERIFICA:

CREATE OR ALTER VIEW dbo.MyContacts
AS
SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00
WITH CHECK OPTION;

A quanto pare, a differenza di un vincolo CHECK, l'opzione di visualizzazione CHECK è progettata per accettare casi TRUE e rifiutare sia i casi FALSE che UNKNOWN. Quindi in realtà è progettato per comportarsi più come fa normalmente il filtro di query anche allo scopo di rafforzare l'integrità.

Prova a inserire una riga con una tariffa oraria positiva attraverso la vista:

INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (7, 'G', 300.00);

Questo tentativo riesce.

Prova a inserire una riga con una tariffa oraria NULL attraverso la vista:

INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (8, 'H', NULL);

Questo tentativo non riesce con il seguente errore:

Msg 550, livello 16, stato 1, riga 473
Il tentativo di inserimento o aggiornamento non è riuscito perché la vista di destinazione specifica WITH CHECK OPTION o si estende su una vista che specifica WITH CHECK OPTION e una o più righe risultanti dall'operazione non qualificarsi sotto il vincolo CHECK OPTION.

Il pensiero qui è che una volta aggiunta l'opzione CHECK alla vista, vuoi solo consentire le modifiche risultanti in righe che vengono restituite dalla vista. È un po' diverso dal pensare con un vincolo CHECK:rifiuta le modifiche per le quali sei certo che il predicato sia falso. Questo può creare un po' di confusione. Se si desidera che la vista consenta le modifiche che impostano la tariffa oraria su NULL, è necessario che il filtro di query consenta anche quelle aggiungendo OR la ​​tariffa oraria IS NULL. Devi solo renderti conto che un vincolo CHECK e un'opzione CHECK sono progettati per funzionare in modo diverso rispetto al caso SCONOSCIUTO. Il primo lo accetta mentre il secondo lo rifiuta.

Interroga la tabella Contatti dopo tutte le modifiche precedenti:

SELECT id, name, hourlyrate
FROM dbo.Contacts;

A questo punto dovresti ottenere il seguente output:

id          name       hourlyrate
----------- ---------- -----------
1           A          100.00
2           B          200.00
3           C          NULL
4           D          150.00
5           E          NULL
7           G          300.00

SE/DURANTE/CASO

Gli elementi del linguaggio IF, WHILE e CASE funzionano con i predicati.

L'istruzione IF è progettata come segue:

IF <predicate>
  <statement or BEGIN-END block when TRUE>
ELSE
  <statement or BEGIN-END block when FALSE or UNKNOWN>

È intuitivo aspettarsi di avere un blocco TRUE dopo la clausola IF e un blocco FALSE dopo la clausola ELSE, ma è necessario rendersi conto che la clausola ELSE viene effettivamente attivata quando il predicato è FALSE o UNKNOWN. Teoricamente, un linguaggio logico a tre valori avrebbe potuto avere un'istruzione IF con una separazione dei tre casi. Qualcosa del genere:

IF <predicate>
  WHEN TRUE
    <statement or BEGIN-END block when TRUE>
  WHEN FALSE
    <statement or BEGIN-END block when FALSE>
  WHEN UNKNOWN
    <statement or BEGIN-END block when UNKNOWN>

E anche consentire combinazioni di risultati logici in modo che se volessi combinare FALSO e SCONOSCIUTO in una sezione, potresti usare qualcosa del genere:

IF <predicate>
  WHEN TRUE
    <statement or BEGIN-END block when TRUE>
  WHEN FALSE OR UNKNOWN
    <statement or BEGIN-END block when FALSE OR UNKNOWN>

Nel frattempo, puoi emulare tali costrutti annidando le istruzioni IF-ELSE e cercando esplicitamente NULL negli operandi con l'operatore IS NULL.

L'istruzione WHILE ha solo un blocco TRUE. È progettato come segue:

WHILE <predicate>
  <statement or BEGIN-END block when TRUE>

L'istruzione o il blocco BEGIN-END che forma il corpo del ciclo viene attivato mentre il predicato è TURE. Non appena il predicato è FALSE o UNKNOWN, il controllo passa all'istruzione che segue il ciclo WHILE.

A differenza di IF e WHILE, che sono istruzioni che eseguono codice, CASE è un'espressione che restituisce un valore. La sintassi di un cercato L'espressione CASE è la seguente:

CASE
  WHEN <predicate 1> THEN <expression 1 when TRUE>
  WHEN <predicate 2> THEN <expression 2 when TRUE >
  ...
  WHEN <predicate n> THEN <expression n when TRUE >
  ELSE <else expression when all are FALSE or UNKNOWN>
END

Un'espressione CASE è progettata per restituire l'espressione che segue la clausola THEN che corrisponde al primo predicato WHEN che restituisce TRUE. Se è presente una clausola ELSE, viene attivata se nessun predicato WHEN è TRUE (tutti sono FALSE o UNKNOWN). In assenza di una clausola ELSE esplicita, viene utilizzata una clausola ELSE NULL implicita. Se vuoi gestire un caso SCONOSCIUTO separatamente, puoi cercare esplicitamente NULL negli operandi del predicato usando l'operatore IS NULL.

Un semplice L'espressione CASE usa confronti basati sull'uguaglianza implicita tra l'espressione di origine e le espressioni confrontate:

CASE <source expression>
  WHEN <comp expression 1> THEN <result expression 1 when TRUE>
  WHEN <comp expression 2> THEN <result expression 2 when TRUE >
  ...
  WHEN <comp expression n> THEN <result expression n when TRUE >
  ELSE <else result expression when all are FALSE or UNKNOWN>
END

L'espressione CASE semplice è progettata in modo simile all'espressione CASE cercata in termini di gestione della logica a tre valori, ma poiché i confronti utilizzano un confronto implicito basato sull'uguaglianza, non è possibile gestire il caso UNKNOWN separatamente. Un tentativo di utilizzare un NULL in una delle espressioni confrontate nelle clausole WHEN non ha significato poiché il confronto non risulterà TRUE anche quando l'espressione di origine è NULL. Considera il seguente esempio:

DECLARE @input AS INT = NULL;
 
SELECT CASE @input WHEN NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Questo viene convertito implicitamente nel seguente:

DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input = NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Di conseguenza, il risultato è:

L'input non è NULL

Per rilevare un input NULL, è necessario utilizzare la sintassi dell'espressione CASE cercata e l'operatore IS NULL, in questo modo:

DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input IS NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Questa volta il risultato è:

L'input è NULL

UNISCI

L'istruzione MERGE viene utilizzata per unire i dati da un'origine a una destinazione. Utilizzi un predicato di unione per identificare i seguenti casi e applicare un'azione rispetto alla destinazione:

  • Una riga di origine è abbinata a una riga di destinazione (attivata quando viene trovata una corrispondenza per la riga di origine in cui il predicato di unione è TRUE):applica UPDATE o DELETE rispetto alla destinazione
  • Una riga di origine non è abbinata a una riga di destinazione (attivata quando non vengono trovate corrispondenze per la riga di origine in cui il predicato di unione è TRUE, piuttosto per tutto il predicato è FALSE o UNKNOWN):applica un INSERT contro la destinazione
  • Una riga di destinazione non è abbinata a una riga di origine (attivata quando non vengono trovate corrispondenze per la riga di destinazione in cui il predicato di unione è TRUE, piuttosto per tutto il predicato è FALSE o UNKNOWN):applica UPDATE o DELETE contro la destinazione

Tutti e tre gli scenari separano TRUE in un gruppo e FALSE o UNKNOWN in un altro. Non ottieni sezioni separate per la gestione di VERO, la gestione di FALSO e la gestione di casi SCONOSCIUTI.

Per dimostrarlo, userò una tabella chiamata T3 che crei e popola eseguendo il seguente codice:

DROP TABLE IF EXISTS dbo.T3;
GO
 
CREATE TABLE dbo.T3(col1 INT NULL, col2 INT NULL, CONSTRAINT UNQ_T3 UNIQUE(col1));
 
INSERT INTO dbo.T3(col1) VALUES(1),(2),(NULL);

Considera la seguente istruzione MERGE:

MERGE INTO dbo.T3 AS TGT
USING (VALUES(1, 100), (3, 300)) AS SRC(col1, col2)
  ON SRC.col1 = TGT.col1
WHEN MATCHED THEN UPDATE
  SET TGT.col2 = SRC.col2
WHEN NOT MATCHED THEN INSERT(col1, col2) VALUES(SRC.col1, SRC.col2)
WHEN NOT MATCHED BY SOURCE THEN UPDATE
  SET col2 = -1;
 
SELECT col1, col2 FROM dbo.T3;

La riga di origine in cui col1 è 1 corrisponde alla riga di destinazione in cui col1 è 1 (il predicato è TRUE) e quindi col2 della riga di destinazione è impostato su 100.

La riga di origine dove col1 è 3 non trova corrispondenza con nessuna riga di destinazione (per tutto il predicato è FALSE o UNKNOWN) e quindi una nuova riga viene inserita in T3 con 3 come valore col1 e 300 come valore col2.

Le righe di destinazione in cui col1 è 2 e dove col1 è NULL non sono abbinate a nessuna riga di origine (per tutte le righe il predicato è FALSE o UNKNOWN) e quindi in entrambi i casi col2 nelle righe di destinazione è impostato su -1.

La query su T3 restituisce il seguente output dopo aver eseguito l'istruzione MERGE precedente:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Mantieni il tavolo T3 intorno; viene utilizzato in seguito.

Distinzione e raggruppamento

A differenza dei confronti eseguiti utilizzando gli operatori di uguaglianza e disuguaglianza, i confronti eseguiti per scopi di distinzione e raggruppamento raggruppano i NULL insieme. Un NULL è considerato non distinto da un altro NULL, ma un NULL è considerato distinto da un valore non NULL. Di conseguenza, l'applicazione di una clausola DISTINCT rimuove le occorrenze duplicate di NULL. La seguente query lo dimostra:

SELECT DISTINCT country, region FROM HR.Employees;

Questa query genera il seguente output:

country         region
--------------- ---------------
UK              NULL
USA             WA

Ci sono più dipendenti con il paese USA e la regione NULL e dopo la rimozione dei duplicati il ​​risultato mostra solo un'occorrenza della combinazione.

Come la distinzione, il raggruppamento raggruppa anche i NULL, come dimostra la query seguente:

SELECT country, region, COUNT(*) AS numemps
FROM HR.Employees
GROUP BY country, region;

Questa query genera il seguente output:

country         region          numemps
--------------- --------------- -----------
UK              NULL            4
USA             WA              5

Anche in questo caso, tutti e quattro i dipendenti con il paese Regno Unito e la regione NULL sono stati raggruppati.

Ordinamento

L'ordinazione considera più NULL come aventi lo stesso valore di ordinazione. Lo standard SQL lascia all'implementazione la scelta se ordinare i NULL per primi o per ultimi rispetto ai valori non NULL. Microsoft ha scelto di considerare i NULL come aventi valori di ordinamento inferiori rispetto ai non NULL in SQL Server, quindi quando si utilizza la direzione dell'ordine crescente, T-SQL ordina prima i NULL. La seguente query lo dimostra:

SELECT id, name, hourlyrate
FROM dbo.Contacts
ORDER BY hourlyrate;

Questa query genera il seguente output:

id          name       hourlyrate
----------- ---------- -----------
3           C          NULL
5           E          NULL
1           A          100.00
4           D          150.00
2           B          200.00
7           G          300.00

Il mese prossimo aggiungerò altro su questo argomento, discutendo gli elementi standard che ti danno il controllo sul comportamento di ordinamento NULL e le soluzioni alternative per quegli elementi in T-SQL.

Unicità

Quando si applica l'univocità su una colonna NULLable utilizzando un vincolo UNIQUE o un indice univoco, T-SQL tratta i NULL proprio come valori non NULL. Rifiuta i NULL duplicati come se un NULL non fosse univoco da un altro NULL.

Ricordiamo che la nostra tabella T3 ha un vincolo UNICO definito su col1. Ecco la sua definizione:

CONSTRAINT UNQ_T3 UNIQUE(col1)

Interroga T3 per vedere il suo contenuto corrente:

SELECT * FROM dbo.T3;

Se hai eseguito tutte le modifiche rispetto a T3 dagli esempi precedenti in questo articolo, dovresti ottenere il seguente output:

col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Tentativo di aggiungere una seconda riga con un NULL in col1:

INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

Viene visualizzato il seguente errore:

Msg 2627, livello 14, stato 1, riga 558
Violazione del vincolo UNIQUE KEY 'UNQ_T3'. Impossibile inserire la chiave duplicata nell'oggetto 'dbo.T3'. Il valore della chiave duplicata è ().

Questo comportamento è in realtà non standard. Il mese prossimo descriverò le specifiche standard e come emularle in T-SQL.

Conclusione

In questa seconda parte della serie sulle complessità NULL mi sono concentrato sulle incongruenze del trattamento NULL tra diversi elementi T-SQL. Ho trattato i calcoli lineari rispetto a quelli aggregati, le clausole di filtraggio e corrispondenza, il vincolo CHECK rispetto all'opzione CHECK, gli elementi IF, WHILE e CASE, l'istruzione MERGE, la distinzione e il raggruppamento, l'ordinamento e l'unicità. Le incongruenze che ho trattato sottolineano ulteriormente quanto sia importante comprendere correttamente il trattamento dei NULL nella piattaforma che stai utilizzando, per assicurarti di scrivere un codice corretto e robusto. Il mese prossimo continuerò la serie trattando le opzioni di trattamento NULL standard SQL che non sono disponibili in T-SQL e fornendo soluzioni alternative supportate in T-SQL.