删除基于多个列的重复记录?


76

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

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

在我的用例中,我的数据库中有汽车的品牌和型号关系。

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

我想删除所有具有相同“名称”,“年份”和“修剪”的Model记录,但保留其中1条记录(这意味着,我需要该记录,但只记录一次)。我正在使用Heroku控制台,因此可以轻松运行一些活动记录查询。

有什么建议么?

Answers:


144
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,2015年

@meetalexjohnson,它应该在您拥有的任何activerecord模型中。
Aditya Sanghi,2015年

3
有趣的方法,但记录集很多,效率低下。想知道是否有一种方法可以主动记录自己。
Ziyan Junaideen

6
可行,但对于大型数据集效率极低。一种更快的方法是使用此算法首先收集数组中的ID,然后使用一个DELETE FROM sql语句删除ID数组。
埃里克·阿尔福德

非常感谢许多正常情况下非常有用的方法。
Paul Watson

52

如果您的用户表数据如下

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

**铁路4 **

ActiveRecord 4引入了.not允许您在步骤2中编写以下内容的方法:

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

谢谢,这对我有帮助!!
瑞安·雷波

1
这很危险:在没有dups的情况下再次运行它会删除比您想要的更多的东西,因为逻辑是“删除除D以外的所有内容”。我认为更好的逻辑是“删除D中的所有内容”,其中D是重复行的ID的列表。
Alex

15

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

10

您可以尝试以下操作:(基于先前的答案)

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所要的,删除所有重复项-第一种方法
将使

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

0

基于@ aditya-sanghi的答案,并提供了一种使用SQL查找重复项的更有效方法。

将此添加到您ApplicationRecord可以删除任何模型的重复数据:

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

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

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

-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);

这将删除所有。
monteirobrena
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.