¿Cuáles son las diferentes formas de reemplazar ISNULL () en una cláusula WHERE que usa solo valores literales?

55

De qué no se trata esto:

No se trata de consultas generales que aceptan la entrada del usuario o usan variables.

Se trata estrictamente de consultas donde ISNULL()se usa en la WHEREcláusula para reemplazar NULLvalores con un valor canario para comparación con un predicado, y diferentes formas de reescribir esas consultas para que sean SARGable en SQL Server.

¿Por qué no tienes un asiento allí?

Nuestra consulta de ejemplo está en contra de una copia local de la base de datos Stack Overflow en SQL Server 2016, y busca usuarios con una NULLedad o edad <18.

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

El plan de consulta muestra una exploración de un índice no agrupado bastante considerado.

Nueces

El operador de escaneo muestra (gracias a las adiciones al plan de ejecución real XML en versiones más recientes de SQL Server) que leemos cada fila apestosa.

Nueces

En general, hacemos 9157 lecturas y usamos aproximadamente medio segundo de tiempo de CPU:

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 = 485 ms,  elapsed time = 483 ms.

La pregunta: ¿Cuáles son las formas de reescribir esta consulta para hacerla más eficiente, y tal vez incluso SARGable?

Siéntase libre de ofrecer otras sugerencias. No creo que mi respuesta sea necesariamente la respuesta, y hay suficientes personas inteligentes para idear alternativas que podrían ser mejores.

Si quiere jugar en su propia computadora, diríjase aquí para descargar la base de datos SO .

¡Gracias!

Erik Darling
fuente

Respuestas:

57

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 ORnos 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 malarkeyal plan de consulta.

Nueces

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:

Nueces

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.

Nueces

Realiza la misma cantidad de lecturas (8233) que la ORconsulta, 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 COUNToperaciones 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);

Nueces

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.

Nueces

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.

Nueces

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.

Nueces

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

Nueces

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

Nueces

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

Nueces

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 EXCEPTy con INTERSECTmás frecuencia que yo.

Si realmente necesita un recuento que utilizo COUNTen 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 CASEexpresió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.

Nueces

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!

Erik Darling
fuente
1
Las NOT EXISTS ( INTERSECT / EXCEPT )consultas pueden funcionar sin las INTERSECT / EXCEPTpartes: WHERE NOT EXISTS ( SELECT u.Age WHERE u.Age >= 18 );Otra forma: que utiliza EXCEPT: 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).
ypercubeᵀᴹ
¿Fue esto probado? 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.
ypercubeᵀᴹ
@ ypercubeᵀᴹ aquí está el plan para eso. Es un poco diferente, pero tiene características similares a los UNION ALLplanes (CPU de 360 ​​ms, lecturas de 11k).
Erik Darling,
Hola Erik, solo estaba deambulando por el mundo de SQL y apareció para decir "columna calculada" solo para molestarte. <3
crisol
17

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:

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

Obtuve los siguientes resultados de sys.dm_exec_sessions en 10 ensayos (la consulta, naturalmente, fue paralela para mí):

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3532                 975          60830 
╚══════════╩════════════════════╩═══════════════╝

La consulta que funcionó mejor para Erik en realidad funcionó peor en mi máquina:

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

Resultados de 10 ensayos:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     5704                1636          60850 
╚══════════╩════════════════════╩═══════════════╝

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 con AGE IS NULLpero 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 NULLfor Ageporque 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:

hilo perezoso

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 NULLfilas de la tabla en cuatro cubos. Una forma de hacerlo es crear un montón de índices en columnas calculadas:

ALTER TABLE dbo.Users
ADD Compute_bucket_0 AS (CASE WHEN Age IS NULL AND Id % 4 = 0 THEN 1 ELSE NULL END),
Compute_bucket_1 AS (CASE WHEN Age IS NULL AND Id % 4 = 1 THEN 1 ELSE NULL END),
Compute_bucket_2 AS (CASE WHEN Age IS NULL AND Id % 4 = 2 THEN 1 ELSE NULL END),
Compute_bucket_3 AS (CASE WHEN Age IS NULL AND Id % 4 = 3 THEN 1 ELSE NULL END);

