¡Verifique la existencia con EXISTS superan a COUNT! ... no?


36

A menudo he leído cuando hay que verificar la existencia de una fila que siempre debe hacerse con EXISTS en lugar de con un COUNT.

Sin embargo, en varios escenarios recientes, he medido una mejora en el rendimiento cuando uso el conteo.
El patrón es así:

LEFT JOIN (
    SELECT
        someID
        , COUNT(*)
    FROM someTable
    GROUP BY someID
) AS Alias ON (
    Alias.someID = mainTable.ID
)

No estoy familiarizado con los métodos para saber qué está sucediendo "dentro" de SQL Server, por lo que me preguntaba si había un defecto no anunciado con EXISTS que diera sentido a las mediciones que hice (¡¿EXISTS podría ser RBAR ?!).

¿Tienes alguna explicación para ese fenómeno?

EDITAR:

Aquí hay un script completo que puede ejecutar:

SET NOCOUNT ON
SET STATISTICS IO OFF

DECLARE @tmp1 TABLE (
    ID INT UNIQUE
)


DECLARE @tmp2 TABLE (
    ID INT
    , X INT IDENTITY
    , UNIQUE (ID, X)
)

; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp1
SELECT n
FROM tally AS T1
WHERE n < 10000


; WITH T(n) AS (
    SELECT
        ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
    FROM master.dbo.spt_values AS S
) 
, tally(n) AS (
    SELECT
        T2.n * 100 + T1.n
    FROM T AS T1
    CROSS JOIN T AS T2
    WHERE T1.n <= 100
    AND T2.n <= 100
)
INSERT @tmp2
SELECT T1.n
FROM tally AS T1
CROSS JOIN T AS T2
WHERE T1.n < 10000
AND T1.n % 3 <> 0
AND T2.n < 1 + T1.n % 15

PRINT '
COUNT Version:
'

WAITFOR DELAY '00:00:01'

SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN n > 0 THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
LEFT JOIN (
    SELECT
        T2.ID
        , COUNT(*) AS n
    FROM @tmp2 AS T2
    GROUP BY T2.ID
) AS T2 ON (
    T2.ID = T1.ID
)
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF

PRINT '

EXISTS Version:'

WAITFOR DELAY '00:00:01'

SET STATISTICS TIME ON

SELECT
    T1.ID
    , CASE WHEN EXISTS (
        SELECT 1
        FROM @tmp2 AS T2
        WHERE T2.ID = T1.ID
    ) THEN 1 ELSE 0 END AS DoesExist
FROM @tmp1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (RECOMPILE) -- Required since table are filled within the same scope

SET STATISTICS TIME OFF 

En SQL Server 2008R2 (Seven 64bits) obtengo este resultado

COUNT Versión:

Tabla '# 455F344D'. Recuento de escaneo 1, lecturas lógicas 8, lecturas físicas 0, lecturas de lectura anticipada 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas de lectura lob 0. 0.
Tabla '# 492FC531'. Cuenta de escaneo 1, lecturas lógicas 30, 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 = 81 ms.

EXISTS Versión:

Tabla '# 492FC531'. Cuenta de escaneo 1, lecturas lógicas 96, lecturas físicas 0, lecturas anticipadas 0, lecturas lógicas lob 0, lecturas físicas lob 0, lecturas anticipadas lob 0.
Tabla '# 455F344D'. Cuenta de escaneo 1, lecturas lógicas 8, 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 = 76 ms.

Respuestas:


44

A menudo he leído cuando hay que verificar la existencia de una fila que siempre debe hacerse con EXISTS en lugar de con un COUNT.

Es muy raro que algo sea siempre cierto, especialmente cuando se trata de bases de datos. Hay varias formas de expresar la misma semántica en SQL. Si hay una regla general útil, podría ser escribir consultas utilizando la sintaxis más natural disponible (y, sí, eso es subjetivo) y solo considerar reescrituras si el plan de consulta o el rendimiento que obtiene es inaceptable.

Por lo que vale, mi propia opinión sobre el tema es que las consultas de existencia se expresan de manera más natural usando EXISTS. También ha sido mi experiencia que EXISTS tiende a optimizar mejor que la alternativa de OUTER JOINrechazo NULL. Usar COUNT(*)y filtrar =0es otra alternativa, que tiene cierto soporte en el optimizador de consultas de SQL Server, pero personalmente he encontrado que esto no es confiable en consultas más complejas. En cualquier caso, me EXISTSparece mucho más natural (para mí) que cualquiera de esas alternativas.

Me preguntaba si había una falla no anunciada con EXISTS que diera sentido a las mediciones que hice

Su ejemplo particular es interesante, porque resalta la forma en que el optimizador trata las subconsultas en CASEexpresiones (y EXISTSpruebas en particular).

Subconsultas en expresiones CASE

Considere la siguiente consulta (perfectamente legal):

DECLARE @Base AS TABLE (a integer NULL);
DECLARE @When AS TABLE (b integer NULL);
DECLARE @Then AS TABLE (c integer NULL);
DECLARE @Else AS TABLE (d integer NULL);

SELECT
    CASE
        WHEN (SELECT W.b FROM @When AS W) = 1
            THEN (SELECT T.c FROM @Then AS T)
        ELSE (SELECT E.d FROM @Else AS E)
    END
FROM @Base AS B;

La semántica deCASE es que las WHEN/ELSEcláusulas generalmente se evalúan en orden textual. En la consulta anterior, sería incorrecto que SQL Server devolviera un error si la ELSEsubconsulta devolvía más de una fila, si se WHENcumplía la cláusula. Para respetar esta semántica, el optimizador produce un plan que utiliza predicados de transferencia:

Predicados de paso

