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).SELECT result = (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age IS NULL) ;
Lo siento si me perdí en el millón de versiones que has probado.UNION ALL
planes (CPU de 360 ms, lecturas de 11k).No era un juego para restaurar una base de datos de 110 GB para una sola tabla, así que creé mis propios datos . Las distribuciones de edad deben coincidir con lo que hay en Stack Overflow, pero obviamente la tabla en sí no coincidirá. No creo que sea un gran problema porque las consultas van a llegar a los índices de todos modos. Estoy probando en una computadora de 4 CPU con SQL Server 2016 SP1. Una cosa a tener en cuenta es que para las consultas que terminan tan rápido, es importante no incluir el plan de ejecución real. Eso puede retrasar un poco las cosas.
Comencé revisando algunas de las soluciones en la excelente respuesta de Erik. Para este:
Obtuve los siguientes resultados de sys.dm_exec_sessions en 10 ensayos (la consulta, naturalmente, fue paralela para mí):
La consulta que funcionó mejor para Erik en realidad funcionó peor en mi máquina:
Resultados de 10 ensayos:
No puedo explicar de inmediato por qué es tan malo, pero no está claro por qué queremos obligar a casi todos los operadores en el plan de consulta a ir en paralelo. En el plan original tenemos una zona serial que encuentra todas las filas con
AGE < 18
. Solo hay unos pocos miles de filas. En mi máquina obtengo 9 lecturas lógicas para esa parte de la consulta y 9 ms de tiempo de CPU informado y tiempo transcurrido. También hay una zona en serie para el agregado global para las filas conAGE IS NULL
pero que solo procesa una fila por DOP. En mi máquina esto son solo cuatro filas.Mi conclusión es que es más importante optimizar la parte de la consulta que encuentra filas con un
NULL
forAge
porque hay millones de esas filas. No pude crear un índice con menos páginas que cubrieran los datos que una página simple comprimida en la columna. Supongo que hay un tamaño de índice mínimo por fila o que gran parte del espacio de índice no se puede evitar con los trucos que probé. Entonces, si estamos atrapados con aproximadamente el mismo número de lecturas lógicas para obtener los datos, entonces la única forma de hacerlo más rápido es hacer que la consulta sea más paralela, pero esto debe hacerse de una manera diferente a la consulta de Erik que usaba TF 8649. En la consulta anterior tenemos una relación de 3,62 para el tiempo de CPU al tiempo transcurrido que es bastante bueno. Lo ideal sería una relación de 4.0 en mi máquina.Una posible área de mejora es dividir el trabajo de manera más uniforme entre los hilos. En la captura de pantalla a continuación, podemos ver que una de mis CPU decidió tomar un pequeño descanso:
El escaneo de índice es uno de los pocos operadores que se pueden implementar en paralelo y no podemos hacer nada sobre cómo se distribuyen las filas a los subprocesos. También hay un elemento de oportunidad, pero de manera bastante constante vi un hilo con poco trabajo. Una forma de evitar esto es hacer paralelismo de la manera difícil: en la parte interna de una unión de bucle anidado. Cualquier cosa en la parte interna de un bucle anidado se implementará de forma serial, pero muchos hilos seriales pueden ejecutarse simultáneamente. Siempre que obtengamos un método de distribución paralela favorable (como round robin), podemos controlar exactamente cuántas filas se envían a cada hilo.
Estoy ejecutando consultas con DOP 4, así que necesito dividir uniformemente las
NULL
filas de la tabla en cuatro cubos. Una forma de hacerlo es crear un montón de índices en columnas calculadas:No estoy muy seguro de por qué cuatro índices separados son un poco más rápidos que un índice, pero eso es lo que encontré en mis pruebas.
Para obtener un plan de bucle anidado paralelo, voy a usar el indicador de seguimiento no documentado 8649 . También voy a escribir el código un poco extraño para alentar al optimizador a no procesar más filas de las necesarias. A continuación se muestra una implementación que parece funcionar bien:
Los resultados de diez ensayos:
¡Con esa consulta tenemos una relación de CPU a tiempo transcurrido de 3.85! ¡Afeitamos 17 ms del tiempo de ejecución y solo se necesitaron 4 columnas e índices calculados para hacerlo! Cada subproceso procesa muy cerca del mismo número de filas en general porque cada índice tiene muy cerca del mismo número de filas y cada subproceso solo escanea un índice:
En una nota final, también podemos presionar el botón fácil y agregar un CCI no agrupado a la
Age
columna:La siguiente consulta termina en 3 ms en mi máquina:
Eso va a ser difícil de superar.
fuente
Aunque no tengo una copia local de la base de datos Stack Overflow, pude probar un par de consultas. Mi idea era obtener un recuento de usuarios de una vista de catálogo del sistema (en lugar de obtener directamente un recuento de filas de la tabla subyacente). Luego obtenga un recuento de filas que coincidan (o tal vez no) con los criterios de Erik, y haga algunos cálculos matemáticos simples.
Utilicé el Explorador de datos de Stack Exchange (junto con
SET STATISTICS TIME ON;
ySET STATISTICS IO ON;
) para probar las consultas. Como punto de referencia, aquí hay algunas consultas y las estadísticas de CPU / IO:Consulta 1
Consulta 2
Consulta 3
1er intento
Esto fue más lento que todas las consultas de Erik que enumeré aquí ... al menos en términos de tiempo transcurrido.
2º intento
Aquí opté por una variable para almacenar el número total de usuarios (en lugar de una subconsulta). El recuento de escaneo aumentó de 1 a 17 en comparación con el primer intento. Las lecturas lógicas permanecieron igual. Sin embargo, el tiempo transcurrido cayó considerablemente.
Otras notas: DBCC TRACEON no está permitido en Stack Exchange Data Explorer, como se indica a continuación:
fuente
SELECT SUM(p.Rows) - (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age >= 18 ) FROM sys.partitions p WHERE p.index_id < 2 AND p.object_id = OBJECT_ID('dbo.Users')
¿Usar variables?
Por el comentario puede omitir las variables
fuente
SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
Bien utilizando
SET ANSI_NULLS OFF;
Esto es algo que acaba de aparecer en mi mente, solo ejecuté esto en https://data.stackexchange.com
Pero no es tan eficiente como @blitz_erik
fuente
Una solución trivial es calcular el recuento (*) - recuento (edad> = 18):
O:
Resultados aqui
fuente