概要
数万件のデータパッチをrakeタスクで行い、当初12時間かかっていた処理が、チューニング後2時間に短縮し、パフォーマンスの重大さを身をもって感じたので備忘録的にまとめたいと思います🏃♀️🏃♀️🏃♀️
前提
- バッチサーバースペック
メモリ20GB CPU 4コア - rails5系
destroy_allは遅い🚶♀️
12時間かかってしまった原因が主にここでした。
当初データ一件ずつに対して複数の関連データをdestroy_all
で行なっていたものを
delete_all
にしたところ7時間もの短縮となりました🏃♀️
関連付けのコールバックも呼ばれるdestroy_all
は大量データを処理する時は要注意です
activerecord-importを利用したBULK INSERT
大量データをinsertする時はBULK INSERTするかと思います。
カラム名を配列で指定し、同じ順序でvalueを格納した配列を含む配列をimportするのが最速らしい🏃♀️
https://github.com/zdennis/activerecord-import#columns-and-arrays
before
users = []
target_users.each do |u|
users << Result.new(user_id: u.id, email: u.email)
end
ForX::UserAppMigrationResult.import users
after
target_users = [[1, 'test@test.com'], [2, 'test@test.com']]
columns = [:user_id, :email]
Result.import columns, target_users
脱N + 1
大量のSQLを発行してしまうやってしまいがちな問題。
今回改めて違いを理解したのでざっくりな理解をメモしておく。
eager_load
left_outer_joinしてassociationをキャッシュする
joins
inner_joinsのみ。associationをキャッシュしない
preload
指定したassociationを複数のクエリに分けて引いてキャッシュする。
include
eager_loadとpreloadをいい感じに使い分けている
※(余談)eager_loadで異なるDBでJOINして詰まった時のメモ。
異なるDBのモデル間でeager_load
を利用したらそんなテーブルないよと怒られました。
User.eager_load(:memos)
#=> Mysql2::Error: Table 'memos' doesn't exist
mysqlではDB名.テーブル名
とDB名を明示してあげれば異なるDB感でもjoinできるので、
eager_load
やjoins
を利用するときもモデルにDB名を明示すればOKでした。
class Memo
self.table_name = "xxx.memos" # xxxはdb名
belongs_to :user
end
マルチプロセス化
rubyで簡単に並列処理を実装できるparallelを利用しました。
おわりに
大量データを処理するバッチを書くのは初めてだったので、パフォーマンスを意識する良いきっかけとなりました!
参照
https://qiita.com/begaborn/items/95e8d6839dad25584bc8
https://qiita.com/nikadon/items/9e6431f5a7b2b8798113#destroy_all-vs-delete_all