Como verificar eficientemente EXISTS em várias colunas?

26

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

Curto circuitos

O plano para os dados do ypercube é

Não curto-circuito

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

Não curto-circuito

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).

Não dinâmico

Martin Smith
fonte

Respostas:

20

E se:

SELECT TOP 3 *
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 T 
  WHERE 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT
Thomas Kejser
fonte
Eu gosto dessa abordagem. Existem alguns problemas possíveis que eu trato nas edições da minha pergunta. Como está escrito, TOP 3pode ser o TOP 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 um hash match (flow distinct)operador em vez de hash match (aggregate)oudistinct sort
Martin Smith
6

Pelo que entendi a pergunta, você deseja saber se existe um nulo em qualquer um dos valores das colunas, em vez de realmente retornar as linhas nas quais B ou C é nulo. Se for esse o caso, por que não:

Select Top 1 'B as nulls' As Col
From T
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T
Where T.C Is Null

Na minha plataforma de teste com o SQL 2008 R2 e um milhão de linhas, obtive os seguintes resultados em ms na guia Estatísticas do Cliente:

Kejser                          2907,2875,2829,3576,3103
ypercube                        2454,1738,1743,1765,2305
OP single aggregate solution    (stopped after 120,000 ms) Wouldn't even finish
My solution                     1619,1564,1665,1675,1674

Se você adicionar a dica nolock, os resultados serão ainda mais rápidos:

Select Top 1 'B as nulls' As Col
From T With(Nolock)
Where T.B Is Null
Union All
Select Top 1 'C as nulls'
From T With(Nolock)
Where T.C Is Null

My solution (with nolock)       42,70,94,138,120

Para referência, usei o SQL Generator da Red-gate para gerar os dados. Fora do meu milhão de linhas, 9.886 linhas tinham um valor B nulo e 10.019 tinham um valor C nulo.

Nesta série de testes, todas as linhas da coluna B têm um valor:

Kejser                          245200  Scan count 1, logical reads 367259, physical reads 858, read-ahead reads 367278
                                250540  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367280

ypercube(1)                     249137  Scan count 2, logical reads 367276, physical reads 850, read-ahead reads 367278
                                248276  Scan count 2, logical reads 367276, physical reads 869, read-ahead reads 368765

My solution                     250348  Scan count 2, logical reads 367276, physical reads 858, read-ahead reads 367278
                                250327  Scan count 2, logical reads 367276, physical reads 854, read-ahead reads 367278

Antes de cada teste (ambos os conjuntos) eu corri CHECKPOINTe DBCC 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) acredito que isso se deve às vantagens da edição Enterprise / Developer ao usar a Verificação avançada . Se você estava usando apenas a edição Standard ou inferior, a solução da Kejser pode muito bem ser a solução mais rápida.

Kejser                          248875  Scan count 1, logical reads 367259, physical reads 860, read-ahead reads 367290

ypercube(1)                     243349  Scan count 2, logical reads 367265, physical reads 851, read-ahead reads 367278
                                242729  Scan count 2, logical reads 367265, physical reads 858, read-ahead reads 367276
                                242531  Scan count 2, logical reads 367265, physical reads 855, read-ahead reads 367278

My solution                     243094  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
                                243444  Scan count 2, logical reads 367265, physical reads 857, read-ahead reads 367278
Thomas
fonte
4

As IFdeclarações são permitidas?

Isso deve permitir que você confirme a existência de B ou C em uma passagem pela tabela:

DECLARE 
  @A INT, 
  @B CHAR(10), 
  @C CHAR(10)

SET @B = 'X'
SET @C = 'X'

SELECT TOP 1 
  @A = A, 
  @B = B, 
  @C = C
FROM T 
WHERE B IS NULL OR C IS NULL 

IF @@ROWCOUNT = 0 
BEGIN 
  SELECT 'No nulls'
  RETURN
END

IF @B IS NULL AND @C IS NULL
BEGIN
  SELECT 'Both null'
  RETURN
END 

IF @B IS NULL 
BEGIN
  SELECT TOP 1 
    @C = C
  FROM T
  WHERE A > @A
  AND C IS NULL

  IF @B IS NULL AND @C IS NULL 
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'B is null'
    RETURN
  END
END

IF @C IS NULL 
BEGIN
  SELECT TOP 1 
    @B = B
  FROM T 
  WHERE A > @A
  AND B IS NULL

  IF @C IS NULL AND @B IS NULL
  BEGIN
    SELECT 'Both null'
    RETURN
  END
  ELSE
  BEGIN
    SELECT 'C is null'
    RETURN
  END
END      
8kb
fonte
4

Testado no SQL-Fiddle nas versões: 2008 R2 e 2012 com 30K linhas.

  • A EXISTSconsulta mostra um enorme benefício em eficiência quando encontra Nulls mais cedo - o que é esperado.
  • Eu obtenho melhor desempenho com a EXISTSconsulta - em todos os casos em 2012, o que não posso explicar.
  • No 2008R2, quando não há Nulos, é mais lento que as outras 2 consultas. Quanto mais cedo encontrar os Nulos, mais rápido ele ficará e quando as duas colunas tiverem nulos mais cedo, será muito mais rápido que as outras 2 consultas.
  • A consulta de Thomas Kejser parece ter um desempenho ligeiramente melhor, mas constantemente, em 2012 e pior em 2008R2, comparada à CASEconsulta de Martin .
  • A versão de 2012 parece ter um desempenho muito melhor. Pode ter a ver com as configurações dos servidores SQL-Fiddle, embora não apenas com melhorias no otimizador.

