Função de partição COUNT () OVER possível usando DISTINCT


88

Estou tentando escrever o seguinte para obter um total em execução de NumUsers distintos, como:

NumUsers = COUNT(DISTINCT [UserAccountKey]) OVER (PARTITION BY [Mth])

O estúdio de gerenciamento não parece muito feliz com isso. O erro desaparece quando eu removo a DISTINCTpalavra - chave, mas não será uma contagem distinta.

DISTINCTnão parece ser possível dentro das funções de partição. Como faço para encontrar a contagem distinta? Devo usar um método mais tradicional , como uma subconsulta correlacionada?

Analisando um pouco mais a fundo, talvez essas OVERfunções funcionem de maneira diferente do Oracle, de modo que não podem ser usadas SQL-Serverpara calcular totais corridos.

Eu adicionei um exemplo ao vivo aqui no SQLfiddle onde tento usar uma função de partição para calcular um total em execução.


2
COUNTcom em ORDER BYvez de PARTITION BYestá mal definido em 2008. Estou surpreso que esteja permitindo que você tenha. Pela documentação , você não tem permissão ORDER BYpara uma função agregada.
Damien_The_Unbeliever

Sim - acho que estou ficando confuso com alguma funcionalidade do oráculo; esses totais e contagens de execução serão um pouco mais envolventes
theq

Respostas:


177

Existe uma solução muito simples usando dense_rank()

dense_rank() over (partition by [Mth] order by [UserAccountKey]) 
+ dense_rank() over (partition by [Mth] order by [UserAccountKey] desc) 
- 1

Isso lhe dará exatamente o que você estava pedindo: o número de UserAccountKeys distintas em cada mês.


23
Deve-se ter cuidado com o fato de dense_rank()que ele contará NULLs, ao passo COUNT(field) OVERque não. Não posso empregá-lo em minha solução por causa disso, mas ainda acho que é muito inteligente.
bf2020

1
Mas estou procurando uma execução total de chaves de conta de usuário distintas ao longo dos meses de cada ano: não tenho certeza de como isso responde a isso?
whytheq

4
@ bf2020, se pode haver NULLvalores no UserAccountKey, então você precisa adicionar esse termo: -MAX(CASE WHEN UserAccountKey IS NULL THEN 1 ELSE 0 END) OVER (PARTITION BY Mth). A ideia foi retirada da resposta de LarsRönnbäck a seguir. Essencialmente, se UserAccountKeytiver NULLvalores, você precisará subtrair o extra 1do resultado, porque DENSE_RANKconta NULLs.
Vladimir Baranov

1
@ahsteele obrigado cara, você explodiu minha mente e resolveu meu problema
Henrique Donati

Aqui, uma discussão sobre como usar essa dense_ranksolução quando a função de janela tem um quadro. O SQL Server não permite o dense_rankuso com uma moldura de janela: stackoverflow.com/questions/63527035/…
K4M

6

Necromante:

É relativamente simples emular COUNT DISTINCT em vez de PARTITION BY com MAX via 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 
    -- Not supported
    --,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

Nota:
Isso pressupõe que os campos em questão são campos NÃO anuláveis.
Se houver uma ou mais entradas NULL nos campos, você precisará subtrair 1.


5

Eu uso uma solução semelhante à de David acima, mas com um toque adicional se algumas linhas forem excluídas da contagem. Isso pressupõe que [UserAccountKey] nunca é nulo.

-- subtract an extra 1 if null was ranked within the partition,
-- which only happens if there were rows where [Include] <> 'Y'
dense_rank() over (
  partition by [Mth] 
  order by case when [Include] = 'Y' then [UserAccountKey] else null end asc
) 
+ dense_rank() over (
  partition by [Mth] 
  order by case when [Include] = 'Y' then [UserAccountKey] else null end desc
)
- max(case when [Include] = 'Y' then 0 else 1 end) over (partition by [Mth])
- 1

Um SQL Fiddle com um exemplo estendido pode ser encontrado aqui.


1
Sua ideia pode ser usada para fazer a fórmula original (sem as complexidades de [Include]que você está falando em sua resposta) com dense_rank()trabalho quando UserAccountKeypuder NULL. Adicionar este termo com a fórmula: -MAX(CASE WHEN UserAccountKey IS NULL THEN 1 ELSE 0 END) OVER (PARTITION BY Mth).
Vladimir Baranov

5

Acho que a única maneira de fazer isso no SQL-Server 2008R2 é usar uma subconsulta correlacionada ou um aplicativo externo:

SELECT  datekey,
        COALESCE(RunningTotal, 0) AS RunningTotal,
        COALESCE(RunningCount, 0) AS RunningCount,
        COALESCE(RunningDistinctCount, 0) AS RunningDistinctCount
FROM    document
        OUTER APPLY
        (   SELECT  SUM(Amount) AS RunningTotal,
                    COUNT(1) AS RunningCount,
                    COUNT(DISTINCT d2.dateKey) AS RunningDistinctCount
            FROM    Document d2
            WHERE   d2.DateKey <= document.DateKey
        ) rt;

Isso pode ser feito no SQL-Server 2012 usando a sintaxe que você sugeriu:

SELECT  datekey,
        SUM(Amount) OVER(ORDER BY DateKey) AS RunningTotal
FROM    document

No entanto, o uso de DISTINCTainda não é permitido, então se DISTINCT for necessário e / ou se a atualização não for uma opção, então eu acho que OUTER APPLYé sua melhor opção


legal, obrigado. Eu encontrei esta resposta SO que apresenta a opção OUTER APPLY que tentarei. Você já viu a abordagem de atualização em loop nessa resposta ... é bem longe e aparentemente rápido. A vida será mais fácil em 2012 - isso é uma cópia direta do Oracle?
whytheq
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.