Loại bỏ các bản ghi trùng lặp dựa trên nhiều cột?


77

Tôi đang sử dụng Heroku để lưu trữ ứng dụng Ruby on Rails của mình và vì lý do này hay lý do khác, tôi có thể có một số hàng trùng lặp.

Có cách nào để xóa các bản ghi trùng lặp dựa trên 2 tiêu chí trở lên nhưng chỉ giữ 1 bản ghi của bộ sưu tập trùng lặp đó không?

Trong trường hợp sử dụng của tôi, tôi có mối quan hệ Kiểu dáng và Kiểu dáng cho ô tô trong cơ sở dữ liệu của mình.

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

Tôi muốn xóa tất cả các bản ghi Model có cùng Tên, Năm và Cắt nhưng giữ lại 1 trong các bản ghi đó (nghĩa là, tôi cần bản ghi nhưng chỉ một lần). Tôi đang sử dụng bảng điều khiển Heroku để có thể chạy một số truy vấn bản ghi đang hoạt động một cách dễ dàng.

Bất kỳ đề xuất?

Câu trả lời:


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
  • Tìm tất cả
  • Nhóm chúng trên các khóa mà bạn cần để tạo sự độc đáo
  • Lặp lại các giá trị băm của mô hình được nhóm lại
  • loại bỏ giá trị đầu tiên vì bạn muốn giữ lại một bản sao
  • xóa phần còn lại

Đây là trong mô hình Model?
Choylton B. Higginbottom

@meetalexjohnson nó phải ở trong bất kỳ mô hình activerecord nào mà bạn có.
Aditya Sanghi

3
Phương pháp thú vị nhưng hơi kém hiệu quả với bộ hồ sơ lớn. Tự hỏi nếu có cách nào để làm điều đó với bản ghi tự hoạt động.
Ziyan Junaideen

6
Hoạt động nhưng cực kỳ kém hiệu quả đối với các tập dữ liệu lớn. Một cách nhanh hơn nhiều là sử dụng thuật ngữ này để thu thập id trong một mảng trước rồi sử dụng một câu lệnh DELETE FROM sql để xóa mảng id.
Eric Alford

Phương pháp rất hữu ích cho nhiều trường hợp bình thường, cảm ơn Aditya.
Paul Watson

52

Nếu dữ liệu bảng Người dùng của bạn như bên dưới

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 email là trùng lặp, vì vậy mục đích của chúng tôi là xóa tất cả id email trùng lặp khỏi bảng người dùng.

Bước 1:

Để có được tất cả id bản ghi email riêng biệt.

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

Bước 2:

Để xóa id trùng lặp khỏi bảng người dùng với id bản ghi email riêng biệt.

Bây giờ mảng id chứa các id sau.

[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 giới thiệu .notphương thức cho phép bạn viết như sau ở Bước 2:

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

Cảm ơn, điều này đã giúp tôi !!
Ryan Rebo

1
Điều này thật nguy hiểm: chạy lại nó khi bạn không có dups sẽ xóa nhiều hơn bạn muốn vì logic là "xóa mọi thứ ngoại trừ D". Tôi nghĩ logic tốt hơn là "xóa mọi thứ trong D", trong đó D là danh sách id của các hàng trùng lặp.
Alex

16

Tương tự như câu trả lời của @Aditya Sanghi, nhưng cách này sẽ hiệu quả hơn vì bạn chỉ chọn các bản sao, thay vì tải mọi đối tượng Model vào bộ nhớ và sau đó lặp lại tất cả chúng.

# 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

Ngoài ra, nếu bạn thực sự không muốn dữ liệu trùng lặp trong bảng này, bạn có thể muốn thêm một chỉ mục duy nhất nhiều cột vào bảng, một cái gì đó dọc theo các dòng:

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

10

Bạn có thể thử những cách sau: (dựa trên các câu trả lời trước đó)

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

để có được tất cả các hồ sơ hợp lệ. Và sau đó:

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

để loại bỏ các bản ghi không cần thiết. Và chắc chắn, bạn có thể thực hiện di chuyển bổ sung một chỉ mục duy nhất cho ba cột để điều này được thực thi ở cấp DB:

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

Tui bỏ lỡ điều gì vậy? Không phải khối mã thứ hai ở đây chỉ xóa toàn bộ bảng ngoại trừ các id được tìm thấy trong khối mã đầu tiên?
Elle Mundy

Đó là những gì OP đang tìm kiếm, xóa tất cả các bản sao - phương pháp đầu tiên giúp bạn tất cả các phi giá trị nhân bản
dLobatog

4

Để chạy nó khi di chuyển, tôi đã làm như sau (dựa trên câu trả lời ở trên của @ 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
Bạn có thể thêm model.unscopedvào các truy vấn để tránh bị mắc kẹt trong phạm vi mặc định không có trong truy vấn nhóm hiện tại.
ErvalhouS

0

Dựa trên câu trả lời của @ aditya-sanghi , với một cách hiệu quả hơn để tìm các bản sao bằng SQL.

Thêm điều này vào của bạn ApplicationRecordđể có thể loại bỏ trùng lặp bất kỳ mô hình nào:

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

Sau đó, bạn có thể gọi destroy_duplicates_byđể hủy tất cả các bản ghi (ngoại trừ bản ghi đầu tiên) có cùng giá trị cho các cột đã cho. Ví dụ:

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

-3

Bạn có thể thử truy vấn sql này, để xóa tất cả các bản ghi trùng lặp trừ bản ghi mới nhất

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

Điều này sẽ loại bỏ tất cả.
monteirobrena
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.