Können Sie COUNT DISTINCT mit einer OVER-Klausel verwenden?

25

Ich versuche, die Leistung der folgenden Abfrage zu verbessern:

        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                            

Momentan dauert es mit meinen Testdaten ca. eine Minute. Ich habe eine begrenzte Menge an Eingaben in Änderungen an der gesamten gespeicherten Prozedur, in der sich diese Abfrage befindet, aber ich kann sie wahrscheinlich dazu bringen, diese eine Abfrage zu ändern. Oder fügen Sie einen Index hinzu. Ich habe versucht, den folgenden Index hinzuzufügen:

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

Und es hat die Zeit, die die Abfrage benötigt, tatsächlich verdoppelt. Ich erhalte den gleichen Effekt mit einem nicht klassifizierten Index.

Ich habe versucht, es wie folgt ohne Wirkung neu zu schreiben.

        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                            

Als nächstes habe ich versucht, eine solche Fensterfunktion zu verwenden.

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

An diesem Punkt fing ich an, den Fehler zu bekommen

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

Ich habe also zwei Fragen. Erstens können Sie mit der OVER-Klausel keine COUNT DISTINCT ausführen, oder habe ich sie einfach falsch geschrieben? Und zweitens kann jemand eine Verbesserung vorschlagen, die ich noch nicht ausprobiert habe? Zu Ihrer Information, dies ist eine SQL Server 2008 R2 Enterprise-Instanz.

BEARBEITEN: Hier ist ein Link zum ursprünglichen Ausführungsplan. Ich sollte auch beachten, dass mein großes Problem darin besteht, dass diese Abfrage 30-50 Mal ausgeführt wird.

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

EDIT2: Hier ist die vollständige Schleife, in der sich die Anweisung befindet, wie in den Kommentaren angefordert. Ich überprüfe regelmäßig mit der Person, die damit arbeitet, den Zweck der Schleife.

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
quelle

Antworten:

28

Diese Konstruktion wird derzeit in SQL Server nicht unterstützt. Es könnte (und sollte meiner Meinung nach) in einer zukünftigen Version implementiert werden.

Wenn Sie eine der Problemumgehungen anwenden, die im Feedback-Element aufgeführt sind, in dem dieser Mangel gemeldet wird, könnte Ihre Abfrage folgendermaßen umgeschrieben werden:

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;

Der resultierende Ausführungsplan lautet:

Planen

Dies hat den Vorteil, dass eine eifrige Tabellenspule für den Halloween-Schutz (aufgrund des Self-Joins) vermieden wird. Es werden jedoch eine Sortierung (für das Fenster) und eine häufig ineffiziente Lazy Table Spool-Konstruktion eingeführt, mit der das SUM OVER (PARTITION BY)Ergebnis berechnet und auf alle Zeilen angewendet werden kann im Fenster. Wie es in der Praxis funktioniert, ist eine Übung, die nur Sie ausführen können.

Der Gesamtansatz ist schwierig, eine gute Leistung zu erzielen. Das rekursive Anwenden von Aktualisierungen (insbesondere von Aktualisierungen, die auf einem Self-Join basieren) auf eine große Struktur kann zwar zum Debuggen hilfreich sein, ist jedoch ein Rezept für eine schlechte Leistung. Wiederholte große Scans, Speicherausfälle und Halloween-Probleme sind nur einige der Probleme. Indizierung und (weitere) temporäre Tabellen können hilfreich sein. Eine sorgfältige Analyse ist jedoch besonders dann erforderlich, wenn der Index durch andere Anweisungen im Prozess aktualisiert wird (das Verwalten von Indizes wirkt sich auf die Auswahl von Abfrageplänen und das Hinzufügen von E / A aus).

Die Lösung des zugrunde liegenden Problems würde letztendlich zu einer interessanten Beratungsarbeit führen, aber es ist zu viel für diese Website. Ich hoffe, dass diese Antwort die Oberflächenfrage anspricht.


Alternative Interpretation der ursprünglichen Abfrage (führt zur Aktualisierung weiterer Zeilen):

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

Hinweis: Wenn Sie die Sortierung aufheben (z. B. durch Angabe eines Index), ist möglicherweise erneut eine Eager-Spule oder etwas anderes erforderlich, um den erforderlichen Halloween-Schutz zu gewährleisten. Sort ist ein Blockierungsoperator, der eine vollständige Phasentrennung ermöglicht.

Paul White sagt GoFundMonica
quelle
6

Nekromanzierung:

Es ist relativ einfach, mit DENSE_RANK eine Anzahl zu emulieren, die von Partition zu Partition verschieden ist:

;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
Dilemma
quelle
3
Die Semantik ist nicht so, als countob die Spalte nullbar wäre. Wenn es irgendwelche Nullen enthält, müssen Sie 1 subtrahieren.
Martin Smith
@ Martin Smith: Schöner Fang. Natürlich müssen Sie WHERE ADR IS NOT NULL hinzufügen, wenn es Nullwerte gibt.
Quandary