Ruby on Rails N+1 対策
Query での N + 1 対策の手段を整理する
そもそもテーブル構造から対策する、キャッシュを用いる、みたいな話は除外する
手を動かせるように環境を作成する
諸々初期化する
code:console
$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) x86_64-linux
$ rails -v
Rails 7.0.6
$ rails new . --api
手を動かして N + 1 を対策を感じるに当たって 1:N や N:M のモデルを作成する
今回はUserとBlogが 1:N の関係、BlogとTagが N:M の関係となる
code:console
$ rails g model user name:string
$ rails g model blog title user:references
$ rails g model tag name:string
$ rails g model BlogTag blog:references tag:references
$ rails db:migrate
データを作成する
db/seeds.rbを以下のように編集
code:rb
User.create!([
{ name: "User A" },
{ name: "User B" },
])
Blog.create!([
{ title: "User A's Blog 1", user_id: 1 },
{ title: "User A's Blog 2", user_id: 1 },
{ title: "User B's Blog 1", user_id: 2 },
{ title: "User B's Blog 2", user_id: 2 },
])
Tag.create!([
{ name: "Tag 1" },
{ name: "Tag 2" },
{ name: "Tag 3" },
])
BlogTag.create!([
{ blog_id: 1, tag_id: 1 },
{ blog_id: 1, tag_id: 2 },
{ blog_id: 2, tag_id: 2 },
{ blog_id: 2, tag_id: 3 },
{ blog_id: 3, tag_id: 3 },
])
seed を DB に流し込む
code:console
$ rails db:seed
双方向で参照できるようにする
code:rb
# それぞれファイルはバラバラだけどまとめて表示
class User < ApplicationRecord
has_many :blogs
end
class Blog < ApplicationRecord
belongs_to :user
has_many :blog_tags
has_many :tags, through: :blog_tags
end
class Tag < ApplicationRecord
has_many :blog_tags
has_many :blogs, through: :blog_tags
end
class BlogTag < ApplicationRecord
belongs_to :blog
belongs_to :tag
end
一応確認
code:console
$ rails c
User.all # 登録したデータが表示されるはず
以下は全部 rails cのコンソール上で実行する
いきなり性能比較とかされてもわからんので、まずはそれぞれが吐く SQL を眺める
joins
code:rb
# 文字列として User.joins(:blogs).to_sql を出力しないとエスケープがついて見にくくなる
puts User.joins(:blogs).to_sql
SELECT "users".* FROM "users" INNER JOIN "blogs" ON "blogs"."user_id" = "users"."id"
ただの内部結合
left_outer_join
code:rb
puts User.left_outer_joins(:blogs).to_sql
SELECT "users".* FROM "users" LEFT OUTER JOIN "blogs" ON "blogs"."user_id" = "users"."id"
puts Blog.left_outer_joins(:user).to_sql
SELECT "blogs".* FROM "blogs" LEFT OUTER JOIN "users" ON "users"."id" = "blogs"."user_id"
ただの LEFT OTUER JOIN
.joinsとは違って、どちらのモデルに対して .left_outer_joinsするかで結果が変わるので注意
eager_load
code:rb
puts User.eager_load(:blogs).to_sql
SELECT "users"."id" AS t0_r0,
"users"."name" AS t0_r1,
"users"."created_at" AS t0_r2,
"users"."updated_at" AS t0_r3,
"blogs"."id" AS t1_r0,
"blogs"."title" AS t1_r1,
"blogs"."user_id" AS t1_r2,
"blogs"."created_at" AS t1_r3,
"blogs"."updated_at" AS t1_r4
FROM "users"
LEFT OUTER JOIN "blogs" ON "blogs"."user_id" = "users"."id"
やっていることはただの LEFT OUTER JOIN なんだ
JOIN の違いはなんだ??(↓で記載)
あと t0_r0とかエイリアスいる??別につけなくても各カラムの一意性は保てるのでは?
preload
code:rb
# なんと puts は出力した SQL の一行目しか表示しないことが発覚、代わりに explain を使用
User.preload(:blogs).explain
# 出力は要約している
SELECT "users".* FROM "users"
SELECT "blogs".* FROM "blogs" WHERE "blogs"."user_id" IN (?, ?, ?, ?) "user_id", 1], "user_id", 2, "user_id", 3, ["user_id", 4
まぁわかるよ
JOIN と比べてどっちが効率いいんだろう
Userがwhereで検索される場合は、joinsに比べて余分な User Record を読まないから効率よしかも
code:rb
User.preload(:blogs).where(name: "User A").explain
SELECT "users".* FROM "users" WHERE "users"."name" = ? "name", "User A"
SELECT "blogs".* FROM "blogs" WHERE "blogs"."user_id" IN (?, ?) "user_id", 1], ["user_id", 3
ほらね
includes
code:rb
User.includes(:blogs).explain
SELECT "users".* FROM "users"
SELECT "blogs".* FROM "blogs" WHERE "blogs"."user_id" IN (?, ?, ?, ?) "user_id", 1], "user_id", 2, "user_id", 3, ["user_id", 4
preload と何か変わったか?
それぞれの違いを調べた
joins
ActiveRecord::QueryMethods
associaction を cache しない
eager_load
ActiveRecord::QueryMethods
Eager Load は一般用語らしい
Rails の固有言語だと思っていた
というか joinsのようなメモリキャッシュを行わないのもを Lazy Loading, eager_load preload includes などのメモリキャッシュを行うものを総じて Eager Load というそうな
ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由 - Qiita
やっていることは join と同じだかが Association の Cache も行う
preload
ActiveRecord::QueryMethods
eager_loadと違ってテーブル全体を join しないので、join 先のテーブルが大きいが条件で絞れる場合はこちらのほうがパフォーマンスが良い
preloadの引数に渡した Table に条件は渡せない
Association の Cache も行う
includes
ActiveRecord::QueryMethods
eager_loadとpreloadをよしなに使い分けてくれる認識
その他参考にしたもの
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita
preload、eager_load、includesの挙動を理解して使い分ける - stmn tech blog
感想
そもそもどんな SQL が DB に優しいかを知らないと、各メソッドが吐く SQL を評価できないな
まぁ当然ね