Database
 sql >> Base de Dados >  >> RDS >> Database

Complexidades NULL - Parte 2


Este artigo é o segundo de uma série sobre complexidades NULL. No mês passado eu introduzi o NULL como marcador de SQL para qualquer tipo de valor ausente. Expliquei que o SQL não oferece a capacidade de distinguir entre ausente e aplicável (valores A) e ausentes e inaplicáveis (valores I) marcadores. Também expliquei como as comparações envolvendo NULLs funcionam com constantes, variáveis, parâmetros e colunas. Este mês eu continuo a discussão cobrindo inconsistências de tratamento NULL em diferentes elementos T-SQL.

Continuarei usando o banco de dados de exemplo TSQLV5 como no mês passado em alguns dos meus exemplos. Você pode encontrar o script que cria e preenche esse banco de dados aqui e seu diagrama ER aqui.

Inconsistências de tratamento NULAS


Como você já percebeu, o tratamento NULL não é trivial. Parte da confusão e complexidade tem a ver com o fato de que o tratamento de NULLs pode ser inconsistente entre diferentes elementos de T-SQL para operações semelhantes. Nas próximas seções, descrevo o tratamento NULL em computações lineares versus agregadas, cláusulas ON/WHERE/HAVING, restrição CHECK versus opção CHECK, elementos IF/WHILE/CASE, a instrução MERGE, distinção e agrupamento, bem como ordenação e exclusividade.

Cálculos lineares versus agregados


O T-SQL, e o mesmo vale para o SQL padrão, usa uma lógica de manipulação NULL diferente ao aplicar uma função de agregação real, como SUM, MIN e MAX nas linhas, em comparação com a aplicação da mesma computação como linear nas colunas. Para demonstrar essa diferença, usarei duas tabelas de exemplo chamadas #T1 e #T2 que você cria e preenche executando o seguinte código:
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);

A tabela #T1 tem três colunas chamadas col1, col2 e col3. Atualmente, possui uma linha com os valores de coluna 10, 5 e NULL, respectivamente:
SELECT * FROM #T1;
col1        col2        col3
----------- ----------- -----------
10          5           NULL

A tabela #T2 tem uma coluna chamada col1. Atualmente possui três linhas com os valores 10, 5 e NULL em col1:
SELECT * FROM #T2;
col1
-----------
10
5
NULL

Ao aplicar o que é, em última análise, uma computação agregada, como adição linear entre colunas, a presença de qualquer entrada NULL produz um resultado NULL. A consulta a seguir demonstra esse comportamento:
SELECT col1 + col2 + col3 AS total
FROM #T1;

Essa consulta gera a seguinte saída:
total
-----------
NULL

Por outro lado, as funções de agregação reais, que são aplicadas em linhas, são projetadas para ignorar entradas NULL. A consulta a seguir demonstra esse comportamento usando a função SUM:
SELECT SUM(col1) AS total
FROM #T2;

Essa consulta gera a seguinte saída:
total
-----------
15

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

Observe o aviso exigido pelo padrão SQL indicando a presença de entradas NULL que foram ignoradas. Você pode suprimir esses avisos desativando a opção de sessão ANSI_WARNINGS.

Da mesma forma, quando aplicada a uma expressão de entrada, a função COUNT conta o número de linhas com valores de entrada não NULL (em oposição a COUNT(*) que simplesmente conta o número de linhas). Por exemplo, substituir SUM(col1) por COUNT(col1) na consulta acima retorna a contagem de 2.

Curiosamente, se você aplicar um agregado COUNT a uma coluna definida como não permitindo NULLs, o otimizador converterá a expressão COUNT() em COUNT(*). Isso permite o uso de qualquer índice para fins de contagem em vez de exigir o uso de um índice que contenha a coluna em questão. Essa é mais uma razão além de garantir a consistência e a integridade de seus dados que deve incentivá-lo a impor restrições como NOT NULL e outras. Tais restrições permitem ao otimizador mais flexibilidade ao considerar alternativas mais ótimas, evitando trabalho desnecessário.

Com base nessa lógica, a função AVG divide a soma de valores não NULL pela contagem de valores não NULL. Considere a seguinte consulta como um exemplo:
SELECT AVG(1.0 * col1) AS avgall
FROM #T2;

