Comptage SQL distinct sur la partition

10

J'ai une table avec deux colonnes, je veux compter les valeurs distinctes sur Col_B sur (conditionné par) Col_A.

Ma table

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

résultat attendu

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

J'ai essayé le code suivant

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

count (distinct col_B) ne fonctionne pas. Comment puis-je réécrire la fonction de comptage pour compter des valeurs distinctes?

sara92
la source

Réponses:

18

Voici comment je le ferais:

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;

La GROUP BYclause est redondante compte tenu des données fournies dans la question, mais peut vous donner un meilleur plan d'exécution. Voir le suivi Q & A CROSS APPLY produit une jointure externe .

Envisagez de voter pour la demande d'amélioration de la clause OVER - clause DISTINCT pour les fonctions d'agrégation sur le site de commentaires si vous souhaitez que cette fonctionnalité soit ajoutée à SQL Server.

Erik Darling
la source
6

Vous pouvez l'émuler en utilisant dense_rank, puis choisir le rang maximum pour chaque partition:

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    

Vous devez exclure tous les null col_bpour obtenir les mêmes résultats que COUNT(DISTINCT).

Lennart
la source
6

C'est, en quelque sorte, une extension de la solution de Lennart , mais c'est si moche que je n'ose pas le suggérer comme un montage. Le but ici est d'obtenir les résultats sans tableau dérivé. Il n'y aura peut-être jamais besoin de cela, et combiné avec la laideur de la requête, l'effort dans son ensemble peut sembler un effort inutile. Je voulais quand même le faire comme un exercice et je voudrais maintenant partager mon résultat:

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
;

La partie centrale du calcul est la suivante (et je voudrais tout d'abord noter que l'idée n'est pas la mienne, j'ai appris cette astuce ailleurs):

  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

Cette expression peut être utilisée sans aucune modification si les valeurs de Col_Bsont garanties de ne jamais avoir de valeurs nulles. Si la colonne peut avoir des valeurs nulles, cependant, vous devez en tenir compte, et c'est exactement à cela que sert l' CASEexpression. Il compare le nombre de lignes par partition avec le nombre de Col_Bvaleurs par partition. Si les nombres diffèrent, cela signifie que certaines lignes ont un zéro dans Col_Bet, par conséquent, le calcul initial ( DENSE_RANK() ... + DENSE_RANK() - 1) doit être réduit de 1.

Notez que parce que cela fait - 1partie de la formule principale, j'ai choisi de le laisser comme ça. Cependant, il peut en fait être incorporé dans l' CASEexpression, dans la tentative futile de rendre la solution entière moins moche:

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
;

Cette démonstration en direct sur logo dbfiddledb <> fiddle.uk peut être utilisée pour tester les deux variantes de la solution.

Andriy M
la source
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
Kevin
la source
1

Alternative si vous êtes légèrement allergique aux sous-requêtes corrélées (réponse d'Erik Darling) et aux CTE (réponse de kevinnwhat) comme moi.

Sachez que lorsque des valeurs nulles sont ajoutées au mixage, aucune de celles-ci ne peut fonctionner comme vous le souhaitez. (mais c'est assez simple de les modifier au goût)

Cas simple:

--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]
;

Comme ci-dessus, mais avec des commentaires sur ce qu'il faut changer pour une gestion nulle:

--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
la source