🎃

【Rails】index_byでN+1問題を解決する

2022/05/27に公開

はじめに

「Rails N+1対策」で検索をすると大体の記事が「includes付けとけば大丈夫!」と書いてある。
でも実はN+1を回避する方法はincludesだけではない。

今回私はRailsのN+1対策としてindex_byメソッドを使った方法を紹介したい。
当然どれくらい早くなるかも計測した。

概要

OS: macOS
ruby: 3.0.2
rails: 6.1.4.1
postgres: 13.4

class MessageRoom < ApplicationRecord
  has_many :chat_messages
end
# 11万件保存済み
class ChatMessage < ApplicationRecord
  belongs_to :message_room
end
# message_roomに紐付けて11万件保存済み
# つまり1つのmessage_roomは1つのchat_messageを持っている状態

上記の条件で、N+1のままの時, eager_loadを使った時, preloadを使った時, index_byを使った時でどれくらい処理速度が変わるのかを調べていく。

N+1のままの時

# 下記のクラスメソッドを実行する
# 一応3回ずつ計測する
def self.try_n_plus_one
  time = Time.now
  chat_messages = ChatMessage.all
  chat_messages.each do |_cm|
    p _cm
    p _cm.message_room
  end
  p (Time.now - time)
end

1回目: 60.057107
2回目: 60.536094
3回目: 63.924306

だいたい1分くらい!!!!

eager_loadを使った時

def self.try_eager_load
  time = Time.now
  chat_messages = ChatMessage.all.eager_load(:message_room)
  chat_messages.each do |_cm|
    p _cm
    p _cm.message_room
  end
  p (Time.now - time)
end

1回目: 22.91626
2回目: 24.420415
3回目: 24.087161

だいたい24秒くらい!!!!

preloadを使った時

def self.try_preload
  time = Time.now
  chat_messages = ChatMessage.all.preload(:message_room)
  chat_messages.each do |_cm|
    p _cm
    p _cm.message_room
  end
  p (Time.now - time)
end

1回目: 22.215129
2回目: 22.933572
3回目: 22.516875

だいたい22秒くらい!!!!
eager_loadよりも若干早いぞ!

index_byを使った時

def self.try_index_by
  time = Time.now
  chat_messages = ChatMessage.all
  message_rooms = MessageRoom.all.index_by(&:id)
  chat_messages.each do |_cm|
    p _cm
    p message_rooms[_cm.message_room_id]
  end
  p (Time.now - time)
end

1回目: 20.539189
2回目: 20.357468
3回目: 20.080073

だいたい20秒くらい!!!!
eager_loadよりも、preloadよりも早い!!!!!!!!

考察

N+1問題の対策を調べるとeager_loadやpreloadは良く出てくるが、index_byを使った対策はあまり出てこない。しかしながら、今回index_byを使ってN+1を回避するやり方も十分早いことが証明できた。

index_byを使うとeager_load, preloadを使った時と比べてコードの可読性が落ちてしまう、結合したテーブルのカラムで絞り込めないなど難点はあるものの、メモリ消費を抑えつつ、N+1の解消もできて、eager_loadやpreloadよりも早いという大きなメリットがある。

まとめ

つまり何が言いたいかというと、
N+1問題を解決する際は、単にincludes付けとけば良いというわけではなく、
DBに入っているレコード件数, メモリ, コードの可読性など様々な面を考慮していく必要があるということ。

Discussion