Contagem SQL distinta sobre partição

10

Eu tenho uma tabela com duas colunas, quero contar os valores distintos em Col_B sobre (condicionado por) Col_A.

Minha mesa

Col_A | Col_B 
A     | 1
A     | 1
A     | 2
A     | 2
A     | 2
A     | 3
b     | 4
b     | 4
b     | 5

resultado esperado

Col_A   | Col_B | Result
A       | 1     | 3
A       | 1     | 3
A       | 2     | 3
A       | 2     | 3
A       | 2     | 3
A       | 3     | 3
b       | 4     | 2
b       | 4     | 2
b       | 5     | 2

Eu tentei o seguinte código

select *, 
count (distinct col_B) over (partition by col_A) as 'Result'
from MyTable

count (distinto col_B) não está funcionando. Como posso reescrever a função count para contar valores distintos?

sara92
fonte

Respostas:

18

É assim que eu faria:

SELECT      *
FROM        #MyTable AS mt
CROSS APPLY (   SELECT COUNT(DISTINCT mt2.Col_B) AS dc
                FROM   #MyTable AS mt2
                WHERE  mt2.Col_A = mt.Col_A
                -- GROUP BY mt2.Col_A 
            ) AS ca;

A GROUP BYcláusula é redundante, dados os dados fornecidos na pergunta, mas pode fornecer um melhor plano de execução. Veja as perguntas e respostas seguintes O CROSS APPLY produz junção externa .

Considere votar na solicitação de aprimoramento da cláusula OVER - cláusula DISTINCT para funções agregadas no site de feedback, se você desejar que esse recurso seja adicionado ao SQL Server.

Erik Darling
fonte
6

Você pode emular usando dense_ranke, em seguida, escolha a classificação máxima para cada partição:

select col_a, col_b, max(rnk) over (partition by col_a)
from (
    select col_a, col_b
        , dense_rank() over (partition by col_A order by col_b) as rnk 
    from #mytable
) as t    

Você precisaria excluir quaisquer valores nulos de col_bpara obter os mesmos resultados que COUNT(DISTINCT).

Lennart
fonte
6

De certa forma, isso é uma extensão da solução de Lennart , mas é tão feio que não ouso sugerir isso como uma edição. O objetivo aqui é obter os resultados sem uma tabela derivada. Pode nunca haver necessidade disso, e combinado com a feiúra da consulta, todo o esforço pode parecer um esforço desperdiçado. Eu ainda queria fazer isso como um exercício, e agora gostaria de compartilhar meu resultado:

SELECT
  Col_A,
  Col_B,
  DistinctCount = DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B ASC )
                + DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B DESC)
                - 1
                - CASE COUNT(Col_B) OVER (PARTITION BY Col_A)
                  WHEN COUNT(  *  ) OVER (PARTITION BY Col_A)
                  THEN 0
                  ELSE 1
                  END
FROM
  dbo.MyTable
;

A parte principal do cálculo é esta (e antes de mais nada, gostaria de observar que a ideia não é minha, aprendi sobre esse truque em outro lugar):

  DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B ASC )
+ DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B DESC)
- 1

Essa expressão pode ser usada sem nenhuma alteração se Col_Bfor garantido que os valores em nunca tenham nulos. Se a coluna pode ter nulos, no entanto, você precisa levar isso em conta, e é exatamente para isso que a CASEexpressão existe. Ele compara o número de linhas por partição com o número de Col_Bvalores por partição. Se os números diferirem, isso significa que algumas linhas têm nulo Col_Be, portanto, o cálculo inicial ( DENSE_RANK() ... + DENSE_RANK() - 1) precisa ser reduzido em 1.

Observe que, como - 1faz parte da fórmula principal, optei por deixá-la assim. No entanto, ele pode realmente ser incorporado à CASEexpressão, na tentativa fútil de fazer com que toda a solução pareça menos feia:

SELECT
  Col_A,
  Col_B,
  DistinctCount = DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B ASC )
                + DENSE_RANK() OVER (PARTITION BY Col_A ORDER BY Col_B DESC)
                - CASE COUNT(Col_B) OVER (PARTITION BY Col_A)
                  WHEN COUNT(  *  ) OVER (PARTITION BY Col_A)
                  THEN 1
                  ELSE 2
                  END
FROM
  dbo.MyTable
;

Esta demonstração ao vivo no logotipo dbfiddledb <> fiddle.uk pode ser usada para testar as duas variações da solução.

Andriy M
fonte
2
create table #MyTable (
Col_A varchar(5),
Col_B int
)

insert into #MyTable values ('A',1)
insert into #MyTable values ('A',1)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',2)
insert into #MyTable values ('A',3)

insert into #MyTable values ('B',4)
insert into #MyTable values ('B',4)
insert into #MyTable values ('B',5)


;with t1 as (

select t.Col_A,
       count(*) cnt
 from (
    select Col_A,
           Col_B,
           count(*) as ct
      from #MyTable
     group by Col_A,
              Col_B
  ) t
  group by t.Col_A
 )

select a.*,
       t1.cnt
  from #myTable a
  join t1
    on a.Col_A = t1.Col_a
kevinnwhat
fonte
1

Alternativa se você é levemente alérgico a subconsultas correlatas (resposta de Erik Darling) e CTEs (resposta de kevinnwhat) como eu.

Esteja ciente de que quando os nulos são lançados na mistura, nenhum deles pode funcionar como você deseja. (mas é bastante simples modificá-las a seu gosto)

Caso simples:

--ignore the existence of nulls
SELECT [mt].*, [Distinct_B].[Distinct_B]
FROM #MyTable AS [mt]

INNER JOIN(
    SELECT [Col_A], COUNT(DISTINCT [Col_B]) AS [Distinct_B]
    FROM #MyTable
    GROUP BY [Col_A]
) AS [Distinct_B] ON
    [mt].[Col_A] = [Distinct_B].[Col_A]
;

O mesmo que acima, mas com comentários sobre o que alterar para manipulação nula:

--customizable null handling
SELECT [mt].*, [Distinct_B].[Distinct_B]
FROM #MyTable AS [mt]

INNER JOIN(
    SELECT 

    [Col_A],

    (
        COUNT(DISTINCT [Col_B])
        /*
        --uncomment if you also want to count Col_B NULL
        --as a distinct value
        +
        MAX(
            CASE
                WHEN [Col_B] IS NULL
                THEN 1
                ELSE 0
            END
        )
        */
    )
    AS [Distinct_B]

    FROM #MyTable
    GROUP BY [Col_A]
) AS [Distinct_B] ON
    [mt].[Col_A] = [Distinct_B].[Col_A]
/*
--uncomment if you also want to include Col_A when it's NULL
OR
([mt].[Col_A] IS NULL AND [Distinct_B].[Col_A] IS NULL)
*/
ap55
fonte