Puoi usare COUNT DISTINCT con una clausola OVER?


25

Sto cercando di migliorare le prestazioni della seguente query:

        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                            

Attualmente con i miei dati di test ci vogliono circa un minuto. Ho una quantità limitata di input per le modifiche su tutta la procedura memorizzata in cui risiede questa query, ma probabilmente posso farli modificare questa query. Oppure aggiungi un indice. Ho provato ad aggiungere il seguente indice:

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

E in realtà ha raddoppiato il tempo impiegato dalla query. Ottengo lo stesso effetto con un indice NON CLUSTER.

Ho provato a riscriverlo come segue senza alcun effetto.

        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                            

Successivamente ho provato a utilizzare una funzione di windowing come questa.

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

A questo punto ho iniziato a ricevere l'errore

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

Quindi ho due domande. Per prima cosa non puoi fare un COUNT DISTINCT con la clausola OVER o l'ho appena scritto in modo errato? E secondo, qualcuno può suggerire un miglioramento che non ho già provato? Cordiali saluti, questa è un'istanza di SQL Server 2008 R2 Enterprise.

EDIT: ecco un link al piano di esecuzione originale. Dovrei anche notare che il mio grande problema è che questa query viene eseguita 30-50 volte.

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

EDIT2: Ecco il ciclo completo in cui si trova l'istruzione come richiesto nei commenti. Sto verificando con la persona che lavora regolarmente con questo riguardo allo scopo del loop.

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

Risposte:


28

Questa costruzione non è attualmente supportata in SQL Server. Potrebbe (e dovrebbe, a mio avviso) essere implementato in una versione futura.

Applicando una delle soluzioni alternative elencate nell'elemento di feedback che segnala questa carenza, la query potrebbe essere riscritta come:

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;

Il piano di esecuzione risultante è:

Piano

Ciò ha il vantaggio di evitare uno spool di tabella desideroso per la protezione di Halloween (a causa del self-join), ma introduce un ordinamento (per la finestra) e una costruzione di spool di tabella pigra spesso inefficiente per calcolare e applicare il SUM OVER (PARTITION BY)risultato a tutte le righe nella finestra. Come si esibisce in pratica è un esercizio che solo tu puoi eseguire.

L'approccio globale è difficile da far funzionare bene. Applicare gli aggiornamenti (specialmente quelli basati su un self-join) in modo ricorsivo su una struttura di grandi dimensioni può essere utile per il debug, ma è una ricetta per prestazioni scadenti. Ripetute scansioni di grandi dimensioni, perdite di memoria e problemi di Halloween sono solo alcuni dei problemi. L'indicizzazione e (più) tabelle temporanee possono aiutare, ma è necessaria un'attenta analisi, specialmente se l'indice viene aggiornato da altre istruzioni nel processo (la gestione degli indici influisce sulle scelte del piano di query e aggiunge I / O).

In definitiva, risolvere il problema di fondo renderebbe interessante il lavoro di consulenza, ma è troppo per questo sito. Spero che questa risposta affronti la domanda di superficie però.


Interpretazione alternativa della query originale (risultati nell'aggiornamento di più righe):

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;

Piano 2

Nota: l'eliminazione dell'ordinamento (ad esempio fornendo un indice) potrebbe reintrodurre la necessità di una bobina desiderosa o qualcos'altro per fornire la protezione di Halloween necessaria. L'ordinamento è un operatore di blocco, quindi fornisce una separazione di fase completa.


6

Necromancing:

È relativamente semplice emulare un conteggio distinto sulla partizione 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

3
La semantica di questo non è la stessa di countse la colonna è nullable. Se contiene dei null è necessario sottrarre 1.
Martin Smith

@Martin Smith: bella cattura. inavvertitamente è necessario aggiungere DOVE ADR NON È NULL se ci sono valori null.
Quandary
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.