N+1の対策メソッド
- joins
- left_outer_joins
- eager_load
- preload
- includes
eager_load、preload、includesは、一度取得したデータをキャッシュする。2回目以降は、DBではなく、メモリ上にキャッシされたデータを取得することになる。
キャッシュが不要な場合は、joins或いはleft_outer_joinsを使用する。これらはSQLの句なので、この記事で詳細は触れない。
N+1問題とは?
ループ処理の中で都度SQLを発行してしまう問題のこと。
【Ruby on Rails】N+1問題ってなんだ?
preload
joinは行われず、2つのテーブに対してそれぞれSQLを発行する。(=2回SQLを発行)
joinを行わないため、where句による絞り込みが出来ない。
レコード量が多い場合、joinの処理を行うeager_loadはパフォーマンスや負荷に影響する場合がある。
where句による絞り込みが不要な場合は、preloadを優先的に使用する。
<% @articles.each do |article| %>
<% article.tags.each do |tag| %>
<%= tag.name %>
<% end %>
<% end %>
def index
@user = User.find(1)
- @articles = @user.articles #修正前
+ @articles = @user.articles.preload(:tags) #修正後
end
修正前
Tag Load (0.1ms) SELECT "tags".* FROM "tags" INNER JOIN "article_tags" ON "tags"."id" = "article_tags"."tag_id" WHERE "article_tags"."article_id" = ? [["article_id", 698]]
↳ app/views/profiles/_articles.html.erb:17
Tag Load (0.1ms) SELECT "tags".* FROM "tags" INNER JOIN "article_tags" ON "tags"."id" = "article_tags"."tag_id" WHERE "article_tags"."article_id" = ? [["article_id", 699]]
↳ app/views/profiles/_articles.html.erb:17
Tag Load (0.1ms) SELECT "tags".* FROM "tags" INNER JOIN "article_tags" ON "tags"."id" = "article_tags"."tag_id" WHERE "article_tags"."article_id" = ? [["article_id", 700]]
修正後
Tag Load (8.9ms) SELECT "tags".* FROM "tags" WHERE "tags"."id" IN (1718, 3985, 937, 1418, 4902, 4760, 2461, 641, 2236, 4994, 4474, 4190, ・・・
eager_load
ネストした関連モデルのデータを一括取得する場合にも使用する。ネストした関連テーブとは、1対多対多(或いは多対多対多など)のような関連付けのテーブルのこと。
下記は単に関連テーブルの取得。
<% @skill_categories.each do |category| %>
<% category.skills.each do |skill| %>
<%= skill.name %>
<% end %>
<% end %>
def user_reccomend_skill_categories
- @user.skills.map(&:skill_category).
filter { |skill_category| skill_category.reccomend }.uniq
+ SkillCategory.eager_load(:skills).
where(reccomend: true).
where(skills: { user_id: @user.id })
end
Skill Load (0.1ms) SELECT "skills".* FROM "skills" WHERE "skills"."skill_category_id" = ? [["skill_category_id", 929]]
↳ app/views/profiles/_user.html.erb:42
Skill Load (0.1ms) SELECT "skills".* FROM "skills" WHERE "skills"."skill_category_id" = ? [["skill_category_id", 896]]
↳ app/views/profiles/_user.html.erb:42
Skill Load (0.1ms) SELECT "skills".* FROM "skills" WHERE "skills"."skill_category_id" = ? [["skill_category_id", 1265]]
↳ app/views/profiles/_user.html.erb:42
SQL (7.4ms) SELECT "skill_categories"."id" AS t0_r0, "skill_categories"."name" AS t0_r1, "skill_categories"."reccomend" AS t0_r2, "skill_categories"."created_at" AS t0_r3, "skill_categories"."updated_at" AS t0_r4, "skills"."id" AS t1_r0, "skills"."name" AS t1_r1, "skills"."user_id" AS t1_r2, "skills"."skill_category_id" AS t1_r3, "skills"."created_at" AS t1_r4, "skills"."updated_at" AS t1_r5 FROM "skill_categories" LEFT OUTER JOIN "skills" ON "skills"."skill_category_id" = "skill_categories"."id" WHERE "skill_categories"."reccomend" = ? AND "skills"."user_id" = ? [["reccomend", 1], ["user_id", 1]]
includes
状況に応じて、よしなにpreload、eager_loadで振る舞う。どちらを意図しているのか、コード上で分かりづらい為非推奨。