はじめに
Webアプリを開発していると、「N+1問題」という言葉を耳にすることがあります。
知らないうちに発生していることも多く、アプリが遅くなる原因の一つです。
この記事では、N+1問題がどういうものなのか、なぜ起きるのか、そしてどう解決するかを順番に説明したいと思います。
N+1問題とは
N+1問題とは、本来1回で済むはずのDBへの問い合わせが、N+1回も発生してしまうパフォーマンス上の問題です。
「N」はレコードの件数を指します。たとえば投稿が10件あれば、10+1=11回のSQLが発行されてしまう、ということです。
具体例で見てみる
テーブル構成
以下のような構成を例に考えます。
-
User(ユーザー)が複数のPost(投稿)を持つ -
User has_many :posts/Post belongs_to :user
N+1が発生するコード
# コントローラー
def index
@posts = Post.all
end
<%# ビュー %>
<% @posts.each do |post| %>
<p><%= post.title %> - 投稿者:<%= post.user.name %></p>
<% end %>
一見、問題なさそうに見えます。しかし、このコードが実行されると裏側では以下のようなSQLが発行されています。
-- 全投稿を取得(1回)
SELECT * FROM posts;
-- 各投稿のユーザーを1件ずつ取得(N回)
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE id = 2;
SELECT * FROM users WHERE id = 3;
-- ...投稿の件数だけ繰り返される
投稿が100件あれば、SQLは101回発行されます。件数が増えるほど、アプリの動作はどんどん遅くなっていきます。
なぜ発生するのか
ActiveRecordは関連レコードを必要になったタイミングで都度取得する仕組みになっています(遅延読み込み/Lazy Loading)。
post.user.name のように関連先のデータにアクセスするたびに、その都度SQLを発行してしまうため、ループの中で関連データを参照するとN+1が起きやすくなります。
解決策:Eager Loading(事前読み込み)
N+1問題の解決策として、**Eager Loading(イーガーローディング)**があります。
これは関連するレコードをあらかじめまとめて取得しておく方法です。Railsでは includes メソッドを使って実現できます。
includes を使った修正
# コントローラー
def index
@posts = Post.includes(:user).all
end
ビューはそのままで構いません。
これだけで、発行されるSQLが次のように変わります。
-- 全投稿を取得(1回)
SELECT * FROM posts;
-- 関連するユーザーをまとめて取得(1回)
SELECT * FROM users WHERE id IN (1, 2, 3, ...);
合計2回のSQLで済むようになりました。投稿が何件あっても、SQLの発行回数は変わりません。
includes 以外の方法
Eager Loadingには includes のほかにも方法があります。用途によって使い分けることがあります。
| メソッド | 特徴 |
|---|---|
includes |
状況に応じてSQLの発行方法を自動で最適化してくれる。基本はこれを使えばOK |
preload |
必ず2回のSQLに分けて取得する |
eager_load |
JOINを使って1回のSQLで取得する。whereで関連テーブルを条件に使いたいときに有効 |
迷ったときは includes を使っておけば問題ありません。
N+1が発生しているか確認する方法
ログを確認する
開発環境でRailsサーバーを起動しているターミナルのログを見ると、発行されているSQLが確認できます。同じようなSQLが何度も繰り返されていたら、N+1が起きているサインです。
bullet gemを使う
bullet というgemを使うと、N+1問題が発生しているときに自動で警告を出してくれます。開発中に導入しておくと便利です。
# Gemfile
gem 'bullet', group: :development
まとめ
N+1問題について整理したいと思います。
- N+1問題とは、DBへの問い合わせが必要以上に多く発生するパフォーマンス問題
- ループの中で関連レコードにアクセスするときに起きやすい
-
includesを使ったEager Loadingで解決できる - bulletなどのツールで検出できる
ActiveRecordは便利な反面、N+1問題のように意識しないと気づきにくい落とし穴もあります。ログを確認する習慣をつけることで、問題に早めに気づけるようになるはずです。