Esse é um problema com o qual me deparo periodicamente e ainda não encontrei uma boa solução.
Supondo a seguinte estrutura de tabela
CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)
e o requisito é determinar se uma das colunas anuláveis Bou Crealmente contém algum NULLvalor (e se sim, qual (s)).
Suponha também que a tabela contenha milhões de linhas (e que nenhuma estatística de coluna esteja disponível que possa ser vista, pois estou interessado em uma solução mais genérica para essa classe de consultas).
Posso pensar em algumas maneiras de abordar isso, mas todas têm fraquezas.
Duas EXISTSdeclarações separadas . Isso teria a vantagem de permitir que as consultas parassem de varrer mais cedo assim que uma NULLdelas fosse encontrada. Mas se as duas colunas de fato não contêm NULLs, resultarão em duas varreduras completas.
Consulta agregada única
SELECT
MAX(CASE WHEN B IS NULL THEN 1 ELSE 0 END) AS B,
MAX(CASE WHEN C IS NULL THEN 1 ELSE 0 END) AS C
FROM T
Isso pode processar as duas colunas ao mesmo tempo, para ter o pior caso de uma verificação completa. A desvantagem é que, mesmo que encontre um NULLem ambas as colunas muito cedo, a consulta ainda acabará examinando todo o restante da tabela.
Variáveis de usuário
Eu posso pensar em uma terceira maneira de fazer isso
BEGIN TRY
DECLARE @B INT, @C INT, @D INT
SELECT
@B = CASE WHEN B IS NULL THEN 1 ELSE @B END,
@C = CASE WHEN C IS NULL THEN 1 ELSE @C END,
/*Divide by zero error if both @B and @C are 1.
Might happen next row as no guarantee of order of
assignments*/
@D = 1 / (2 - (@B + @C))
FROM T
OPTION (MAXDOP 1)
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 /*Divide by zero*/
BEGIN
SELECT 'B,C both contain NULLs'
RETURN;
END
ELSE
RETURN;
END CATCH
SELECT ISNULL(@B,0),
ISNULL(@C,0)
mas isso não é adequado para o código de produção, pois o comportamento correto para uma consulta de concatenação agregada é indefinido. e encerrar a verificação lançando um erro é uma solução bastante horrível de qualquer maneira.
Existe outra opção que combina os pontos fortes das abordagens acima?
Editar
Apenas para atualizar isso com os resultados que recebo em termos de leituras para as respostas enviadas até o momento (usando os dados de teste do @ ypercube)
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | 2 * EXISTS | CASE | Kejser | Kejser | Kejser | ypercube | 8kb |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
| | | | | MAXDOP 1 | HASH GROUP, MAXDOP 1 | | |
| No Nulls | 15208 | 7604 | 8343 | 7604 | 7604 | 15208 | 8346 (8343+3) |
| One Null | 7613 | 7604 | 8343 | 7604 | 7604 | 7620 | 7630 (25+7602+3) |
| Two Null | 23 | 7604 | 8343 | 7604 | 7604 | 30 | 30 (18+12) |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
Por @ resposta de Thomas eu mudei TOP 3para TOP 2permitir potencialmente lo para sair mais cedo. Por padrão, eu tenho um plano paralelo para essa resposta, então tentei com uma MAXDOP 1dica para tornar o número de leituras mais comparável aos outros planos. Fiquei um pouco surpreso com os resultados, pois em meu teste anterior eu havia visto a consulta em curto-circuito sem ler a tabela inteira.
O plano para os meus dados de teste em que curto-circuito está abaixo

O plano para os dados do ypercube é

Portanto, ele adiciona um operador de classificação de bloqueio ao plano. Eu também tentei com a HASH GROUPdica, mas isso ainda acaba lendo todas as linhas

Portanto, a chave é conseguir que um hash match (flow distinct)operador permita que esse plano entre em curto-circuito, pois as outras alternativas bloquearão e consumirão todas as linhas de qualquer maneira. Não acho que exista uma dica para forçar isso especificamente, mas aparentemente "em geral, o otimizador escolhe um Flow Distinct onde determina que menos linhas de saída são necessárias do que valores distintos no conjunto de entradas". .
Os dados do @ ypercube têm apenas 1 linha em cada coluna com NULLvalores (cardinalidade da tabela = 30300) e as linhas estimadas entrando e saindo do operador são ambas 1. Ao tornar o predicado um pouco mais opaco para o otimizador, ele gerou um plano com o operador Flow Distinct.
SELECT TOP 2 *
FROM (SELECT DISTINCT
CASE WHEN b IS NULL THEN NULL ELSE 'foo' END AS b
, CASE WHEN c IS NULL THEN NULL ELSE 'bar' END AS c
FROM test T
WHERE LEFT(b,1) + LEFT(c,1) IS NULL
) AS DT
Editar 2
Um último ajuste que me ocorreu é que a consulta acima ainda pode acabar processando mais linhas do que o necessário, caso a primeira linha que encontrar com a NULLtenha NULLs na coluna Be C. Ele continuará a digitalização em vez de sair imediatamente. Uma maneira de evitar isso seria desviar as linhas à medida que elas são verificadas. Portanto, minha alteração final à resposta de Thomas Kejser está abaixo
SELECT DISTINCT TOP 2 NullExists
FROM test T
CROSS APPLY (VALUES(CASE WHEN b IS NULL THEN 'b' END),
(CASE WHEN c IS NULL THEN 'c' END)) V(NullExists)
WHERE NullExists IS NOT NULL
Provavelmente seria melhor para o predicado, WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLmas contra os dados de teste anteriores, que não se fornece um plano com um Flow Distinct, enquanto o NullExists IS NOT NULLfaz (plano abaixo).

TOP 3pode ser oTOP 2que está sendo digitalizado até encontrar um dos seguintes(NOT_NULL,NULL)itens(NULL,NOT_NULL),,(NULL,NULL). Quaisquer 2 desses 3 seriam suficientes - e se encontrar(NULL,NULL)primeiro, o segundo também não seria necessário. Além disso, a fim de curto-circuito o plano seria necessário para implementar a nítida através de umhash match (flow distinct)operador em vez dehash match (aggregate)oudistinct sort