Aqui a soma dos valores não-NULL col1 15 é dividida pela contagem de valores não-NULL 2. Você multiplica col1 pelo literal numérico 1.0 para forçar a conversão implícita dos valores de entrada inteiros para valores numéricos para obter a divisão numérica e não o inteiro divisão. Essa consulta gera a seguinte saída:
avgall
---------
7.500000

Da mesma forma, os agregados MIN e MAX ignoram as entradas NULL. Considere a seguinte consulta:
SELECT MIN(col1) AS mincol1, MAX(col1) AS maxcol1
FROM #T2;

Essa consulta gera a seguinte saída:
mincol1     maxcol1
----------- -----------
5           10

Tentar aplicar cálculos lineares, mas emular a semântica da função agregada (ignore NULLs) não é bonito. Emular SUM, COUNT e AVG não é muito complexo, mas exige que você verifique todas as entradas para NULLs, assim:
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;

Essa consulta gera a seguinte saída:
col1        col2        col3        sumall      cntall      avgall
----------- ----------- ----------- ----------- ----------- ---------------
10          5           NULL        15          2           7.500000000000

Tentar aplicar um mínimo ou máximo como um cálculo linear a mais de duas colunas de entrada é bastante complicado mesmo antes de adicionar a lógica para ignorar NULLs, pois envolve o aninhamento de várias expressões CASE direta ou indiretamente (quando você reutiliza aliases de coluna). Por exemplo, aqui está uma consulta computando o máximo entre col1, col2 e col3 em #T1, sem a parte que ignora NULLs:
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);

Essa consulta gera a seguinte saída:
col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        NULL

Se você examinar o plano de consulta, encontrará a seguinte expressão expandida computando o resultado final:
[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)

E é aí que há apenas três colunas envolvidas. Imagine ter uma dúzia de colunas envolvidas!

Agora adicione a isso a lógica para ignorar NULLs:
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);

Essa consulta gera a seguinte saída:
col1        col2        col3        maxall
----------- ----------- ----------- -----------
10          5           NULL        10

O Oracle possui um par de funções chamadas GREATEST e LEAST que aplicam cálculos mínimos e máximos, respectivamente, como lineares aos valores de entrada. Essas funções retornam um NULL dado qualquer entrada NULL como a maioria dos cálculos lineares fazem. Havia um item de feedback aberto pedindo para obter funções semelhantes no T-SQL, mas essa solicitação não foi portada na última alteração do site de feedback. Se a Microsoft adicionar essas funções ao T-SQL, seria ótimo ter uma opção controlando se os NULLs devem ser ignorados ou não.

Enquanto isso, há uma técnica muito mais elegante em comparação com as mencionadas acima que calcula qualquer tipo de agregação como linear entre colunas usando a semântica da função agregada real ignorando NULLs. Você usa uma combinação do operador CROSS APPLY e uma consulta de tabela derivada em um construtor de valor de tabela que gira colunas para linhas e aplica a agregação como uma função de agregação real. Aqui está um exemplo demonstrando os cálculos MIN e MAX, mas você pode usar essa técnica com qualquer função agregada que desejar:
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);

Essa consulta gera a seguinte saída:
col1        col2        col3        maxall      minall
----------- ----------- ----------- ----------- -----------
10          5           NULL        10          5

E se você quiser o contrário? E se você precisar calcular um agregado entre as linhas, mas produzir um NULL se houver alguma entrada NULL? Por exemplo, suponha que você precise somar todos os valores col1 de #T1, mas retornar NULL se alguma das entradas for NULL. Isso pode ser feito com a seguinte técnica:
SELECT SUM(col1) * NULLIF(MIN(CASE WHEN col1 IS NULL THEN 0 ELSE 1 END), 0) AS sumall
FROM #T2;

Você aplica um agregado MIN a uma expressão CASE que retorna zeros para entradas NULL e uns para entradas não NULL. Se houver qualquer entrada NULL, o resultado da função MIN é 0, caso contrário é 1. Então, usando a função NULLIF você converte um resultado 0 em um NULL. Você então multiplica o resultado da função NULLIF pela soma original. Se houver qualquer entrada NULL, você multiplica a soma original por um NULL, resultando em NULL. Se não houver entrada NULL, você multiplica o resultado da soma original por 1, produzindo a soma original.

