Herokuを使用して、Ruby on Railsアプリケーションであり、何らかの理由で、行が重複している場合があります。
2つ以上の基準に基づいて重複レコードを削除する方法はありますが、その重複コレクションのレコードを1つだけ保持する方法はありますか?
私のユースケースでは、データベースに車のメーカーとモデルの関係があります。
Make Model
--- ---
Name Name
Year
Trim
MakeId
同じName、Year、Trimを持つすべてのModelレコードを削除しますが、それらのレコードを1つ保持します(つまり、必要なのは1回だけです)。 Herokuコンソールを使用しているため、いくつかのアクティブレコードクエリを簡単に実行できます。
助言がありますか?
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
以下のようなユーザーテーブルデータの場合
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 >
メールIDは重複しているため、ユーザーテーブルから重複したメールIDをすべて削除することを目標としています。
ステップ1:
すべての個別の電子メールレコードIDを取得します。
ids = User.select("MIN(id) as id").group(:email,:name).collect(&:id)
=> [15, 16, 18, 19, 17]
ステップ2:
重複するIDを、個別のメールレコードIDを持つユーザーテーブルから削除します。
これで、ids配列は次のIDを保持します。
[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は.not
メソッド:ステップ2で以下を記述できます。
User.where.not(id: ids).destroy_all
@Aditya Sanghiの答えに似ていますが、すべてのModelオブジェクトをメモリにロードしてからそれらをすべて繰り返すのではなく、重複を選択するだけなので、この方法はよりパフォーマンスが高くなります。
# 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
また、このテーブルに重複データが本当に必要ない場合は、次のような行に沿って、複数列の一意のインデックスをテーブルに追加することをお勧めします。
add_index :models, [:name, :year, :trim], unique: true, name: 'index_unique_models'
次を試すことができます:(以前の回答に基づいて)
ids = Model.group('name, year, trim').pluck('MIN(id)')
すべての有効なレコードを取得します。その後:
Model.where.not(id: ids).destroy_all
不要なレコードを削除します。そして確かに、3つの列に一意のインデックスを追加する移行を行うことができるため、これはDBレベルで実施されます。
add_index :models, [:name, :year, :trim], unique: true
移行でそれを実行するために、私は次のようにしました( 上記の回答 by @ 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