Contando DISTINCT em várias colunas

213

Existe uma maneira melhor de fazer uma consulta como esta:

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

Preciso contar o número de itens distintos dessa tabela, mas o distinto tem mais de duas colunas.

Minha consulta funciona bem, mas eu queria saber se posso obter o resultado final usando apenas uma consulta (sem usar uma subconsulta)

Novitzky
fonte
IordanTanev, Mark Brackett, RC - obrigado pelas respostas, foi uma boa tentativa, mas você precisa verificar o que está fazendo antes de postar no SO. As consultas que você forneceu não são equivalentes à minha consulta. Você pode ver facilmente que sempre tenho um resultado escalar, mas sua consulta retorna várias linhas.
Novitzky 24/09/09
Apenas atualizei a pergunta para incluir seu comentário esclarecedor de uma das respostas
Jeff
Essa é uma boa pergunta. Eu queria saber também se havia uma maneira mais simples de fazer isso
Anupam

Respostas:

73

Se você estiver tentando melhorar o desempenho, tente criar uma coluna computada persistente em um hash ou valor concatenado das duas colunas.

Depois de persistida, desde que a coluna seja determinística e você esteja usando configurações de banco de dados "sãs", ela pode ser indexada e / ou estatísticas podem ser criadas nela.

Acredito que uma contagem distinta da coluna computada seria equivalente à sua consulta.

Jason Horner
fonte
4
Excelente sugestão! Quanto mais leio, mais percebo que o SQL tem menos a ver com sintaxe e funções e mais com a aplicação de lógica pura. Eu gostaria de ter 2 votos positivos!
tumchaaditya
Boa sugestão. Isso me evitou escrever código desnecessário para isso.
Avrajit Roy
1
Você poderia adicionar um exemplo ou amostra de código para mostrar mais sobre o que isso significa e como fazê-lo?
jayqui 20/02
52

Edit: Alterado da consulta somente de soma de verificação menos que confiável, descobri uma maneira de fazer isso (no SQL Server 2005) que funciona muito bem para mim e posso usar quantas colunas forem necessárias (adicionando-as a a função CHECKSUM ()). A função REVERSE () transforma as entradas em varchars para tornar as distintas mais confiáveis

SELECT COUNT(DISTINCT (CHECKSUM(DocumentId,DocumentSessionId)) + CHECKSUM(REVERSE(DocumentId),REVERSE(DocumentSessionId)) )
FROM DocumentOutPutItems
JayTee
fonte
1
+1 Nice one, funciona perfeito (quando você tem os tipos de colunas direito de realizar uma verificação em ...;)
Bernoulli TI
8
Com hashes como Checksum (), há poucas chances de que o mesmo hash seja retornado para entradas diferentes, portanto a contagem pode estar um pouco desligada. HashBytes () é uma chance ainda menor, mas ainda não é zero. Se esses dois Ids fossem int's (32b), um "hash sem perdas" poderia combiná-los em um bigint (64b) como Id1 << 32 + Id2.
crokusek
1
a chance não é tão pequena, especialmente quando você começa a combinar colunas (que era para isso que ela deveria ser criada). Fiquei curioso com essa abordagem e, em um caso específico, a soma de verificação acabou com uma contagem 10% menor. Se você pensar um pouco mais, o Checksum retorna apenas um int; portanto, se você fizer um checksum em um intervalo grande e completo, você terá uma contagem distinta cerca de 2 bilhões de vezes menor do que realmente existe. -1
pvolders
A consulta foi atualizada para incluir o uso de "REVERSE" para remover a chance de duplicatas
JayTee 4/14
4
Poderíamos evitar CHECKSUM - poderíamos concatenar os dois valores juntos? Suponho que corre o risco de considerar a mesma coisa: ('ele', 'arte') == 'ouvir', 't'). Mas acho que isso pode ser resolvido com um delimitador, como o @APC propõe (algum valor que não aparece em nenhuma coluna), então 'he | ​​art'! = 'Hear | t' Existem outros problemas com uma simples "concatenação" abordagem?
The Red Pea
31

Do que você não gosta na sua consulta existente? Se você está preocupado que DISTINCTduas colunas não retornem apenas as permutações exclusivas, por que não tentar?

Certamente funciona como você pode esperar no 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>

editar

Desci um beco sem saída com análises, mas a resposta era deprimente óbvia ...

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

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

SQL>

editar 2

Dados os seguintes dados, a solução de concatenação fornecida acima descontará:

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

Então, vamos incluir um separador ...

select col1 + '*' + col2 from t23
/

Obviamente, o separador escolhido deve ser um caractere ou conjunto de caracteres, que nunca pode aparecer em nenhuma coluna.

APC
fonte
+1 de mim. Obrigado pela sua resposta. Minha consulta funciona bem, mas eu queria saber se eu posso obter o resultado final usando apenas uma consulta (sem usar uma subconsulta)
Novitzky
20