Consultas e horários. Os horários foram feitos:

  • 1º sem Nulos
  • 2º com coluna Btendo um NULLem um pequeno id.
  • 3º com as duas colunas tendo uma NULLem ids pequenos.

Aqui vamos nós (há um problema com os planos, tentarei novamente mais tarde. Siga os links por enquanto):


Consulta com 2 subconsultas EXISTS

SELECT 
      CASE WHEN EXISTS (SELECT * FROM test WHERE b IS NULL)
             THEN 1 ELSE 0 
      END AS B,
      CASE WHEN EXISTS (SELECT * FROM test WHERE c IS NULL)
             THEN 1 ELSE 0 
      END AS C ;

-------------------------------------
Times in ms (2008R2): 1344 - 596 -  1  
Times in ms   (2012):   26 -  14 -  2

Consulta agregada única de Martin Smith

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 test ;

--------------------------------------
Times in ms (2008R2):  558 - 553 - 516  
Times in ms   (2012):   37 -  35 -  36

Consulta de Thomas Kejser

SELECT TOP 3 *
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 
    (B IS NULL AND C IS NOT NULL) 
    OR (B IS NOT NULL AND C IS NULL) 
    OR (B IS NULL AND C IS NULL)
) AS DT ;

--------------------------------------
Times in ms (2008R2):  859 - 705 - 668  
Times in ms   (2012):   24 -  19 -  18

Minha sugestão (1)

WITH tmp1 AS
  ( SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id 
  ) 

  SELECT 
      tmp1.*, 
      NULL AS id2, NULL AS b2, NULL AS c2
  FROM tmp1
UNION ALL
  SELECT *
  FROM
    ( SELECT TOP (1)
          tmp1.id, tmp1.b, tmp1.c,
          test.id AS id2, test.b AS b2, test.c AS c2 
      FROM test
        CROSS JOIN tmp1
      WHERE test.id >= tmp1.id
        AND ( test.b IS NULL AND tmp1.c IS NULL
           OR tmp1.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id
    ) AS x ;

--------------------------------------
Times in ms (2008R2): 1089 - 572 -  16   
Times in ms   (2012):   28 -  15 -   1

Ele precisa de um polimento na saída, mas a eficiência é semelhante à EXISTSconsulta. Eu pensei que seria melhor quando não há nulos, mas o teste mostra que não é.


Sugestão (2)

Tentando simplificar a lógica:

CREATE TABLE tmp
( id INT
, b CHAR(1000)
, c CHAR(1000)
) ;

DELETE  FROM tmp ;

INSERT INTO tmp 
    SELECT TOP (1) 
        id, b, c
    FROM test
    WHERE b IS NULL OR c IS NULL
    ORDER BY id  ; 

INSERT INTO tmp 
    SELECT TOP (1)
        test.id, test.b, test.c 
      FROM test
        JOIN tmp 
          ON test.id >= tmp.id
      WHERE ( test.b IS NULL AND tmp.c IS NULL
           OR tmp.b IS NULL AND test.c IS NULL
            )
      ORDER BY test.id ;

SELECT *
FROM tmp ;

Parece ter um desempenho melhor em 2008R2 do que a sugestão anterior, mas pior em 2012 (talvez o segundo INSERTpossa ser reescrito usando IF, como a resposta de @ 8kb):

------------------------------------------
Times in ms (2008R2): 416+6 - 1+127 -  1+1   
Times in ms   (2012):  14+1 - 0+27  -  0+29
ypercubeᵀᴹ
fonte
0

Quando você usa EXISTS, o SQL Server sabe que você está fazendo uma verificação de existência. Quando encontra o primeiro valor correspondente, ele retorna TRUE e para de procurar.

quando você concatinar 2 colunas e se alguma for nula, o resultado será nulo

por exemplo

null + 'a' = null

então verifique este código

IF EXISTS (SELECT 1 FROM T WHERE B+C is null)
SELECT Top 1 ISNULL(B,'B ') + ISNULL(C,'C') as [Nullcolumn] FROM T WHERE B+C is null
AmmarR
fonte
-3

E se:

select 
    exists(T.B is null) as 'B is null',
    exists(T.C is null) as 'C is null'
from T;

Se isso funcionar (eu não testei), produziria uma tabela de uma linha com 2 colunas, cada uma delas TRUE ou FALSE. Não testei a eficiência.

David Horowitz
fonte
2
Mesmo que isso seja válido em qualquer outro SGBD, duvido que tenha a semântica correta. Supondo que T.B is nullseja tratado como um resultado booleano EXISTS(SELECT true)e EXISTS(SELECT false)que ambos retornem true. Este exemplo MySQL indica que ambas as colunas contêm NULL quando nenhum de fato fazer
Martin Smith