Este é 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, em caso afirmativo, qual(is)).
Suponha também que a tabela contém milhões de linhas (e que não há estatísticas de coluna disponíveis que possam ser espiadas, 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 pontos fracos.
Duas EXISTS
declarações separadas. Isso teria a vantagem de permitir que as consultas parem de varrer antecipadamente assim que um NULL
for encontrado. Mas se ambas as colunas de fato não contiverem nenhum 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, portanto, o pior caso é uma varredura completa. A desvantagem é que, mesmo que encontre um NULL
em ambas as colunas muito cedo, a consulta ainda acabará varrendo todo o restante da tabela.
Variáveis do 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 código de produção, pois o comportamento correto para uma consulta de concatenação agregada é indefinido. e encerrar a varredura lançando um erro é uma solução horrível de qualquer maneira.
Existe outra opção que combine os pontos fortes das abordagens acima?
Editar
Apenas para atualizar isso com os resultados que recebo em termos de leituras das respostas enviadas até agora (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) |
+----------+------------+------+---------+----------+----------------------+----------+------------------+
Para a resposta de @Thomas, mudei TOP 3
para TOP 2
potencialmente permitir que ela saísse mais cedo. Eu tenho um plano paralelo por padrão para essa resposta, então também 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 no meu teste anterior eu havia visto esse curto-circuito na consulta sem ler a tabela inteira.
O plano para meus dados de teste que curtos-circuitos 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 parece ser obter um hash match (flow distinct)
operador para permitir 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 haja uma dica para forçar isso especificamente, mas aparentemente "em geral, o otimizador escolhe um Flow Distinct onde determina que são necessárias menos linhas de saída do que valores distintos no conjunto de entrada". .
Os dados do @ypercube têm apenas 1 linha em cada coluna com NULL
valores (cardinalidade da tabela = 30300) e as linhas estimadas que entram e saem do operador são ambas 1
. Ao tornar o predicado um pouco mais opaco para o otimizador, 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 no C
. Ele continuará digitalizando em vez de sair imediatamente. Uma maneira de evitar isso seria desarticular as linhas à medida que são digitalizadas. Então, minha correção final para a 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 que o predicado fosse, WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULL
mas contra os dados de teste anteriores que não me dê um plano com um Flow Distinct, enquanto o NullExists IS NOT NULL
outro faz (plano abaixo).
Que tal:
As I understand the question, you want to know whether a null exists in any of the columns values as opposed to actually returning the rows in which either B or C is null. If that is the case, then why not:
On my test rig with SQL 2008 R2 and one million rows, I got the following results in ms from the Client Statistics tab:
If you add the nolock hint, the results are even faster:
For reference I used Red-gate's SQL Generator to generate the data. Out of my one million rows, 9,886 rows had a null B value and 10,019 had a null C value.
In this series of tests, every row in column B has a value:
Before each test (both sets) I ran
CHECKPOINT
andDBCC DROPCLEANBUFFERS
.Aqui estão os resultados quando não há nulos na tabela. Observe que as 2 soluções existentes fornecidas pelo ypercube são quase idênticas às minhas em termos de leituras e tempo de execução. Eu (nós) acreditamos que isso se deve às vantagens da edição Enterprise/Developer usando o Advanced Scanning . Se você estava usando apenas a edição Standard ou inferior, a solução de Kejser pode muito bem ser a solução mais rápida.
Testado em SQL-Fiddle nas versões: 2008 r2 e 2012 com 30K linhas.
EXISTS
consulta mostra um grande benefício em eficiência quando encontra Nulos antecipadamente - o que é esperado.EXISTS
consulta - em todos os casos em 2012, o que não consigo explicar.CASE
consulta de Martin.Dúvidas e horários. Horários onde foi feito:
B
tendo umNULL
em um pequenoid
.NULL
cada em pequenas ids.Aqui vamos nós (há um problema com os planos, vou tentar novamente mais tarde. Siga os links por enquanto):
Consulta com 2 subconsultas EXISTS
Consulta agregada única de Martin Smith
A pergunta de Thomas Kejser
Minha sugestão (1)
Ele precisa de algum polimento na saída, mas a eficiência é semelhante à
EXISTS
consulta. Eu pensei que seria melhor quando não houvesse nulos, mas os testes mostram que não.Sugestão (2)
Tentando simplificar a lógica:
Parece ter um desempenho melhor em 2008R2 do que a sugestão anterior, mas pior em 2012 (talvez o 2º
INSERT
possa ser reescrito usandoIF
, como a resposta de @8kb):As
IF
declarações são permitidas?Isso deve permitir que você confirme a existência de B ou C em uma passagem pela tabela:
Quando você usa EXISTS, o SQL Server sabe que você está fazendo uma verificação de existência. Quando encontra o primeiro valor correspondente, retorna TRUE e para de procurar.
quando você concatenar 2 colunas e se alguma for nula o resultado será nulo
por exemplo
então verifique este código
Que tal:
Se isso funcionar (não testei), resultaria em uma tabela de uma linha com 2 colunas, cada uma TRUE ou FALSE. Não testei a eficiência.