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 B
ou C
realmente contém algum NULL
valor (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 EXISTS
declarações separadas . Isso teria a vantagem de permitir que as consultas parassem de varrer mais cedo assim que uma NULL
delas fosse encontrada. Mas se as duas colunas de fato não contêm NULL
s, 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 NULL
em 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 3
para TOP 2
permitir potencialmente lo para sair mais cedo. Por padrão, eu tenho um plano paralelo para essa resposta, então tentei com uma MAXDOP 1
dica 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 GROUP
dica, 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 NULL
valores (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 NULL
tenha NULLs na coluna B
e 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 NULL
mas contra os dados de teste anteriores, que não se fornece um plano com um Flow Distinct, enquanto o NullExists IS NOT NULL
faz (plano abaixo).
TOP 3
pode ser oTOP 2
que 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