49
48

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.

N+1の予感がしたらincludesを追加🔎

Last updated at Posted at 2019-05-06

こんにちは!
入社したてのころ、右も左もわからずにコーディングをしていました。
そんな中で、僕もよく悩まされたN+1についての対策について簡単にまとめてみました。
N+1は簡単に防げてパフォーマンスをあげることができます。
すぐできて、効果大なのでぜひ実践してみてください!

そもそもN+1とは

SQLクエリが 「データ量N + 1回 」走ってしまい、取得するデータが多くなるにつれて(Nの回数が増えるにつれて)パフォーマンスを低下させてしまう問題です。

N+1問題 / Eager Loading とは
(引用させていただきました)

→簡単にいうとデータ取得の際、余計にSQLを発行してしまいパフォーマンスを下げてしまうことです。

テーブルの定義

例えば配列展開でBook(本)からAuthor(著者)の名前を出力したい場合(以下ER図&作成データになります)。
スクリーンショット 2019-05-05 20.13.02.png

念の為コードも

Author

class Author < ApplicationRecord
  has_many :books
end

Book

class Book < ApplicationRecord
  belongs_to :author
end

データの用意

author(諫山さん)が6冊の本(book)のリレーションを持っています。

irb(main):004:0> Author.all # 全てのAuthorレコード
  Author Load (0.5ms)  SELECT "authors".* FROM "authors"
+----+--------+-------------------------+-------------------------+
| id | name   | created_at              | updated_at              |
+----+--------+-------------------------+-------------------------+
| 1  | 諫山創 | 2019-05-05 10:44:51 UTC | 2019-05-05 10:44:51 UTC |
+----+--------+-------------------------+-------------------------+
irb(main):003:0> Book.all # 全てのBookレコード
  Book Load (1.3ms)  SELECT "books".* FROM "books"
+----+---------+-----------+-------------------------+-------------------------+
| id | title   | author_id | created_at              | updated_at              |
+----+---------+-----------+-------------------------+-------------------------+
| 1  | 進撃の1 | 1         | 2019-05-05 10:46:17 UTC | 2019-05-05 10:46:17 UTC |
| 2  | 進撃の2 | 1         | 2019-05-05 10:46:25 UTC | 2019-05-05 10:46:25 UTC |
| 3  | 進撃の3 | 1         | 2019-05-05 10:46:28 UTC | 2019-05-05 10:46:28 UTC |
| 4  | 進撃の4 | 1         | 2019-05-05 10:46:31 UTC | 2019-05-05 10:46:31 UTC |
| 5  | 進撃の5 | 1         | 2019-05-05 10:46:35 UTC | 2019-05-05 10:46:35 UTC |
| 6  | 進撃の6 | 1         | 2019-05-05 10:46:38 UTC | 2019-05-05 10:46:38 UTC |
+----+---------+-----------+-------------------------+-------------------------+
irb(main):004:0> Author.first.books # 諫山さんが6冊の本(book)のリレーションを保持
  Author Load (1.7ms)  SELECT  "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Book Load (0.2ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ?  [["author_id", 1]]
+----+---------+-----------+-------------------------+-------------------------+
| id | title   | author_id | created_at              | updated_at              |
+----+---------+-----------+-------------------------+-------------------------+
| 1  | 進撃の1 | 1         | 2019-05-05 10:46:17 UTC | 2019-05-05 10:46:17 UTC |
| 2  | 進撃の2 | 1         | 2019-05-05 10:46:25 UTC | 2019-05-05 10:46:25 UTC |
| 3  | 進撃の3 | 1         | 2019-05-05 10:46:28 UTC | 2019-05-05 10:46:28 UTC |
| 4  | 進撃の4 | 1         | 2019-05-05 10:46:31 UTC | 2019-05-05 10:46:31 UTC |
| 5  | 進撃の5 | 1         | 2019-05-05 10:46:35 UTC | 2019-05-05 10:46:35 UTC |
| 6  | 進撃の6 | 1         | 2019-05-05 10:46:38 UTC | 2019-05-05 10:46:38 UTC |
+----+---------+-----------+-------------------------+-------------------------+