De volta aos cálculos lineares que geram um NULL para qualquer entrada NULL, a mesma lógica se aplica à concatenação de strings usando o operador +, conforme demonstra a consulta a seguir:
USE TSQLV5;
 
SELECT empid, country, region, city,
  country + N',' + region + N',' + city AS emplocation
FROM HR.Employees;

Essa consulta gera a seguinte saída:
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

Você deseja concatenar as partes de localização dos funcionários em uma string, usando uma vírgula como separador. Mas você deseja ignorar entradas NULL. Em vez disso, quando qualquer uma das entradas é NULL, você obtém um NULL como resultado. Alguns desativam a opção de sessão CONCAT_NULL_YIELDS_NULL, que faz com que uma entrada NULL seja convertida em uma string vazia para fins de concatenação, mas essa opção não é recomendada, pois aplica um comportamento fora do padrão. Além disso, você ficará com vários separadores consecutivos quando houver entradas NULL, o que normalmente não é o comportamento desejado. Outra opção é substituir explicitamente as entradas NULL por uma string vazia usando as funções ISNULL ou COALESCE, mas isso geralmente resulta em um código extenso e detalhado. Uma opção muito mais elegante é usar a função CONCAT_WS, que foi introduzida no SQL Server 2017. Essa função concatena as entradas, ignorando NULLs, usando o separador fornecido como primeira entrada. Aqui está a consulta de solução usando esta função:
SELECT empid, country, region, city,
  CONCAT_WS(N',', country, region, city) AS emplocation
FROM HR.Employees;

Essa consulta gera a seguinte saída:
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/ONDE/TENDO


Ao usar as cláusulas de consulta WHERE, HAVING e ON para fins de filtragem/correspondência, é importante lembrar que elas usam lógica de predicado de três valores. Quando você tem lógica de três valores envolvida, você deseja identificar com precisão como a cláusula trata os casos VERDADEIRO, FALSO e DESCONHECIDO. Essas três cláusulas são projetadas para aceitar casos TRUE e rejeitar casos FALSE e UNKNOWN.

Para demonstrar esse comportamento, usarei uma tabela chamada Contacts que você cria e preenche executando o seguinte código:.
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);

Observe que os contatos 1 e 2 têm taxas horárias aplicáveis ​​e o contato 3 não, portanto, sua taxa horária é definida como NULL. Considere a seguinte consulta procurando contatos com uma taxa horária positiva:
SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00;

Este predicado é avaliado como TRUE para os contatos 1 e 2 e como UNKNOWN para o contato 3, portanto, a saída contém apenas os contatos 1 e 2:
id          name       hourlyrate
----------- ---------- -----------
1           A          100.00
2           B          200.00

O pensamento aqui é que, quando você tem certeza de que o predicado é verdadeiro, deseja retornar a linha, caso contrário, deseja descartá-la. Isso pode parecer trivial no início, até você perceber que alguns elementos da linguagem que também usam predicados funcionam de maneira diferente.

Restrição VERIFICAR versus opção VERIFICAR


Uma restrição CHECK é uma ferramenta que você usa para impor a integridade em uma tabela com base em um predicado. O predicado é avaliado quando você tenta inserir ou atualizar linhas na tabela. Ao contrário da filtragem de consulta e das cláusulas de correspondência que aceitam casos TRUE e rejeitam casos FALSE e UNKNOWN, uma restrição CHECK é projetada para aceitar casos TRUE e UNKNOWN e rejeitar casos FALSE. O pensamento aqui é que, quando você tem certeza de que o predicado é falso, deseja rejeitar a tentativa de alteração, caso contrário, deseja permitir.

Se você examinar a definição da nossa tabela de contatos, notará que ela possui a seguinte restrição CHECK, rejeitando contatos com taxas horárias não positivas:
CONSTRAINT CHK_Contacts_hourlyrate CHECK(hourlyrate > 0.00)

Observe que a restrição usa o mesmo predicado que você usou no filtro de consulta anterior.

Tente adicionar um contato com uma taxa horária positiva:
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (4, 'D', 150.00);

Esta tentativa é bem sucedida.

Tente adicionar um contato com uma taxa horária NULA:
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (5, 'E', NULL);

