わからないことが一杯!!今回は ActiveRecord で JOIN するために使う :include オプションと :joins オプションの違いを調べてみました。ActiveRecord 2.3.5 です。

利用するデータとしては blog has many articles な関係で、1つの blog あたり10個の記事を持っているとします。具体的にはこのような状態です。

# blogs テーブル
+----+--------+---------------------+---------------------+
| id | name   | created_at          | updated_at          |
+----+--------+---------------------+---------------------+
|  1 | name_1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  2 | name_2 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  3 | name_3 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  4 | name_4 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
| .. |  ....  |   ...............   |   ...............   |

# articles テーブル
+----+----------+---------+---------------------+---------------------+
| id | title    | blog_id | created_at          | updated_at          |
+----+----------+---------+---------------------+---------------------+
|  1 | title_1  |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  2 | title_2  |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  3 | title_3  |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  4 | title_4  |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  5 | title_5  |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  6 | title_6  |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  7 | title_7  |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  8 | title_8  |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
|  9 | title_9  |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
| 10 | title_10 |       1 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
| 11 | title_1  |       2 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
| 12 | title_2  |       2 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
| 13 | title_3  |       2 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
| 14 | title_4  |       2 | 2010-04-24 14:48:58 | 2010-04-24 14:48:58 |
| .. |  .....   |      .. |   ...............   |   ...............   |

実際に、:include オプションを使った場合と :joins オプションを使った場合で試してみます。コメントで書かれている SQL が実際に発行される SQL です。

@blogs = Blog.all(:include => :articles)
# SELECT * FROM `blogs`
# SELECT `articles`.* FROM `articles` WHERE (`articles`.blog_id IN (1,2,3,4,5,6,....))

@blogs = Blog.all(:joins => :articles)
# SELECT `blogs`.* FROM `blogs` INNER JOIN `articles` ON articles.blog_id = blogs.id

どうですか? :joins の方が予想しやすい SQL が発行されているかなと思いますが、注意すべき二つの違いがあります。

1) 先読み(eager reading)されるのは :include の場合だけ
2) そもそも発行される SQL が全然違う!!:include は単純な JOIN じゃない

1) は今までも何となく知っていました。関連テーブルを先に読むのが :include 、必要になったら読むのが :joins です(よね?)。:joins は関連のテーブルを条件としてデータを引っ張ってくるとき(関連テーブルのデータ自体は取得する必要が無いとき)に使うのでしょう。

このように関連テーブルのデータを読み込む場合、:include だと SQL は発行されません(先読みしているため)が、:joins の場合はその都度、SQL が発行されます。

<% @blogs.each do |blog| -%>
  <%= blog.articles.each do |article| -%>
    <%= article.name %>
  <% end -%>
<% end -%>

が、2) はびっくりしました。全然発行される SQL が違うんですね。以前の ActiveRecord では :include は LEFT JOIN で、:joins が INNER JOIN ていう違いだった気がする。。
※ ActiveRecord 2.1 以前はそうだったようです→ eager loadingって何?

これは要注意な気がします。例えばこんな感じで LIMIT を付けた場合

@blogs = Blog.all(:include => :articles, :limit => 15)
# SELECT * FROM `blogs` LIMIT 15
# SELECT `articles`.* FROM `articles` WHERE (`articles`.blog_id IN (1,2,3,4,5,6,....,15))

@blogs = Blog.all(:joins => :articles, :limit => 15)
# SELECT `blogs`.* FROM `blogs` INNER JOIN `articles` ON articles.blog_id = blogs.id LIMIT 15

:include では blogs テーブルから15個のデータを取得して(id:1~15)、それに対応する articles テーブルのデータを取ってきます。それに対して :joins の場合は INNER JOIN した結果を15個取ってきます。この違いは怖い。。

もうちょっと実用的な例として :conditions で関連テーブルでの条件を付けてみます。その場合はこんな感じになりました(ちょっと見にくいですが)。:conditions で関連テーブルを指定すればどちらも同じ結果が取得できるようですね。ひと安心。

@blogs = Blog.all(:include => :articles, :conditions => {:articles => {:title => "title_1"}}, :limit => 15)
# SELECT `blogs`.* FROM `blogs` INNER JOIN `articles` ON articles.blog_id = blogs.id WHERE (`articles`.`title` = 'title_1') LIMIT 15

@blogs = Blog.all(:joins => :articles, :conditions => {:articles => {:title => "title_1"}}, :limit => 15)
# SELECT 
#   DISTINCT `blogs`.id 
# FROM 
#   `blogs` LEFT OUTER JOIN `articles` ON articles.blog_id = blogs.id 
# WHERE 
#   (`articles`.`title` = 'title_1') LIMIT 15
# SELECT 
#   `blogs`.`id` AS t0_r0, `blogs`.`name` AS t0_r1, `blogs`.`created_at` AS t0_r3, `blogs`.`updated_at` AS t0_r4, `articles`.`id` AS t1_r0, `articles`.`title` AS t1_r1, `articles`.`blog_id` AS t1_r2, `articles`.`created_at` AS t1_r4, `articles`.`updated_at` AS t1_r5 
# FROM 
#   `blogs` LEFT OUTER JOIN `articles` ON articles.blog_id = blogs.id 
# WHERE (`articles`.`title` = 'title_1') AND `blogs`.id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)

何はともあれ、:include で発行される SQL は直感的じゃないと思うので、どのような SQL が発行されているのかには気を付けたいところです!

ところで INNER JOIN で先読みさせたいときはどうしたらいいんでしょうか。。
このエントリーをはてなブックマークに追加