Compter DISTINCT sur plusieurs colonnes


214

Existe-t-il une meilleure façon de faire une requête comme celle-ci:

SELECT COUNT(*) 
FROM (SELECT DISTINCT DocumentId, DocumentSessionId
      FROM DocumentOutputItems) AS internalQuery

J'ai besoin de compter le nombre d'éléments distincts de ce tableau mais le distinct est sur deux colonnes.

Ma requête fonctionne bien mais je me demandais si je pouvais obtenir le résultat final en utilisant une seule requête (sans utiliser de sous-requête)


IordanTanev, Mark Brackett, RC - merci pour les réponses, c'était un bon essai, mais vous devez vérifier ce que vous faites avant de poster sur SO. Les requêtes que vous avez fournies ne sont pas équivalentes à ma requête. Vous pouvez facilement voir que j'ai toujours un résultat scalaire, mais votre requête renvoie plusieurs lignes.
Novitzky le

Je viens de mettre à jour la question pour inclure votre commentaire de clarification d'une des réponses
Jeff


C'est une bonne question. Je me demandais aussi s'il y avait un moyen plus simple de le faire
Anupam

Réponses:


73

Si vous essayez d'améliorer les performances, vous pouvez essayer de créer une colonne calculée persistante sur un hachage ou une valeur concaténée des deux colonnes.

Une fois qu'elle est persistante, à condition que la colonne soit déterministe et que vous utilisiez des paramètres de base de données "sains", elle peut être indexée et / ou des statistiques peuvent être créées dessus.

Je crois qu'un nombre distinct de la colonne calculée serait équivalent à votre requête.


4
Excellente suggestion! Plus je lis, plus je me rends compte que SQL est moins sur la connaissance de la syntaxe et des fonctions que sur l'application de la logique pure. J'aimerais avoir 2 votes positifs!
tumchaaditya

Trop bonne suggestion. Cela m'a évité d'y écrire du code inutile.
Avrajit Roy

1
Pourriez-vous ajouter un exemple ou un exemple de code pour en savoir plus sur ce que cela signifie et comment le faire?
jayqui

52

Edit: Modifié de la requête de somme de contrôle moins que fiable, j'ai découvert un moyen de le faire (dans SQL Server 2005) qui fonctionne assez bien pour moi et je peux utiliser autant de colonnes que nécessaire (en les ajoutant à la fonction CHECKSUM ()). La fonction REVERSE () transforme les entiers en varchars pour rendre le distinct plus fiable

SELECT COUNT(DISTINCT (CHECKSUM(DocumentId,DocumentSessionId)) + CHECKSUM(REVERSE(DocumentId),REVERSE(DocumentSessionId)) )
FROM DocumentOutPutItems

1
+1 Sympa, fonctionne parfaitement (lorsque vous avez les bons types de colonnes pour effectuer un CheckSum sur ...;)
Bernoulli IT

8
Avec des hachages comme Checksum (), il y a peu de chances que le même hachage soit renvoyé pour différentes entrées, de sorte que le nombre peut être très légèrement différent. HashBytes () est une chance encore plus petite mais toujours pas nulle. Si ces deux Id étaient des int (32b), un "hachage sans perte" pourrait les combiner en un bigint (64b) comme Id1 << 32 + Id2.
crokusek

1
la chance n'est même pas si petite, surtout lorsque vous commencez à combiner des colonnes (ce à quoi il était censé être destiné). J'étais curieux de cette approche et dans un cas particulier, la somme de contrôle s'est retrouvée avec un décompte 10% plus petit. Si vous y pensez un peu plus longtemps, Checksum renvoie juste un entier, donc si vous faites une somme de contrôle complète, vous vous retrouverez avec un compte distinct environ 2 milliards de fois plus petit qu'il n'y en a réellement. -1
pvolders

Mise à jour de la requête pour inclure l'utilisation de "REVERSE" pour supprimer le risque de doublons
JayTee

