初学者向けに書きました。
これはまだ自分が初学者だった頃のメモです。もはや何番煎じかわからない内容ですが、何か記事を書きたかったので書きました。
間違い等ありましたら申し訳ございません。ボコボコにコメントしてやってください。すぐ直します。
伝えたいこと
- bulletを使用していても検知されないN+1問題がある。
- bulletによる警告が出ていないからといって満足して終わらないように。ちゃんとチェックしよう。
- N+1問題が解決していても本当にそれが最適な処理になっているのか吟味しよう。
bullet とは
- N+1問題を検知するgemです。詳しくは公式で。
問題①:検知できないN+1問題
例えば、以下のコードはN+1問題を発生させますが、この問題はbulletでは検知できません。
class Post < ApplicationRecord
has_many :comments
end
@posts = Post.preload(:comments).all
@posts.each do |post|
post.comments.count
end
これはcomments自体はキャッシュしているので、post.comments
とした時にSQLが実行されることはありません。
しかし、このcount
メソッド(ActiveRecord::Associations::CollectionProxy)は、実行の度にSQLを必ず実行するためN+1問題が発生します。仕様はここ。
このcount
メソッドような、モデルのアソシエーションの自動ロードの仕様に関わらず、そのメソッド自身の仕様によるクエリによって発生するN+1問題はbulletでは検知できません。ActiveRecord::FinderMethodsのfindメソッド等を使用した場合でも同様の問題が発生します。
なので、bulletの警告が無くなったとしてもちゃんとログを確認するようにしましょう。
問題②:アソシエーションの「キャッシュの方法」については言及しない
例えば、以下のコードの時、N+1問題は発生せず問題無いように見えます。
class Post < ApplicationRecord
belongs_to :user
end
@posts = Post.preload(:user).all
@posts.each do |post|
p post.user
end
ところがこれは、完璧ではありません。
基本的にこのような場合は、preload
ではなくeager_load
をすべきです。例外は除きます。
この例のような処理だけではどっちでキャッシュしても大差ないですが、大規模なデータを処理する場合は処理に係るコストがまったく異なります。
詳しい説明はこの記事の本旨と異なるので行いませんが、簡単に言うと以下の2点です。
- 外部結合と内部結合の違い
- 実行クエリ数の違い
こういった問題はbulletでは警告を受けません。あくまでeager loading
に対して警告をするだけです。
警告内容は、`includes@メソッドを使用するかしないかで抽象的に指摘しているに過ぎません。
なので、eager_load
とpreload
の仕様を理解することを初めに、ログの実行時間や全体の処理内容を考慮しつつ最適化を行うよう気をつけてください。bulletによる警告が無くなったからといって油断しては行けません。
まとめ
- bulletを使用していても検知されないN+1問題がある。
- モデルのアソシエーションの自動ロードの仕様に関わらず、そのメソッド自身の仕様によるクエリによって発生するN+1問題はbulletでは検知できない。
-
eager loading
の方法については言及されない。
- ActiveRecordに係るメソッドの仕様は正確に捉えられるようにしよう。