基于多个列,如何删除重复记录?

81

我正在使用Heroku来托管我的Ruby on Rails应用程序,由于某种原因,我可能会有一些重复的行。

是否有一种方法可以基于2个或更多的条件删除重复记录,但保留该重复集合中的1个记录?

在我的用例中,在数据库中为汽车建立了制造商和型号关系。

Make      Model
---       ---
Name      Name
          Year
          Trim
          MakeId
我想删除所有具有相同名称、年份和修剪的模型记录,但保留其中一条记录(即我需要该记录,但仅需一次)。我正在使用Heroku控制台,以便可以轻松运行一些Active Record查询。
有什么建议吗?
8个回答

154
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
  • 查找所有
  • 按照需要唯一性的键对它们进行分组
  • 循环遍历哈希表中分组模型的值
  • 删除第一个值,因为你想保留一份副本
  • 删除其余部分

这是在 Model 模型中吗? - Choylton B. Higginbottom
@meetalexjohnson 这应该在你所拥有的任何 ActiveRecord 模型中。 - Aditya Sanghi
3
有趣的方法,但对于大量记录来说有点低效。想知道是否有一种使用Active Record本身进行操作的方法。 - Ziyan Junaideen
8
这种方法在处理大数据集时效率极低。更快的方法是先使用该算法在数组中收集ID,然后使用一个DELETE FROM SQL语句删除这些ID的数组。 - Eric Alford

54
如果您的用户表数据如下所示。
User.all =>
[
    #<User id: 15, name: "a", email: "a@gmail.com", created_at: "2013-08-06 08:57:09", updated_at: "2013-08-06 08:57:09">, 
    #<User id: 16, name: "a1", email: "a@gmail.com", created_at: "2013-08-06 08:57:20", updated_at: "2013-08-06 08:57:20">, 
    #<User id: 17, name: "b", email: "b@gmail.com", created_at: "2013-08-06 08:57:28", updated_at: "2013-08-06 08:57:28">, 
    #<User id: 18, name: "b1", email: "b1@gmail.com", created_at: "2013-08-06 08:57:35", updated_at: "2013-08-06 08:57:35">, 
    #<User id: 19, name: "b11", email: "b1@gmail.com", created_at: "2013-08-06 09:01:30", updated_at: "2013-08-06 09:01:30">, 
    #<User id: 20, name: "b11", email: "b1@gmail.com", 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方法,允许您在第二步中编写以下内容:

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

谢谢,这对我很有帮助!! - Ryan Rebo
4
这很危险:如果没有重复项再次运行它将删除更多内容,因为其逻辑是“删除除D之外的所有内容”。我认为更好的逻辑是“删除D中的所有内容”,其中D是重复行的ID列表。 - Alex

19

与 @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' 

15

您可以尝试以下方法:(基于之前的回答)

ids = Model.group('name, year, trim').pluck('MIN(id)')
获取所有有效记录。然后:
Model.where.not(id: ids).destroy_all

删除不必要的记录。当然,您可以进行迁移,在数据库级别上添加一个唯一索引来强制实施这三个列:

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

我有什么遗漏吗?这里的第二个代码块不是只会清除整个表格,除了在第一个代码块中找到的id之外吗? - Elle Mundy
这就是 OP 所寻找的,删除所有重复项 - 第一种方法可以获取所有非重复项。 - dLobatog

4

我最终在迁移时采用了以下方法(基于@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

1
你可以在查询中添加 model.unscoped 来避免被当前组查询中不存在默认范围所限制。 - ErvalhouS

4
基于@aditya-sanghi的答案,使用SQL更高效地查找重复项。
将此代码添加到您的ApplicationRecord中,以便能够对任何模型进行去重:
class ApplicationRecord < ActiveRecord::Base
  # …

  def self.destroy_duplicates_by(*columns, order: :id)
    groups = select(columns).group(columns).having(Arel.star.count.gt(1))
    groups.each do |duplicate|
      records = where(duplicate.attributes.symbolize_keys.slice(*columns))
      records.limit(nil).order(order).offset(1).destroy_all
    end
  end
end

然后,您可以调用destroy_duplicates_by来销毁所有具有相同给定列值的记录(除第一个记录外)。例如:

Model.destroy_duplicates_by(:name, :year, :trim)

0

我选择了一个稍微安全一些的方法(在我看来)。我首先获取了所有唯一的记录。

    ids = Model.where(other_model_id: 1).uniq(&:field).map(&:id)

然后我获取了所有的ID

    all_ids = Model.where(other_model_id: 1).map(&:id)

这使我能够对重复项进行矩阵减法。
    dups = all_ids - ids

我会映射重复的 ID 并获取模型,因为我想确保我拥有我感兴趣的记录。

    records = dups.map do |id| Model.find(id) end

当我确定要删除时,我再次迭代以删除。

    records.map do |record| record.delete end

在生产系统中删除重复记录时,您需要非常确定您不会删除重要的实时数据,因此在此过程中,我可以仔细检查每一项。

因此,在上述情况下:

    all_ids =  Model.all.map(&:ids)

    uniq_ids = Model.all.group_by do |model|
      [model.name, model.year, model.trim] 
    end.values.map do |duplicates|
      duplicates.first.id
    end

    dups = all_ids - uniq_ids

    records = dups.map { |id| Model.find(id) }

    records.map { |record| record.delete }

或者类似这样的东西。


-3
您可以尝试使用以下 SQL 查询,删除所有重复记录,只保留最新的一条。
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);

1
这将删除所有内容。 - monteirobrena

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接