Видалити повторювані записи на основі кількох стовпців?


77

Я використовую Heroku для розміщення своєї програми Ruby on Rails, і з тієї чи іншої причини у мене можуть бути кілька повторюваних рядків.

Чи є спосіб видалити дублікати записів на основі 2 або більше критеріїв, але зберегти лише 1 запис цієї колекції дублікатів?

У моєму випадку використання у моїй базі даних є взаємозв'язок "Марка та модель" для автомобілів.

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

Я хотів би видалити всі записи моделі, що мають однакове ім’я, рік та трим, але зберігати 1 із цих записів (мається на увазі, запис мені потрібен, але лише один раз). Я використовую консоль Heroku, щоб легко виконувати деякі активні запити записів.

Будь-які пропозиції?

Відповіді:


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
  • Знайти все
  • Згрупуйте їх за клавішами, які вам потрібні для унікальності
  • Цикл на згрупованих значеннях хешу моделі
  • видаліть перше значення, оскільки ви хочете зберегти одну копію
  • видалити решту

Це в моделі Model?
Choylton B. Higginbottom

@meetalexjohnson це має бути в будь-якій моделі активного запису, яка у вас є.
Aditya Sanghi

3
Цікавий метод, але трохи неефективний з великим набором записів. Цікаво, чи є спосіб зробити це за допомогою активної самозапису.
Ziyan Junaideen

6
Працює, але надзвичайно неефективно для великих наборів даних. Набагато швидший спосіб - використовувати цей алгоритм, щоб спочатку зібрати ідентифікатори в масиві, а потім за допомогою одного оператора DELETE FROM sql видалити масив ідентифікаторів.
Ерік Алфорд,

Дуже корисний метод для багатьох звичайних ситуацій, дякую Адіті.
Пол Уотсон,

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 > 

Ідентифікатори електронної пошти повторюються, тому наша мета - видалити всі повторювані ідентифікатори електронної пошти з таблиці користувачів.

Крок 1:

Щоб отримати всі різні ідентифікатори записів електронної пошти.

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

Крок 2:

Видалити повторювані ідентифікатори з таблиці користувачів із різними ідентифікаторами записів електронної пошти.

Тепер масив ids містить такі ідентифікатори.

[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
Це небезпечно: запустіть його знову, коли у вас немає дупків, буде видалено більше, ніж ви хочете, оскільки логіка полягає в тому, що "видалити все, крім D". Я думаю, що краща логіка - "видалити все в D", де D - це список ідентифікаторів повторених рядків.
Олексій

16

Подібно до відповіді @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' 

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

Мені чогось не вистачає? Чи не буде другий блок коду тут просто очистити всю таблицю, за винятком ідентифікаторів, знайдених у першому блоці коду?
Elle Mundy

Це те, що шукав ОП, видаляючи всі дублікати - перший метод дає вам усі не
дурні

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.