0
0

associationをキャッシュした場合の速度

Last updated at Posted at 2024-07-20

はじめに

N+1はパフォーマンスに影響することは知っていると思います。
ただ、パフォーマンスに影響するということはわかっても具体的にどれくらいの差があるのかを数値で見る機会は、なかなかったので、キャッシュしないものとキャッシュするものを比較してみたいと思います。

テーブル関係

親usersテーブル、子postsテーブルを用意します。

app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end
app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

seedの用意

以下のようなseedデータを用意します。データの件数は1000件です。

db/seeds.rb
1000.times do |n|
  user = User.create!(
    name: "山田 太郎",
    email: "sample#{n + 1}@sample.com"
  )

  Post.create!(
    user_id: user.id,
    title: "タイトル#{ n + 1 }",
    body: "これは~のであり、~である。"
  )
end

view側

usersテーブルに紐づくpostsテーブルの要素を表示しています。

<% @users.each do |user| %>
  <% user.posts.each do |post| %>
    <%= post.title %>
  <% end %>
<% end %>

joinsの速度

以下のコードを実行します。

@users = User.joins(:posts)

内部結合はassociationをキャッシュをしません。なのでActiveRecord: 329.3msの時間がかかっています。

User Load (2.4ms)  SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id`

Post Load (0.9ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 1
Post Load (0.5ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 2
Post Load (1.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 3
Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 4
Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 5
Post Load (0.2ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 6
---
Rendered users/test.html.erb within layouts/application (Duration: 1261.9ms | Allocations: 526914)
Rendered layout layouts/application.html.erb (Duration: 1264.0ms | Allocations: 529231)
Completed 200 OK in 1346ms (Views: 937.9ms | ActiveRecord: 329.3ms | Allocations: 588057)

left_joinsの速度

以下のコードを実行します。

@users = User.left_joins(:posts)

左外部結合もassociationをキャッシュしません。なので、joinsと同じような速度ActiveRecord: 297.3msになります。

User Load (1.6ms)  SELECT `users`.* FROM `users` LEFT OUTER JOIN `posts` ON `posts`.`user_id` = `users`.`id`

Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 1
Post Load (0.5ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 2
Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 3
Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 4
Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 5
Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` = 6
---
Rendered users/test.html.erb within layouts/application (Duration: 1127.0ms | Allocations: 531034)
Rendered layout layouts/application.html.erb (Duration: 1141.5ms | Allocations: 556218)
Completed 200 OK in 1210ms (Views: 853.5ms | ActiveRecord: 297.3ms | Allocations: 620498)

includesの速度

以下のコードを実行します。

@users = User.includes(:posts)

includesはassociationをキャッシュします。速度としてはActiveRecord: 4.9msとなりました。キャッシュしていないものに比べて約1/60の速度となっています。

User Load (0.9ms)  SELECT `users`.* FROM `users`
Post Load (2.7ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, 4, 5, 6,~,1000)

Rendered users/test.html.erb within layouts/application (Duration: 8.3ms | Allocations: 17176)
Rendered layout layouts/application.html.erb (Duration: 19.6ms | Allocations: 41066)
Completed 200 OK in 103ms (Views: 20.9ms | ActiveRecord: 4.9ms | Allocations: 140693)

eager_loadの速度

以下のコードを実行します。

@users = User.eager_load(:posts)

eager_loadはassociationをキャッシュします。速度としてはActiveRecord: 5.2msとなりました。キャッシュしていないものに比べてこちらも約1/60の速度となっています。
eager_loadはLEFT OUTER JOINを使用するので、SQLクエリが一つになります。また、eager_loadの場合はeager_load(:posts).where(posts: { id: 1 })のような絞り込みをすることができます。大量のデータを持たないテーブルで絞り込みたい場合はこちらを使うのがよさそうです。

SQL (3.0ms)  SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, 
`users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `posts`.`id` AS t1_r0, 
`posts`.`title` AS t1_r1, `posts`.`body` AS t1_r2, `posts`.`user_id` AS t1_r3, 
`posts`.`created_at` AS t1_r4, `posts`.`updated_at` AS t1_r5 FROM `users` LEFT OUTER JOIN 
`posts` ON `posts`.`user_id` = `users`.`id`

Rendered users/test.html.erb within layouts/application (Duration: 5.8ms | Allocations: 17194)
Rendered layout layouts/application.html.erb (Duration: 14.5ms | Allocations: 41078)
Completed 200 OK in 81ms (Views: 15.8ms | ActiveRecord: 5.2ms | Allocations: 143458)

preloadの速度

以下のコードを実行します。

@users = User.preload(:posts)

preloadはassociationをキャッシュします。速度としてはActiveRecord: 5.8msとなります。キャッシュしていないものに比べてこちらも約1/60の速度となっています。
preloadはjoinをしないので、大量のデータを持つテーブルや、複雑なクエリの場合はこちらを使うのが適しているでしょう。ただ、eager_loadのような子テーブルの絞り込みはpreloadではできません。

User Load (0.9ms)  SELECT `users`.* FROM `users`
Post Load (1.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1, 2, 3, 4, 5, 6,~,1000)

Rendered users/test.html.erb within layouts/application (Duration: 6.6ms | Allocations: 17180)
Rendered layout layouts/application.html.erb (Duration: 17.4ms | Allocations: 41355)
Completed 200 OK in 95ms (Views: 18.5ms | ActiveRecord: 5.8ms | Allocations: 141373)

まとめ

メソッド クエリ数 JOINの使用 絞り込み ActiveRecord時間 大量のデータを扱うのに適しているか アソシエーション
joins N + 1 はい 可能 約329.3ms いいえ キャッシュしない
left_joins N + 1 はい 可能 約297.3ms いいえ キャッシュしない
includes 1または2 場合による 場合による 約4.9ms 場合による キャッシュする
eager_load 1 はい 可能 約5.2ms いいえ キャッシュする
preload 2 いいえ いいえ 約5.8ms はい キャッシュする

includesは場合によって、eager_loadの挙動かpreloadの挙動を行うようです。
ただ、意図した挙動を行いたい場合は、eager_load、preloadを指定するのが良いかと思います。

参考

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