Railsのfind_eachやfind_in_batchesでorderにid以外を指定したい場合の解決方法

はじめに

はじめまして!ラクマの小田です。

大量のデータにアクセスして処理を行う場合、

  • メモリ不足で処理が中断されないよう、少しずつメモリに展開したい
  • 途中で処理が中断されても問題ないよう、一定件数ごとにコミットをしたい

と考えることがあると思います。

そんなときに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の切れ目で同じ値が続く場合、処理されないレコードが存在してしまうためです。