N+1問題とは
N+1問題とは必要以上にクエリを発行することによりパフォーマンスを低下させてしまうことです。
例えばUser : Post = 1 : 多
の場合を考えます。
Railsで実行すると以下のようになります。
[16] pry(main)> users = User.all
User Load (1.3ms) SELECT "users".* FROM "users"
[17] pry(main)> users.each do |user|
[17] pry(main)* puts user.posts
[17] pry(main)* end
#<Post:0x0000557716805150>
#<Post:0x0000557716804fe8>
Post Load (1.8ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1 [["user_id", 2]]
Post Load (1.0ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1 [["user_id", 4]]
#<Post:0x00005577167c8868>
Post Load (1.6ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1 [["user_id", 5]]
Post Load (1.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = $1 [["user_id", 6]]
Userに結びつくPostが都度フェッチされていることがわかります。
N+1問題の解消
Railsでの解決方法
Railsでは、includes
メソッドを使えば解消することができます。
[19] pry(main)> users = User.all.includes(:posts)
=> User Load (1.2ms) SELECT "users".* FROM "users"
Post Load (9.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN ($1, $2, $3, $4, $5) [["user_id", 1], ["user_id", 2], ["user_id", 4], ["user_id", 5], ["user_id", 6]]
[20] pry(main)> users.each do |user|
[20] pry(main)* puts user.posts
[20] pry(main)* end
#<Post:0x00005577165626c8>
#<Post:0x0000557716562560>
#<Post:0x0000557716562830>
これで解消はするのですが、実際にこれは何をしているのでしょうか?
RailsでのN+1問題の解消方法は分かりましたが、N+1問題自体の解消方法は理解できていないように思います。
リレーションを呼ぶメソッドの定義箇所
N+1問題が起きている場合でも起きていない場合でもuser.posts
のようにリレーションを使って呼び出しているので、それが定義されている場所を見ていきます。
-
https://github.com/rails/rails/blob/3272335f8f4739629283cdeff7c3db723f34a3f5/activerecord/lib/active_record/associations/association.rb#L72
上記のコードを確認すると、loadedの値がfalse
の場合は、データフェッチ(クエリの発行)が行われることが分かります。
確認してみると下記のようになりました。 -
includesしない場合
[1] pry(main)> users = User.all
=> User Load (4.8ms) SELECT "users".* FROM "users"
[2] pry(main)> users[0].posts.loaded?
=> false
- includesする場合
[1] pry(main)> users = User.all.includes(:posts)
=> User Load (2.3ms) SELECT "users".* FROM "users"
Post Load (6.3ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN ($1, $2, $3, $4, $5) [["user_id", 1], ["user_id", 2], ["user_id", 4], ["user_id", 5], ["user_id", 6]]
[2] pry(main)> users[0].posts.loaded?
=> true
結論
先に1回クエリを発行し、キャッシュをすることによってN+1問題を解消している。
それ以外にもSQLのJOINを使っても解消できるようなので、試しにやってみたいと思います。
終わりに
ActiveRecordに頼りすぎていてSQLの勉強をまともにしていなかったので、そのつけが今来ているように感じてます。
Railsガイド推奨のSQL tutorialを試しにやってみようと思います。