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).
fuente
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
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:
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:
Si agrega la sugerencia nolock, los resultados son aún más rápidos:
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:
Antes de cada prueba (ambas series) corrí
CHECKPOINT
yDBCC 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.
fuente
¿Se
IF
permiten declaraciones?Esto debería permitirle confirmar la existencia de B o C en una pasada a través de la tabla:
fuente
Probado en SQL-Fiddle en versiones: 2008 r2 y 2012 con 30K filas.
EXISTS
consulta muestra un gran beneficio en eficiencia cuando encuentra Nulls temprano, lo que se espera.EXISTS
consulta, en todos los casos en 2012, que no puedo explicar.CASE
consulta de Martin .Consultas y horarios. Tiempos donde se hace:
B
teniendo unoNULL
a la pequeñaid
.NULL
cada 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
Consulta agregada única de Martin Smith
La consulta de Thomas Kejser
Mi sugerencia (1)
Necesita un poco de pulido en la salida, pero la eficiencia es similar a la
EXISTS
consulta. Pensé que sería mejor cuando no hay nulos, pero las pruebas muestran que no lo es.Sugerencia (2)
Intentando simplificar la lógica:
Parece funcionar mejor en 2008R2 que la sugerencia anterior, pero peor en 2012 (tal vez la segunda
INSERT
puede reescribirse usandoIF
, como la respuesta de @ 8kb):fuente
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
así que revisa este código
fuente
Qué tal si:
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.
fuente
T.B is null
se trate como un resultado booleanoEXISTS(SELECT true)
yEXISTS(SELECT false)
que ambos devuelvan verdadero. Este ejemplo de MySQL indica que ambas columnas contienen NULL cuando ninguno de los dos lo tiene