web-dev-qa-db-fra.com

Supprimer les enregistrements en double basés sur plusieurs colonnes?

J'utilise Heroku pour héberger mon Ruby on Rails application et pour une raison ou une autre, je peux avoir des lignes en double).

Existe-t-il un moyen de supprimer les enregistrements en double en fonction de 2 critères ou plus, mais de conserver un seul enregistrement de cette collection en double?

Dans mon cas d'utilisation, j'ai une relation Marque et Modèle pour les voitures dans ma base de données.

Make      Model
---       ---
Name      Name
          Year
          Trim
          MakeId

Je voudrais supprimer tous les enregistrements de modèle qui ont le même nom, l'année et le trim, mais conserver 1 de ces enregistrements (ce qui signifie que j'ai besoin de l'enregistrement mais une seule fois). J'utilise la console Heroku pour pouvoir exécuter facilement des requêtes d'enregistrement actives.

Aucune suggestion?

72
sergserg
class Model

  def self.dedupe
    # find all models and group them on keys which should be common
    grouped = all.group_by{|model| [model.name,model.year,model.trim,model.make_id] }
    grouped.values.each do |duplicates|
      # the first one we want to keep right?
      first_one = duplicates.shift # or pop for last one
      # if there are any more left, they are duplicates
      # so delete all of them
      duplicates.each{|double| double.destroy} # duplicates can now be destroyed
    end
  end

end

Model.dedupe
  • Trouver tout
  • Regroupez-les sur les clés dont vous avez besoin pour l'unicité
  • Boucle sur les valeurs du hachage du modèle groupé
  • supprimez la première valeur car vous souhaitez conserver une copie
  • supprimer le reste
131
Aditya Sanghi

Si vos données de table utilisateur comme ci-dessous

User.all =>
[
    #<User id: 15, name: "a", email: "[email protected]", created_at: "2013-08-06 08:57:09", updated_at: "2013-08-06 08:57:09">, 
    #<User id: 16, name: "a1", email: "[email protected]", created_at: "2013-08-06 08:57:20", updated_at: "2013-08-06 08:57:20">, 
    #<User id: 17, name: "b", email: "[email protected]", created_at: "2013-08-06 08:57:28", updated_at: "2013-08-06 08:57:28">, 
    #<User id: 18, name: "b1", email: "[email protected]", created_at: "2013-08-06 08:57:35", updated_at: "2013-08-06 08:57:35">, 
    #<User id: 19, name: "b11", email: "[email protected]", created_at: "2013-08-06 09:01:30", updated_at: "2013-08-06 09:01:30">, 
    #<User id: 20, name: "b11", email: "[email protected]", created_at: "2013-08-06 09:07:58", updated_at: "2013-08-06 09:07:58">] 
1.9.2p290 :099 > 

Les identifiants des e-mails sont en double, notre objectif est donc de supprimer tous les identifiants des e-mails en double de la table des utilisateurs.

Étape 1:

Pour obtenir tous les identifiants des enregistrements de messagerie distincts.

ids = User.select("MIN(id) as id").group(:email,:name).collect(&:id)
=> [15, 16, 18, 19, 17]

Étape 2:

Pour supprimer les identifiants en double de la table des utilisateurs avec un identifiant d'enregistrements de messagerie distinct.

Le tableau ids contient maintenant les identifiants suivants.

[15, 16, 18, 19, 17]
User.where("id NOT IN (?)",ids)  # To get all duplicate records
User.where("id NOT IN (?)",ids).destroy_all

** Rails 4 **

ActiveRecord 4 présente le .not méthode qui vous permet d'écrire ce qui suit à l'étape 2:

User.where.not(id: ids).destroy_all
49
Aravind encore

Semblable à la réponse de @Aditya Sanghi, mais cette méthode sera plus performante car vous ne sélectionnez que les doublons, plutôt que de charger chaque objet Model en mémoire puis de répéter sur chacun d'eux.

# returns only duplicates in the form of [[name1, year1, trim1], [name2, year2, trim2],...]
duplicate_row_values = Model.select('name, year, trim, count(*)').group('name, year, trim').having('count(*) > 1').pluck(:name, :year, :trim)

# load the duplicates and order however you wantm and then destroy all but one
duplicate_row_values.each do |name, year, trim|
  Model.where(name: name, year: year, trim: trim).order(id: :desc)[1..-1].map(&:destroy)
end

De plus, si vous ne voulez vraiment pas de données en double dans cette table, vous souhaiterez probablement ajouter un index unique à plusieurs colonnes à la table, quelque chose comme:

add_index :models, [:name, :year, :trim], unique: true, name: 'index_unique_models' 
12
mackshkatz

Vous pouvez essayer ce qui suit: (basé sur les réponses précédentes)

ids = Model.group('name, year, trim').pluck('MIN(id)')

pour obtenir tous les enregistrements valides. Et alors:

Model.where.not(id: ids).destroy_all

pour supprimer les enregistrements inutiles. Et certainement, vous pouvez effectuer une migration qui ajoute un index unique pour les trois colonnes afin que cela soit appliqué au niveau de la base de données:

add_index :models, [:name, :year, :trim], unique: true
7
LuisFelipe22

Pour l'exécuter sur une migration, j'ai fini par faire comme suit (basé sur le réponse ci-dessus par @ aditya-sanghi)

class AddUniqueIndexToXYZ < ActiveRecord::Migration
  def change
    # delete duplicates
    dedupe(XYZ, 'name', 'type')

    add_index :xyz, [:name, :type], unique: true
  end

  def dedupe(model, *key_attrs)
    model.select(key_attrs).group(key_attrs).having('count(*) > 1').each { |duplicates|
      dup_rows = model.where(duplicates.attributes.slice(key_attrs)).to_a
      # the first one we want to keep right?
      dup_rows.shift

      dup_rows.each{ |double| double.destroy } # duplicates can now be destroyed
    }
  end
end
3
Nuno Costa