Doppelte Datensätze basierend auf mehreren Spalten entfernen?

76

Ich verwende Heroku, um meine Ruby on Rails-Anwendung zu hosten, und aus dem einen oder anderen Grund habe ich möglicherweise doppelte Zeilen.

Gibt es eine Möglichkeit, doppelte Datensätze basierend auf zwei oder mehr Kriterien zu löschen, aber nur einen Datensatz dieser doppelten Sammlung zu behalten?

In meinem Anwendungsfall habe ich eine Marken- und Modellbeziehung für Autos in meiner Datenbank.

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

Ich möchte alle Modelldatensätze löschen, die denselben Namen, dasselbe Jahr und denselben Schnitt haben, aber einen dieser Datensätze behalten (dh ich benötige den Datensatz nur einmal). Ich verwende die Heroku-Konsole, damit ich problemlos einige aktive Datensatzabfragen ausführen kann.

Irgendwelche Vorschläge?

sergserg
quelle

Antworten:

145
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
  • Finde alle
  • Gruppieren Sie sie nach Schlüsseln, die Sie für die Eindeutigkeit benötigen
  • Schleife der Hash-Werte des gruppierten Modells
  • Entfernen Sie den ersten Wert, da Sie eine Kopie behalten möchten
  • den Rest löschen
Aditya Sanghi
quelle
Dies ist im Modellmodell?
Choylton B. Higginbottom
@meetalexjohnson sollte es in jedem Activerecord-Modell sein, das Sie haben.
Aditya Sanghi
3
Interessante Methode, aber ein wenig ineffizient mit großen Datensätzen. Ich frage mich, ob es eine Möglichkeit gibt, dies mit einer aktiven Aufzeichnung selbst zu tun.
Ziyan Junaideen
6
Funktioniert, ist aber für große Datenmengen äußerst ineffizient. Eine viel schnellere Möglichkeit besteht darin, mit diesem Algo zuerst die IDs in einem Array zu sammeln und dann mit einer SQL-Anweisung DELETE FROM das Array mit den IDs zu löschen.
Eric Alford
Sehr nützliche Methode für viele normale Situationen, danke Aditya.
Paul Watson
52

Wenn Ihre Benutzertabelle Daten wie unten

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 > 

E-Mail-IDs sind doppelt vorhanden. Daher ist es unser Ziel, alle doppelten E-Mail-IDs aus der Benutzertabelle zu entfernen.

Schritt 1:

Um alle eindeutigen E-Mail-Datensatz-ID zu erhalten.

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

Schritt 2:

So entfernen Sie doppelte IDs aus der Benutzertabelle mit einer eindeutigen E-Mail-Datensatz-ID.

Jetzt enthält das IDs-Array die folgenden IDs.

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

** SCHIENEN 4 **

ActiveRecord 4 führt die .notMethode ein, mit der Sie in Schritt 2 Folgendes schreiben können:

User.where.not(id: ids).destroy_all
Aravind Zugabe
quelle
Danke, das hat mir geholfen !!
Ryan Rebo
1
Dies ist gefährlich: Wenn Sie es erneut ausführen, wenn Sie keine Dups haben, wird mehr gelöscht, als Sie möchten, da die Logik "Alles außer D löschen" lautet. Ich denke, die bessere Logik ist "alles in D löschen", wobei D die Liste der IDs doppelter Zeilen ist.
Alex
15

Ähnlich wie die Antwort von @Aditya Sanghi, aber dieser Weg ist leistungsfähiger, da Sie nur die Duplikate auswählen, anstatt jedes Modellobjekt in den Speicher zu laden und dann alle zu durchlaufen.

# 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

Wenn Sie wirklich keine doppelten Daten in dieser Tabelle möchten, möchten Sie der Tabelle wahrscheinlich einen mehrspaltigen eindeutigen Index hinzufügen, der wie folgt aussieht:

add_index :models, [:name, :year, :trim], unique: true, name: 'index_unique_models' 
Mackshkatz
quelle
10

Sie können Folgendes versuchen: (basierend auf vorherigen Antworten)

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

um alle gültigen Datensätze zu erhalten. Und dann:

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

um die nicht benötigten Datensätze zu entfernen. Und natürlich können Sie eine Migration durchführen, die einen eindeutigen Index für die drei Spalten hinzufügt, damit dies auf DB-Ebene erzwungen wird:

add_index :models, [:name, :year, :trim], unique: true
LuisFelipe22
quelle
Vermisse ich etwas Würde der zweite Codeblock hier nicht nur die gesamte Tabelle löschen, mit Ausnahme der IDs, die im ersten Codeblock gefunden wurden?
Elle Mundy
Das war es, wonach das OP gesucht hat und alle Duplikate gelöscht hat - die erste Methode bringt Ihnen alle Nicht-
Dupes
4

Um es auf einer Migration auszuführen, habe ich am Ende Folgendes getan (basierend auf der obigen Antwort von @ 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
Nuno Costa
quelle
1
Sie können model.unscopedAbfragen hinzufügen, um zu vermeiden, dass der Standardbereich in der aktuellen Gruppenabfrage nicht vorhanden ist.
ErvalhouS
0

Basierend auf der Antwort von @ aditya-sanghi , mit einer effizienteren Möglichkeit, Duplikate mithilfe von SQL zu finden.

Fügen Sie dies zu Ihrem hinzu ApplicationRecord, um jedes Modell deduplizieren zu können:

class ApplicationRecord < ActiveRecord::Base
  # …

  def self.destroy_duplicates_by(*columns)
    groups = select(columns).group(columns).having(Arel.star.count.gt(1))
    groups.each do |duplicates|
      records = where(duplicates.attributes.symbolize_keys.slice(*columns))
      records.offset(1).destroy_all
    end
  end
end

Sie können dann aufrufen destroy_duplicates_by, um alle Datensätze (außer dem ersten) zu zerstören, die dieselben Werte für die angegebenen Spalten haben. Zum Beispiel:

Model.destroy_duplicates_by(:name, :year, :trim, :make_id)
Sonnig
quelle
-3

Sie können diese SQL-Abfrage versuchen, um alle doppelten Datensätze außer dem neuesten zu entfernen

DELETE FROM users USING users user WHERE (users.name = user.name AND users.year = user.year AND users.trim = user.trim AND users.id < user.id);
Mahendra Gawas
quelle
Dadurch werden alle entfernt.
Monteirobrena