60
16

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 1 year has passed since last update.

DMM WEBCAMPAdvent Calendar 2023

Day 16

【脱 初心者】Railsで学ぶN+1問題

Last updated at Posted at 2023-12-15

はじめに

DMM WEBCAMP Advent Calendar 2023 16日目です:christmas_tree:
メンター・卒業生が記事を投稿しておりますので、是非他の記事もご覧ください!

読んで欲しい人

  • RailsでCRUD処理が書けるようになってきた!
  • 次なるステップアップをしたい!

初学者向けにわかりやすい言葉を使って説明していきます

N+1問題とは?

データベースへの問い合わせ(クエリの実行)をたくさん行ってしまうことによってパフォーマンスが落ちてしまう問題のことです。

本を投稿できるようなアプリを例にします

books_controller.rb
def index
    @books = Book.all
end

本の一覧ページでは、アソシエーションを利用して本を投稿したユーザーの名前を表示しています。

books/index.html.erb
<% @books.each do |book| %>
    <%= book.user.name %>
<% end %>

本が5冊ある場合、以下のように
全ての本を取得するクエリ + (投稿者を取得するクエリ × 本の数)
1 + 5 = 6 回クエリが実行されます。

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

つまり、1 + N 回もDBへ問い合わせをしてしまうため、N + 1問題と呼ばれています。

解消方法

ずばり、関連するモデルのデータも予め全て取得しておくのです。
Railsでは3つのメソッドが用意されています。

preload

@books = Book.preload(:user)
Book Load (0.1ms)  SELECT "books".* FROM "books"
User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?, ?, ?)  [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5]]

eager_load

@books = Book.eager_load(:user)
SQL (0.1ms)  SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, "books"."body" AS t0_r2, "books"."user_id" AS t0_r3, "books"."created_at" AS t0_r4, "books"."updated_at" AS t0_r5, "users"."id" AS t1_r0, "users"."email" AS t1_r1, "users"."encrypted_password" AS t1_r2, "users"."reset_password_token" AS t1_r3, "users"."reset_password_sent_at" AS t1_r4, "users"."remember_created_at" AS t1_r5, "users"."name" AS t1_r6, "users"."introduction" AS t1_r7, "users"."created_at" AS t1_r8, "users"."updated_at" AS t1_r9 FROM "books" LEFT OUTER JOIN "users" ON "users"."id" = "books"."user_id"

この2つの主な違いは関連するモデル毎に取得するか、全て結合して一度で取得するかです。
そのため、preloadでは2回、eager_loadでは1回だけクエリが発行されています。

しかし、発行回数が少ないからeager_loadの方が良い!という単純な問題ではありません。(難しいですね😢)

心が折れかけたそこのあなた、まだ最後のメソッドが残っています😌

includes

@books = Book.includes(:user)

これはデフォルトでpreloadと同じ処理になり、特定の条件下ではeager_loadと同じ処理になります。
つまり、先程紹介した2つのメソッドを状況に応じて自動で使い分けてくれます。便利!

includesは使いやすく初学者におすすめですが、どちらの挙動をするのか不明瞭なメソッドでもあります。
慣れてきたら明示的にpreloadeager_loadを使うほうがbetterです

まとめ

データの数が多くなるほどN+1問題によりパフォーマンスが低下してしまいます。自分のコードを見直して対策しましょう!

初学者向けの記事ですので、preloadeager_loadの使い分けまでは解説しませんでした。気になる方は調べてみてください!

60
16
1

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
60
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?