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 B
o C
realmente contiene algún NULL
valor (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 EXISTS
declaraciones separadas . Esto tendría la ventaja de permitir que las consultas dejen de escanear temprano tan pronto como NULL
se encuentre. Pero si ambas columnas de hecho no contienen NULL
s, 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 NULL
en 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 3
a TOP 2
para permitir que salga antes. Obtuve un plan paralelo por defecto para esa respuesta, así que también lo probé con una MAXDOP 1
pista 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 GROUP
pista, 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 NULL
valores (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 NULL
tenga NULL en ambas columnas B
y 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 NULL
pero en comparación con los datos de prueba anteriores, uno no me da un plan con Flow Distinct, mientras que el NullExists IS NOT NULL
que sí lo hace (plan a continuación).
TOP 3
podría ser sóloTOP 2
como 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