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


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.