LoginSignup
1
1

More than 3 years have passed since last update.

【N+1問題】概要と解決方法を簡単にまとめてみた

Last updated at Posted at 2019-08-05

簡潔に、どんな問題なの??

情報をDBから抽出する際に生じる問題で、
全レコードの取得に一回 + 各レコードの取得分にN回、 それぞれSQLを発行してしまっている状況 を指します。

⬇️SQL文で実際に見てみると分かりやすいかもしれません。⬇️

N+1問題が発生しているSQL文の例
SELECT 'phones'.* FROM 'phones' # Phone.allの的なコードを想定。
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 1
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 2
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 3
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 4
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 5
:                           :                           :
:                           :                           :
:                           :                           :

まず結論をお伝えするためにcontrollerやModelの定義を省きましたが、
「一人のUserが一台の電話(phone)を所有している関係(一対一の関係性)における、情報の一覧取得」を想定しています。
恥ずかしながら初学者の私は、⬆️のようなSQLを見ても「どこがN+1なんだ??」と疑問に思いました。笑

そこで上記SQL文を少し分割して見てみると、「N+1問題」と呼ばれる訳が腹落ちするようになります。

一行目・・・N+1の『+1』にあたる部分
SELECT phones.* FROM phones # phone.all の実行
二行目以降・・・N+1の『N』にあたる部分
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 1
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 2
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 3
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 4
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' = 5
:                           :                           :
:                           :                           :
:                           :                           :

一覧に表示するデータを取得するために SELECT を 1 回実行
各データの関連データを取得するために SELECT を N 回実行
つまり、DBへのアクセス(SELECT)が合計 N+1 回実行されてしまうという状況です。
SQL文の発行順通りに解釈するならば、N+1問題というよりも「1+N問題」と捉えた方が分かりやすそうですね。

いずれにせよ膨大なDBを利用する場合は、『N』にあたる部分の行数が何万行にも及ぶため、大きな問題となりそうです...!!

じゃあどうやって解決するの??

順序が逆になりましたが、

上記N+1問題が発生しているコード(例)は以下のようなものです。

はじめに、PhoneテーブルとUserテーブルは下記によって一対一の関係性で結合しています。

Phone.rb
has_one :user

次に、phone_controllerでのuserテーブルのカラムの呼び出しについて、
例えば、phone_idが1の予約者を@userに代入したいとすると、

phones_controller.rb
@phone = phone.find_by(phone_id: 1)
@user = @phone.user.user_name

上記のようになります。

そして実際にphoneのviewで予約者一覧を表示したいときには、「each do」文がまず思い浮かぶのではないでしょうか。

phone一覧.rb
@phones = phone.all
@phones.each do |@phone|
    @user = @phone.user.user_name
    @user
end

これにて一覧の取得は可能となるはずですが、上述の通りこれではN+1問題が発生してしまうのです。

includesメソッドによる解決方法

includesメソッドは、関連している複数のテーブルからデータを取得してくるときのアクセス回数を大きく減らすことができるメソッドです。

phone_controller.rb
@phones = phone.all.includes(:user)

上記のように記述する事で、DBへのアクセス回数が1+N行に及んだSQLが下記のようになります。

N+1問題が解消されたSQL
SELECT phone.* FROM phones
SELECT 'users'.* FROM 'users' WHERE 'users'.'user_id' IN (1,2,3,4,5)

includesメソッドによってSQLの発行がわずか二行に収まり、N+1問題を解決する事ができました!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1