El lado interno de las uniones de bucle anidado solo se evalúa cuando el predicado de paso devuelve falso. El efecto general es que las CASEexpresiones se prueban en orden, y las subconsultas solo se evalúan si no se satisfizo ninguna expresión previa.

Expresiones CASE con una subconsulta EXISTS

Cuando se CASEutiliza una subconsulta EXISTS, la prueba de existencia lógica se implementa como una semiunión, pero las filas que normalmente serían rechazadas por la semiunión deben conservarse en caso de que una cláusula posterior las necesite. Las filas que fluyen a través de este tipo especial de semi-unión adquieren una bandera para indicar si la semi-unión encontró una coincidencia o no. Esta bandera se conoce como la columna de la sonda .

Los detalles de la implementación es que la subconsulta lógica se reemplaza por una unión correlacionada ('aplicar') con una columna de sonda. El trabajo se realiza mediante una regla de simplificación en el optimizador de consultas llamado RemoveSubqInPrj(eliminar subconsulta en proyección). Podemos ver los detalles usando la marca de seguimiento 8606:

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000
OPTION (QUERYTRACEON 3604, QUERYTRACEON 8606);

Parte del árbol de entrada que muestra la EXISTSprueba se muestra a continuación:

ScaOp_Exists 
    LogOp_Project
        LogOp_Select
            LogOp_Get TBL: #T2
            ScaOp_Comp x_cmpEq
                ScaOp_Identifier [T2].ID
                ScaOp_Identifier [T1].ID

Esto se transforma RemoveSubqInPrjen una estructura encabezada por:

LogOp_Apply (x_jtLeftSemi probe PROBE:COL: Expr1008)

Esta es la aplicación de semiunión izquierda con la sonda descrita anteriormente. Esta transformación inicial es la única disponible en los optimizadores de consultas de SQL Server hasta la fecha, y la compilación simplemente fallará si esta transformación está deshabilitada.

Una de las formas posibles del plan de ejecución para esta consulta es una implementación directa de esa estructura lógica:

NLJ Semi Join con sonda

El Escalar Compute final evalúa el resultado de la CASEexpresión utilizando el valor de la columna de la sonda:

Calcular expresión escalar

La forma básica del árbol del plan se conserva cuando la optimización considera otros tipos de unión física para la semiunión. Solo merge join admite una columna de sonda, por lo que no se considera una semiunión hash, aunque lógicamente posible:

Fusionar con columna de sonda

Observe que la fusión genera una expresión etiquetada Expr1008(que el nombre es el mismo que antes es una coincidencia), aunque no aparece ninguna definición en ningún operador del plan. Esta es solo la columna de la sonda nuevamente. Como antes, el Escalar Compute final utiliza este valor de sonda para evaluar el CASE.

El problema es que el optimizador no explora completamente las alternativas que solo valen la pena con la combinación (o hash) de semiunión. En el plan de bucles anidados, no hay ninguna ventaja en verificar si las filas T2coinciden con el rango en cada iteración. Con un plan de fusión o hash, esto podría ser una optimización útil.

Si agregamos un BETWEENpredicado coincidente T2en la consulta, todo lo que sucede es que esta verificación se realiza para cada fila como un residuo en la semiunión de fusión (difícil de detectar en el plan de ejecución, pero está allí):

SELECT
    T1.ID,
    CASE
        WHEN EXISTS 
        (
            SELECT 1
            FROM #T2 AS T2
            WHERE T2.ID = T1.ID
            AND T2.ID BETWEEN 5000 AND 7000 -- New
        ) THEN 1 
    ELSE 0
    END AS DoesExist
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

Predicado residual

Esperamos que el BETWEENpredicado sea empujado hacia abajo para dar como T2resultado una búsqueda. Normalmente, el optimizador consideraría hacer esto (incluso sin el predicado adicional en la consulta). Reconoce predicados implícitos ( BETWEENon T1y el predicado de unión entre T1y T2juntos implican el BETWEENon T2) sin que estén presentes en el texto de consulta original. Desafortunadamente, el patrón de aplicación de la sonda significa que esto no se explora.

Hay formas de escribir la consulta para generar búsquedas en ambas entradas para una semiunión de fusión. Una forma implica escribir la consulta de una manera bastante poco natural (derrotando la razón que generalmente prefiero EXISTS):

WITH T2 AS
(
    SELECT TOP (9223372036854775807) * 
    FROM #T2 AS T2 
    WHERE ID BETWEEN 5000 AND 7000
)
SELECT 
    T1.ID, 
    DoesExist = 
        CASE 
            WHEN EXISTS 
            (
                SELECT * FROM T2 
                WHERE T2.ID = T1.ID
            ) THEN 1 ELSE 0 END
FROM #T1 AS T1
WHERE T1.ID BETWEEN 5000 AND 7000;

Plan de truco TOP

No estaría contento de escribir esa consulta en un entorno de producción, es solo para demostrar que la forma de plan deseada es posible. Si la consulta real que necesita escribir se usa CASEde esta manera en particular, y el rendimiento sufre al no haber una búsqueda en el lado de la sonda de una semiunión de fusión, puede considerar escribir la consulta usando una sintaxis diferente que produzca los resultados correctos y un plan de ejecución más eficiente.


6

El argumento "COUNT (*) vs EXISTS" tiene que ver con verificar si existe un registro. Por ejemplo:

WHERE (SELECT COUNT(*) FROM Table WHERE ID=@ID)>0

vs

WHERE EXISTS(SELECT ID FROM Table WHERE ID=@ID)

Su secuencia de comandos SQL no se está utilizando COUNT(*)como una verificación de registro y, por lo tanto, no diría que es aplicable en su escenario.


¿Algún pensamiento / conclusión basado en el script que publiqué?
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.