¿Puedes usar COUNT DISTINCT con una cláusula OVER?

25

Estoy tratando de mejorar el rendimiento de la siguiente consulta:

        UPDATE  [#TempTable]
        SET     Received = r.Number
        FROM    [#TempTable] 
        INNER JOIN (SELECT  AgentID,
                            RuleID,
                            COUNT(DISTINCT (GroupId)) Number
                    FROM    [#TempTable]
                    WHERE   Passed = 1
                    GROUP BY AgentID,
                            RuleID
                   ) r ON r.RuleID = [#TempTable].RuleID AND
                          r.AgentID = [#TempTable].AgentID                            

Actualmente con mis datos de prueba, me lleva aproximadamente un minuto. Tengo una cantidad limitada de información sobre los cambios en todo el procedimiento almacenado donde reside esta consulta, pero probablemente pueda hacer que modifiquen esta consulta. O agregue un índice. Intenté agregar el siguiente índice:

CREATE CLUSTERED INDEX ix_test ON #TempTable(AgentID, RuleId, GroupId, Passed)

Y en realidad duplicó la cantidad de tiempo que lleva la consulta. Obtengo el mismo efecto con un índice NO AGRUPADO.

Traté de reescribirlo de la siguiente manera sin efecto.

        WITH r AS (SELECT  AgentID,
                            RuleID,
                            COUNT(DISTINCT (GroupId)) Number
                    FROM    [#TempTable]
                    WHERE   Passed = 1
                    GROUP BY AgentID,
                            RuleID
            ) 
        UPDATE  [#TempTable]
        SET     Received = r.Number
        FROM    [#TempTable] 
        INNER JOIN r 
            ON r.RuleID = [#TempTable].RuleID AND
               r.AgentID = [#TempTable].AgentID                            

Luego intenté usar una función de ventanas como esta.

        UPDATE  [#TempTable]
        SET     Received = COUNT(DISTINCT (CASE WHEN Passed=1 THEN GroupId ELSE NULL END)) 
                    OVER (PARTITION BY AgentId, RuleId)
        FROM    [#TempTable] 

En este punto comencé a recibir el error

Msg 102, Level 15, State 1, Line 2
Incorrect syntax near 'distinct'.

Entonces tengo dos preguntas. Primero, ¿no puede hacer un COUNT DISTINCT con la cláusula OVER o simplemente lo escribí incorrectamente? Y segundo, ¿alguien puede sugerir una mejora que no haya probado? Para su información, esta es una instancia de SQL Server 2008 R2 Enterprise.

EDITAR: Aquí hay un enlace al plan de ejecución original. También debo tener en cuenta que mi gran problema es que esta consulta se ejecuta 30-50 veces.

https://onedrive.live.com/redir?resid=4C359AF42063BD98%21772

EDIT2: Aquí está el ciclo completo en el que se encuentra la declaración según lo solicitado en los comentarios. Estoy consultando con la persona que trabaja con esto de forma regular en cuanto al propósito del bucle.

DECLARE @Counting INT              
SELECT  @Counting = 1              

--  BEGIN:  Cascading Rule check --           
WHILE @Counting <= 30              
    BEGIN      

        UPDATE  w1
        SET     Passed = 1
        FROM    [#TempTable] w1,
                [#TempTable] w3
        WHERE   w3.AgentID = w1.AgentID AND
                w3.RuleID = w1.CascadeRuleID AND
                w3.RulePassed = 1 AND
                w1.Passed = 0 AND
                w1.NotFlag = 0      

        UPDATE  w1
        SET     Passed = 1
        FROM    [#TempTable] w1,
                [#TempTable] w3
        WHERE   w3.AgentID = w1.AgentID AND
                w3.RuleID = w1.CascadeRuleID AND
                w3.RulePassed = 0 AND
                w1.Passed = 0 AND
                w1.NotFlag = 1        

        UPDATE  [#TempTable]
        SET     Received = r.Number
        FROM    [#TempTable] 
        INNER JOIN (SELECT  AgentID,
                            RuleID,
                            COUNT(DISTINCT (GroupID)) Number
                    FROM    [#TempTable]
                    WHERE   Passed = 1
                    GROUP BY AgentID,
                            RuleID
                   ) r ON r.RuleID = [#TempTable].RuleID AND
                          r.AgentID = [#TempTable].AgentID                            

        UPDATE  [#TempTable]
        SET     RulePassed = 1
        WHERE   TotalNeeded = Received              

        SELECT  @Counting = @Counting + 1              
    END
Kenneth Fisher
fuente

Respuestas:

28

Esta construcción no es compatible actualmente en SQL Server. Podría (y debería, en mi opinión) implementarse en una versión futura.

Aplicando una de las soluciones alternativas enumeradas en el elemento de comentarios que informa esta deficiencia, su consulta podría reescribirse como:

WITH UpdateSet AS
(
    SELECT 
        AgentID, 
        RuleID, 
        Received, 
        Calc = SUM(CASE WHEN rn = 1 THEN 1 ELSE 0 END) OVER (
            PARTITION BY AgentID, RuleID) 
    FROM 
    (
        SELECT  
            AgentID,
            RuleID,
            Received,
            rn = ROW_NUMBER() OVER (
                PARTITION BY AgentID, RuleID, GroupID 
                ORDER BY GroupID)
        FROM    #TempTable
        WHERE   Passed = 1
    ) AS X
)
UPDATE UpdateSet
SET Received = Calc;

El plan de ejecución resultante es:

Plan

Esto tiene la ventaja de evitar un carrete de tabla ansioso para la protección de Halloween (debido a la unión automática), pero presenta una clasificación (para la ventana) y una construcción de carrete de tabla perezosa a menudo ineficiente para calcular y aplicar el SUM OVER (PARTITION BY)resultado a todas las filas en la ventana. Cómo funciona en la práctica es un ejercicio que solo tú puedes realizar.

El enfoque general es difícil de hacer funcionar bien. La aplicación de actualizaciones (especialmente las basadas en una autounión) de forma recursiva a una estructura grande puede ser buena para la depuración, pero es una receta para un bajo rendimiento. Escaneos repetidos grandes, derrames de memoria y problemas de Halloween son solo algunos de los problemas. La indexación y (más) tablas temporales pueden ayudar, pero se necesita un análisis muy cuidadoso, especialmente si el índice es actualizado por otras declaraciones en el proceso (el mantenimiento de los índices afecta las opciones del plan de consulta y agrega E / S).

En última instancia, resolver el problema subyacente resultaría en un trabajo de consultoría interesante, pero es demasiado para este sitio. Sin embargo, espero que esta respuesta aborde la pregunta superficial.


Interpretación alternativa de la consulta original (resultados en la actualización de más filas):

WITH UpdateSet AS
(
    SELECT 
        AgentID, 
        RuleID, 
        Received, 
        Calc = SUM(CASE WHEN Passed = 1 AND rn = 1 THEN 1 ELSE 0 END) OVER (
            PARTITION BY AgentID, RuleID) 
    FROM 
    (
        SELECT  
            AgentID,
            RuleID,
            Received,
            Passed,
            rn = ROW_NUMBER() OVER (
                PARTITION BY AgentID, RuleID, Passed, GroupID
                ORDER BY GroupID)
        FROM    #TempTable
    ) AS X
)
UPDATE UpdateSet
SET Received = Calc
WHERE Calc > 0;

Plan 2

Nota: eliminar el tipo (por ejemplo, proporcionando un índice) podría reintroducir la necesidad de un Eager Spool u otra cosa para proporcionar la necesaria protección de Halloween. Sort es un operador de bloqueo, por lo que proporciona una separación de fase completa.

Paul White dice GoFundMonica
fuente
6

Nigromancia

Es relativamente simple emular un recuento distinto sobre la partición con DENSE_RANK:

;WITH baseTable AS
(
              SELECT 'RM1' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM1' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR2' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR2' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR3' AS ADR
    UNION ALL SELECT 'RM3' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM3' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM3' AS RM, 'ADR2' AS ADR
)
,CTE AS
(
    SELECT RM, ADR, DENSE_RANK() OVER(PARTITION BY RM ORDER BY ADR) AS dr 
    FROM baseTable
)
SELECT
     RM
    ,ADR

    ,COUNT(CTE.ADR) OVER (PARTITION BY CTE.RM ORDER BY ADR) AS cnt1 
    ,COUNT(CTE.ADR) OVER (PARTITION BY CTE.RM) AS cnt2 
    -- Geht nicht / Doesn't work 
    --,COUNT(DISTINCT CTE.ADR) OVER (PARTITION BY CTE.RM ORDER BY CTE.ADR) AS cntDist
    ,MAX(CTE.dr) OVER (PARTITION BY CTE.RM ORDER BY CTE.RM) AS cntDistEmu 
FROM CTE
Dilema
fuente
3
La semántica de esto no es la misma que countsi la columna fuera anulable. Si contiene nulos, debe restar 1.
Martin Smith
@ Martin Smith: Buena captura. obviamente, debe agregar WHERE ADR NO ES NULL si hay valores nulos.
Dilema del