Essa tentativa também é bem-sucedida, pois uma restrição CHECK é projetada para aceitar casos TRUE e UNKNOWN. Esse é o caso em que um filtro de consulta e uma restrição CHECK são projetados para funcionar de maneira diferente.

Tente adicionar um contato com uma taxa horária não positiva:
INSERT INTO dbo.Contacts(id, name, hourlyrate) VALUES (6, 'F', -100.00);

Esta tentativa falha com o seguinte erro:
Msg 547, Level 16, State 0, Line 454
A instrução INSERT entrou em conflito com a restrição CHECK "CHK_Contacts_hourlyrate". O conflito ocorreu no banco de dados "TSQLV5", tabela "dbo.Contacts", coluna 'hourlyrate'.
O T-SQL também permite que você imponha a integridade das modificações por meio de exibições usando uma opção CHECK. Alguns pensam nessa opção como servindo a um propósito semelhante a uma restrição CHECK, desde que você aplique a modificação por meio da exibição. Por exemplo, considere a visualização a seguir, que usa um filtro baseado no predicado taxa horária> 0,00 e é definido com a opção CHECK:
CREATE OR ALTER VIEW dbo.MyContacts
AS
SELECT id, name, hourlyrate
FROM dbo.Contacts
WHERE hourlyrate > 0.00
WITH CHECK OPTION;

Acontece que, ao contrário de uma restrição CHECK, a opção de visualização CHECK foi projetada para aceitar casos TRUE e rejeitar casos FALSE e UNKNOWN. Portanto, na verdade, ele foi projetado para se comportar mais como o filtro de consulta normalmente também com o objetivo de reforçar a integridade.

Tente inserir uma linha com uma taxa horária positiva por meio da visualização:
INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (7, 'G', 300.00);

Esta tentativa é bem sucedida.

Tente inserir uma linha com uma taxa horária NULL por meio da visualização:
INSERT INTO dbo.MyContacts(id, name, hourlyrate) VALUES (8, 'H', NULL);

Esta tentativa falha com o seguinte erro:
Msg 550, Level 16, State 1, Line 473
A tentativa de inserção ou atualização falhou porque a visualização de destino especifica WITH CHECK OPTION ou abrange uma visualização que especifica WITH CHECK OPTION e uma ou mais linhas resultantes da operação não se qualificar sob a restrição CHECK OPTION.
O pensamento aqui é que uma vez que você adiciona a opção CHECK à view, você só quer permitir modificações que resultem em linhas que são retornadas pela view. Isso é um pouco diferente do pensamento com uma restrição CHECK - rejeite as alterações para as quais você tem certeza de que o predicado é falso. Isso pode ser um pouco confuso. Se você quiser que a exibição permita modificações que definam a taxa horária como NULL, você precisará do filtro de consulta para permitir essas modificações adicionando OR hourlyrate IS NULL. Você só precisa perceber que uma restrição CHECK e uma opção CHECK são projetadas para funcionar de forma diferente em relação ao caso UNKNOWN. O primeiro o aceita, enquanto o segundo o rejeita.

Consulte a tabela de contatos após todas as alterações acima:
SELECT id, name, hourlyrate
FROM dbo.Contacts;

Você deve obter a seguinte saída neste momento:
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/ENQUANTO/CASO


Os elementos de linguagem IF, WHILE e CASE trabalham com predicados.

A instrução IF é projetada da seguinte forma:
IF <predicate>
  <statement or BEGIN-END block when TRUE>
ELSE
  <statement or BEGIN-END block when FALSE or UNKNOWN>

É intuitivo esperar ter um bloco TRUE seguindo a cláusula IF e um bloco FALSE seguindo a cláusula ELSE, mas você precisa perceber que a cláusula ELSE realmente é ativada quando o predicado é FALSE ou UNKNOWN. Teoricamente, uma linguagem de lógica de três valores poderia ter uma instrução SE com uma separação dos três casos. Algo assim:
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 até mesmo permitir combinações de resultados lógicos para que, se você quisesse combinar FALSE e UNKNOWN em uma seção, pudesse usar algo assim:
IF <predicate>
  WHEN TRUE
    <statement or BEGIN-END block when TRUE>
  WHEN FALSE OR UNKNOWN
    <statement or BEGIN-END block when FALSE OR UNKNOWN>

