N+1問題って聞いたことはありますか?
N+1問題はかなり怖い問題で、ぼくはポートフォリオ作成のときに、N+1問題に直面しました。
そのときに、調べた内容をぼくの理解の範囲で、アウトプットしていきます。
#N+1問題とは
そもそもN+1問題とはどんな問題のことをいうのでしょうか?
データベースからデータベースを取り出すときに、大量のSQLが発行されて、動作が重くなる問題
のことです。
たとえば、usersテーブルとpostsテーブルで、下記のようなアソシエーションがあったとします。
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
で、投稿の一覧を表示していきます。
Postモデルから全データ取得して、それをeach文で回していきます。
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
<%= @posts.each do |post| %>
<%= post.content %>
<%= post.user.name %>
<% end %>
これで、このビューにアクセスし、ログを確認してみましょう。
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
:
このようにPostの数だけ、post.userを探すため、SQLが大量に発行されます。
勉強段階の開発環境等では、データ数もそこまで多くないので、そこまで問題になりませんが、実際に運用していくとなると、投稿数も増えていくことが想定されます。
データ数が、1000になると考えると、毎回1000件のデータ全てにSQLを発行していたら、遅くなるのは明白ですね。
なにも対策をとらないとデータ量が増えるにつれて、動作が遅くなる問題が、N+1問題です。
#対策
では、N+1問題がどんなものか分かったところで、対策方法をみていきましょう!
includesメソッド
を使っていきます。
includesメソッド
は、`アソシエーションの関連付けを事前に取得してくれます。
事前に取得ってよくわかりませんよね。
例をあげて考えてみましょう。
基本構文はこうです。
# 関連名はアソシエーションの関連先
モデル名.inculudes(:関連名)
class PostsController < ApplicationController
def index
# .allは省略できます
@posts = Post.includes(:user)
end
end
これで、ログを見てみましょう!
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?) [["id", 1], ["id", 2], ["id", 4]]
このように、post.userに関するSQLは1行で完了しています。
つまりどういうことかというと、
Post.allの場合
は、Postのデータを全て取得して、ビューのeach文で表示するときに、postのuser_idとどのユーザーが一致するか全てのユーザーの中から検証していました。
しかし、Post.includes(:user)の場合
は、postsテーブルからデータを取得するときに、関連するusersテーブルのデータも取得しているので、each文で表示する時に、その都度SQLを発行する必要はなくなったのです。
#まとめ
N+1問題の対策としては、includesメソッド
を使う!
ポートフォリオ作成時に、N+1問題のことはなにも考えずにコーディングしていて、動作確認のときに「あれ?なんか重たいな」となったのが気づいたきっかけでした。
早いうちにN+1問題に触れられてよかったです。
ただ今回は自分なりに調べてみての結論で、他の記事の中では、includesメソッド
を使う方法が推奨されていなかったりした記事がありました。
あとしょうがないのかもしれませんが、includesメソッド
を使うことで、SQLの発行は改善されたのですが、コードの可読性がかなり下がるなと思いました。
まだまだ理解しきれていないことも多いので、これからもっとデータベースの知識を深めていきたいと思います!