【问题标题】:How do I optimize an ActiveRecord find_in_batches query?如何优化 ActiveRecord find_in_batches 查询?
【发布时间】:2016-08-06 15:11:59
【问题描述】:

我正在使用 Rails 4.0.0 和 Ruby 2.0.0。我的Post(如在博客文章中)模型与用户相关联,并结合了用户的用户名、名字、姓氏。我想迁移数据,以便通过外键(即用户 ID)将帖子与用户相关联。

posts 表中有大约 1100 万条记录。

我正在运行以下代码来迁移数据,在 Linux 服务器上使用 rake 任务。但是,我的任务不断被服务器“杀死”,大概是由于 rake 任务,特别是下面的代码,消耗了太多内存。

我发现将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

【问题讨论】:

  • 如果有比 find_in_batches 更好的 AR 方式,那将是一个受欢迎的答案!
  • 您可以使用单个 UPDATE 语句来完成此操作,而不必通过模型层。同样使用sleep 对内存使用的影响为零,它只会减慢它的速度。如果您真的在内存使用方面遇到困难,请在每次迭代后致电GC.start。很确定您可以通过简单的迁移完成所有这些工作。

标签: ruby ruby-on-rails-4 activerecord


【解决方案1】:

在数据库中完成所有工作,这比来回移动数据要快得多。

这可以通过 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 上有一个索引,那么这将有助于这个特定查询更快地运行。

【讨论】:

  • 这将在数据上运行脚本的时间减少了大约一半!非常感谢。就其本身而言,它并没有阻止我的脚本被服务器杀死。我仍然需要使用批量查询。
  • 很高兴它有帮助。如果帖子有一个 user_id 索引,并且 users 有一个 user_name 索引,那将使这个特定的查询更快。不知道你有什么。
【解决方案2】:

查看 AR 模型上的 #uncached 方法。基本上,为了请求优化,AR在做#find_in_batches的时候会缓存很多查询数据,但是对于像这样的大型处理脚本是个障碍。

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

最终,如果这不起作用,请考虑使用 mysql2 gem 来避免 AR 开销,只要您不依赖更新中的任何回调/业务逻辑。

【讨论】:

  • 令人惊讶的是,这帮助我运行我的代码而不会被服务器杀死!但是,它对速度的影响并不明显。
【解决方案3】:

如果可以加入,我会采用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 + 迭代每个帖子,但可能不会更快(请参阅Active Record Query Interface 上的 Rails 指南)

祝你好运!

【讨论】:

  • 谢谢。似乎不是一个坏主意,但我正试图让我的数据库远离通过三列组合查找用户,所以我认为如果可以避免的话最好不要索引。
【解决方案4】:

结合其他答案,我能够以 1000 行为一组,连接表并更新多个列,同时降低了速度,而且我的进程不会被服务器杀死。

这是我发现效果最好的组合方法,尽可能将代码保留在 ActiveRecord API 中。

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

【讨论】:

    【解决方案5】:

    添加新的临时布尔属性更新

    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
    

    【讨论】:

      猜你喜欢
      • 2011-09-02
      • 1970-01-01
      • 2015-05-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多