Enquanto isso, você pode emular essas construções aninhando instruções IF-ELSE e procurando explicitamente por NULLs nos operandos com o operador IS NULL.

A instrução WHILE possui apenas um bloco TRUE. Ele é projetado da seguinte forma:
WHILE <predicate>
  <statement or BEGIN-END block when TRUE>

A instrução ou bloco BEGIN-END que forma o corpo do loop é ativado enquanto o predicado for TURE. Assim que o predicado for FALSE ou UNKNOWN, o controle passa para a instrução após o loop WHILE.

Ao contrário de IF e WHILE, que são instruções que executam código, CASE é uma expressão que retorna um valor. A sintaxe de um pesquisado A expressão CASE é a seguinte:
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

Uma expressão CASE é projetada para retornar a expressão após a cláusula THEN que corresponde ao primeiro predicado WHEN avaliado como TRUE. Se houver uma cláusula ELSE, ela será ativada se nenhum predicado WHEN for TRUE (todos são FALSE ou UNKNOWN). Na ausência de uma cláusula ELSE explícita, um ELSE NULL implícito é usado. Se você quiser tratar um caso UNKNOWN separadamente, você pode procurar explicitamente por NULLs nos operandos do predicado usando o operador IS NULL.

Um simples A expressão CASE usa comparações implícitas baseadas em igualdade entre a expressão de origem e as expressões comparadas:
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

A expressão CASE simples foi projetada de forma semelhante à expressão CASE pesquisada em termos de manipulação da lógica de três valores, mas como as comparações usam uma comparação implícita baseada em igualdade, você não pode manipular o caso UNKNOWN separadamente. Uma tentativa de usar um NULL em uma das expressões comparadas nas cláusulas WHEN não tem sentido, pois a comparação não resultará em TRUE mesmo quando a expressão de origem for NULL. Considere o seguinte exemplo:
DECLARE @input AS INT = NULL;
 
SELECT CASE @input WHEN NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Isso é convertido implicitamente para o seguinte:
DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input = NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Consequentemente, o resultado é:
A entrada não é NULL
Para detectar uma entrada NULL, você precisa usar a sintaxe da expressão CASE pesquisada e o operador IS NULL, assim:
DECLARE @input AS INT = NULL;
 
SELECT CASE WHEN @input IS NULL THEN 'Input is NULL' ELSE 'Input is not NULL' END;

Desta vez o resultado é:
A entrada é NULA

MERGURAR


A instrução MERGE é usada para mesclar dados de uma origem em um destino. Você usa um predicado de mesclagem para identificar os seguintes casos e aplicar uma ação no destino:
  • Uma linha de origem é correspondida por uma linha de destino (ativada quando uma correspondência é encontrada para a linha de origem em que o predicado de mesclagem é TRUE):aplique UPDATE ou DELETE no destino
  • Uma linha de origem não corresponde a uma linha de destino (ativada quando nenhuma correspondência é encontrada para a linha de origem em que o predicado de mesclagem é TRUE, em vez de todo o predicado for FALSE ou UNKNOWN):aplique um INSERT no destino
  • Uma linha de destino não é correspondida por uma linha de origem (ativada quando nenhuma correspondência é encontrada para a linha de destino em que o predicado de mesclagem é TRUE, em vez de todo o predicado for FALSE ou UNKNOWN):aplique UPDATE ou DELETE no destino

Todos os três cenários separam TRUE para um grupo e FALSE ou UNKNOWN para outro. Você não recebe seções separadas para lidar com TRUE, lidar com FALSE e lidar com casos UNKNOWN.

Para demonstrar isso, usarei uma tabela chamada T3 que você cria e preenche executando o seguinte código:
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);

Considere a seguinte instrução 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;

A linha de origem em que col1 é 1 é correspondida pela linha de destino em que col1 é 1 (o predicado é TRUE) e, portanto, a col2 da linha de destino é definida como 100.

A linha de origem em que col1 é 3 não corresponde a nenhuma linha de destino (para todos os predicados é FALSE ou UNKNOWN) e, portanto, uma nova linha é inserida em T3 com 3 como o valor de col1 e 300 como o valor de col2.

As linhas de destino em que col1 é 2 e onde col1 é NULL não são correspondidas por nenhuma linha de origem (para todas as linhas, o predicado é FALSE ou UNKNOWN) e, portanto, em ambos os casos, col2 nas linhas de destino é definido como -1.

