Pouvez-vous utiliser COUNT DISTINCT avec une clause OVER?

25

J'essaie d'améliorer les performances de la requête suivante:

        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                            

Actuellement, avec mes données de test, cela prend environ une minute. J'ai une quantité limitée d'entrée dans les modifications de la procédure stockée dans laquelle réside cette requête, mais je peux probablement les amener à modifier cette seule requête. Ou ajoutez un index. J'ai essayé d'ajouter l'index suivant:

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

Et cela a en fait doublé le temps nécessaire à la requête. J'obtiens le même effet avec un index NON CLUSTERED.

J'ai essayé de le réécrire comme suit sans effet.

        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                            

Ensuite, j'ai essayé d'utiliser une fonction de fenêtrage comme celle-ci.

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

À ce stade, j'ai commencé à obtenir l'erreur

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

J'ai donc deux questions. Tout d'abord, ne pouvez-vous pas faire un COUNT DISTINCT avec la clause OVER ou l'ai-je simplement écrit de manière incorrecte? Et deuxièmement, quelqu'un peut-il suggérer une amélioration que je n'ai pas encore essayée? Pour info, il s'agit d'une instance SQL Server 2008 R2 Enterprise.

EDIT: Voici un lien vers le plan d'exécution d'origine. Je dois également noter que mon gros problème est que cette requête est exécutée 30 à 50 fois.

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

EDIT2: Voici la boucle complète dans laquelle se trouve l'instruction comme demandé dans les commentaires. Je vérifie régulièrement avec la personne qui travaille avec cela le but de la boucle.

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
la source

Réponses:

28

Cette construction n'est pas actuellement prise en charge dans SQL Server. Il pourrait (et devrait, à mon avis) être implémenté dans une future version.

En appliquant l'une des solutions de contournement répertoriées dans l' élément de rétroaction signalant cette lacune, votre requête pourrait être réécrite comme suit:

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;

Le plan d'exécution qui en résulte est le suivant:

Plan

Cela a l'avantage d'éviter une bobine de table désireuse pour la protection d'Halloween (en raison de l'auto-jointure), mais cela introduit un tri (pour la fenêtre) et une construction de bobine de table paresseuse souvent inefficace pour calculer et appliquer le SUM OVER (PARTITION BY)résultat à toutes les lignes dans la fenêtre. Ce qu'il fait en pratique est un exercice que vous seul pouvez effectuer.

L'approche globale est difficile à faire bien performer. L'application récursive de mises à jour (en particulier celles basées sur une auto-jointure) à une grande structure peut être bonne pour le débogage mais c'est une recette pour de mauvaises performances. Les analyses répétées à grande échelle, les déversements de mémoire et les problèmes d'Halloween ne sont que quelques-uns des problèmes. L'indexation et (plus) de tables temporaires peuvent aider, mais une analyse très minutieuse est nécessaire, surtout si l'index est mis à jour par d'autres instructions dans le processus (la maintenance des index affecte les choix du plan de requête et ajoute des E / S).

En fin de compte, la résolution du problème sous-jacent rendrait le travail de conseil intéressant, mais c'est trop pour ce site. J'espère cependant que cette réponse répond à la question de surface.


Interprétation alternative de la requête d'origine (entraîne la mise à jour de plusieurs lignes):

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

Remarque: l'élimination du tri (par exemple en fournissant un index) pourrait réintroduire le besoin d'une bobine désireuse ou autre pour fournir la protection d'Halloween nécessaire. Le tri est un opérateur de blocage, il fournit donc une séparation de phase complète.

Paul White dit GoFundMonica
la source
6

Nécromancement:

Il est relativement simple d'émuler un nombre distinct sur une partition avec 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
Dilemme
la source
3
La sémantique de ceci n'est pas la même que countsi la colonne est nullable. S'il contient des valeurs nulles, vous devez soustraire 1.
Martin Smith
@Martin Smith: Belle prise. évidemment, vous devez ajouter WHERE ADR IS NOT NULL s'il y a des valeurs nulles.
Quandary