preload, eager_load, include, joins が実際にどういうSQLを吐くかを確かめた。※Rails4/5両方で確認。
※説明に使用している各テーブルは blogs ← entries ← comments という感じにFK参照している。DDLは最後の方に記載。
いきなりまとめ
- preload
- SELECT複数回(親子で1回ずつ)
- 子テーブルでの絞り込みは不可
- eager_load
- LEFT OUTER JOIN
- 子/孫テーブルでの絞り込みも可能
- include
- preloadとeager_loadのハイブリッド
-
絞り込み条件なし OR 親テーブルへの絞り込みのみの場合 => preload
子/孫テーブルへの絞り込み条件が指定されている場合 => eager_load - joins ※これだけ少し毛色が異なる
- INNER JOIN
- 結果がベタに(=1行1インスタンスで)返ってくる
preload: 2段階SELECT(子テーブルの先読み)
- 親テーブル/子テーブルで1回ずつSELECTが走る
- 子テーブル側では絞り込めない
# 子テーブル(?)側の先読み
# SELECT * FROM blogs WHERE id IN (1, 2)
# SELECT * FROM entries WHERE blog_id IN (1, 2)
r = Blog.preload(:entries).where(id: [1, 2])
r.each do |b|
p b
p b.entries
end
# 子テーブル側では絞り込めない
r = Blog.preload(:entries).where(entrires: { id: [1, 2] } )
# PG::UndefinedTable - ERROR: missing FROM-clause entry for table "entrires"
# LINE 1: SELECT "blogs".* FROM "blogs" WHERE "entrires"."id" IN ($1, ...
# ^
eager_load: LEFT OUTER JOIN
# SELECT *
# FROM entries E LEFT OUTER JOIN comments C ON C.entry_id = E.id
# WHERE E.id IN (3, 4, 5)
r = Entry.eager_load(:comments).where(id: [*3..5])
r.each do |b|
p b
b.comments.each do |c|
puts "======="
p c
end
end
2段階に(孫テーブルも)LEFT OUTER JOIN + 子テーブルでの絞り込み
r = Blog.eager_load(entries: :comments).where(entries: {id: [1, 2]})
SELECT *
FROM blogs B
LEFT OUTER JOIN entries E ON E.blog_id = B.id
LEFT OUTER JOIN comments C ON C.entry_id = E.id
WHERE E.id IN (1, 2)
子/孫テーブルの絞り込み条件をハッシュで渡せない場合でも references をかまさなくて良い(includesとは違う点)
r = Blog.eager_load(entries: :comments).where("comments.id <= ?", 2)
includes: preload
と eager_load
を自動切り替え
絞り込みなし or 親テーブルでの絞り込みのみ の場合は preload
(親/子テーブルで1回ずつSELECTが走る)
子テーブル/孫テーブルで絞り込んだ場合は eager_load
(LEFT OUTER JOIN)
ではまず、普通に子テーブルまで先読みした例。絞り込みは親テーブルに対してのみ。
# 以下2つのSELECTが実行される
# SELECT * FROM entries WHERE id = 1
# SELECT * FROM comments WHERE entry_id IN (1)
r = Entry.includes(:comments)
.where(id: 1)
r.each do |e|
# 親テーブル側のインスタンス
p e
# コレクションとして保持されている
p e.comments
end
2段階に(孫テーブルまで)先読みする例。
こちらも絞り込みは親テーブルのみ。
# SELECTが3回走る
# SELECT * FROM blogs WHERE (2 <= id)
# SELECT * FROM entries WHERE blog_id IN (2, 3)
# SELECT * FROM comments WHERE entry_id IN (1, 2)
r = Blog.includes(entries: :comments).where("? <= id", 2)
r.each do |b|
puts "###### Blog #{b.id}"
p b
b.entries.each do |e|
puts " ====== Entry #{e.id}"
p e
e.comments.each do |c|
puts " -------"
p c
end
end
end
子テーブル側で絞り込むと LEFT OUTER JOIN に。
r = Blog.includes(entries: :comments).where(entries: { id: 1 })
SELECT *
FROM blogs B
LEFT OUTER JOIN entries E ON E.blog_id = B.id -- ←OUTERを使っているが、ここはINNERでも結果は同じ
LEFT OUTER JOIN comments C ON C.entry_id = E.id
WHERE E.id = 1 --←子テーブルentriesのidで絞り込んでいる
絞り込み条件をハッシュで渡せないときは references
をかます必要があるっぽい。生成されるSQLはWHERE句以外は上のと同じ。
r = Blog.includes(entries: :comments).references(:entries).where("entries.id < ?", 2)
孫テーブルで絞り込む ⇒やっぱりLEFT OUTER JOIN
r = Blog.includes(entries: :comments).where(comments: { id: [*1..3] })
SELECT *
FROM blogs B
LEFT OUTER JOIN entries E ON E.blog_id = B.id
LEFT OUTER JOIN comments C ON C.entry_id = E.id
WHERE C.id IN (1, 2, 3) -- 孫テーブルcommentsのidで絞り込んでいる
joins: 単なる INNER JOIN
こいつだけ他のメソッドとは毛色がちょっと違う。
- 先読みではなく、結果がベタに(=1レコード1インスタンスで)返ってくる
- JOINしたテーブルのカラムは
select
で明示的に指定しないと見えない
# SELECT blogs.*, entries.title AS entry_title, entries.body
# FROM blogs
# INNER JOIN entries
# ON entries.blog_id = blogs.id
# WHERE blogs.id = 1
#
r = Blog.joins(:entries)
.where(id: 1)
.select("blogs.*, entries.title AS entry_title, entries.body")
r.each do |e|
p e # Blogの属性しか見えない
p e.attributes # Blog以外の属性が格納されていることが分かる
# blogsテーブルの属性も、entriesテーブルの属性も読める
puts "blog title: #{e.title}, entry title: #{e.entry_title}"
end
2段階(子/孫テーブルへの)INNER JOINもいける。
r = Blog.joins(entries: :comments)
.where(id: 1)
.select("blogs.*, entries.title AS entry_title, entries.body, comments.body AS comment_body")
参考: 説明で使用しているテーブルの構成
-- ブログテーブル
CREATE TABLE blogs
(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
title VARCHAR(255),
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
-- エントリーテーブル
CREATE TABLE entries
(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
title VARCHAR(255),
body TEXT,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
blog_id INT(11),
CONSTRAINT fk_rails_1c7d1d2722 FOREIGN KEY (blog_id) REFERENCES blogs (id)
);
CREATE INDEX index_entries_on_blog_id ON entries (blog_id);
-- コメントテーブル
CREATE TABLE comments
(
id INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
body TEXT,
status VARCHAR(255),
entry_id INT(11),
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
CONSTRAINT fk_rails_24508b7aa5 FOREIGN KEY (entry_id) REFERENCES entries (id)
);
CREATE INDEX index_comments_on_entry_id ON comments (entry_id);
環境
OS | Ruby | Rails(ActiveRecord) |
---|---|---|
Windows7 x64 | 2.2 | 4.2.6 |
macOS Mojave | 2.6.1 | 5.2.2 |
参考サイト
ActiveRecordのjoinsとpreloadとincludesとeager_loadの違い - Qiita
似ているようで全然違う!?Activerecordにおけるincludesとjoinsの振る舞いまとめ - Qiita
Rails Guides - Active Record クエリインターフェイス