0
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問題の解消

Posted at

#はじめに
既存アプリにおいてActiveStorageで画像を取得する際発生していたN+1問題を改善したため、これを機にN+1問題についてまとめてみました。

##実行環境
・macOS
・Rails 6.1.4
・Ruby 2.6.7

#N+1問題とは
モデルに対してeach文などで繰り返しを行う時、そのモデルに紐付く要素に対してモデルの数分SQLクエリが発行されてしまう問題のこと。レコード数が多くなってしまうとレスポンスが遅くなるなどの影響が出てしまう。

自分のアプリで実際にN+1問題が起きていた箇所を例に見ていきます。

shop.rb
class Shop < ApplicationRecord
  belongs_to :user
end

userがshopを投稿するため上の関連付けになっています

shops_controller.rb
class ShopsController < ApplicationController
  def index
    @shops = Shop.all
  end
end

shopレコードは上のコードで取得します。

viewコード
<% if current_user?(shop.user) %>
  <div class="manipulate_shop">
    <%= link_to icon("far", "edit"), edit_shop_path(shop), class: "edit_icon" %>
    | <%= link_to icon("far", "trash-alt"), shop, method: :delete, data: { confirm: "本当に削除しますか?" }, class: "trash_icon" %>
  </div>
<% end %>

ログインユーザーが店舗の投稿主かどうかでレイアウトを変えるコードです。eachがありませんがshopを繰り返し読み込んでいます。
このviewコードを読み込む際N+1問題が以下のように発生します。

ターミナル
SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]

shopに紐付くuserの数だけSQLを発行しています。
このままではレコードが多くなるほどSQLも多く発行されるので、将来的にレスポンスが遅くなってしまいます。
そのため、コントローラでshopを取得するときにincludesを用いて一緒にそのshopを投稿したuserも取得します。

shops_controller.rb
class ShopsController < ApplicationController
  def index
    @shops = Shop.all.includes(:user)
  end
end

以下がincludesを使った後のSQLクエリです。一回の問い合わせでレコードを取得してくれるようになりました。
IN (1, 2, 3)は WHERE "users"."id" = 1 OR "users"."id" = 2 OR "users"."id" = 3と同じでusersテーブルからidが1か2か3がtrueになるものでテーブルを作成しています。

ターミナル
SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]

これでN+1問題が解消されました。

#ActiveStorageでのN+1問題
shopモデルに画像を紐づけるためにActiveStorageを使いimagesとしてアタッチしました。

shop.rb
class Shop < ApplicationRecord
  belongs_to :user
  has_many_attached :images
end

先ほどのshops_contorollerと同じindexアクションから取得した@shopsを使って以下のコードを書きました。
画像が紐づいているかどうかで画像表示を変えています。このshop.images.firstの部分でN+1問題が発生しました。

viewコード
<%= link_to shop do %>
  <% if shop.images.attached? %>
    <%= image_tag shop.images.first, class: "card-img" %>
  <% else %>
    <%= image_tag "noimage.png", class: "card-img" %>
  <% end %>
<% end %>

以下が発行されたSQLクエリです。N+1問題が発生しております。

ターミナル
SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 ORDER BY "active_storage_attachments"."id" ASC LIMIT $4  [["record_id", 3], ["record_type", "Shop"], ["name", "images"], ["LIMIT", 1]]
SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 5], ["LIMIT", 1]]
SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 ORDER BY "active_storage_attachments"."id" ASC LIMIT $4  [["record_id", 2], ["record_type", "Shop"], ["name", "images"], ["LIMIT", 1]]
SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 ORDER BY "active_storage_attachments"."id" ASC LIMIT $4  [["record_id", 1], ["record_type", "Shop"], ["name", "images"], ["LIMIT", 1]]
SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

ActiveStorageの場合はwith_attached_つけた名前でN+1問題を解消できます。shopレコードに関連するactive_storage_attachmentsとactive_storage_blobsをまとめて取得してくれます。

shops_controller
class ShopsController < ApplicationController
  def index
    @shops = Shop.all.includes(:user).with_attached_images
  end
end

以下が改善後のSQLクエリです。今回の場合、改善後により多くのデータを取得してしまっていますがSQLクエリの回数が抑えられています。

ターミナル
SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = $1 AND "active_storage_attachments"."name" = $2 AND "active_storage_attachments"."record_id" IN ($3, $4, $5)  [["record_type", "Shop"], ["name", "images"], ["record_id", 3], ["record_id", 2], ["record_id", 1]]
SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" IN ($1, $2, $3, $4, $5, $6, $7, $8)  [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5], ["id", 6], ["id", 7], ["id", 8]]

以上です。

0
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
0
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?