4
Pourrions-nous éviter CHECKSUM - pourrions-nous simplement concaténer les deux valeurs ensemble? Je suppose que cela risque de se considérer comme la même chose: ('il', 'art') == 'entendre', 't'). Mais je pense que cela peut être résolu avec un délimiteur comme le propose @APC (une valeur qui n'apparaît dans aucune des colonnes), donc 'he | ​​art'! = 'Hear | t' Y a-t-il d'autres problèmes avec une simple "concaténation" approche?
The Red Pea

32

Qu'est-ce que vous n'aimez pas dans votre requête existante? Si vous craignez que DISTINCTsur deux colonnes ne renvoie pas uniquement les permutations uniques, pourquoi ne pas l'essayer?

Cela fonctionne certainement comme vous pouvez vous y attendre dans Oracle.

SQL> select distinct deptno, job from emp
  2  order by deptno, job
  3  /

    DEPTNO JOB
---------- ---------
        10 CLERK
        10 MANAGER
        10 PRESIDENT
        20 ANALYST
        20 CLERK
        20 MANAGER
        30 CLERK
        30 MANAGER
        30 SALESMAN

9 rows selected.


SQL> select count(*) from (
  2  select distinct deptno, job from emp
  3  )
  4  /

  COUNT(*)
----------
         9

SQL>

Éditer

Je suis descendu dans une ruelle aveugle avec des analyses mais la réponse était d'une évidence déprimante ...

SQL> select count(distinct concat(deptno,job)) from emp
  2  /

COUNT(DISTINCTCONCAT(DEPTNO,JOB))
---------------------------------
                                9

SQL>

modifier 2

Compte tenu des données suivantes, la solution de concaténation fournie ci-dessus sera incorrecte:

col1  col2
----  ----
A     AA
AA    A

Nous devons donc inclure un séparateur ...

select col1 + '*' + col2 from t23
/

Évidemment, le séparateur choisi doit être un caractère, ou un ensemble de caractères, qui ne peut jamais apparaître dans aucune des colonnes.


+1 de moi. Merci pour votre réponse. Ma requête fonctionne bien mais je me demandais si je pouvais obtenir le résultat final en utilisant une seule requête (sans utiliser de sous-requête)
Novitzky

20

Pour exécuter en tant que requête unique, concaténez les colonnes, puis obtenez le nombre distinct d'instances de la chaîne concaténée.

SELECT count(DISTINCT concat(DocumentId, DocumentSessionId)) FROM DocumentOutputItems;

Dans MySQL, vous pouvez faire la même chose sans l'étape de concaténation comme suit:

SELECT count(DISTINCT DocumentId, DocumentSessionId) FROM DocumentOutputItems;

Cette fonctionnalité est mentionnée dans la documentation MySQL:

http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_count-distinct


Il s'agissait d'une question SQL Server, et les deux options que vous avez publiées ont déjà été mentionnées dans les réponses suivantes à cette question: stackoverflow.com/a/1471444/4955425 et stackoverflow.com/a/1471713/4955425 .
sstan

1
FWIW, cela fonctionne presque dans PostgreSQL; juste besoin de parenthèses supplémentaires:SELECT COUNT(DISTINCT (DocumentId, DocumentSessionId)) FROM DocumentOutputItems;
ijoseph

14

Que diriez-vous de quelque chose comme:

sélectionner le nombre (*)
de
  (sélectionnez le nombre (*) cnt
   de DocumentOutputItems
   regrouper par DocumentId, DocumentSessionId) t1

Fait probablement la même chose que vous êtes déjà, mais cela évite le DISTINCT.


dans mes tests (en utilisant SET SHOWPLAN_ALL ON), il avait le même plan d'exécution et exactement le même TotalSubtreeCost
KM.

1
Selon la complexité de la requête d'origine, la résolution de ce problème GROUP BYpeut introduire quelques défis supplémentaires à la transformation de la requête pour atteindre la sortie souhaitée (par exemple, lorsque la requête d'origine avait déjà des clauses GROUP BYou des HAVINGclauses ...)
Lukas Eder

8

Voici une version plus courte sans la sous-sélection:

SELECT COUNT(DISTINCT DocumentId, DocumentSessionId) FROM DocumentOutputItems

Cela fonctionne très bien dans MySQL, et je pense que l'optimiseur a plus de facilité à comprendre celui-ci.

Edit: Apparemment, j'ai mal lu MSSQL et MySQL - désolé, mais peut-être que cela aide de toute façon.


6
dans SQL Server, vous obtenez: Msg 102, niveau 15, état 1, ligne 1 syntaxe incorrecte près de ','.
KM.

Voilà à quoi je pensais. Je veux faire la même chose dans MSSQL si possible.
Novitzky

@Kamil Nowicki, dans SQL Server, vous ne pouvez avoir qu'un seul champ dans un COUNT (), dans ma réponse, je montre que vous pouvez concaténer les deux champs en un seul et essayer cette approche. Cependant, je resterais fidèle à l'original car les plans de requête finiraient par être les mêmes.
KM.

