【问题标题】:How can I run updates in batches in Rails 3/4?如何在 Rails 3/4 中批量运行更新?
【发布时间】:2014-04-23 18:48:48
【问题描述】:

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

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

这行得通,但它显然会运行一个选择,然后是更新,而不是基于我的“位置”条件的单个更新。有什么办法可以清理这个问题,这样 select 和 update 就不必是单独的查询了?

【问题讨论】:

  • 但是您必须分批进行更新吗?你的 where 子句产生了多少行?
  • where 子句将检索数十万条记录,这就是我使用 find_in_batches 一次处理 1000 条更新的原因。
  • 和玛丽安一样的问题,我不明白你的推理。如果您执行 Foo.where().update_all 它不会将记录加载到 Rails,只需执行数据库更新查询。
  • @MichaelSzyndel,我正在分批执行更新,以避免在更新数十万条记录时锁定我的表。

标签: sql ruby-on-rails


【解决方案1】:

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

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

查看documentation了解详情。

【讨论】:

  • 使用这个 gem in_batches 在 rails 3/4 中使用这个方便的方法。
【解决方案2】:

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

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_size。例如,如果Foo.count == 10500 你会得到:[0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]
  2. 遍历这些数字并将它们用作 SQL 查询中的偏移量,确保按id 排序,并限制为batch_size
  3. 最多更新“索引”大于offsetbatch_size记录。

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

【讨论】:

  • 对我来说,这并没有完全按照指定的方式工作(在第一次运行时没有更新所有记录)所以我包裹在一个 while 语句中,该语句一直处理到完成:query = -> { Foo.where(conditions).count } ; while (count = query.call) > 0 ; #run above ; end跨度>
  • 很遗憾,这不起作用,因为 Rails 不支持 offsetupdate_all,请参见此处:github.com/rails/rails/issues/10849
  • 这仅在您遍历整个表时才有效。如果您尝试更新包含 50 亿条记录的表的一亿行,这将不起作用。
【解决方案3】:

这已经晚了 2 年,但这里的答案是 a) 对于大型数据集非常慢 b) 忽略内置的 rails 功能 (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

【讨论】:

  • find_each 内部不使用偏移量?
  • @Naremy 没有。 find_each 将运行查询并添加where id > X order by id asc limit 1000。当它迭代批处理时,它会不断将 id 更新为最新的 id,然后发出新的调用。这样它就永远不会使用偏移量(因为它需要在完成任何偏移操作之前加载所有数据,所以它会变得越来越慢)
  • 不需要ids = array_of_foo.collect &:id。您可以将对象数组传递到 where 子句中,如下所示:Foo.where(id: array_of_foo).update_all(x: y)
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    尚未有机会对此进行测试,但您或许可以使用 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
    

    【讨论】:

      猜你喜欢
      • 2014-07-15
      • 2013-06-09
      • 1970-01-01
      • 2018-11-05
      • 2016-07-26
      • 1970-01-01
      • 2018-04-01
      • 2020-07-10
      • 2013-03-24
      相关资源
      最近更新 更多