📅  2021-06-12

N+1問題を解決する

🔖Ruby on Rails🔖SQL

Railsでモデルの絞り込みなどをする場合にN+1問題に対処する必要があるが、よく includes で解決している記事を見る。しかしどうも万能ではないのであまり使わない方がいいことがわかった。

rails g scaffold artist name:string
# artist.rb
class Artist < ApplicationRecord
  has_many :albums
end

rails g scaffold album title:string artist:references
# album.rb
class Album < ApplicationRecord
  has_many :albums
  belongs_to :artist
end

N+1問題が発生するパターン

# albums_controller.rb
class AlbumsController < ApplicationController
  def index
    @albums = Album.all
  end
end
# albums/index.html.erb
<div>
  <% @albums.each do |album| %>
    <p><%= album.title %></p>
    <p><%= album.artist.name %></p>
  <% end %>
</div>

この場合だとSQLは

SELECT "albums".* FROM "albums" 
# Album.allの実行

SELECT "artists".* FROM "artists" WHERE "artists"."id" = 1 LIMIT 1 
SELECT "artists".* FROM "artists" WHERE "artists"."id" = 2 LIMIT 1
SELECT "artists".* FROM "artists" WHERE "artists"."id" = 3 LIMIT 1
# artist.nameをartistの数SQLを発行してしまう

となってしまい、N+1問題と言われている。

解決方法

@albums = Album.all.includes(:artist)
SELECT "albums".* FROM "albums"
SELECT "artists".* FROM "artists" WHERE "artists"."id" IN (?, ?, ?)
# artistテーブルから全てのidを取得してからまとめて1回で実行している

include について

include メソッドはアソシエーションによって preload もしくは eager_load が呼ばれているので、データの数が多くなるにつれて意図せず動作が遅くなってしまうことがある。

なので状況に応じて preload もしくは eager_load を使い分けた方が良い。

また複数のアソシエーションを渡した場合は必ずどちらか一方の挙動になる

使い分けの目安

上の例で試してみる

preload を使う

@albums = Album.all.preload(:artist)
SELECT "albums".* FROM "albums"
SELECT "artists".* FROM "artists" WHERE "artists"."id" IN (?, ?, ?)
# includes のときと同じSQLを発行している

eager_load を使う

@albums = Album.all.eager_load(:artist)
SELECT 
"albums"."id" AS t0_r0, 
"albums"."title" AS t0_r1, 
"albums"."artist_id" AS t0_r2, 
"albums"."created_at" AS t0_r3, 
"albums"."updated_at" AS t0_r4, 
"artists"."id" AS t1_r0, 
"artists"."name" AS t1_r1, 
"artists"."created_at" AS t1_r2, 
"artists"."updated_at" AS t1_r3 FROM "albums" LEFT OUTER JOIN "artists" ON "artists"."id" = "albums"."artist_id"
# JOINを使っている

ActiveRecordのincludes, preload, eager_load の個人的な使い分け | Money Forward Engineers' Blog

ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由 - Qiita

タグ