はじめに
久しぶりにER図や、データ設計について考えることがあったので、理解が若干怪しいN+1問題についてアウトプットしようと思ったので記事にしました
間違っていればコメントいただけると嬉しいです
n+1問題とは
一覧を取得して、ループ処理のたびにクエリが発行されることです
具体例を以下に出します
ユーザー一覧を表示する際に、それぞれの投稿も表示したい場合
users = User.all
users.each do |user|
puts user.posts
end
最初の1回
SELECT * FROM users で100人を取得したとする
それ以降のn回
ループ処理の中で1人目の投稿を取るために
SELECT * FROM posts WHERE user_id = 1
2人目
SELECT * FROM posts WHERE user_id = 2
これを100人分繰り返す
ユーザーが増えるほど、クエリは増えるのでn+1となる
解決方法
preload
クエリを分割して実行し、子テーブルのデータを取得してキャッシュするメソッドです
users = User.preload(:posts)
users.each do |user|
puts user.posts
end
SELECT * FROM users
SELECT posts.* FROM posts WHERE posts.user_id IN (1, 2, 3...)
また、子テーブルでWHERE(絞り込み)した場合はエラーとなります
eager_load
eager_loadは、関連するテーブルをLEFT OUTER JOINで結合し、キャッシュします
最初に関連するテーブルを結合しているので、クエリの発行量が1回で済みます
users = User.eager_load(:posts)
users.each do |user|
puts user.posts
end
SELECT users.*, posts.*
FROM users
LEFT OUTER JOIN posts ON posts.user_id = users.id
includes
includesは条件によって、挙動が変わるメソッドです
デフォルトでは、preloadと同じですが、子テーブルの要素でWHEREした時などはeager_loadと同じ挙動をします
[余談] LEFT OUTER JOIN
LEFT OUTER JOINは、指定したテーブル(FROM)を全て含み、結合条件(ON)が一致すれば右側のテーブルにデータが入ります
一致しない場合右のテーブルはNULLとなります
まとめ
N+1問題の対策は、エンジニアにとって必須のスキルと考えています
運用や保守のしやすさを考えて設計をしなければいけないので、まだまだ先は長い...
忘れないように見返そうと思います