LoginSignup
1
0

More than 3 years have passed since last update.

eager_load/preload/includes/joinsの違いメモ

Last updated at Posted at 2019-11-26

動機

日頃Goしか書いてないGopherがRails書いたときにつまったActiveRecord、特にjoins周りの挙動が直感的によくわからなかったので実際にデータ用意して調べてみた
(メソッドの具体的な説明は他にたくさん記事があるのでそちらを参照してください。)

実行環境

Rails公式Docのサンプルブログアプリ
article.rbcomment.rb のモデルを使用。
今回は、それぞれ約25万行を用意した。

article.rb
class Article < ApplicationRecord
  has_many :comments, dependent: :destroy
  validates :title, presence: true,
                    length: { minimum: 5 }
end
comment.rb
class Comment < ApplicationRecord
  belongs_to :article
end

Controllerから preload/eager_load/joins/includes を用い、それぞれの吐くSQLやパフォーマンスを比較してみる

0. 素

Article.limit(100)

SELECT `articles`.* FROM `articles` LIMIT 100
-- Completed 200 OK in 154ms (Views: 127.1ms | ActiveRecord: 15.7ms | Allocations: 20418)

まずは検証用にそのままで...

1. eager_load

Article.eager_load(:comments).limit(100)

SQL (1.9ms)  SELECT DISTINCT `articles`.`id` FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` LIMIT 100
SQL (575.4ms)  SELECT `articles`.`id` AS t0_r0, `articles`.`title` AS t0_r1, `articles`.`text` AS t0_r2, `articles`.`created_at` AS t0_r3, `articles`.`updated_at` AS t0_r4, `comments`.`id` AS t1_r0, `comments`.`commenter` AS t1_r1, `comments`.`body` AS t1_r2, `comments`.`article_id` AS t1_r3, `comments`.`created_at` AS t1_r4, `comments`.`updated_at` AS t1_r5 FROM `articles` LEFT OUTER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 4869ms (Views: 4280.1ms | ActiveRecord: 585.9ms | Allocations: 2103879)

万能感ある。駆動表のキャッシュをしながら、絞り込みもできる。IN長すぎワロタ。

2. preload

Article.preload(:comments).limit(100)

Article Load (1.9ms)    SELECT `articles`.* FROM `articles` LIMIT 100
Comment Load (405.8ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 4304ms (Views: 3883.6ms | ActiveRecord: 416.9ms | Allocations: 1658640)

eager_loadより省エネな感じ。ただ、最後のSELECT文がcommentsテーブルしか参照してないので絞り込みができない

3. includes

Article.includes(:comments).limit(100)


Article Load (1.5ms)  SELECT `articles`.* FROM `articles` LIMIT 100
Comment Load (384.1ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` 
IN (1, 2, 3, 4, 6, 7, 8, 9, 13, 14, 15, 16, 17, 18, 19, 20, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157)
-- Completed 200 OK in 3737ms (Views: 3339.1ms | ActiveRecord: 395.3ms | Allocations: 1652329)

今回はpreloadと同じ挙動
(includes は場合によって挙動が変わる. 最後のSELECT文にないテーブルのwhereとかはできないので、そういう時はeager_loadになる)

def eager_loading?
  @should_eager_load ||=
    eager_load_values.any? ||
      includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
end

4. joins

Article.joins(:comments).limit(100)

Article Load (2.1ms)  SELECT `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` LIMIT 100
-- Completed 200 OK in 67ms (Views: 56.1ms | ActiveRecord: 7.0ms | Allocations: 21703)

いつものアレ。安心感ある。

まとめ

そもそもActiveRecordは LazyLoad、すなわち 必要になったときにSQLが実行される
for文の中でSQLが複数回発行(N+1)されないために、こういう機構が必要になってくる。
(生SQLerの私にはこの感覚があんまりない)

後は、where使いたいなら eager_load 何もしないなら preload って感じ。
頭使いたくない人は 「とりあえず includes」 しとけばなんとかなる。
ただ、複数紐づくテーブル扱い出すと最終的に吐き出されるSQLが予測困難になるので、ちゃんと使い分けたいところ。
ちゃんと使い分けたい人はこの記事とか読みましょう。実装追ってて勉強になります。

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