コンソールで実行

配列展開でBookから親モデルのAuthorのnameを呼び出すとBookのデータ数分(6個)SQLを発行してしまいます。
→つまり5回もSQLが無駄に発行されてしまうのです。

books = Book.all
> Book Load (0.2ms)  SELECT "books".* FROM "books"
irb(main):037:0* books.each do |book|
irb(main):038:1*  book.author.name
irb(main):039:1> end
  Author Load (0.6ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  Author Load (0.1ms)  SELECT  "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
irb(main):040:1>
スクリーンショット 2019-05-05 20.25.32.png

この無駄なクエリを防ぐにはincludesを追加するのがもっとも簡単です

includes追加

  books = Book.all.includes(:author) #追加
  Book Load (2.1ms)  SELECT "books".* FROM "books"
  Author Load (0.2ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 1]]

お気づきでしょうか、includes追加により関連レコード(author)も一緒に取得されています。
→つまり、Bookと一緒に紐づいたAuthorモデルのレコードも取得し変数に代入していることになります。

Before

books = Book.all
> Book Load (0.2ms)  SELECT "books".* FROM "books"

books = Bookモデルの全てのデータ

After
スクリーンショット 2019-05-05 20.33.31.png
books = Bookモデルの全てのデータとそれらにひもづくAuthorデータ

この状態でもう一度booksを展開してみましょう!

irb(main):051:0* books.each do |book|
irb(main):052:1*   book.author.name
irb(main):053:1> end
irb(main):054:0>

今度は展開のたびにAuthorを取得していません。
SQLの発行をおさえてパフォーマンス低下を防ぐことができましたね。

includesで色々なリレーションを取得する

先ほどは**N:1(Book:Author)**でのパターンでしたが、実際はもっと複雑な利用パターンが多いと思います。
そんな時に利用できる書き方をご紹介します。

N:1 = Book:Author

  books = Book.all.includes(:author)

こちらは先ほどのパターンでしたね

N:1:1 = Book:Author:Profile

では、Authorのプロフィール情報を保存するAuthors::Profileがあった場合

  books = Book.all.includes(author: :profile)
   Book Load (1.6ms)  SELECT  "books".* FROM "books" LIMIT ?  [["LIMIT", 11]]
  Author Load (0.4ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 1]]
  Authors::Profile Load (0.4ms)  SELECT "authors_profiles".* FROM "authors_profiles" WHERE "authors_profiles"."author_id" = ?  [["author_id", 1]]

N:1:1:1 = Book : Author : Profile : ProfileImage

さらにProfileに1つのプロフィール写真Authors::ProfileImageひもづく場合

  books = Book.all.includes(author: [profile: :profile_image])
  Book Load (0.5ms)  SELECT  "books".* FROM "books" LIMIT ?  [["LIMIT", 11]]
  Author Load (0.1ms)  SELECT "authors".* FROM "authors" WHERE "authors"."id" = ?  [["id", 1]]
  Authors::Profile Load (0.2ms)  SELECT "authors_profiles".* FROM "authors_profiles" WHERE "authors_profiles"."author_id" = ?  [["author_id", 1]]
  Authors::ProfileImage Load (0.1ms)  SELECT "authors_profile_images".* FROM "authors_profile_images" WHERE "authors_profile_images"."id" = ?  [["id", nil]]

逆に**1:N(Author:Book)**なら?
これはリレーションを使い関連データを全て取得できますね。

  > Author.first.books
  Author Load (0.3ms)  SELECT  "authors".* FROM "authors" ORDER BY "authors"."id" ASC LIMIT ?  [["LIMIT", 1]]
  Book Load (0.3ms)  SELECT "books".* FROM "books" WHERE "books"."author_id" = ?  [["author_id", 1]]

N+1の予感とタイトルにありますが、余計にクエリを投げるケースは配列展開が多いと思います。
慣れていない方はeachやmap, selectなど配列展開のメソッドを使う際にN+1が起きていないかぜひ意識してみてください!

49
48
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
49
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?