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

El plan para los datos de ypercube es

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

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

TOP 3podría ser sóloTOP 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 unhash match (flow distinct)operador en lugar dehash match (aggregate)odistinct sort