¿Cómo verificar eficientemente EXISTE en múltiples columnas?

26

Este es un problema con el que me encuentro periódicamente y aún no he encontrado una buena solución.

Suponiendo la siguiente estructura de tabla

CREATE TABLE T
(
A INT PRIMARY KEY,
B CHAR(1000) NULL,
C CHAR(1000) NULL
)

y el requisito es determinar si alguna de las columnas anulables Bo Crealmente contiene algún NULLvalor (y de ser así, cuál (es)).

También suponga que la tabla contiene millones de filas (y que no hay estadísticas de columnas disponibles que puedan ser observadas ya que estoy interesado en una solución más genérica para esta clase de consultas).

Se me ocurren algunas formas de abordar esto, pero todas tienen debilidades.

Dos EXISTSdeclaraciones separadas . Esto tendría la ventaja de permitir que las consultas dejen de escanear temprano tan pronto como NULLse encuentre. Pero si ambas columnas de hecho no contienen NULLs, se obtendrán dos escaneos completos.

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

Esto podría procesar ambas columnas al mismo tiempo, así que tenga el peor de los casos de una exploración completa. La desventaja es que incluso si encuentra una NULLen ambas columnas muy temprano en la consulta, terminará escaneando todo el resto de la tabla.

Variables de usuario

Yo puedo pensar en una tercera forma de hacer esto

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)

pero esto no es adecuado para el código de producción ya que el comportamiento correcto para una consulta de concatenación agregada no está definido. y terminar el escaneo arrojando un error es una solución bastante horrible de todos modos.

¿Hay otra opción que combine las fortalezas de los enfoques anteriores?

Editar

