学習中の初学者がこの記事を書いているため、間違えた知識を共有している可能性があります。
発端
N+1問題をincludesメソッドを用いて対処するという方法がなぜN+1問題を解決するのか理解に少し戸惑ったのでもう少し細分化してまとめてみた。
前提
ユーザと投稿が1対多のリレーションになっていて
class Post < ApplicationRecord
belongs_to :user
end
class User < ApplicationRecord
has_many :posts
end
posts/index/html/erbで投稿一覧を表示するためにこういった処理をする。
@posts.each do |post|
puts post.user.name
end
N+1問題とは
N+1問題とは、SQLでデータベースからレコードを取得する際にN件のレコードに対してN回の実行(プラス 最初の1回を合わせてN+1回)を行なってしまうことで実行回数が膨らんでしまう問題である。
N+1問題が発生するケース
まず全てのPostを取得(最初に1回目のクエリ)
SELECT * FROM posts;
取得したPostのuserを1つずつ取得する(N個の投稿に対してN回のクエリ)
SELECT * FROM users WHERE user_id = 1;
SELECT * FROM users WHERE user_id = 2;
SELECT * FROM users WHERE user_id = 3;
..(以下略)
このように「一つの投稿の中のuser_idを見て、それに合致するuserのレコードを取得」を全ての投稿で行ってしまう。
ここで例を出します。
あなたが100人友達をパーティーに招待するとして、招待状を100人に送らないといけないとします。
あなたは1人1人の住所を参照して、家まで走って招待状を渡しに行きます。
「1人目に渡した」
「2人目に渡した」
「3人目に渡した」
...
「100人目に渡した」
この間、いちいち家を往復して100回届ける作業をします。
これがN+1問題のNの部分にあたります。
100人に対して100回届ける作業が発生しました。
(N件に対してN回の実行)
そこであなたは考えます。
もっと一括で楽にできる方法があるのではないかと。
そこで利用するのが郵便局です。
100件の招待状を束ねて郵便局に持っていけば、郵便局が1回で送り届けてくれます!
うおお!!
この場合の郵便局は「なんかいい感じにまとめて1回で処理してくれる存在」ぐらいの認識にします。
(実際は郵便局の配達員たちが届けていますので厳密には違いますがご愛嬌)
N+1問題に対する対処(includesメソッド)
この一括で処理してくれる郵便局の役割を果たすのがincludesメソッドです。
@posts = Post.includes(:user)
以上のように取得すると、以下の順序でユーザーのレコードまでまとめて取得してくれます。
1. まず全てのpostを取得(1回目のクエリ)
ここまではN+1問題が発生するケースと同じです。
SELECT * FROM posts;
2. postに関連するユーザ情報をまとめて取得(2回目のクエリ!)
SELECT * FROM users WHERE users.id IN (1, 2, 3, ...);
これで2回の実行で全ての投稿とそれに関連するユーザーの情報を全て取得してきました。
IN句でuserのidを括っているので、1回で実行出来ています。
500件でも1000件でも結果的に合計2回の実行で取得してくれます。
結局includesって何をしてる?
「一括でまとめて実行してくれるのは分かったけど、なんでそうなってるの?」という納得の出来なさがあったので調べた結果、
includesメソッドを使うと以下の順序で処理が行われていることが分かりました。
@posts = Post.includes(:user)
1 - Post を取得したときに、Rails が 「user_id が x, y, z, ... のuserのデータが必要」 だと自動で判断する
(取得したpostのテーブルの中からuser_idの部分を見て「これらの情報に関連する情報が必要なんだな...」と判断してくれます。)
2 - N回の SELECT ではなく、たった1回の IN 句を使ったクエリに切り替えて選択してくれる
(「これらの情報を取るならIN句でまとめて取得しよう」と判断してくれます。)
以上から、うまいことまとめるべき部分を判断してくれて、IN句でうまいこと括ってくれるメソッドということが分かります。
「通常だとIN句を使ってくれないが、includesだとIN句を使ってまとめてくれるという判断をしてくれる」というのが本質的な部分だと思われます。
終わりに
今回はincludesの中身を細分化して順序よく知ると、N+1問題になぜ対処できるのかの理解が容易になったので共有しました。
もし間違った解釈をしている場合は、お叱りのコメントをいただければと思います。