Para executar como uma única consulta, concatene as colunas e obtenha a contagem distinta de instâncias da sequência concatenada.

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

No MySQL, você pode fazer a mesma coisa sem a etapa de concatenação da seguinte maneira:

SELECT count(DISTINCT DocumentId, DocumentSessionId) FROM DocumentOutputItems;

Este recurso é mencionado na documentação do MySQL:

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

spelunk1
fonte
Esta foi uma pergunta do SQL Server e as duas opções que você postou já foram mencionadas nas seguintes respostas a esta pergunta: stackoverflow.com/a/1471444/4955425 e stackoverflow.com/a/1471713/4955425 .
sstan
1
FWIW, isso quase funciona no PostgreSQL; só precisa de parênteses extras:SELECT COUNT(DISTINCT (DocumentId, DocumentSessionId)) FROM DocumentOutputItems;
ijoseph
14

Que tal algo como:

selecione contagem (*)
de
  (selecione contagem (*) cnt
   de DocumentOutputItems
   agrupar por DocumentId, DocumentSessionId) t1

Provavelmente, apenas faz o mesmo que você já está, mas evita o DISTINCT.

Trevor Tippins
fonte
nos meus testes (usando SET SHOWPLAN_ALL ON), ele tinha o mesmo plano de execução e exatamente o mesmo TotalSubtreeCost
KM.
1
Dependendo da complexidade da consulta original, resolvê-lo GROUP BYpode introduzir alguns desafios adicionais na transformação da consulta para obter a saída desejada (por exemplo, quando a consulta original já possui GROUP BYou HAVINGcláusulas ...)
Lukas Eder
8

Aqui está uma versão mais curta sem a subseleção:

SELECT COUNT(DISTINCT DocumentId, DocumentSessionId) FROM DocumentOutputItems

Funciona bem no MySQL, e acho que o otimizador tem mais facilidade para entender este.

Edit: Aparentemente, eu li mal o MSSQL e o MySQL - desculpe por isso, mas talvez ajude de qualquer maneira.

