Sección de respuestas
Hay varias formas de reescribir esto usando diferentes construcciones T-SQL. Veremos los pros y los contras y haremos una comparación general a continuación.
Primero : usandoOR
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;
El uso OR
nos da un plan de búsqueda más eficiente, que lee el número exacto de filas que necesitamos, sin embargo, agrega lo que el mundo técnico llama a whole mess of malarkey
al plan de consulta.
También tenga en cuenta que la Búsqueda se ejecuta dos veces aquí, lo que realmente debería ser más obvio desde el operador gráfico:
Table 'Users'. Scan count 2, logical reads 8233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 469 ms, elapsed time = 473 ms.
Segundo : el uso de tablas derivadas con UNION ALL
nuestra consulta también se puede reescribir de esta manera
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records);
Esto produce el mismo tipo de plan, con mucha menos maldad y un grado más aparente de honestidad acerca de cuántas veces se buscó (¿buscó?) El índice.
Realiza la misma cantidad de lecturas (8233) que la OR
consulta, pero ahorra aproximadamente 100 ms de tiempo de CPU.
CPU time = 313 ms, elapsed time = 315 ms.
Sin embargo, debe tener mucho cuidado aquí, porque si este plan intenta ir en paralelo, las dos COUNT
operaciones separadas se serializarán, porque cada una se considera un agregado escalar global. Si forzamos un plan paralelo usando Trace Flag 8649, el problema se vuelve obvio.
SELECT SUM(Records)
FROM
(
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Esto se puede evitar cambiando ligeramente nuestra consulta.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
Ahora ambos nodos que realizan una Búsqueda están completamente paralelos hasta que lleguemos al operador de concatenación.
Por lo que vale, la versión totalmente paralela tiene algunos buenos beneficios. Con el costo de aproximadamente 100 lecturas más y aproximadamente 90 ms de tiempo de CPU adicional, el tiempo transcurrido se reduce a 93 ms.
Table 'Users'. Scan count 12, logical reads 8317, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 500 ms, elapsed time = 93 ms.
¿Qué pasa con CROSS APPLY?
¡Ninguna respuesta está completa sin la magia de CROSS APPLY
!
Desafortunadamente, nos encontramos con más problemas con COUNT
.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
Este plan es horrible. Este es el tipo de plan con el que terminas cuando apareces por última vez en el Día de San Patricio. Aunque es bastante paralelo, por alguna razón está escaneando el PK / CX. Ew. El plan tiene un costo de 2198 dólares de consulta.
Table 'Users'. Scan count 7, logical reads 31676233, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 29532 ms, elapsed time = 5828 ms.
Lo cual es una elección extraña, porque si lo forzamos a usar el índice no agrupado, el costo se reduce significativamente a 1798 dólares de consulta.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT COUNT(Id)
FROM dbo.Users AS u2 WITH (INDEX(ix_Id_Age))
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
¡Hola, busca! Te veo por allá. También tenga en cuenta que con la magia de CROSS APPLY
, no necesitamos hacer nada tonto para tener un plan en su mayoría totalmente paralelo.
Table 'Users'. Scan count 5277838, logical reads 31685303, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 27625 ms, elapsed time = 4909 ms.
La aplicación cruzada termina yendo mejor sin las COUNT
cosas allí.
SELECT SUM(Records)
FROM dbo.Users AS u
CROSS APPLY
(
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u2
WHERE u2.Id = u.Id
AND u2.Age IS NULL
) x (Records);
El plan se ve bien, pero las lecturas y la CPU no son una mejora.
Table 'Users'. Scan count 20, logical reads 17564, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Workfile'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 4844 ms, elapsed time = 863 ms.
Reescribir la cruz se aplica para ser un resultado de unión derivado en exactamente el mismo todo. No voy a volver a publicar el plan de consulta y la información de estadísticas, realmente no cambiaron.
SELECT COUNT(u.Id)
FROM dbo.Users AS u
JOIN
(
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT u.Id
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x ON x.Id = u.Id;
Álgebra relacional : Para ser minucioso y para evitar que Joe Celko persiga mis sueños, necesitamos al menos probar algunas cosas raras de relación. ¡Aquí no pasa nada!
Un intento con INTERSECT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
INTERSECT
SELECT u.Age WHERE u.Age IS NOT NULL );
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 1094 ms, elapsed time = 1090 ms.
Y aquí hay un intento con EXCEPT
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18
EXCEPT
SELECT u.Age WHERE u.Age IS NULL);
Table 'Users'. Scan count 7, logical reads 9247, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 2126 ms, elapsed time = 376 ms.
Puede haber otras formas de escribir esto, pero lo dejaré en manos de personas que tal vez lo usen EXCEPT
y con INTERSECT
más frecuencia que yo.
Si realmente necesita un recuento
que utilizo COUNT
en mis consultas como un poco de taquigrafía (léase: a veces soy demasiado flojo para pensar en escenarios más complicados). Si solo necesita un recuento, puede usar una CASE
expresión para hacer casi lo mismo.
SELECT SUM(CASE WHEN u.Age < 18 THEN 1
WHEN u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
SELECT SUM(CASE WHEN u.Age < 18 OR u.Age IS NULL THEN 1
ELSE 0 END)
FROM dbo.Users AS u
Ambos obtienen el mismo plan y tienen la misma CPU y características de lectura.
Table 'Users'. Scan count 1, logical reads 9157, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:
CPU time = 719 ms, elapsed time = 719 ms.
¿El ganador?
En mis pruebas, el plan paralelo forzado con SUM sobre una tabla derivada funcionó mejor. Y sí, muchas de estas consultas podrían haberse ayudado agregando un par de índices filtrados para dar cuenta de ambos predicados, pero quería dejar algunos experimentos a los demás.
SELECT SUM(Records)
FROM
(
SELECT 1
FROM dbo.Users AS u
WHERE u.Age < 18
UNION ALL
SELECT 1
FROM dbo.Users AS u
WHERE u.Age IS NULL
) x (Records)
OPTION(QUERYTRACEON 8649);
¡Gracias!
NOT EXISTS ( INTERSECT / EXCEPT )
consultas pueden funcionar sin lasINTERSECT / EXCEPT
partes:WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );
Otra forma: que utilizaEXCEPT
:SELECT COUNT(*) FROM (SELECT UserID FROM dbo.Users EXCEPT SELECT UserID FROM dbo.Users WHERE u.Age >= 18) AS u ;
(donde UserID es la PK o cualquier columna no nula).