CREATE INDEX IX_Compute_bucket_0 ON dbo.Users (Compute_bucket_0) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_1 ON dbo.Users (Compute_bucket_1) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_2 ON dbo.Users (Compute_bucket_2) WITH (DATA_COMPRESSION = PAGE);
CREATE INDEX IX_Compute_bucket_3 ON dbo.Users (Compute_bucket_3) WITH (DATA_COMPRESSION = PAGE);

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:

SELECT SUM(t.cnt) + (SELECT COUNT(*) FROM dbo.Users AS u WHERE u.Age < 18)
FROM 
(VALUES (0), (1), (2), (3)) v(x)
CROSS APPLY 
(
    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_0 = CASE WHEN v.x = 0 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_1 = CASE WHEN v.x = 1 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_2 = CASE WHEN v.x = 2 THEN 1 ELSE NULL END

    UNION ALL

    SELECT COUNT(*) cnt 
    FROM dbo.Users 
    WHERE Compute_bucket_3 = CASE WHEN v.x = 3 THEN 1 ELSE NULL END
) t
OPTION (QUERYTRACEON 8649);

Los resultados de diez ensayos:

╔══════════╦════════════════════╦═══════════════╗
 cpu_time  total_elapsed_time  logical_reads 
╠══════════╬════════════════════╬═══════════════╣
     3093                 803          62008 
╚══════════╩════════════════════╩═══════════════╝

¡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:

trabajo bien dividido

En una nota final, también podemos presionar el botón fácil y agregar un CCI no agrupado a la Agecolumna:

CREATE NONCLUSTERED COLUMNSTORE INDEX X_NCCI ON dbo.Users (Age);

La siguiente consulta termina en 3 ms en mi máquina:

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18 OR u.Age IS NULL;

Eso va a ser difícil de superar.

Joe Obbish
fuente
7

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;y SET STATISTICS IO ON;) para probar las consultas. Como punto de referencia, aquí hay algunas consultas y las estadísticas de CPU / IO:

Consulta 1

--Erik's query From initial question.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE ISNULL(u.Age, 17) < 18;

Tiempos de ejecución de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 0 ms. (1 fila (s) devuelta)

Tabla 'Usuarios'. Cuenta de escaneo 17, lecturas lógicas 201567, lecturas físicas 0, lecturas anticipadas 2740, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.

Tiempos de ejecución de SQL Server: tiempo de CPU = 1829 ms, tiempo transcurrido = 296 ms.

Consulta 2

--Erik's "OR" query.
SELECT COUNT(*)
FROM dbo.Users AS u
WHERE u.Age < 18
OR u.Age IS NULL;

Tiempos de ejecución de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 0 ms. (1 fila (s) devuelta)

Tabla 'Usuarios'. Cuenta de escaneo 17, lecturas lógicas 201567, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.

Tiempos de ejecución de SQL Server: tiempo de CPU = 2500 ms, tiempo transcurrido = 147 ms.

Consulta 3

--Erik's derived tables/UNION ALL query.
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);

Tiempos de ejecución de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 0 ms. (1 fila (s) devuelta)

Tabla 'Usuarios'. Cuenta de escaneo 34, lecturas lógicas 403134, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.

Tiempos de ejecución de SQL Server: tiempo de CPU = 3156 ms, tiempo transcurrido = 215 ms.

1er intento

Esto fue más lento que todas las consultas de Erik que enumeré aquí ... al menos en términos de tiempo transcurrido.

SELECT SUM(p.Rows)  -
  (
    SELECT COUNT(*)
    FROM dbo.Users AS u
    WHERE u.Age >= 18
  ) 
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

Tiempos de ejecución de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 0 ms. (1 fila (s) devuelta)

