Удалить повторяющиеся записи на основе нескольких столбцов?

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

Есть ли способ удалить повторяющиеся записи на основе 2 или более критериев, но сохранить только 1 запись этой повторяющейся коллекции?

В моем случае использования у меня есть отношения марки и модели для автомобилей в моей базе данных.

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

Я хотел бы удалить все записи модели с одинаковым именем, годом и отделкой, но сохранить 1 из этих записей (это означает, что мне нужна запись, но только один раз). Я использую консоль Heroku, поэтому я могу легко выполнять некоторые запросы активных записей.

Какие-либо предложения?


person sergserg    schedule 02.01.2013    source источник


Ответы (7)


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
  • Найти все
  • Сгруппируйте их по нужным вам для уникальности ключам
  • Цикл по сгруппированным значениям модели хэша
  • удалите первое значение, потому что вы хотите сохранить одну копию
  • удалить остальные
person Aditya Sanghi    schedule 02.01.2013
comment
Это в модельной модели? - person Choylton B. Higginbottom; 03.07.2015
comment
@meetalexjohnson это должно быть в любой вашей модели ActiveRecord. - person Aditya Sanghi; 04.07.2015
comment
Интересный метод, но немного неэффективный с большим набором записей. Интересно, есть ли способ сделать это с помощью активной записи. - person Ziyan Junaideen; 30.12.2015
comment
Работает, но крайне неэффективно для больших наборов данных. Гораздо быстрее использовать этот алгоритм, чтобы сначала собрать идентификаторы в массив, а затем использовать одну инструкцию DELETE FROM sql для удаления массива идентификаторов. - person Eric Alford; 02.06.2016
comment
Очень полезный метод для многих обычных ситуаций, спасибо Адитья. - person Paul Watson; 24.03.2020

Если данные вашей пользовательской таблицы, как показано ниже

User.all =>
[
    #<User id: 15, name: "a", email: "[email protected]", created_at: "2013-08-06 08:57:09", updated_at: "2013-08-06 08:57:09">, 
    #<User id: 16, name: "a1", email: "[email protected]", created_at: "2013-08-06 08:57:20", updated_at: "2013-08-06 08:57:20">, 
    #<User id: 17, name: "b", email: "[email protected]", created_at: "2013-08-06 08:57:28", updated_at: "2013-08-06 08:57:28">, 
    #<User id: 18, name: "b1", email: "[email protected]", created_at: "2013-08-06 08:57:35", updated_at: "2013-08-06 08:57:35">, 
    #<User id: 19, name: "b11", email: "[email protected]", created_at: "2013-08-06 09:01:30", updated_at: "2013-08-06 09:01:30">, 
    #<User id: 20, name: "b11", email: "[email protected]", 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
person Aravind encore    schedule 06.08.2013
comment
Спасибо, мне помогло!! - person Ryan Rebo; 13.05.2015
comment
Это опасно: повторный запуск, когда у вас нет дубликатов, удалит больше, чем вы хотите, потому что логика заключается в удалении всего, кроме D. Я думаю, что лучшая логика — удалить все в D, где D — это список идентификаторов дублированных строк. . - person Alex; 29.04.2020

Подобно ответу @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' 
person mackshkatz    schedule 12.01.2016

Вы можете попробовать следующее: (на основе предыдущих ответов)

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

чтобы получить все действительные записи. А потом:

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

для удаления ненужных записей. И, конечно же, вы можете выполнить миграцию, которая добавит уникальный индекс для трех столбцов, чтобы это применялось на уровне БД:

add_index :models, [:name, :year, :trim], unique: true
person LuisFelipe22    schedule 06.04.2018
comment
Я что-то упускаю? Разве второй блок кода здесь просто не очистит всю таблицу, кроме идентификаторов, найденных в первом блоке кода? - person Elle Mundy; 20.04.2020
comment
Это то, что искал ОП, удаляя все дубликаты - первый метод дает вам все не дубликаты. - person dLobatog; 29.04.2020

Чтобы запустить его при миграции, я сделал следующее (на основе ответа выше от @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
person Nuno Costa    schedule 22.03.2016
comment
Вы можете добавить model.unscoped к запросам, чтобы избежать попадания в область по умолчанию, отсутствующую в текущем групповом запросе. - person ErvalhouS; 29.10.2018

На основе @aditya-sanghi 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)
person Sunny    schedule 20.05.2020

Вы можете попробовать этот 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);
person mahendra gawas    schedule 01.06.2015
comment
Это удалит все. - person monteirobrena; 07.12.2017