はじめに
DMM WEBCAMP Advent Calendar 2023 16日目です
メンター・卒業生が記事を投稿しておりますので、是非他の記事もご覧ください!
読んで欲しい人
- RailsでCRUD処理が書けるようになってきた!
- 次なるステップアップをしたい!
初学者向けにわかりやすい言葉を使って説明していきます
N+1問題とは?
データベースへの問い合わせ(クエリの実行)をたくさん行ってしまうことによってパフォーマンスが落ちてしまう問題のことです。
例
本を投稿できるようなアプリを例にします
def index
@books = Book.all
end
本の一覧ページでは、アソシエーションを利用して本を投稿したユーザーの名前を表示しています。
<% @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は使いやすく初学者におすすめですが、どちらの挙動をするのか不明瞭なメソッドでもあります。
慣れてきたら明示的にpreload
かeager_load
を使うほうがbetterです
まとめ
データの数が多くなるほどN+1問題によりパフォーマンスが低下してしまいます。自分のコードを見直して対策しましょう!
初学者向けの記事ですので、preload
とeager_load
の使い分けまでは解説しませんでした。気になる方は調べてみてください!