如何在Rails 3/4中批量运行更新?

时间:2014-04-23 18:48:49

标签: sql ruby-on-rails

我需要批量更新数千条记录,我想分批处理更新。首先,我试过了:

Foo.where(bar: 'bar').find_in_batches.update_all(bar: 'baz')

...我希望能生成如下的SQL:

"UPDATE foo SET bar = 'baz' where bar='bar' AND id > (whatever id is passed in by find_in_batches)"

这不起作用,因为find_in_batches返回一个数组,而update_all需要一个ActiveRecord关系。

这是我接下来尝试的:

Foo.where(bar: 'bar').select('id').find_in_batches do |foos|
  ids = foos.map(&:id)
  Foo.where(id: ids).update_all(bar: 'baz')
end

这样可行,但它显然会运行一个选择后跟更新,而不是基于我的'其中'条件。有没有办法清理它,以便选择和更新不必是单独的查询?

6 个答案:

答案 0 :(得分:49)

在Rails 5中,有一个新的方便方法ActiveRecord::Relation#in_batches来解决这个问题:

Foo.in_batches.update_all(bar: 'baz')

查看documentation了解详情。

答案 1 :(得分:10)

我也很惊讶,没有更简单的方法可以做到这一点......但我确实提出了这种方法:

batch_size = 1000
0.step(Foo.count, batch_size).each do |offset|
  Foo.where(bar: 'bar').order(:id)
                       .offset(offset)
                       .limit(batch_size)
                       .update_all(bar: 'baz')
end

基本上这将:

  1. 每次0Foo.count之间创建batch_sizeFoo.count == 10500之间的偏移数组。例如,如果您[0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]获得id
  2. 循环显示这些数字并在SQL查询中将它们用作OFFSET,确保按batch_size排序,并限制为batch_size
  3. 最多更新“索引”大于offset的{​​{1}}条记录。
  4. 这基本上是在生成的SQL中执行您所说的希望的手动方式。太糟糕了,它不能仅仅通过标准库方法以这种方式完成......虽然我确信你可以创建自己的方法。

答案 2 :(得分:5)

这是迟了2年,但这里的答案是:a)对于大型数据集来说非常慢; b)忽略内置轨道功能(http://api.rubyonrails.org/classes/ActiveRecord/Batches.html)。

当偏移值增加时,根据您的数据库服务器,它将执行序列扫描,直到它到达您的块,然后提取数据进行处理。当您的偏移量达到数百万时,这将非常慢。

使用“find_each”迭代器方法:

Foo.where(a: b).find_each do |bar|
   bar.x = y
   bar.save
end

这具有每次保存运行模型回调的额外好处。如果您不关心回调,请尝试:

Foo.where(a: b).find_in_batches do |array_of_foo|
  ids = array_of_foo.collect &:id
  Foo.where(id: ids).update_all(x: y)
end

答案 3 :(得分:3)

pdobb的答案是正确的,但在Rails 3.2.21中对我没有用,因为ActiveRecord没有用UPDATE调用解析OFFSET这个问题:

https://github.com/rails/rails/issues/10849

我相应地修改了代码,它在我的Postgres表上同时设置默认值时工作正常:

batch_size = 1000
0.step(Foo.count, batch_size).each do |offset|
  Foo.where('id > ? AND id <= ?', offset, offset + batch_size).
      order(:id).
      update_all(foo: 'bar')
end

答案 4 :(得分:0)

我已经编写了一个小方法来批量调用update_all:

https://gist.github.com/VarunNatraaj/420c638d544be59eef85

希望它有用! :)

答案 5 :(得分:0)

Haven还没有机会对此进行测试,但您可以使用ARel和子查询。

Foo.where(bar: 'bar').select('id').find_in_batches do |foos|
  Foo.where( Foo.arel_table[ :id ].in( foos.to_arel ) ).update_all(bar: 'baz')
end