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

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

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 

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

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

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

检查文档以获取详细信

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

 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. 每次在batch_size之间创建一个介于0Foo.count之间的偏移量数组。 例如,如果Foo.count == 10500你会得到: [0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000] Foo.count == 10500 [0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]
  2. 循环遍历这些数字并在SQL查询中将它们用作OFFSET,确保按id排序,并限制为batch_size
  3. 更新大多数“index”大于offset batch_size记录。

这基本上是在生成的SQL中执行您所说的希望的手动方式。 太糟糕了,它不能仅仅通过标准库方法以这种方式完成……虽然我确信你可以创建自己的一个。

这是迟了2年,但这里的答案是a)对于大型数据集来说非常慢; b)忽略内置轨道function( 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 

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 

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

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

希望它有用! 🙂

还没有机会测试这个,但你可以使用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