【Rails】index_byでN+1問題を解決する
はじめに
「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