web-dev-qa-db-ja.com

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

それは機能しますが、明らかに、「場所」条件に基づく単一の更新ではなく、選択に続いて更新が実行されます。選択と更新を別々のクエリにする必要がないように、これをクリーンアップする方法はありますか?

31
MothOnMars

Rails 5では、新しい便利なメソッドActiveRecord::Relation#in_batches この問題を解決するために:

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

詳細については documentation を確認してください。

58
dlackty

私もこれを行う簡単な方法がないことに驚いています...しかし、私はこのアプローチを思いつきました:

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クエリでOFFSETとして使用します。必ずidで並べ替え、batch_sizeに限定してください。
  3. 「インデックス」がoffsetより大きい最大batch_sizeレコードを更新します。

これは基本的に、生成されたSQLで望んでいたと言ったことを手動で実行する方法です。残念なことに、標準のライブラリメソッドでは、この方法で既にこれを行うことはできません。ただし、独自のライブラリメソッドを作成できると確信しています。

11
pdobb

これは2年遅れですが、ここでの答えは、a)大規模なデータセットでは非常に遅く、b)組み込みのRails機能( http://api.rubyonrails.org /classes/ActiveRecord/Batches.html )。

オフセット値が増加すると、DBサーバーに応じて、ブロックに到達するまでシーケンススキャンが実行され、処理のためにデータがフェッチされます。オフセットが数百万に達すると、これは非常に遅くなります

「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
6
Faisal

pdobbの答えは正しい軌道に乗っていますが、Rails 3.2.21ではActiveRecordがUPSET呼び出しで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
3
Charlie Tran

Update_allをバッチで呼び出す小さなメソッドを作成しました。

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

それが役に立つことを願っています! :)

0
Varun Natraaj

これをテストする機会はまだありませんが、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
0
Paul Alexander