5
1

More than 1 year has passed since last update.

N+1問題の解決方法(初心者向け)

Last updated at Posted at 2022-03-01

環境

Ruby 2.7.3

Rails 6.1.4

N+1問題とは

必要以上にSQLが発行されてしまい、パフォーマンスが悪くなる問題のこと

実際にN+1問題の例を以下に示す。
シンプルにUserカラムGroupカラムが中間テーブルGroupUserを跨いで多対多の関係になっている。

Database ER diagram (crow's foot) - Database ER diagram (crow's foot).png

N+1問題について考えず、各GroupのUserに対してある処理をしたい。
以下のようにUserデータを取得するために、随所随所でSQLが発行されておりパフォーマンスが悪い。

問題のあるコード

Groups.each do |group|
  group.users.each do |user|
    id = user.id
    pp id
  end
end

---以下 実行内容---

Group Load (26.8ms)  SELECT `group`.* FROM `group` 
User Load (17.2ms)  SELECT `user`.* FROM `user` INNER JOIN `group_user` ON `user`.`user_id` = `group_user`.`user_id` WHERE `group_user`.`group_id` = 97
"sample37"
"sample26"
"sample41"
"sample54"
User Load (16.5ms)  SELECT `user`.* FROM `user` INNER JOIN `group_user` ON `user`.`user_id` = `group_user`.`user_id` WHERE `group_user`.`group_id` = 98
  # ↑の処理が随所で出てくるのはよろしくない
"sample36"
"sample12"
"sample4"
"sample49"
User Load (15.3ms)  SELECT `user`.* FROM `user` INNER JOIN `group_user` ON `user`.`user_id` = `group_user`.`user_id` WHERE `group_user`.`group_id` = 99
"sample2"
"sample51"
"sample48"
"sample24"

解決方法

・includes メソッド(極力使わない)
・preloadメソッド
・eager_loadメソッド

3種類あるけど、includeメソッドは極力使わず、preload, eager_load のどちらかに
絞りたい。includesがダメなわけではないが、思考停止でincludesに頼るのは良くない。

▼以下記事を参照▼
ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由

includesは利用しない方が良いでしょう。
なぜなら、includesは、preloadとeager_loadをよしなに振り分けるので。
preloadと、eager_loadの特徴を理解していれば、includesが登場する場面は、
ほとんどないと思います。データが少ないうちはincludesしていても問題にならないかもしれませんが、
データが増えてきたときにジワジワと問題が顕在化してくるので、includesの挙動も正しく知って
おきましょう。リファクタの際に役立つと思います。

使い分けとしては、関連先のテーブルデータを

・参照した絞り込みを行う場合:eager_load
・参照せず、アソシエーションを取得する場合:preload

では、実際にそれぞれのメソッドでどのようなSQLが走るかを確認します。

preload メソッドを使用した場合

実は、includesメソッドと全く同じSQLを発行することがわかる。
つまり、今回の例ではincludesメソッドの中でpreloadが選択され、実行されている。

Groups.preload(:users).each do |group|
  group.users.each do |user|
    id = user.id
    pp id
  end
end

--- 以下 実行結果 ---

Group Load (225.8ms)  SELECT `group`.* FROM `group` WHERE `group`.`ttt_status` = 'checking'
GroupUser Load (12.3ms)  SELECT `group_user`.* FROM `group_user` WHERE `group_user`.`group_user_id` IN (97, 98, 99, 100, 101)
User Load (12.6ms)  SELECT `user`.* FROM `user` WHERE `user`.`user_id` IN (39, 28, 43, 56, 38, 14, 5, 51, 3, 53, 50, 26, 1, 42, 40, 18, 29, 45, 20, 23)
"sample37"
"sample26"
"sample41"
"sample54"
"sample12"
"sample4"
"sample49"
"sample2"
"sample51"
"sample48"
"sample24"

eager_load メソッドを使用した場合

nameslack_idなどのアソシエーション先であるUserカラム情報も取得している。
そして、特徴的なのは1回のクエリで関連先のデータを取得していること。
そのため、関連先のモデルデータで絞り込みができる。

Groups.eager_load(:users).each do |group|
  group.users.each do |user|
    id = user.id
    pp id
  end
end

--- 以下 実行結果 ---

SQL (168.2ms)  SELECT `group`.`group_id` AS t0_r0,
`group`.`event_date` AS t0_r1, `group`.`group_name` AS t0_r2,
`group`.`confirm_flag` AS t0_r3, `group`.`status` AS t0_r4,
`group`.`delete_flag` AS t0_r5, `group`.`deletion_datetime` AS t0_r6,
`group`.`registration_datetime` AS t0_r7, `group`.`update_datetime` AS t0_r8,
`group`.`update_timestamp` AS t0_r9,
`user`.`user_id` AS t1_r0,
`user`.`name` AS t1_r1, `user`.`slack_id` AS t1_r2, `user`.`image_url` AS t1_r3,
`user`.`email` AS t1_r4, `user`.`recess_flag` AS t1_r5,
`user`.`delete_flag` AS t1_r6, `user`.`deletion_datetime` AS t1_r7,
`user`.`registration_datetime` AS t1_r8, `user`.`update_datetime` AS t1_r9,
`user`.`update_timestamp` AS t1_r10
FROM `group` LEFT OUTER JOIN `group_user`
ON `group_user`.`group_id` = `group`.`group_id`
LEFT OUTER JOIN `user` ON `user`.`user_id` = `group_user`.`user_id`
WHERE `group`.`status` = 'checking'

"sample37"
"sample26"
"sample41"
"sample54"
"sample12"
"sample4"
"sample49"
"sample2"
"sample51"
"sample48"
"sample24"
-----補足-----

以下のように、連結先のテーブルの条件で絞り込みをしたい時に使える。
※Group テーブルに GroupUser テーブルを連結し、その GroupUserにおいて、status='present'で絞る

Group.eager_load(:group_user).where(group: {status: 'present'})

結論

N+1問題を解決するためにincludes メソッドpreloadメソッドeager_loadメソッド
を使用する。includes メソッドは極力使わず、やりたいことに合わせてpreloadメソッドeager_loadメソッドで取捨選択する。

参考

ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い

ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由

ActiveRecordのincludes, preload, eager_load の個人的な使い分け

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