LoginSignup
0
0

More than 1 year has passed since last update.

【Ruby on Rails】N+1問題対策(preload、eager_load、includes)

Posted at

はじめに

Railsなどを中心に勉強中のエンジニア初心者が他の記事を参考にしたり、実際に実装してみたりして、アウトプットの一環としてまとめたものです。
間違っていることもあると思われるので、その際は指摘いただけると幸いです。

N+1問題とは

N+1問題とは、データベースからデータを取り出す際に、大量のSQLが発行されて動作が遅くなる問題のこと。

もう少し具体的に言うと、ループ処理の中で都度SQLを発行してしまうことで、1つ目の値の関連データを取得するためにSLQを発行し、2つ目の値の関連データを取得するためにSQLを発行し、3つ目の、、、というイメージ。

関連データもまとめて取得してからループ処理に入ればN+1問題は起きない(はず)。

N+1問題の例

RoomモデルとUserモデルがそれぞれ関連づけられており、各roomに紐づくuser.nameを取得したいとする。

コントローラとビュー

# コントローラ
@rooms = Room.all
# ビュー
@rooms.each do |room|
	room.user.name
end

N+1問題の発生

コントローラでroom一覧を取得する際のSQL。

Room Load (0.1ms)  SELECT "rooms".* FROM "rooms"

ビューでroom.user.nameを表示する際のSQL文。

room毎に紐づいているuserを都度取得しているため、roomの数が増えるほど発行されるSQLも増えてしまう。

User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]

N+1問題対策

N+1問題の対策として、以下のメソッドを使った対策が挙げられる。

以下は、アソシエーションされたモデルのデータを効率よく取得するためのメソッドである。

preloadメソッド

preloadメソッドは2つのクエリを発行する。

まずは、room一覧を取得するSQLが発行され、その次にそのroomに紐づくuserを一括で取得するSQLが発行されている。

# コントローラ
@rooms = Room.all.preload(:user)
Room Load (0.1ms)  SELECT "rooms".* FROM "rooms"
User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?)  [["id", 1], ["id", 2], ["id", 3]]

eager_loadメソッド

eager_load引数に取ったマスターテーブルとして外部結合する。

今回はusersテーブルをマスターテーブルとして、roomsテーブルと外部結合(left outer join)している。

# コントローラ
@rooms = Room.all.eager_load(:user)
SQL (0.2ms)
SELECT
	"rooms"."id" AS t0_r0,
	"rooms"."room_name" AS t0_r1,
	"rooms"."room_introduction" AS t0_r2,
	"rooms"."usage_fee" AS t0_r3,
	"rooms"."address" AS t0_r4,
	"rooms"."room_picture" AS t0_r5,
	"rooms"."created_at" AS t0_r6,
	"rooms"."updated_at" AS t0_r7,
	"rooms"."user_id" AS t0_r8,
	"users"."id" AS t1_r0,
	"users"."name" AS t1_r1,
	"users"."email" AS t1_r2,
	"users"."password_digest" AS t1_r3,
	"users"."created_at" AS t1_r4,
	"users"."updated_at" AS t1_r5,
	"users"."icon_img" AS t1_r6,
	"users"."introduction" AS t1_r7
FROM "rooms" LEFT OUTER JOIN "users" ON "users"."id" = "rooms"."user_id"

includesメソッド

デフォルトでは、preloadと同様の挙動を示す。

関連先のテーブルで絞り込みを行っている場合などは、eager_loadと同じ挙動になるとのこと。

いろいろな記事を調べていると、preloadeager_loadを使って挙動を明確にすることを推奨している記事が多い印象。

# コントローラ
@rooms = Room.all.includes(:user)
Room Load (0.2ms)  SELECT "rooms".* FROM "rooms"
User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?)  [["id", 1], ["id", 2], ["id", 3]]

参考

最後に

いかがでしたでしょうか。
ここ違うよ!でしたり、こうした方がいいよ!などがあればコメントいただけると幸いです。

他にも下記のような記事を投稿しております。
興味がありましたら、ぜひご覧ください。

0
0
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
0
0