【Rails】 N + 1 문제 (eager_load 및 preload) 앱에서 배우기

소개



최근 SQL의 지식이 붙어 왔기 때문에, N+1 문제를 재차 공부.
그렇다면 아직 아직 모르는 부분이 있었으므로, 아웃풋 해 간다.
출력 방법은 교재 앱으로 실시 (아래 URL 참조)
실제로 손을 움직여 지식의 정착을 도모한다

우리가 학교에서 취급하고 있던 데이터 수는 털 같은 것이므로, 이번은 700 가까운 데이터 수로 실시.
데이터 수가 많기 때문에 표시 속도에 영향을 미치는 것을 알 수 있습니다.

대책 방법





자세한 것은 가득 기사가 있으므로, 할애합니다.
아래의 2개의 기사가 알기 쉬웠으므로 봐 주세요.



그러면 실제로 손을 움직여 가려고 생각합니다.

첫 번째 preload를 사용합니다.



app/views/profiles/_articles.html.erb
<% @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 의 내용을 확인.
컨트롤러를 보러갑니다.

app/controllers/profiles_controller.rb
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 
速度改善することができた。

두 번째 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라는 로그가 출력됩니다.

app/views/profiles/_user.html.erb
  <% @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 을 컨트롤러에 기재되어 있다고 생각하기 때문에 보겠습니다.

app/controllers/profiles_controller.rb
 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를 사용합니다.

보기를보고 로그를 확인하면 · ·
Completed 200 OK in 6792ms ➩ Completed 200 OK in 2609ms 
初回からここまで速度改善することができた。

이런 식으로 이번에는 eager_load와 preload를 사용해 보았습니다.

사이고에게



알고있는 것만으로 크게 성능을 향상시킬 수 있으므로 앞으로도 배워 나갈 것입니다.
데이터베이스 지식을 구축하고 싶습니다.

좋은 웹페이지 즐겨찾기