はじめに
はじめまして!ラクマの小田です。
大量のデータにアクセスして処理を行う場合、
- メモリ不足で処理が中断されないよう、少しずつメモリに展開したい
- 途中で処理が中断されても問題ないよう、一定件数ごとにコミットをしたい
と考えることがあると思います。
そんなときにRailsで役に立つのがfind_eachやfind_in_batchesですね。
ただしこの2つのメソッドには弱点があり、id(主キー/primary key)の昇順(ASC)でしかデータを扱うことができません。
※Rails v6.1.0時点での情報です。
# NOTE: Order can be ascending (:asc) or descending (:desc). It is automatically set to # ascending on the primary key ("id ASC"). # This also means that this method only works when the primary key is # orderable (e.g. an integer or string).
rails/batches.rb at 914caca2d31bd753f47f9168f2a375921d9e91cc · rails/rails · GitHub
そこで、今回は自分が指定したorderで大量データを扱いたい場合の解決方法を紹介いたします。
解決方法
前提
Rankingモデルのrank(順位)順で処理をしたい、とします。
実装
@rank_offset = 0 @batch_size = 1000 def find_rankings_in_batches loop do rankings = Ranking.where('rank > ?', @rank_offset).order(rank: :asc).limit(@batch_size) break if rankings.blank? rankings.each do |ranking| yield(ranking) end @rank_offset = rankings.last.rank end end find_rankings_in_batches do |ranking| # ここに処理を書く end
発行されたSQL
1ループ目
SELECT `rankings`.* FROM `rankings` WHERE (rank > 0) ORDER BY `rankings`.`rank` ASC LIMIT 1000
2ループ目
SELECT `rankings`.* FROM `rankings` WHERE (rank > 1000) ORDER BY `rankings`.`rank` ASC LIMIT 1000
rankの昇順かつbatch_size単位で取得できていることがわかります。
注意点
基本的には、orderに指定するカラムはUNIQUE制約が設定されているものにしてください。 batchの切れ目で同じ値が続く場合、処理されないレコードが存在してしまうためです。