0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RailsのN+1問題を回避するメソッド(preload、eager_load、includes、joins)の違いを整理

Last updated at Posted at 2025-05-29

目的

N+1問題を回避するメソッドの違いがわからなかったのでまとめる。

N+1問題とは?

N+1問題とは、関連データを個別に取得しようすると、データ個数分だけ余計なクエリが発生してしまう問題のこと。

例えば、全てのユーザーを取得してから、各ユーザーの投稿を取得すると、ユーザーの数だけクエリが発行されてしまう。

SELECT * FROM users;  -- 1回のクエリ
SELECT * FROM posts WHERE user_id = 1;  -- 1人目の投稿を取得(1回目)
SELECT * FROM posts WHERE user_id = 2;  -- 2人目の投稿を取得(2回目)
SELECT * FROM posts WHERE user_id = 3;  -- 3人目のの投稿を取得(3回目)

合計で4回のクエリ(1回+3回)を発行。
ユーザーが増えれば増えるほどクエリの数も増え、データベースへの負荷が増加する。

ポイント

  • preloadeager_loadincludesjoinsいずれも、効率よくデータ取得するための準備を行うメソッドである
    • 実際にSQLが発行されるのは、eachmapなどのメソッドで結果を取得する時!

preload

  • 親データと子データを別々のクエリで取得
    • 関連する子データすべてのカラムを取得するので、子データについて特定のカラムだけに絞り込むことはできない
users = User.preload(:posts).all
SELECT * FROM users;  -- ユーザーを取得(1回のクエリ)
SELECT * FROM posts WHERE user_id IN (1, 2, 3);  -- すべての投稿を一度に取得(1回のクエリ)

eager_load

  • LEFT JOINを使用して親データと子データを一度のクエリで取得
    • LEFT JOINなので、親データ(例:ユーザー)に関連する子データ(例:投稿)が存在しない場合でも、親データは取得される
    • 子データについて必要なカラムだけに絞り込むことが可能
users = User.eager_load(:posts).all
SELECT users.*, posts.* FROM users
LEFT JOIN posts ON posts.user_id = users.id;  -- ユーザーと投稿をJOINして取得(1回のクエリ)
子データについて必要なカラムだけに絞り込む
# ユーザーとその投稿のタイトルだけを取得する
users = User.eager_load(:posts).select('users.*, posts.title').all
子データについて必要なカラムだけに絞り込む
SELECT users.*, posts.title FROM users
LEFT JOIN posts ON posts.user_id = users.id;  -- ユーザーと投稿をJOINして取得(1回のクエリ)

icons8-内部結合を左でクエリ-100.png
内部結合を左でクエリ アイコン by Icons8

includes

  • preloadまたはeager_loadを選択して、関連する子データを取得
    • Railsが最適な方法を選ぶ
    • 子データについて必要なカラムだけに絞り込むことが可能
users = User.includes(:posts).all
この場合、preloadが選択されたと仮定
SELECT * FROM users;  -- ユーザーを取得(1回のクエリ)
SELECT * FROM posts WHERE user_id IN (1, 2, 3);  -- すべての投稿を一度に取得(1回のクエリ)

joins

  • INNER JOINを使用して、関連する子データが存在する親データのみを取得
  • 今までの3つのメソッドと異なり、結合条件が適切に設定されていれば、親子関係がなくてもデータを取得できる
    • 親データの条件絞り込み(フィルタリング)を目的としているメソッド
      • そのため関連データの情報自体は取得してこない
      • 関連データの情報自体も必要な場合にはselectで明示する必要あり
# 公開された投稿を持つユーザーを取得
users_with_published_posts = User.joins(:posts).where(posts: { published: true })
SELECT users.* FROM users
INNER JOIN posts ON posts.user_id = users.id
WHERE posts.published = true;  -- ユーザーと投稿をJOINして取得(1回のクエリ)

icons8-クエリの内部結合-100 (1).png
クエリの内部結合 アイコン by Icons8

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?