Encontre linhas com vários campos duplicados com Active Record, Rails e Postgres

103

Qual é a melhor maneira de encontrar registros com valores duplicados em várias colunas usando Postgres e Activerecord?

Encontrei esta solução aqui :

User.find(:all, :group => [:first, :email], :having => "count(*) > 1" )

Mas não parece funcionar com postgres. Estou recebendo este erro:

PG :: GroupingError: ERROR: a coluna "parts.id" deve aparecer na cláusula GROUP BY ou ser usada em uma função agregada

newUserNameHere
fonte
3
Em SQL regular, eu usaria uma auto-se juntar, algo como select a.id, b.id, name, email FROM user a INNER JOIN user b USING (name, email) WHERE a.id > b.id. Não faço ideia de como expressar isso na linguagem ActiveRecord.
Craig Ringer

Respostas:

221

Versão testada e de trabalho

User.select(:first,:email).group(:first,:email).having("count(*) > 1")

Além disso, isso é um pouco independente, mas útil. Se você quiser ver quantas vezes cada combinação foi encontrada, coloque .size no final:

User.select(:first,:email).group(:first,:email).having("count(*) > 1").size

e você obterá um conjunto de resultados semelhante a este:

{[nil, nil]=>512,
 ["Joe", "[email protected]"]=>23,
 ["Jim", "[email protected]"]=>36,
 ["John", "[email protected]"]=>21}

Achei muito legal e não tinha visto antes.

Crédito para Taryn, esta é apenas uma versão ajustada de sua resposta.

newUserNameHere
fonte
7
Tive que passar um array explícito para select()como em: User.select([:first,:email]).group(:first,:email).having("count(*) > 1").countpara funcionar.
Rafael Oliveira
4
adicionando a .countPG::UndefinedFunction: ERROR: function count
Magne
1
Você pode tentar User.select ([: first,: email]). Group (: first,: email) .having ("count (*)> 1"). Map.count
Serhii Nadolynskyi
3
Estou tentando o mesmo método, mas tentando obter o User.id também, adicioná-lo ao select e group retorna um array vazio. Como posso retornar todo o modelo de usuário, ou pelo menos incluir o: id?
Ashbury
5
use em .sizevez de.count
Charles Hamel
32

Esse erro ocorre porque POSTGRES requer que você coloque colunas de agrupamento na cláusula SELECT.

experimentar:

User.select(:first,:email).group(:first,:email).having("count(*) > 1").all

(nota: não testado, pode ser necessário ajustá-lo)

EDITADO para remover coluna id

Taryn East
fonte
7
Isso não vai funcionar; a idcoluna não faz parte do grupo, portanto você não pode referenciá-la a menos que a agregue (por exemplo, array_agg(id)ou json_agg(id))
Craig Ringer
9

Se você precisar dos modelos completos, tente o seguinte (com base na resposta de @ newUserNameAqui).

User.where(email: User.select(:email).group(:email).having("count(*) > 1").select(:email))

Isso retornará as linhas em que o endereço de e-mail da linha não é exclusivo.

Não conheço uma maneira de fazer isso com vários atributos.

Ben Aubin
fonte
`` `User.where (email: User.select (: email) .group (: email) .having (" count (*)> 1 "))` ``
chet corey
Obrigado, isso funciona muito bem :) Também parece que o último .select(:email)é redundante. Acho que isso é um pouco mais limpo, mas posso estar errado. User.where(email: User.select(:email).group(:email).having("count(*) > 1"))
chet corey
2

Obtenha todas as duplicatas com uma única consulta se usar PostgreSQL :

def duplicated_users
  duplicated_ids = User
    .group(:first, :email)
    .having("COUNT(*) > 1")
    .select('unnest((array_agg("id"))[2:])')

  User.where(id: duplicated_ids)
end

irb> duplicated_users
itsnikolay
fonte
-1

Com base na resposta de @newUserNameAqui, acredito que a maneira certa de mostrar a contagem de cada um é

res = User.select('first, email, count(1)').group(:first,:email).having('count(1) > 1')

res.each {|r| puts r.attributes } ; nil
Nuno Costa
fonte