web-dev-qa-db-fra.com

Comptage SQL distinct sur la partition

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

MaTable

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

Résultat attend

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?

10
sara92

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;

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

Envisagez de voter pour 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.

18
Erik Darling

Vous pouvez l'émuler en utilisant dense_rank, Puis choisissez 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 les valeurs nulles de col_b Pour obtenir les mêmes résultats que COUNT(DISTINCT).

6
Lennart

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 une modification. 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 dans Col_B Sont 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'expression CASE. Il compare le nombre de lignes par partition avec le nombre de valeurs Col_B par partition. Si les nombres diffèrent, cela signifie que certaines lignes ont une valeur nulle dans Col_B Et, par conséquent, le calcul initial (DENSE_RANK() ... + DENSE_RANK() - 1) doit être réduit de 1.

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

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émo en direct à dbfiddle logodb <> fiddle.uk peut être utilisé pour tester les deux variantes de la solution.

6
Andriy M
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
2
kevinnwhat

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 il est assez simple de les modifier à votre 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)
*/
1
ap55