2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Rails】ループ処理(each文など)の中でSQLクエリを投げてはいけない。n+1問題とは何かと解決法

Posted at

個人メモです。

eachやmapなど繰り返し処理をする中で、SQLクエリを投げるのは禁止。(稀にSQLを投げた方が早い場合もあるそう、、)

eachという早い処理の中で、短時間に何度もDBにアクセスすることになる。要素の数が1000個あったら1000回DBに接続することになる。

この問題を総じてN+1問題と呼ぶ。どういった場合がNGでどう対処したらいいかと、N+1問題とはそもそもどういう意味かについて


## 目次
  1. NG事例(これはやってはダメ!)
  2. 対処法
  3. [preload, includes, eager_loadの違い](#preload_ includes_eager_loadの違い)
  4. N+1問題とは?

NG事例(これはやってはダメ!)

NG
# Userモデルでデータ取得
users = ::User.all

user_names = []
users.each { |user|
      #UserProfileモデルでデータ取得
      user_names << ::UserProfile.find_by(user_id: user.id).name
 }

<<は配列の末尾に指定したデータを入れるメソッド(pushメソッドみたいなもの)。

usersの要素の数だけ、::UserProfile.find_by(user_id: user.id)でSQLクエリを投げる処理が繰り返されてしまう。

繰り返し実行されるSQLの例
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 1 LIMIT 11
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 2 LIMIT 11
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 3 LIMIT 11
.
.
・

関連付けられたDBの場合

has_onehas_manyで関連付けられたモデルの場合、省略して表記できるが、同じくSQLを投げてしまうのでNG。

▼例
UserモデルとUserProfileモデルを関連付けた場合。

user.rb(モデル)
class User < ApplicationRecord
  has_one :user_profile
end
UserProfile.rb(モデル)
class UserProfile < ApplicationRecord
  belongs_to :user
end

この時、Userモデルを呼び出して、.user_profileを付ければ、UserProfileの関連するデータを呼び出せる。

コントローラの処理
users = ::User.where(medium_id: 34)
user_name_arr = []

users.each { |user|
  #Userモデルに.user_profleをつけるだけ
  uesr_name_arr << user.user_profile.name
}

モデル冒頭の::は名前空間の中にない(最上位の階層)ことを明示している。(バグ防止目的。基本的には、Userも::Userも同じ処理結果になる)

この場合でもSQLが投げられるので、user.user_prifileの度に以下のSQL処理が実行される。

SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 1 LIMIT 11
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 2 LIMIT 11
SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."user_id" = 3 LIMIT 11
・
・
・

DBの負荷が上がってしまうためこれも避けなくてはいけない。


**※関連付けモデルの注意点** 関連づけられたモデルを指定するためには、データを絞り込む必要がある。例えば、allメソッドは絞り込みしていないので、`User.all.user_profile`は取得できない。

User.first.user_profileUser.find_by(user_id: 7).user_profileなど、絞り込みした後であれば問題なく動く。


## 対処法 SQLを投げる回数を最小限に抑えるために、eachでループ処理を実行する前にDBからデータを取得しておく。

関連づけられていないモデルの場合

OK
# Userモデルでデータ取得
user_profs = ::UserProfile.all

user_names = []
user_profs.each { |user|
      user_names << user.name
 }

### 関連づけられたモデルの場合 関連付けられたモデルの場合は`include(:関連づけたモデル名)`を使う。

▼実例
Userモデルにhas_one :user_profileが関連づけられている場合の例。

コントローラの処理
user_profiles = ::User.where(medium_id: 34).includes(:user_profiles)

user_name_arr = []
user_profiles.each { |user_profile|
  uesr_name_arr << user_profile.name
}

**実行されるSQLクエリ**
SELECT "users".* FROM "users" WHERE "users"."medium_id" = 34 LIMIT 11

SELECT "user_profiles".* FROM "user_profiles" WHERE "user_profiles"."media_user_id" IN (6, 7)

usersテーブルと、user_profilesテーブルのそれぞれからデータを取得するため、SQLは2回実行される。

これで必要なデータの取得は完了するので、あとはeachで取り出すだけ。


## preload, includes, eager_loadの違い 関連するテーブルを読み込む方法は、includesの他にpreloadとeager_loadがある。

preloadとeager_loadはクエリを複数投げるかどうか、絞り込みができるができないかといった挙動の差がある。

includesは状況に応じてpreloadとeager_loadの処理を切り分けるので、基本的にはincludesを使っておけば問題ない。


## N+1問題とは? **余計なSQLを実行しているという問題**。処理速度は遅くなるし、DBアクセス回数が増えて負荷が上がるというデメリットしかない。

例えば、ある会員の情報が入ったUserモデルから、会員一人一人の情報を抜き出す場合に、SQLを投げる方法がいくつかある。

(1)会員数の数だけSQLを投げる(会員全員のデータを取得済み)
・会員全員のデータ取得のためのSELCT文を投げる(1回)
・SELECTが会員数の数N会投げられる。(N回)

(2)会員数の数だけSQLを投げる。(全会員データはなし)
・SELECTが会員数の数N会投げられる。(N回)

(3)全会員のデータを取得してループで回す
・会員全員のデータ取得のためのSELCT文を投げる(1回)


(1)から順番に処理負荷が高く、コードも冗長になっている。この冗長なコードで**投げるSQLクエリの回数がN+1になっているので、N+1問題と呼ぶ**。

なお、(2)はN問題で、N+1より僅かにマシだが、ほぼ変わらない。

2
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?