##はじめに
最近SQLの知識がついてきたので、N+1問題を改めて勉強。
そしたらまだまだわかっていない箇所があったので、アウトプットしていく。
アウトプットのやり方は 教材アプリで実施(下記URL参照)
実際に手を動かして知識の定着をはかる
僕らがスクールで扱っていたデータ数なんて毛みたいなものなので、今回は700近いデータ数で実施。
データ数多いので、表示スピードにもろ影響することがわかる。
対策メソッド
詳細はいっぱい記事があるので、割愛します。
下記2つの記事がわかりやすかったので見て下さい。
それでは実際に手を動かしていこうと思います。
1つ目 preloadを使う。
<% @articles.each do |article| %>
<div class="box">
<article class="media">
<div class="media-content">
<div class="content">
<h3>
<span class="icon has-text-success">
<i class="fas fa-file-alt"></i>
</span>
<%= article.title %>
</h3>
<p><%= article.description %></p>
<% article.tags.each do |tag| %>
<span class="tag is-light"><%= tag.name %></span>
<% end %>
</div>
</div>
</article>
</div>
<% end %>
@articlesの中身を確認。
コントローラを見に行きます。
class ProfilesController < ApplicationController
def index
@user = User.find(1)
raise Forbidden unless user_safe?
@skill_categories = user_reccomend_skill_categories
- @articles = @user.articles ←このコードが原因
+ @articles = @user.articles.preload(:tags)
end
# 略
メソッド一覧から今回のケースを探すと"ループ内で関連テーブルの値を使用する場合"が合致します。
この事からeager_loadか、preloadを使い、記事データの取得時にタグデータを一括取得する対応が効きそうです。
また今回のケースでは関連テーブルでの絞り込みが無く、LEFT OUTER JOIN句を使うメリットが無いのでpreloadを使用します。
Completed 200 OK in 6792ms ➩ Completed 200 OK in 5137ms
速度改善することができた。
2つ目 eager_loadを使う。
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の下に↳ app/views/profiles/_user.html.erb:42というログが出力されています。
<% @skill_categories.each do |category| %>
<div class="box is-medium">
<h4 class="title is-6">
<span class="icon has-text-success">
<i class="fas fa-toolbox"></i>
</span>
<%= category.name %>
</h4>
<% category.skills.each do |skill| %> ←42行目
<span class="icon has-text-success">
<i class="fas fa-check"></i>
</span>
<span class="tag is-light"><%= skill.name %></span>
<% end %>
</div>
<% end %>
categoryは@skill_categories.eachのブロック変数である事が確認できました。
では@skill_categoriesとはなんなのでしょうか?
@skill_categoriesをコントローラに記載してあると思うのでみにいきます。
class ProfilesController < ApplicationController
def index
@user = User.find(1)
raise Forbidden unless user_safe?
@skill_categories = user_reccomend_skill_categories ←このコード
@articles = @user.articles.preload(:tags)
end
private
def user_safe?
@user.user_cautions.all? do |user_caution|
Time.zone.now > user_caution.caution_freeze.end_time
end
end
def user_reccomend_skill_categories
@user.skills.map(&:skill_category).
filter { |skill_category| skill_category.reccomend }.uniq
end
end
@skill_categoriesはprivateメソッドのuser_reccomend_skill_categoriesメソッドの返り値の様です。
同じくapp/controllers/profiles_controller.rb内で定義されていますね。
これです。
def user_reccomend_skill_categories
@user.skills.map(&:skill_category).
filter { |skill_category| skill_category.reccomend }.uniq
end
このメソッドは下記の内容がかいてあります。
`
①@userに紐づくskillsを全て取得
②mapメソッドでskill毎に紐づくskill_categoryを取得
③filterメソッドでskill_categoryインスタンスのreccomend属性がtrueのインスタンスのみを取得
④ ③までの実行結果である、複数のskill_categoryを要素に持つArray内のskill_categoryインスタンスを一意にしたArrayを返す
※ちなみにfilterメソッドは、条件に一致する要素のみ抜き出し新たな配列を作ることができるメソッドです。
では。メソッドを修正していきます。
def user_reccomend_skill_categories
SkillCategory.eager_load(:skills).
where(reccomend: true).
where(skills: { user_id: @user.id })
end
メソッド一覧から今回のケースを探すと"ループ内で関連テーブルの値を使用する場合"が合致します。
この事からeager_loadか、preloadを使い、スキルカテゴリーデータの取得時にスキルデータを一括取得する対応が効きそうです。
またデータ取得時にWHERE句を使用し絞り込みを行う事で1度のSQLでデータ取得が行えます。
今回のケースでは、whereメソッドを使用する為、eager_loadを使用します。
ではviewを表示してログを確認すると・・
Completed 200 OK in 6792ms ➩ Completed 200 OK in 2609ms
初回からここまで速度改善することができた。
こんな感じで、今回はeager_loadとpreloadを使ってみました。
##さいごに
知ってるだけで、大きくパフォーマンス向上できるのでこれからも学習していき、
データベースの知識を構築していきたいです。