235
210

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ActiveRecord ~ 複数テーブルにまたがる検索(preload, eager_load, include, joins)

Last updated at Posted at 2016-06-16

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 + 子テーブルでの絞り込み

ruby
r = Blog.eager_load(entries: :comments).where(entries: {id: [1, 2]})
SQL
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とは違う点)

ruby
r = Blog.eager_load(entries: :comments).where("comments.id <= ?", 2)

includes: preloadeager_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 に。

ruby
r = Blog.includes(entries: :comments).where(entries: { id: 1 })
SQL
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句以外は上のと同じ。

ruby
r = Blog.includes(entries: :comments).references(:entries).where("entries.id < ?", 2)

孫テーブルで絞り込む ⇒やっぱりLEFT OUTER JOIN

ruby
r = Blog.includes(entries: :comments).where(comments: { id: [*1..3] })
SQL
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もいける。

ruby
r = Blog.joins(entries: :comments)
        .where(id: 1)
        .select("blogs.*, entries.title AS entry_title, entries.body, comments.body AS comment_body")

参考: 説明で使用しているテーブルの構成

DDL
-- ブログテーブル
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 クエリインターフェイス

235
210
1

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
235
210

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?