1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

N+1問題を解決しても尚、とにかく描画速度が遅いページのクエリチューニングをした話

Last updated at Posted at 2021-02-06

N+1問題を解決しても尚、とにかくページの描画速度が遅かった

Rails gemのbulletを入れていたので、N+1を検出できるようにはしていたものの、キャッシするデータが多すぎたのか、速度が遅いページがありました。

結論から言うと、ビジネス用件が複雑であった為、クエリキャッシュさせるのではなく、ハッシュデータを作る方法を採用しました。

飛躍的にページ速度改善につながったので、残しておきたいと思います。

改善前
Completed 200 OK in 5301ms (Views: 1691.3ms | ActiveRecord: 3137.2ms)

改善後
Completed 200 OK in 1361ms (Views: 1217.0ms | ActiveRecord: 16.7ms)

既存実装(テーブル関係と、要件)

(命名情報、詳細な実装内容等については実際の物ととは異なり、あくまで例です。)

class Qr < ActiveRecord::Base
  belongs_to :item
  belongs_to :shape
  belongs_to :size
  belongs_to :staff
end
class Item < ActiveRecord::Base
  has_many :shapes
  has_many :sizes
  has_many :qrs
end
class Staff < ActiveRecord::Base
  belongs_to :warehouse
  has_many :qr
end
class Warehouse < ActiveRecord::Base
  belongs_to :warehouse
  has_many :staffs
  has_many :qs
end
class Shape < ActiveRecord::Base
  has_many :items
end
class Size < ActiveRecord::Base
  has_many :items
end
class QrsController < ApplicationController
  def index
    @item = Item.find(params[:item_id])
    @all_warehouses = Warehouse.joins({staffs: :qrs})
                      .where(qrs: {item_id: @item.id})
                      .eager_load({qrs: :shape}, {qrs: :staff}, {qrs: :size})
                      .preload(qrs: {staff: :warehouse})
    @q = @all_warehouses.ransack(params[:q])
    @warehouses = @q&.result&.uniq&.page(params[:page])&.per(10)
    @staffs = @all_warehouses.uniq.flat_map{|warehouse| warehouse.qrs.map(&:staff)}
  end
end

# 今回は可読性を考慮して、これ以降の記述でransack処理を省いてるものの、
# ransackがあったがゆえに奇妙なレコードの取り方が行われていた為controllerでは残しています.
# また上記理由により、view側で使っていない変数を作っています。
# app/views/qrs/index.html.erb

<% @warehouses.each do |warehouse| %>
  <div id="hoge<%= warehouse.id %>" role="tab" class="panel-hoge">
    <h4 class="panel-title">
      <a data-toggle="collapse" data-parent="#accordion" href="#collapse<%= warehouse.id %>"<%= warehouse.name %></a>
    </h4>
  </div>
  <div id="collapse<%= warehouse.id %>" role="fuga" aria-labelledby="hoge<%= warehouse.id %>" class="panel-collapse collapse">
    <div class="table-responsive">
      <table class="table table-hover">
        <tbody>
          <% warehouse.qrs.each do |qr| %>
            <tr>
              <td><%= qr.staff.name %></td>
              <% if qr.shape.present? %><%= qr.shape.name %><% end %>
              <% if qr.size.present? %><%= qr.size.name %><% end %>
            </tr>
          <% end %>
        </tbody>
      </table>
    </div>
  </div>
</div>

# controller側にも追記した通り、本題から逸れる部分を含めていないので、ちょっとありえない形になっています。

見ての通り、明らかにcontrollerの処理が悪かった。
①余計なeager_loadを利用している(全てpreloaddで良かった)
②レコードの取り方が変則的(わざわざ遠いレコードから取り始める必要性がない)
③viewsで二重にeachを回すために、クエリキャッシュが多くなりすぎている(load時間自体が結構長い)

controllerをリファクタリング

class QrsController < ApplicationController
  def index
    @item = Item.find(params[:item_id])
    default_qrs = Qr.preload({staff: :warehouse}).where(item_id: @item.id)
    @q = default_qrs.ransack(params[:q])
    searched_qrs = @q.result.preload(:shape, :size).sort_by{|qr| qr.staff.warehouse.position}
    hash_warehouses = searched_qrs.group_by{|qr| qr.staff.warehouse}
    @qrs_per_warehouses = Kaminari.paginate_array(hash_warehouses.to_a).page(params[:page]).per(10)
  end
end

リファクタのポイントは以下。
①クエリキャッシュする際は全てpreloadで実施
②最短でレコードを取れるようにQrからActiverRecordをスタート
③余計なクエリキャッシュを除き、最低限の(クエリキャッシュ済)変数が出来た時点で、ハッシュ化してやり、そのままviews側に投下
 →views側で分解してやることで、紐付け関係が余計なクエリキャッシュで崩れることを防いだ。

views側をcontrollerに合わせて、arrayではなくhashに対応させる

# app/views/qrs/index.html.erb

<% @warehouses.to_h.each do |warehouse, qrs| %>
  <div id="hoge<%= warehouse.id %>" role="tab" class="panel-hoge">
    <h4 class="panel-title">
      <a data-toggle="collapse" data-parent="#accordion" href="#collapse<%= warehouse.id %>"<%= warehouse.name %></a>
    </h4>
  </div>
  <div id="collapse<%= warehouse.id %>" role="fuga" aria-labelledby="hoge<%= warehouse.id %>" class="panel-collapse collapse">
    <div class="table-responsive">
      <table class="table table-hover">
        <tbody>
          <% qrs.each do |qr| %>
            <tr>
              <td><%= qr.staff.name %></td>
              <% if qr.shape.present? %><%= qr.shape.name %><% end %>
              <% if qr.size.present? %><%= qr.size.name %><% end %>
            </tr>
          <% end %>
        </tbody>
      </table>
    </div>
  </div>

まとめ

ビジネスサイドの担当者から、ページの描画速度が遅いという問合せがあり、今回のリファクタリングに取り組むことになった経緯でした。
いくら効率的なクエリキャッシュを行っても、大幅な速度改善には至らず、悩んでいたところ、先輩エンジニアからハッシュ化せよとの助言をもらい、上記の実装を行いました。
目から鱗だったので、同じ悩みを持つエンジニアがいるのではと思い、今回は記事にしてみました。

ちなみに、とにかくページ速度自体は改善したので、これで終わりにしたのですが、なぜ既存のクエリキャッシュ効率化で速度改善に至らないのか、原因は調べきれておりません。
このハッシュ化させる方法が最適解だったのかは、自分でも未だ確信を持つには至っていない為、「いや、これはアンチパターンである」ということであればご指摘いただけるとありがたいですm(_ _)m

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?