A consulta em T3 retorna a seguinte saída após executar a instrução MERGE acima:
col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Mantenha a mesa T3 por perto; é usado mais tarde.

Distinção e agrupamento


Ao contrário das comparações feitas usando operadores de igualdade e desigualdade, as comparações feitas para fins de distinção e agrupamento agrupam NULLs. Um NULL é considerado não distinto de outro NULL, mas um NULL é considerado distinto de um valor não NULL. Conseqüentemente, a aplicação de uma cláusula DISTINCT remove ocorrências duplicadas de NULLs. A consulta a seguir demonstra isso:
SELECT DISTINCT country, region FROM HR.Employees;

Essa consulta gera a seguinte saída:
country         region
--------------- ---------------
UK              NULL
USA             WA

Existem vários funcionários com o país USA e a região NULL, e após a remoção das duplicatas o resultado mostra apenas uma ocorrência da combinação.

Assim como a distinção, o agrupamento também agrupa NULLs, como demonstra a consulta a seguir:
SELECT country, region, COUNT(*) AS numemps
FROM HR.Employees
GROUP BY country, region;

Essa consulta gera a seguinte saída:
country         region          numemps
--------------- --------------- -----------
UK              NULL            4
USA             WA              5

Novamente, todos os quatro funcionários com o país UK e a região NULL foram agrupados.

Pedido


A ordenação trata vários NULLs como tendo o mesmo valor de ordenação. O padrão SQL deixa para a implementação escolher se deseja ordenar NULLs primeiro ou por último em comparação com valores não NULL. A Microsoft optou por considerar NULLs como tendo valores de ordenação mais baixos em comparação com não NULLs no SQL Server, portanto, ao usar a direção de ordem ascendente, o T-SQL ordena os NULLs primeiro. A consulta a seguir demonstra isso:
SELECT id, name, hourlyrate
FROM dbo.Contacts
ORDER BY hourlyrate;

Essa consulta gera a seguinte saída:
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

No próximo mês, adicionarei mais sobre esse tópico, discutindo elementos padrão que fornecem controle sobre o comportamento de ordenação NULL e as soluções alternativas para esses elementos no T-SQL.

Singularidade


Ao impor exclusividade em uma coluna NULLable usando uma restrição UNIQUE ou um índice exclusivo, o T-SQL trata NULLs como valores não NULL. Ele rejeita NULLs duplicados como se um NULL não fosse exclusivo de outro NULL.

Lembre-se de que nossa tabela T3 tem uma restrição UNIQUE definida em col1. Aqui está a sua definição:
CONSTRAINT UNQ_T3 UNIQUE(col1)

Consulte T3 para ver seu conteúdo atual:
SELECT * FROM dbo.T3;

Se você executou todas as modificações no T3 dos exemplos anteriores deste artigo, deverá obter a seguinte saída:
col1        col2
----------- -----------
1           100
2           -1
NULL        -1
3           300

Tente adicionar uma segunda linha com um NULL em col1:
INSERT INTO dbo.T3(col1, col2) VALUES(NULL, 400);

Você recebe o seguinte erro:
Msg 2627, Level 14, State 1, Line 558
Violação da restrição UNIQUE KEY 'UNQ_T3'. Não é possível inserir a chave duplicada no objeto 'dbo.T3'. O valor da chave duplicada é ().
Esse comportamento é realmente fora do padrão. No próximo mês, descreverei a especificação padrão e como emulá-la em T-SQL.

Conclusão


Nesta segunda parte da série sobre complexidades NULL, concentrei-me nas inconsistências de tratamento NULL entre diferentes elementos T-SQL. Abordei cálculos lineares versus agregados, cláusulas de filtragem e correspondência, a restrição CHECK versus a opção CHECK, elementos IF, WHILE e CASE, a instrução MERGE, distinção e agrupamento, ordenação e exclusividade. As inconsistências que abordei enfatizam ainda mais o quão importante é entender corretamente o tratamento de NULLs na plataforma que você está usando, para garantir que você escreva um código correto e robusto. No próximo mês, continuarei a série abordando as opções de tratamento NULL padrão do SQL que não estão disponíveis no T-SQL e fornecerei soluções alternativas com suporte no T-SQL.