1
Veuillez jeter un œil dans la réponse @JayTee. Il fonctionne comme un charme. count ( distinct CHECKSUM ([Field1], [Field2])
Custodio

5

De nombreuses bases de données SQL (la plupart?) Peuvent fonctionner avec des tuples comme des valeurs, vous pouvez donc simplement le faire: SELECT COUNT(DISTINCT (DocumentId, DocumentSessionId)) FROM DocumentOutputItems; si votre base de données ne le prend pas en charge, il peut être simulé selon la suggestion de @ oncel-umut-turer de CHECKSUM ou d'une autre fonction scalaire offrant une bonne unicité par exemple COUNT(DISTINCT CONCAT(DocumentId, ':', DocumentSessionId)).

Une utilisation connexe des tuples effectue des INrequêtes telles que: SELECT * FROM DocumentOutputItems WHERE (DocumentId, DocumentSessionId) in (('a', '1'), ('b', '2'));


quelles bases de données prennent en charge select count(distinct(a, b))? : D
Vytenis Bivainis

@VytenisBivainis Je sais que PostgreSQL ne le fait pas - je ne sais pas depuis quelle version.
karmakaze

3

Il n'y a rien de mal à votre requête, mais vous pouvez également le faire de cette façon:

WITH internalQuery (Amount)
AS
(
    SELECT (0)
      FROM DocumentOutputItems
  GROUP BY DocumentId, DocumentSessionId
)
SELECT COUNT(*) AS NumberOfDistinctRows
  FROM internalQuery

3

J'espère que cela fonctionne, j'écris sur prima vista

SELECT COUNT(*) 
FROM DocumentOutputItems 
GROUP BY DocumentId, DocumentSessionId

7
Pour que cela donne la réponse finale, vous devez l'envelopper dans un autre SELECT COUNT (*) FROM (...). Essentiellement, cette réponse vous donne simplement une autre façon de répertorier les valeurs distinctes que vous souhaitez compter. Ce n'est pas mieux que votre solution d'origine.
Dave Costa

Merci Dave. Je sais que vous pouvez utiliser group by au lieu de distinct dans mon cas. Je me demandais si vous obtenez le résultat final en utilisant une seule requête. Je pense que c'est impossible mais je peux me tromper.
Novitzky

3

J'ai utilisé cette approche et cela a fonctionné pour moi.

SELECT COUNT(DISTINCT DocumentID || DocumentSessionId) 
FROM  DocumentOutputItems

Pour mon cas, il fournit un résultat correct.


Il ne vous donne pas le nombre de valeurs distinctes en conjonction de deux colonnes. Du moins pas dans MySQL 5.8.
Anwar Shaikh

Cette question est étiquetée SQL Server, et ce n'est pas la syntaxe SQL Server
Tab Alleman

2

si vous n'aviez qu'un seul champ à "DISTINCT", vous pourriez utiliser:

SELECT COUNT(DISTINCT DocumentId) 
FROM DocumentOutputItems

et cela retourne le même plan de requête que l'original, comme testé avec SET SHOWPLAN_ALL ON. Cependant, vous utilisez deux champs afin que vous puissiez essayer quelque chose de fou comme:

    SELECT COUNT(DISTINCT convert(varchar(15),DocumentId)+'|~|'+convert(varchar(15), DocumentSessionId)) 
    FROM DocumentOutputItems

mais vous aurez des problèmes si des NULL sont impliqués. Je m'en tiendrai à la requête d'origine.


+1 de moi. Merci mais je m'en tiendrai à ma requête comme vous l'avez suggéré. L'utilisation de "convertir" peut réduire encore plus les performances.
Novitzky

2

J'ai trouvé cela lorsque j'ai recherché mon propre problème sur Google, j'ai constaté que si vous comptez les objets DISTINCT, vous obtenez le bon nombre retourné (j'utilise MySQL)

SELECT COUNT(DISTINCT DocumentID) AS Count1, 
  COUNT(DISTINCT DocumentSessionId) AS Count2
  FROM DocumentOutputItems

5
La requête ci-dessus renverra un ensemble de résultats différent de celui recherché par l'OP (les combinaisons distinctes de DocumentIdet DocumentSessionId). Alexander Kjäll a déjà publié la bonne réponse si l'OP utilisait MySQL et non MS SQL Server.
Anthony Geoghegan

1

Je souhaite que MS SQL puisse également faire quelque chose comme COUNT (DISTINCT A, B). Mais ça ne peut pas.

