Я использую Rails 4.0.0 и Ruby 2.0.0. Моя модель Post (как в сообщениях в блоге) связана с пользователем с комбинацией user_name, first_name, last_name. Я хотел бы перенести данные, чтобы сообщения связывались с пользователями по внешнему ключу, который является идентификатором пользователя.

У меня около 11 миллионов записей в таблице posts.

Я запускаю приведенный ниже код для переноса данных с помощью задачи rake на сервере Linux. Тем не менее, моя задача продолжает получать "Убита" сервером, предположительно из-за задачи с граблями, в частности из-за приведенного ниже кода, потребляющего слишком много памяти.

Я обнаружил, что уменьшение batch_size до 20 и увеличение sleep(10) до sleep(60) позволяет задаче работать дольше, обновляя больше записей в целом, не будучи убитыми, но занимает значительно больше времени.

Как мне оптимизировать этот код для увеличения скорости и использования памяти?

Post.where(user_id: nil).find_in_batches(batch_size: 1000) do |posts|
  puts "*** Updating batch beginning with post #{posts.first.id}..."
  sleep(10) # Hopefully, saving some memory usage.
  posts.each do |post|
    begin
      user = User.find_by(user_name: post.user_name, first_name: post.first_name, last_name: post.last_name)
      post.update(user_id: user.id)
    rescue NoMethodError => error # user could be nil, so user.id will raise a NoMethodError
      puts "No user found."
    end
  end
  puts "*** Finished batch."
end
9
sealocal 14 Апр 2016 в 23:48

5 ответов

Лучший ответ

Выполняйте всю работу с базой данных, что НАМНОГО быстрее, чем перемещение данных туда и обратно.

Это можно сделать с помощью ActiveRecord. Конечно, ПОЖАЛУЙСТА, проверьте это, прежде чем использовать важные данные.

Post
  .where(user_id: nil)
  .joins("inner join users on posts.user_name = users.user_name")
  .update_all("posts.user_id = users.id")

Кроме того, если сообщения имеют индекс на user_id, а пользователи имеют индекс на user_name, то это поможет быстрее выполнить этот конкретный запрос.

10
z5h 19 Апр 2016 в 18:09

Проверьте метод #uncached на моделях AR. В основном, для оптимизации запросов, AR будет кэшировать много данных запроса, как это делает #find_in_batches, но это препятствие для больших сценариев обработки, подобных этому.

Post.uncached do
  # perform all your heavy query magic here
end

В конечном итоге, если это не сработает, рассмотрите возможность использования драгоценного камня mysql2, чтобы избежать накладных расходов AR, если вы не зависите от каких-либо обратных вызовов / бизнес-логики в обновлении.

2
Tim H 14 Апр 2016 в 21:11

Если соединение возможно, я бы воспользовался подходом из z5h. В противном случае вы можете добавить индекс к модели пользователя (возможно, в отдельной миграции), а также пропустить проверки, обратные вызовы и прочее при обновлении каждого сообщения:

add_index :users, [:user_name, :first_name, :last_name] # Speed up search queries
Post.where(user_id: nil).find_each do |post|
  if user = User.find_by(user_name:  post.user_name,
                         first_name: post.first_name,
                         last_name:  post.last_name)
    post.update_columns(user_id: user.id) # ...to skip validations and callbacks.
  end
end

Обратите внимание, что find_each эквивалентно find_in_batches + итерации по каждому сообщению, но, возможно, не быстрее (см. Руководства по Rails на Active Record Query Interface)

Удачи!

2
Community 23 Май 2017 в 10:28

Объединив другие ответы, я смог объединить таблицы и обновить несколько столбцов партиями по 1000 строк, со снижением скорости и без остановки моего процесса сервером.

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

Post.uncached do
  Post.where(user_id: nil, organization_id: nil).find_each do |posts|
    puts "** Updating batch beginning with post #{posts.first.id}..."

    # Update 1000 records at once
    posts.map!(&:id) # posts is an array, not a relation
    Post.where(id: posts).
      joins("INNER JOIN users ON (posts.user_name = users.user_name)").
      joins("INNER JOIN organizations ON (organizations.id = users.organization_id)").
      update_all("posts.user_id = users.id, posts.organization_id = organizations.id")

    puts "** Finished batch."
  end
end
1
sealocal 19 Апр 2016 в 18:04

Добавить новый временный логический атрибут обновлен

Post.where(updated: false).find_in_batches(batch_size: 1000) do |posts|
  ActiveRecord::Base.transaction do
    puts "*** Updating batch beginning with post #{posts.first.id}..."
    posts.each do |post|
      user = User.find_by(user_name: post.user_name, first_name: post.first_name, last_name: post.last_name)
      if user
        post.update_columns(user_id: user.id, updated: true)
      else
        post.update_columns(updated: true)
      end
    end
    puts "*** Finished batch."
  end
end
0
Artem Biserov 21 Апр 2016 в 08:07