#はじめに
本記事ではRailsのアプリケーションを作成した際にN+1問題に直面したので、N+1問題の概要とその解決方法を紹介します。
#N+1問題とは?
N+1問題とはループ処理を行う際にSQLの発行がたくさん行われてしまい、サイトのパフォーマンスが下がってしまう現象です。
SQLの発行はサーバーの負担が大きいので、なるべく少ない回数にすることが望まれます。
N+1問題の例
わかりやすくするために、以下のような記事を投稿できるアプリケーションを例にします。
usersテーブル
id | name |
---|---|
1 | 佐藤 |
2 | 鈴木 |
3 | 高橋 |
4 | 田中 |
5 | 伊藤 |
postsテーブル
id | user_id | text |
---|---|---|
1 | 3 | こんにちは |
2 | 4 | おはよう |
3 | 1 | おやすみなさい |
4 | 5 | ありがとう |
5 | 1 | さよなら |
6 | 2 | もしもし |
7 | 4 | 楽しい |
8 | 1 | 悲しい |
一人のユーザーからたくさんの投稿があるため、userモデルとpostモデルは以下のようになります。
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
class Post < ApplicationRecord
belongs_to :user
end
またposts_controllerは以下のようにします。
class PostsController < ApplicationController
def index
@posts = Post.all
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
private
def post_params
params.require(:post).permit(:text)
end
end
viewは全ての投稿の投稿者名と投稿内容を表示するようにします。
<% @post.each do |post| %>
<p><%= post.user.name %></p>
<p><%= post.text %></p>
<% end %>
このコードの実行ログは以下のようになります。
実行ログ
Post Load (1.1ms) SELECT `posts`.* FROM `posts`
User Load (1.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 LIMIT 1
User Load (0.2ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 4 LIMIT 1
User Load (3.1ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
User Load (1.2ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 5 LIMIT 1
User Load (3.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 4 LIMIT 1
User Load (0.7ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
はじめにpostsテーブルに1回SQLが走り、その後usersテーブルに記事数である8回SQLが走っています。
今回は記事数がそれほど多くないのでサイトパフォーマンスはあまり下がりませんが、記事数が大量になっていくとサイトパフォーマンスが大幅に低下して、ユーザーが使いにくいアプリケーションになってしまいます。
#N+1問題の解決方法
今回はincludesメソッドを使用して解決します。
方法は簡単で、controllerのindexアクションにincludesメソッドを追加するだけです。
class PostsController < ApplicationController
def index
#indexアクションにincludesメソッドを追加
@posts = Post.includes(:user)
end
def new
@post = Post.new
end
def create
Post.create(post_params)
end
private
def post_params
params.require(:post).permit(:text)
end
end
このようにすると、実行ログは以下のようになります。
Post Load (0.3ms) SELECT `posts`.* FROM `posts`
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (5, 1, 3, 4, 2)
先ほど9回SQLを発行していましたが、今回は2回で完了しました。
これはcontroller側でincludesメソッドを使うことで、すでにpostsテーブルのレコードに関連するusersテーブルのレコードが取得されているからです。
#まとめ
- n+1問題はSQLが大量に発行されてしまい、サイトのパフォーマンスが下がってしまう事である。
- controller側でincludesメソッドを使うことで解消できる
アプリケーションではレスポンス速度を少しでも短くすることが大事なので、Railsエンジニアを目指している方はぜひ覚えておきましょう。