目的
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回)を発行。
ユーザーが増えれば増えるほどクエリの数も増え、データベースへの負荷が増加する。
ポイント
-
preload
、eager_load
、includes
、joins
いずれも、効率よくデータ取得するための準備を行うメソッドである- 実際にSQLが発行されるのは、
each
やmap
などのメソッドで結果を取得する時!
- 実際にSQLが発行されるのは、
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回のクエリ)
内部結合を左でクエリ アイコン 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回のクエリ)