Rails を使ってある程度の大きさのレコード群を一括更新する処理の実装方針を考えていた。愚直にイテレートして一件ずつ update していくのは論ずるまでもなくバッドプラクティスであるとして、その他の選択肢の優劣は検討してみないとよくわからないというのが正直なところであったため、ベンチマークをとって調査してみた。この記事はその記録である。
TL;DR
- activerecord-import を選択する積極的な理由はなさそう
update_allで実装できるのであればそうするのが一番
方針
ほぼ裸のテーブルを作成し、n件のレコードを一括更新する。nは任意の自然数でいいが、今回は10,000件と100,000件でそれぞれ調査してみた。
比較対象は以下の5つ。ただし素の .update については最初から期待しておらず、ほとんど参考記録としての扱いとなる。
- activerecord に生の SQL を実行させる
- activerecord の
.updateで更新する - activerecord の
.update_allで更新する - activerecord の
.upsert_allで更新する - activerecord-import の
.importで:on_duplicate_key_updateオプションを使って更新する
作ったもの
sato11/benchmark-bulk-update-rb (https://github.com/sato11/benchmark-bulk-update-rb)
結果
https://github.com/sato11/benchmark-bulk-update-rb/blob/master/README.md
解釈と所感
まず目に付くのは、 .import と .upsert_all はレコード数におおむね比例して処理時間と消費メモリが増加しているが、 .upsert_all の方が20%ほどパフォーマンスに優れている点である。
.upsert_all は Rails 6 で鳴り物入りで実装された新機能であるわけで、優れた結果を出すのは当然といえば当然といえる。いずれにせよ、これから実装を検討するうえでは、 activerecord-import を選択する積極的な理由は個人的には見出せない結果となった。
他方、生の UPDATE 文を .execute に渡すコードと、 .update_all で同等のクエリを生成するコードは、パフォーマンスのうえで有意な差はほとんどなかった。メモリ消費量と実行速度において一長一短あるように見えつつも、レコードの数に関わらず一定のパフォーマンスが担保されていることもあり、これはほとんど無視できる程度の差異にすぎない。
一行で書けるだけ .update_all に軍配が上がるだろう。.update_all ではカバーできないような複雑なクエリを扱うというのでなければ、 .update_all を使っておくのが安心そうである。