【问题标题】:Remove duplicate records based on multiple columns?删除基于多列的重复记录?
【发布时间】:2013-01-02 15:00:56
【问题描述】:

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

有没有办法根据 2 个或更多条件删除重复记录,但只保留该重复集合的 1 条记录?

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

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

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

有什么建议吗?

【问题讨论】:

    标签: ruby-on-rails-3 activerecord duplicates destroy


    【解决方案1】:
    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 模型中吗?
    • @meetalexjohnson 它应该在您拥有的任何 activerecord 模型中。
    • 有趣的方法,但是对于大量记录来说效率有点低。想知道是否有办法通过自己的主动记录来做到这一点。
    • 有效,但对于大型数据集效率极低。一种更快的方法是使用此算法首先收集数组中的 id,然后使用一条 DELETE FROM sql 语句删除 id 数组。
    【解决方案2】:

    如果您的用户表数据如下所示

    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
    

    【讨论】:

    • 谢谢,这对我有帮助!!
    • 这很危险:当你没有 dups 时再次运行它会删除比你想要的更多,因为逻辑是“删除除 D 之外的所有内容”。我认为更好的逻辑是“删除 D 中的所有内容”,其中 D 是重复行的 id 列表。
    【解决方案3】:

    类似于@Aditya Sanghi 的回答,但这种方式会更高效,因为您只选择重复项,而不是将每个模型对象加载到内存中然后遍历所有对象。

    # 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' 
    

    【讨论】:

      【解决方案4】:

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

      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吗?
      • 这就是 OP 正在寻找的,删除所有重复项 - 第一种方法让您获得所有非重复项
      【解决方案5】:

      为了在迁移中运行它,我最终做了如下操作(基于 @aditya-sanghi 的 answer above

      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
      

      【讨论】:

      • 您可以将model.unscoped 添加到查询中,以避免被当前组查询中不存在的默认范围捕获。
      【解决方案6】:

      基于@aditya-sanghi's answer,以更高效的方式使用 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)
      

      【讨论】:

        【解决方案7】:

        你可以试试这个 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);
        

        【讨论】:

        • 这将删除所有内容。
        猜你喜欢
        • 1970-01-01
        • 2018-03-14
        • 2021-11-03
        • 1970-01-01
        • 2018-12-27
        • 2013-06-13
        • 1970-01-01
        • 2022-01-24
        相关资源
        最近更新 更多