memo.log

技術情報の雑なメモ

【Rails】N+1のメモ( where するとキャッシュが効かないからRuby上で計算すること)

前提

  • 「カード」と「カードセット」というモデルがあるとする
  • 「カードセット」に複数の「カード」が紐づいているとする
  • 「カード」には「remembered」属性がある
  • あるページで各カードセットのカードの総数や「remembered」の true の数をカウントする
  • したがって、単純に実装するとN+1が発生する

サンプルコード

# == Schema Information
#
# Table name: cards
#
#  id          :bigint           not null, primary key
#  remembered  :boolean          not null
#  card_set_id :bigint           not null
#  created_at  :datetime         not null
#  updated_at  :datetime         not null
#
class Card < ApplicationRecord
  belongs_to :card_set
end


# == Schema Information
#
# Table name: card_sets
#
#  id         :bigint           not null, primary key
#  name       :string           not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class CardSet < ApplicationRecord
  has_many :cards
end

メモ

コントローラーで以下のように実装すると(1)(2)両方で、クエリが発行されてN+1状態になってしまう。

  def test
    @card_sets = CardSet.all
    @card_sets.each do |card_set|
      card_set.cards.size # (1)
      card_set.cards.where(remembered: true).size # (2)
    end
  end

そこで以下のようにカードセットを取得するときに eager_load すると上記の (1) でキャッシュが効き、クエリが発行されなくなる。

@card_sets = CardSet.all.eager_load(:cards)

しかし、(2)ではまだクエリが都度発行されてしまう。これは where メソッドが都度SQLを発行してしまうことによる。Ruby上では各カードの関連データをキャッシュしているので、 (2) を以下のようにRuby上で計算するように変更する。

card_set.cards.select(&:remembered).size

するとクエリの発行が無くなりN+1を回避できる。

さらにメモ

preload は単純にSQLが2回発行される

CardSet.where(user_id: user_id).preload(:cards)
SELECT "card_sets".* FROM "card_sets" WHERE "card_sets"."user_id" = $1 LIMIT $2 OFFSET $3  [["user_id", 1], ["LIMIT", 10], ["OFFSET", 0]]
SELECT "cards".* FROM "cards" WHERE "cards"."card_set_id" IN ($1, $2, $3)  [["card_set_id", 1], ["card_set_id", 2], ["card_set_id", 3]]

eager_load は LEFTER JOIN するテーブルがキャッシュされる

CardSet.where(user_id: user_id).eager_load(:cards)
SELECT DISTINCT "card_sets"."id" FROM "card_sets" LEFT OUTER JOIN "cards" ON "cards"."card_set_id" = "card_sets"."id" WHERE "card_sets"."user_id" = $1 LIMIT $2 OFFSET $3  [["user_id", 1], ["LIMIT", 10], ["OFFSET", 0]]
SELECT "card_sets"."id" AS t0_r0, "card_sets"."public" AS t0_r1, "card_sets"."forked" AS t0_r2, "card_sets"."name" AS t0_r3, "card_sets"."user_id" AS t0_r4, "card_sets"."created_at" AS t0_r5, "card_sets"."updated_at" AS t0_r6, "cards"."id" AS t1_r0, "cards"."answer" AS t1_r1, "cards"."question" AS t1_r2, "cards"."hint" AS t1_r3, "cards"."remembered" AS t1_r4, "cards"."card_set_id" AS t1_r5, "cards"."created_at" AS t1_r6, "cards"."updated_at" AS t1_r7 FROM "card_sets" LEFT OUTER JOIN "cards" ON "cards"."card_set_id" = "card_sets"."id" WHERE "card_sets"."user_id" = $1 AND "card_sets"."id" IN ($2, $3, $4)

関連メモ

zenn.dev

tech.stmn.co.jp

zenn.dev

moneyforward-dev.jp