Au début, la réponse de JayTee m'a semblé être une solution mais après que certains tests, CHECKSUM () n'a pas réussi à créer des valeurs uniques. Un exemple rapide est que CHECKSUM (31 467 519) et CHECKSUM (69 1 120 823) donnent la même réponse qui est 55.

Ensuite, j'ai fait des recherches et j'ai découvert que Microsoft ne recommande PAS d'utiliser CHECKSUM à des fins de détection des modifications. Dans certains forums, certains ont suggéré d'utiliser

SELECT COUNT(DISTINCT CHECKSUM(value1, value2, ..., valueN) + CHECKSUM(valueN, value(N-1), ..., value1))

mais ce n'est pas non plus réconfortant.

Vous pouvez utiliser la fonction HASHBYTES () comme suggéré dans l' énigme TSQL CHECKSUM . Cependant, cela a également une petite chance de ne pas renvoyer de résultats uniques.

Je suggère d'utiliser

SELECT COUNT(DISTINCT CAST(DocumentId AS VARCHAR)+'-'+CAST(DocumentSessionId AS VARCHAR)) FROM DocumentOutputItems

1

Que dis-tu de ça,

Select DocumentId, DocumentSessionId, count(*) as c 
from DocumentOutputItems 
group by DocumentId, DocumentSessionId;

Cela nous donnera le nombre de toutes les combinaisons possibles de DocumentId et DocumentSessionId


0

Ça marche pour moi. Dans l'oracle:

SELECT SUM(DECODE(COUNT(*),1,1,1))
FROM DocumentOutputItems GROUP BY DocumentId, DocumentSessionId;

En jpql:

SELECT SUM(CASE WHEN COUNT(i)=1 THEN 1 ELSE 1 END)
FROM DocumentOutputItems i GROUP BY i.DocumentId, i.DocumentSessionId;

0

J'avais une question similaire, mais la requête que j'avais était une sous-requête avec les données de comparaison dans la requête principale. quelque chose comme:

Select code, id, title, name 
(select count(distinct col1) from mytable where code = a.code and length(title) >0)
from mytable a
group by code, id, title, name
--needs distinct over col2 as well as col1

ignorant la complexité de cela, je me suis rendu compte que je ne pouvais pas obtenir la valeur de a.code dans la sous-requête avec la double sous-requête décrite dans la question d'origine

Select count(1) from (select distinct col1, col2 from mytable where code = a.code...)
--this doesn't work because the sub-query doesn't know what "a" is

Finalement, j'ai compris que je pouvais tricher et combiner les colonnes:

Select count(distinct(col1 || col2)) from mytable where code = a.code...

C'est ce qui a fini par fonctionner


0

Si vous travaillez avec des types de données de longueur fixe, vous pouvez effectuer un cast pour binaryle faire très facilement et très rapidement. En supposant DocumentIdet DocumentSessionIdsont tous deux ints, et sont donc de 4 octets de long ...

SELECT COUNT(DISTINCT CAST(DocumentId as binary(4)) + CAST(DocumentSessionId as binary(4)))
FROM DocumentOutputItems

Mon problème spécifique m'obligeait à diviser un SUMpar le COUNTde la combinaison distincte de diverses clés étrangères et d'un champ de date, regroupé par une autre clé étrangère et parfois filtré par certaines valeurs ou clés. Le tableau est très volumineux et l'utilisation d'une sous-requête a considérablement augmenté le temps de requête. Et en raison de la complexité, les statistiques n'étaient tout simplement pas une option viable. La CHECKSUMsolution était également beaucoup trop lente dans sa conversion, notamment en raison des différents types de données, et je ne pouvais pas risquer son manque de fiabilité.

Cependant, l'utilisation de la solution ci-dessus n'a pratiquement pas augmenté le temps de requête (comparé à l'utilisation de simplement SUM) et devrait être complètement fiable! Il devrait pouvoir aider d'autres personnes dans une situation similaire, alors je le poste ici.


-1

Vous pouvez simplement utiliser la fonction de comptage deux fois.

Dans ce cas, ce serait:

SELECT COUNT (DISTINCT DocumentId), COUNT (DISTINCT DocumentSessionId) 
FROM DocumentOutputItems

cela ne fait pas ce qui est requis dans la question, il compte le distinct en séparé pour chaque colonne
naviram

-1

Ce code utilise distinct sur 2 paramètres et fournit le nombre de lignes spécifiques à ces valeurs distinctes. Cela a fonctionné pour moi dans MySQL comme un charme.

select DISTINCT DocumentId as i,  DocumentSessionId as s , count(*) 
from DocumentOutputItems   
group by i ,s;
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.