Alexander Kjäll
fonte
6
no SQL Server, você obtém: Mensagem 102, Nível 15, Estado 1, Linha 1 Sintaxe incorreta próxima a ','.
KM.
Era nisso que eu estava pensando. Quero fazer algo semelhante no MSSQL, se possível.
Novitzky 24/09/09
@ Kamil Nowicki, no SQL Server, você pode ter apenas um campo em COUNT (). Na minha resposta, mostro que você pode concatenar os dois campos em um e tentar essa abordagem. No entanto, eu continuaria com o original, pois os planos de consulta terminariam os mesmos.
KM.
1
Por favor, dê uma olhada na resposta @JayTee. Ele funciona como um encanto. count ( distinct CHECKSUM ([Field1], [Field2])
Custodio
5

Muitos bancos de dados SQL (a maioria?) Podem trabalhar com tuplas, como valores, para que você possa fazer isso: SELECT COUNT(DISTINCT (DocumentId, DocumentSessionId)) FROM DocumentOutputItems; Se o banco de dados não suportar isso, ele poderá ser simulado conforme a sugestão do CHECKSUM ou de outra função escalar do @ oncel-umuturererer, fornecendo boa exclusividade por exemplo COUNT(DISTINCT CONCAT(DocumentId, ':', DocumentSessionId)).

Um uso relacionado de tuplas está executando INconsultas como: SELECT * FROM DocumentOutputItems WHERE (DocumentId, DocumentSessionId) in (('a', '1'), ('b', '2'));

karmakaze
fonte
quais bancos de dados suportam select count(distinct(a, b))? : D
Vytenis Bivainis 11/11
@VytenisBivainis sei que o PostgreSQL faz - não tenho certeza desde qual versão.
karmakaze
3

Não há nada errado com sua consulta, mas você também pode fazer o seguinte:

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

Espero que isso funcione, estou escrevendo em prima vista

SELECT COUNT(*) 
FROM DocumentOutputItems 
GROUP BY DocumentId, DocumentSessionId
IordanTanev
fonte
7
Para que isso dê a resposta final, você teria que envolvê-la em outro SELECT COUNT (*) FROM (...). Essencialmente, esta resposta é apenas uma outra maneira de listar os valores distintos que você deseja contar. Não é melhor que a sua solução original.
Dave Costa
Obrigado Dave. Eu sei que você pode usar o grupo em vez de distinto no meu caso. Eu queria saber se você obtém o resultado final usando apenas uma consulta. Eu acho que é impossível, mas posso estar errado.
Novitzky 24/09/09
3

Eu usei essa abordagem e funcionou para mim.

SELECT COUNT(DISTINCT DocumentID || DocumentSessionId) 
FROM  DocumentOutputItems

Para o meu caso, fornece o resultado correto.

Jaanis Veinberg
fonte
Não fornece a contagem de valores distintos em conjunto com duas colunas. Pelo menos não no MySQL 5.8.
Anwar Shaikh
Esta questão está etiquetada com o SQL Server e esta não é a sintaxe do SQL Server
Tab Alleman
2

se você tivesse apenas um campo para "DISTINCT", poderia usar:

SELECT COUNT(DISTINCT DocumentId) 
FROM DocumentOutputItems

e isso retorna o mesmo plano de consulta que o original, testado com SET SHOWPLAN_ALL ON. No entanto, você está usando dois campos para tentar algo louco como:

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

mas você terá problemas se NULLs estiverem envolvidos. Eu apenas ficaria com a consulta original.

KM.
fonte
+1 de mim. Obrigado, mas continuarei com minha consulta, como você sugeriu. Usar "converter" pode diminuir ainda mais o desempenho.
Novitzky
2

Encontrei isso quando pesquisei no Google por meu próprio problema, descobri que se você contar objetos DISTINCT, obterá o número correto retornado (estou usando o MySQL)

SELECT COUNT(DISTINCT DocumentID) AS Count1, 
  COUNT(DISTINCT DocumentSessionId) AS Count2
  FROM DocumentOutputItems
tehaugmenter
fonte
5
A consulta acima retornará um conjunto diferente de resultados do que o OP estava procurando (as combinações distintas de DocumentIde DocumentSessionId). Alexander Kjäll já postou a resposta correta se o OP estava usando o MySQL e não o MS SQL Server.
Anthony Geoghegan
1

Gostaria que o MS SQL também pudesse fazer algo como COUNT (DISTINCT A, B). Mas não pode.

No começo, a resposta de JayTee parecia uma solução para mim, depois de alguns testes que CHECKSUM () falhou em criar valores únicos. Um exemplo rápido é que CHECKSUM (31.467.519) e CHECKSUM (69,1120.823) fornecem a mesma resposta, que é 55.

Fiz algumas pesquisas e descobri que a Microsoft NÃO recomenda o uso do CHECKSUM para fins de detecção de alterações. Em alguns fóruns, alguns sugeriram o uso de

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

mas isso também não é reconfortante.

Você pode usar a função HASHBYTES () conforme sugerido no enigma TSQL CHECKSUM . No entanto, isso também tem uma pequena chance de não retornar resultados exclusivos.

Eu sugeriria usar

SELECT COUNT(DISTINCT CAST(DocumentId AS VARCHAR)+'-'+CAST(DocumentSessionId AS VARCHAR)) FROM DocumentOutputItems
Oncel Umut TURER
fonte
1

Que tal agora,

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

Isso nos fornecerá a contagem de todas as combinações possíveis de DocumentId e DocumentSessionId

Nikhil Singh
fonte
0

Funciona para mim. No oráculo:

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

No jpql:

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

Eu tinha uma pergunta semelhante, mas a consulta que eu tinha era uma subconsulta com os dados de comparação na consulta principal. algo como:

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

ignorando as complexidades disso, percebi que não era possível obter o valor de a.code na subconsulta com a subconsulta dupla descrita na pergunta original

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

Então, finalmente, descobri que poderia trapacear e combinar as colunas:

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

Foi isso que acabou funcionando

Mark Rogers
fonte
0

Se você estiver trabalhando com tipos de dados de comprimento fixo, poderá binaryfazer isso com muita facilidade e rapidez. Assumindo DocumentIde DocumentSessionIdsão ambos ints e, portanto, têm 4 bytes de comprimento ...

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

Meu problema específico exigia que eu dividisse a SUMpela COUNTcombinação distinta de várias chaves estrangeiras e um campo de data, agrupando por outra chave estrangeira e ocasionalmente filtrando por determinados valores ou chaves. A tabela é muito grande e o uso de uma subconsulta aumentou drasticamente o tempo de consulta. E devido à complexidade, as estatísticas simplesmente não eram uma opção viável. oCHECKSUM solução também foi muito lenta na conversão, principalmente como resultado dos vários tipos de dados, e não pude arriscar sua falta de confiabilidade.

No entanto, o uso da solução acima praticamente não aumentou o tempo de consulta (em comparação com o simples uso de SUM) e deve ser totalmente confiável! Deve ser capaz de ajudar outras pessoas em uma situação semelhante, por isso estou postando aqui.

IphStich
fonte
-1

Você pode apenas usar a função Count duas vezes.

Nesse caso, seria:

SELECT COUNT (DISTINCT DocumentId), COUNT (DISTINCT DocumentSessionId) 
FROM DocumentOutputItems
Bibek
fonte
isto não fazer como exigem na pergunta, ele conta a nítida em separado para cada coluna
naviram
-1

Esse código usa distintos em 2 parâmetros e fornece a contagem do número de linhas específicas para esses valores distintos. Funcionou para mim no MySQL como um encanto.

select DISTINCT DocumentId as i,  DocumentSessionId as s , count(*) 
from DocumentOutputItems   
group by i ,s;
rishi jain
fonte