Solo para actualizar esto con los resultados que obtengo en términos de lecturas de las respuestas enviadas hasta ahora (usando los datos de prueba de @ 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 la respuesta de @ Thomas, cambié TOP 3a TOP 2para permitir que salga antes. Obtuve un plan paralelo por defecto para esa respuesta, así que también lo probé con una MAXDOP 1pista para hacer que el número de lecturas sea más comparable a los otros planes. Los resultados me sorprendieron un poco, ya que en mi prueba anterior había visto esa consulta cortocircuito sin leer toda la tabla.

El plan para mis datos de prueba de cortocircuitos está debajo

Corto circuitos

El plan para los datos de ypercube es

No cortocircuito

Por lo tanto, agrega un operador de clasificación de bloqueo al plan. También probé con la HASH GROUPpista, pero todavía termina leyendo todas las filas

No cortocircuito

Entonces, la clave parece ser lograr que un hash match (flow distinct)operador permita que este plan se cortocircuite ya que las otras alternativas bloquearán y consumirán todas las filas de todos modos. No creo que haya indicios de forzar esto específicamente, pero aparentemente "en general, el optimizador elige un Flow Distinct donde determina que se requieren menos filas de salida que valores distintos en el conjunto de entrada". .

Los datos de @ypercube solo tienen 1 fila en cada columna con NULLvalores (cardinalidad de tabla = 30300) y las filas estimadas que entran y salen del operador son ambas 1. Al hacer que el predicado sea un poco más opaco para el optimizador, generó un plan con el 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

Un último ajuste que se me ocurrió es que la consulta anterior aún podría terminar procesando más filas de las necesarias en el caso de que la primera fila que encuentre con un NULLtenga NULL en ambas columnas By C. Continuará escaneando en lugar de salir inmediatamente. Una forma de evitar esto sería desconectar las filas a medida que se escanean. Entonces, mi enmienda final a la respuesta de Thomas Kejser está abajo

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

Probablemente sería mejor que el predicado sea, WHERE (b IS NULL OR c IS NULL) AND NullExists IS NOT NULLpero en comparación con los datos de prueba anteriores, uno no me da un plan con Flow Distinct, mientras que el NullExists IS NOT NULLque sí lo hace (plan a continuación).

Sin pivotar

Martin Smith
fuente

Respuestas:

20

Qué tal si:

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
fuente
Me gusta este enfoque. Sin embargo, hay algunos posibles problemas que abordo en las ediciones de mi pregunta. Como está escrito TOP 3podría ser sólo TOP 2como actualmente se explorará hasta que encuentra uno de cada uno de los siguientes (NOT_NULL,NULL), (NULL,NOT_NULL), (NULL,NULL). Cualquier 2 de esos 3 sería suficiente, y si encuentra (NULL,NULL)primero, entonces el segundo tampoco sería necesario. También para hacer un cortocircuito, el plan necesitaría implementar el distintivo a través de un hash match (flow distinct)operador en lugar de hash match (aggregate)odistinct sort
Martin Smith
6

Como entiendo la pregunta, desea saber si existe un valor nulo en cualquiera de los valores de las columnas en lugar de devolver realmente las filas en las que B o C son nulos. Si ese es el caso, entonces por qué no:

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

En mi plataforma de prueba con SQL 2008 R2 y un millón de filas, obtuve los siguientes resultados en ms de la pestaña Estadísticas del 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

Si agrega la sugerencia nolock, los resultados son aún más 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

Como referencia, utilicé el Generador SQL de Red-gate para generar los datos. De mi millón de filas, 9.886 filas tenían un valor B nulo y 10.019 tenían un valor C nulo.

En esta serie de pruebas, cada fila de la columna B tiene un 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 prueba (ambas series) corrí CHECKPOINTy DBCC DROPCLEANBUFFERS.

Aquí están los resultados cuando no hay nulos en la tabla. Tenga en cuenta que la solución 2 existente proporcionada por ypercube es casi idéntica a la mía en términos de lecturas y tiempo de ejecución. Yo (nosotros) creemos que esto se debe a las ventajas de la edición Enterprise / Developer con el uso de Advanced Scanning . Si usaba solo la edición Standard o inferior, la solución de Kejser podría muy bien ser la solución más 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
fuente
4

¿Se IFpermiten declaraciones?

Esto debería permitirle confirmar la existencia de B o C en una pasada a través de la tabla:

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
fuente
4

Probado en SQL-Fiddle en versiones: 2008 r2 y 2012 con 30K filas.

  • La EXISTSconsulta muestra un gran beneficio en eficiencia cuando encuentra Nulls temprano, lo que se espera.
  • Obtengo un mejor rendimiento con la EXISTSconsulta, en todos los casos en 2012, que no puedo explicar.
  • En 2008R2, cuando no hay Nulls, es más lento que las otras 2 consultas. Cuanto más temprano encuentra los Nulos, más rápido se vuelve y cuando ambas columnas tienen nulos antes, es mucho más rápido que las otras 2 consultas.
  • La consulta de Thomas Kejser parece tener un rendimiento leve pero constante mejor en 2012 y peor en 2008R2, en comparación con la CASEconsulta de Martin .
  • La versión 2012 parece tener un rendimiento mucho mejor. Sin embargo, puede tener que ver con la configuración de los servidores SQL-Fiddle y no solo con mejoras en el optimizador.

Consultas y horarios. Tiempos donde se hace:

  • Primero sin nulos en absoluto
  • 2do con columna Bteniendo uno NULLa la pequeña id.
  • Tercero con ambas columnas teniendo una NULLcada una en pequeños identificadores.

Aquí vamos (hay un problema con los planes, lo intentaré más tarde. Sigue los enlaces por ahora):


Consulta con 2 subconsultas EXISTENTES

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

La 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

Mi sugerencia (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

Necesita un poco de pulido en la salida, pero la eficiencia es similar a la EXISTSconsulta. Pensé que sería mejor cuando no hay nulos, pero las pruebas muestran que no lo es.


Sugerencia (2)

Intentando simplificar la 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 funcionar mejor en 2008R2 que la sugerencia anterior, pero peor en 2012 (tal vez la segunda INSERTpuede reescribirse usando IF, como la respuesta de @ 8kb):

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

Cuando usa EXISTS, SQL Server sabe que está haciendo una verificación de existencia. Cuando encuentra el primer valor coincidente, devuelve VERDADERO y deja de buscar.

cuando concatene 2 columnas y si alguna es nula, el resultado será nulo

p.ej

null + 'a' = null

así que revisa 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
fuente
-3

Qué tal si:

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

Si esto funciona (no lo he probado), generaría una tabla de una fila con 2 columnas, cada una VERDADERA o FALSA. No probé la eficiencia.

David Horowitz
fuente
2
Incluso si esto es válido en cualquier otro DBMS, dudo que tenga la semántica correcta. Suponiendo que T.B is nullse trate como un resultado booleano EXISTS(SELECT true)y EXISTS(SELECT false)que ambos devuelvan verdadero. Este ejemplo de MySQL indica que ambas columnas contienen NULL cuando ninguno de los dos lo tiene
Martin Smith