Mesa 'Mesa de trabajo'. Recuento de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0. Tabla 'sysrowsets'. Recuento de escaneo 2, lecturas lógicas 10, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura anticipada lob 0. Tabla 'sysschobjs'. Cuenta de escaneo 1, lecturas lógicas 4, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas lob lectura anticipada 0. Tabla 'Usuarios'. Cuenta de escaneo 1, lecturas lógicas 201567, lecturas físicas 0, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.

Tiempos de ejecución de SQL Server: tiempo de CPU = 593 ms, tiempo transcurrido = 598 ms.

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.

DECLARE @Total INT;

SELECT @Total = SUM(p.Rows)
FROM sys.objects o
JOIN sys.partitions p
    ON p.object_id = o.object_id
WHERE p.index_id < 2
AND o.name = 'Users'
AND SCHEMA_NAME(o.schema_id) = 'dbo'
GROUP BY o.schema_id, o.name

SELECT @Total - COUNT(*)
FROM dbo.Users AS u
WHERE u.Age >= 18

Tiempos de ejecución de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 0 ms. Mesa 'Mesa de trabajo'. Recuento de escaneo 0, lecturas lógicas 0, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0. Tabla 'sysrowsets'. Recuento de escaneo 2, lecturas lógicas 10, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura anticipada lob 0. Tabla 'sysschobjs'. Cuenta de escaneo 1, lecturas lógicas 4, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas lob de lectura anticipada 0.

Tiempos de ejecución de SQL Server: tiempo de CPU = 0 ms, tiempo transcurrido = 1 ms. (1 fila (s) devuelta)

Tabla 'Usuarios'. Cuenta de escaneo 17, lecturas lógicas 201567, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0.

Tiempos de ejecución de SQL Server: tiempo de CPU = 1471 ms, tiempo transcurrido = 98 ms.

Otras notas: DBCC TRACEON no está permitido en Stack Exchange Data Explorer, como se indica a continuación:

El usuario 'STACKEXCHANGE \ svc_sede' no tiene permiso para ejecutar DBCC TRACEON.

Dave Mason
fuente
1
Probablemente no tengan los mismos índices que yo, de ahí las diferencias. ¿Y quien sabe? Tal vez el servidor de mi casa está en un hardware mejor;) ¡Gran respuesta, sin embargo!
Erik Darling
debería haber utilizado la siguiente consulta para su primer intento (será mucho más rápido, ya que elimina gran parte de los sys.objects-gastos generales): 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')
Thomas Franz
PD: tenga en cuenta que los índices en memoria (HASH NO CLUSTRADO) no tienen una identificación de índice = 0/1 como lo tendría un montón común / índice agrupado)
Thomas Franz
1

¿Usar variables?

declare @int1 int = ( select count(*) from table_1 where bb <= 1 )
declare @int2 int = ( select count(*) from table_1 where bb is null )
select @int1 + @int2;

Por el comentario puede omitir las variables

SELECT (select count(*) from table_1 where bb <= 1) 
     + (select count(*) from table_1 where bb is null);
paparazzo
fuente
3
También:SELECT (select count(*) from table_1 where bb <= 1) + (select count(*) from table_1 where bb is null);
ypercubeᵀᴹ
3
Podría intentarlo mientras verifica CPU y IO. Pista: es lo mismo que una de las respuestas de Erik.
Brent Ozar
0

Bien utilizando SET ANSI_NULLS OFF;

SET ANSI_NULLS OFF; 
SET STATISTICS TIME ON;
SET STATISTICS IO ON;

SELECT COUNT(*)
FROM dbo.Users AS u
WHERE age=NULL or age<18

Table 'Users'. Scan count 17, logical reads 201567

 SQL Server Execution Times:
 CPU time = 2344 ms,  elapsed time = 166 ms.

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

Biju jose
fuente
0

Una solución trivial es calcular el recuento (*) - recuento (edad> = 18):

SELECT
    (SELECT COUNT(*) FROM Users) -
    (SELECT COUNT(*) FROM Users WHERE Age >= 18);

O:

SELECT COUNT(*)
     - COUNT(CASE WHEN Age >= 18)
FROM Users;

Resultados aqui

Salman A
fuente