はじめに
大量のデータにアクセスして処理を行う場合、
- メモリ不足で処理が中断されないよう、少しずつメモリに展開したい
- 途中で処理が中断されても問題ないよう、一定件数ごとにコミットをしたい
と考えることがあると思います。
そんなときに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).
そこで、今回は自分が指定した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の切れ目で同じ値が続く場合、処理されないレコードが存在してしまうためです。