【Rails】n+1問題を徹底解説!

Rails

n+1問題とは

n+1問題とはデータベースからデータを取り出す際、大量のSQLが実行されて動作が重くなるという問題です。

具体的な内容

railsではallメソッドやfindメソッドを使ってデータベースからデータを取得しています。
ですがターミナルのログを見ると実際には下のようにその都度SQLが実行されています。

SQL

例をあげて考えてみましょう。
ユーザーがツイートを投稿できるアプリがあったとします。
ユーザーの情報はusersテーブルに、ツイートの情報はtweetsテーブルに保存します。
トップページにはツイートの一覧を表示させるので、コントローラーでallメソッドを使い、tweetsテーブルからデータを取得します。
その際、上のように1回SQLが実行されます。

そして、トップページのビューでeachメソッドを使い、一つ一つのツイートを表示させるとします。
この時、usersテーブルとtweetsテーブルでアソシエーションを組んでいて、ツイートした人の名前もトップページで表示するようにします。
ツイートの情報全てはallメソッドで取得してきましたが、ツイートを投稿したユーザーの名前はusersテーブルから取得してこなければなりません。

ですのでeachメソッドで一つのツイートのユーザーの名前を表示させるたびにusersテーブルからレコードを取得するSQLが自動で実行されます。
もしツイートが5個あれば5回SQLが実行されます。
N個あればN回SQLが実行されるので、全てのレコードを取得する1回+N回SQLが実行されるのでN+1問題と呼ばれます。
1+N問題と言った方がわかりやすいかもしれません。

SQLが実行されるときには1回あたりわずかですが時間がかかります。
5回ぐらいの実行であれば動作にそれほど影響はないですが、これが100万回あったときはどうでしょうか?
読み込むまで相当な時間がかかってしまいますよね。
ですのでこの問題が起きないよう気をつける必要があります。

N+1問題の対処法

N+1問題に気付いたときには対処をする必要があります。
その際に使うのがincludesメソッドです。

includesメソッド

includesメソッドは関連づいたモデルを先に取得するメソッドです。
下記のように使用します。

ruby
1
@tweets = Tweet.includes(:user)

このように「includes(:モデル名)」と指定します。
するとtweetsモデルからデータを取得するときに、関連するusersモデルのデータも取得してくれます。
なのでeachメソッドで一つ一つ表示する際でもすでに表示させるデータを全て取得しているので、その都度SQLを実行する必要は無くなります。

便利なGemを使ってみよう

複雑なアプリになってくると自分でN+1問題が発生しているか気づかない時があります。
そんなときに便利なGemが「bullet」です。
bulletを使うとN+1問題が発生しているビューが表示されたときにポップアップで知らせてくれるので大変便利です。

bulletの使い方

それではbulletをアプリに導入する方法を解説します。
まずはGemfileのdevelopment環境にgemを追加します。

ruby
1
2
3
group :development do
  gem 'bullet'
end

そしてbundle installをします。
次に「config/environments/development.rb」に下記の記述をします。

ruby
1
2
3
4
5
6
7
8
9
AppName::Application.configure do
  config.after_initialize do
    Bullet.enable = true
    Bullet.alert = true
    Bullet.bullet_logger = true
    Bullet.console = true
    Bullet.rails_logger = true
  end
end

これで準備完了です。
実際にn+1問題が発生しているビューを開くと下のようにポップアップウィンドウが開き、教えてくれます。

bullet
とても便利なgemなのでぜひインストールしておきましょう。

ターミナルのログでn+1問題を確認してみよう

それでは実際にどういうSLQが発行されているのか、ターミナルのログを見ると確認することができます。
まずはincludesメソッドを使う前を見てみます。

includeなし
このようにたくさんのSQLが実行されているがわかりますね。
次にincludesメソッドを使った時を見てみましょう。

includeあり
このように明らかにSQLが実行されている回数が違いますよね。

N+1問題はアプリのパフォーマンスを低下させる原因となるので注意しましょう。

まとめ

n+1問題とは対策をしないと、大量のSQLが実行されて動作が重くなるという問題です。パフォーマンスに影響を与えるので判別してくれるgemを利用するなどしてしっかり対策をしましょう。