Você pode usar COUNT DISTINCT com uma cláusula OVER?


25

Estou tentando melhorar o desempenho da seguinte 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                            

Atualmente, com meus dados de teste, leva cerca de um minuto. Eu tenho uma quantidade limitada de entrada para alterações no procedimento armazenado em geral em que essa consulta reside, mas provavelmente posso fazê-las modificar essa consulta. Ou adicione um índice. Tentei adicionar o seguinte índice:

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

E na verdade dobrou a quantidade de tempo que a consulta leva. Eu obtenho o mesmo efeito com um índice NÃO CLUSTERED.

Tentei reescrevê-lo da seguinte maneira, sem nenhum efeito.

        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                            

Em seguida, tentei usar uma função de janelas como esta.

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

Nesse ponto, comecei a receber o erro

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

Então, eu tenho duas perguntas. Primeiro, você não pode executar um COUNT DISTINCT com a cláusula OVER ou acabei de escrevê-lo incorretamente? E segundo, alguém pode sugerir uma melhoria que eu ainda não tentei? Para sua informação, esta é uma instância do SQL Server 2008 R2 Enterprise.

EDIT: Aqui está um link para o plano de execução original. Também devo observar que meu grande problema é que essa consulta está sendo executada 30 a 50 vezes.

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

EDIT2: Aqui está o loop completo em que a declaração está, conforme solicitado nos comentários. Estou checando com a pessoa que trabalha com isso regularmente quanto ao objetivo do 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

Respostas:


28

No momento, essa construção não é suportada no SQL Server. Poderia (e deveria, na minha opinião) ser implementado em uma versão futura.

Aplicando uma das soluções alternativas listadas no item de feedback que relata essa deficiência, sua consulta pode ser reescrita 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;

O plano de execução resultante é:

Plano

Isso tem a vantagem de evitar um spool de tabela ansioso para proteção de Halloween (devido à auto-junção), mas introduz uma classificação (para a janela) e uma construção de spool de tabela preguiçoso muitas vezes ineficiente para calcular e aplicar oSUM OVER (PARTITION BY) resultado a todas as linhas na janela. O desempenho na prática é um exercício que somente você pode realizar.

A abordagem geral é difícil de ter um bom desempenho. A aplicação de atualizações (especialmente baseadas em uma associação automática) recursivamente a uma estrutura grande pode ser boa para depuração, mas é uma receita para um desempenho ruim. Varreduras grandes e repetidas, perda de memória e problemas de Halloween são apenas alguns dos problemas. A indexação e as tabelas (mais) temporárias podem ajudar, mas é necessária uma análise muito cuidadosa, especialmente se o índice for atualizado por outras instruções no processo (a manutenção de índices afeta as opções do plano de consulta e adiciona E / S).

Por fim, resolver o problema subjacente traria um trabalho interessante de consultoria, mas é demais para este site. Espero que esta resposta aborde a questão superficial.


Interpretação alternativa da consulta original (resulta na atualização de mais linhas):

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;

Plano 2

Nota: a eliminação da classificação (por exemplo, fornecendo um índice) pode reintroduzir a necessidade de um Eager Spool ou de qualquer outra coisa para fornecer a Proteção de Halloween necessária. Sort é um operador de bloqueio, por isso fornece uma separação completa de fases.


6

Necromancia:

É relativamente simples emular uma contagem distinta sobre a partição com 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
A semântica disso não é a mesma que countse a coluna fosse anulável. Se ele contiver algum valor nulo, você precisará subtrair 1.
Martin Smith

@ Martin Smith: Boa captura. obviamente você precisa adicionar WHERE ADR NÃO É NULL se houver valores nulos.
Quandary
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.