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?

preload、eager_load、includesの違いをいつも忘れてしまうのでメモしておく

Posted at

概要

忘れん坊な自分のために、preload、eager_load、includesの違いをメモしておきます😮‍💨

これらのメソッドは何のためにあるのか

結論、N+1問題が発生しないようにするため。

特定のレコードを取得する際、これらのメソッドを使って関連レコードを先読みして取得しておくことでN+1が発生しません。

N+1のおさらい

N+1とは データベースへのアクセス回数が余計に多くなってしまう現象 です。

例えば全てのユーザーの名前と、関連する最初の投稿(posts)のタイトルを出力するコードがあるとします。

users = User.all

users.each do |user|
  puts "#{user.name}の最初の投稿は#{user.posts.first}です。"
end

上記を実行した時のSQLは以下になります。

SELECT "users".* FROM "users"

SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 2 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 3 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 4 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 5 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 6 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 7 ORDER BY "posts"."id" ASC LIMIT 1
SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 8 ORDER BY "posts"."id" ASC LIMIT 1

eachの内部でuser.posts.firstが呼び出される度に、postsを取得する余分なSELECT文が発行されており、N+1が発生しています。

少ないレコード件数であればそこまで問題にはならないですが、これが10万、100万件の場合はパフォーマンスに大きな影響が出てしまいます。

includes、preload、eager_loadでN+1は解消できる

しかし、以下のようにincludes(preload、eager_loadでも同じ)を呼び出した上でeachすれば

users = User.includes(:posts)

users.each do |user|
  puts "#{user.name}の最初の投稿は#{user.posts.first}です。"
end

users取得時に関連するpostsを一括取得するSQLが発行されるので、N+1が起きません。

SELECT "users".* FROM "users"

SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (1, 2, 3, 4...)

ではそれぞれの違いはなに?

関連レコードを一緒に読み込むという点では一緒ですが、発行されるSQLに違いがあります。

preload

関連レコードを別のクエリ(SELECT)で取得します。

users = User.preload(:posts)
SELECT "users".* FROM "users"

SELECT "posts".* FROM "posts" WHERE "user_id" IN (1, 2, 3, 4...)

特徴

  • データ量が大きいものを扱う場合にeager_load(left joinを)よりも早く取得できる
    • ただしレコード件数が非常に大きい場合、IN句に指定される数も膨大になるため、以下のようなSQLのメモリ上限に気を付ける
      • SQLのサイズがmax_allowed_packetの設定値を超えた場合にエラーとなる
      • range_optimizer_max_mem_sizeに設定された範囲検索のメモリ上限をオーバーしてしまい、インデックスが使われない
  • 関連レコードを複数指定した場合、その数だけ発行されるクエリが増える
  • whereなどで関連レコードの絞り込みができない(絞り込もうとするとエラーになる)

eager_load

関連レコードをLEFT OUTER JOINを使って一つのクエリで一括取得します。

users = User.eager_load(:posts)
SELECT "users".* FROM "users"
LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"

特徴

  • whereなどでレコードの絞り込みができる
  • 関連レコードを複数指定した場合でも一つのクエリで済む
  • left joinでテーブル同士を結合するため、データ量が大きいものを扱う場合にスロークエリとなる可能性がある

left joinでテーブル同士を結合するため、データ量が大きいものを扱う場合にスロークエリとなる可能性がある

こちらですが、特にhas_manyアソシエーションかつデータ量が大きい場合はスロークエリが発生しやすいので注意が必要です。

User.eager_load(:has_many_association).limit(10)

たとえば、上記のようにhas_manyの関連先があるUserをlimit(10)で絞り込む場合、eager_loadを使うと1対N関連のテーブルをleft joinで取得するため、重複を含んでいた場合は10件以上のレコードが取得されてしまいます。

なので、ActiveRecord内部では「has_manyをeager_loadしているかつ、limit or offsetで絞り込みしている場合」にdistinct付きのSQLが発行されるようになっている(参考)のですが、データ量が大きいと、このdistinctが要因でスロークエリとなりやすいわけです。

includes

Railsが自動的にpreload、eager_loadを使い分けてくれます。

includesのみを使った場合は、preloadが内部的に使われます。

users = User.includes(:posts)
SELECT "users".* FROM "users"
SELECT "posts".* FROM "posts" WHERE "user_id" IN (1, 2, 3, 4...)

includesと絞り込みのクエリ(whereやlimit, offsetなど)を使った場合は、eager_loadが内部的に使われます。

users = User.includes(:posts).where(posts: {title: 'preload、eager_load、includesの違いをいつも忘れてしまうのでメモしておく'})
SELECT "users".* FROM "users" 
LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" 
WHERE "posts"."title" = 'test'

特徴

  • 絞り込みが使える/使えないを意識しなくてもrailsがよしなに変換してくれるので楽
  • 使い方によってクエリが変わるので、何も考えずに使うとスロークエリになる可能性がある

それぞれどう使い分けるのが良いか

使い方によってクエリが変わっても良いという場合や、パフォーマンスを気にしないのであれば、深く考えずにincludesを使うで良いと思います。

しかしクエリを制御したいという場合や、パフォーマンスを気にするのであれば、発行されるSQLを意識した上でeager_loadとpreloadを使い分けた方が良いです。


個人的な目安としては以下のように考えています。

preloadを使う場合

  • 関連レコードで絞り込みの必要がない
  • データ量が大きいものを扱う
  • 取得する関連先が多い
    • eager_loadだとleft joinで関連先が多ければ多いほど重くなる可能性がある

eager_loadを使う場合

  • 関連レコードで絞り込みが必要
  • データ量がそこまで大きくないものを扱う
  • has_manyでない(has_one)テーブルで絞り込みを行うとき

終わりに

これで違いを忘れることがなさそうです😌

またパフォーマンス向上のためには、Active Recordのメソッドを使う際にどのようなSQLが発行されるかを意識していくことが大事だと改めて感じました。

参考にさせていただいた記事

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?