Rails :include和:joins的区别

371
这更像是一个“为什么事情会这样工作”的问题,而不是“我不知道如何做到这一点”的问题...
因此,在获取已知将要使用的关联记录时,所传授的方法是使用:include,因为您会得到连接并避免了大量的额外查询:
Post.all(:include => :comments)

但是当你查看日志时,没有发生任何连接:

Post Load (3.7ms)   SELECT * FROM "posts"
Comment Load (0.2ms)   SELECT "comments.*" FROM "comments" 
                       WHERE ("comments".post_id IN (1,2,3,4)) 
                       ORDER BY created_at asc) 

它正在采取一种捷径,因为它一次性获取所有评论,但它仍然不是一个联接(这就是所有文档似乎都在说的)。我能够获得联接的唯一方法是使用:joins而不是:include

Post.all(:joins => :comments)

日志显示:

Post Load (6.0ms)  SELECT "posts".* FROM "posts" 
                   INNER JOIN "comments" ON "posts".id = "comments".post_id
我有遗漏的吗?我有一个应用程序,有半打关联,在一个屏幕上显示来自所有关联的数据。看起来最好只使用一个连接查询而不是6个单独的查询。我知道性能方面并不总是连接查询比单独查询更好(实际上如果按时间计算,上面两个单独查询似乎比连接查询更快),但在阅读了所有文档之后,我很惊讶看到:include没有像广告中宣传的那样工作。
也许Rails确实意识到性能问题,并且只在某些情况下进行连接查询?

3
如果您使用的是较旧版本的Rails,请通过标签或问题正文说明。否则,如果您现在正在使用Rails 4,则应该使用includes(对于任何阅读此内容的人)。 - onebree
这篇 Scout 工程博客文章是一个不错的参考:https://scoutapm.com/blog/activerecord-includes-vs-joins-vs-preload-vs-eager_load-when-and-where - Allison
现在还有:preload:eager_load:https://www.bigbinary.com/blog/preload-vs-eager-load-vs-joins-vs-includes - Dave Powers
8个回答

185

看起来Rails 2.1版本改变了:include功能。Rails以前在所有情况下都会执行连接,但出于性能原因,现在在某些情况下改为使用多个查询。Fabio Akita的博客文章有关于这一变化的一些很好的信息(请查看“Optimized Eager Loading”部分)。


2
请参考:http://samsaffron.com/archive/2008/03/15/You+should+be+very+careful+when+using+ActiveRecord+eager+loading - Sam Saffron
这非常有帮助,谢谢。不过我希望有一种方法可以强制Rails进行连接,即使没有需要它的'where'条件。在某些情况下,您知道连接将更有效,并且不会产生重复的风险。 - Jonathan Swartz
@JonathanSwartz 现在已经支持Eager load - Dave Powers

121

.joins 只是简单地连接表并返回选定的字段。如果您在连接查询结果上调用关联对象,那么它将再次触发数据库查询。

:includes 将预加载包含的关联对象并将它们存储在内存中。:includes 加载所有包含的表属性。如果您在 include 查询结果上调用关联对象,那么它不会触发任何查询。


79
使用 include 语句的区别在于它会生成一个更大的 SQL 查询,将其他表中的所有属性加载到内存中。例如,如果您有一个包含评论的表,并使用 :joins => users 来检索所有用户信息以进行排序等操作,则此方法可以正常运行且所需时间较少。但是,如果要显示评论以及用户姓名、电子邮件等信息,则使用 :joins 获取信息需要为每个获取的用户单独执行 SQL 查询,而如果使用 :include,则已准备好这些信息供使用。
良好的示例: http://railscasts.com/episodes/181-include-vs-joins

69
我最近在阅读Rails中:joins:includes之间的区别。以下是我理解的解释(附有示例):
考虑以下情况:
  • 用户(User)有多条评论(Comment),每条评论属于一个用户。

  • 用户(User)模型具有以下属性: 姓名(Name)(字符串), 年龄(Age)(整数)。评论(Comment)模型具有以下属性:内容(Content), 用户ID(user_id). 评论可以没有用户ID。

Joins:

:joins在两个表之间执行内连接(inner join)操作。因此,

Comment.joins(:user)

#=> <ActiveRecord::Relation [#<Comment id: 1, content: "Hi I am Aaditi.This is my first   comment!", user_id: 1, created_at: "2014-11-12 18:29:24", updated_at: "2014-11-12 18:29:24">, 
     #<Comment id: 2, content: "Hi I am Ankita.This is my first comment!", user_id: 2, created_at: "2014-11-12 18:29:29", updated_at: "2014-11-12 18:29:29">,    
     #<Comment id: 3, content: "Hi I am John.This is my first comment!", user_id: 3, created_at: "2014-11-12 18:30:25", updated_at: "2014-11-12 18:30:25">]>

将获取所有用户ID等于comments表中的user_id(users表)的记录。因此,如果您执行

Comment.joins(:user).where("comments.user_id is null")

#=> <ActiveRecord::Relation []>

你将会得到一个空数组,如下所示。
此外,联结不会将关联的表加载到内存中。因此,如果执行以下操作:
comment_1 = Comment.joins(:user).first

comment_1.user.age
#=> User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT 1 [["id", 1]]
#=> 24

正如您所看到的,comment_1.user.age 会在后台再次发出数据库查询以获取结果。

includes:

:includes 在两个表之间执行 左外连接。因此,

Comment.includes(:user)

#=><ActiveRecord::Relation [#<Comment id: 1, content: "Hi I am Aaditi.This is my first comment!", user_id: 1, created_at: "2014-11-12 18:29:24", updated_at: "2014-11-12 18:29:24">,
   #<Comment id: 2, content: "Hi I am Ankita.This is my first comment!", user_id: 2, created_at: "2014-11-12 18:29:29", updated_at: "2014-11-12 18:29:29">,
   #<Comment id: 3, content: "Hi I am John.This is my first comment!", user_id: 3, created_at: "2014-11-12 18:30:25", updated_at: "2014-11-12 18:30:25">,    
   #<Comment id: 4, content: "Hi This is an anonymous comment!", user_id: nil, created_at: "2014-11-12 18:31:02", updated_at: "2014-11-12 18:31:02">]>

这将导致一个包含来自评论表的所有记录的联接表。 因此,如果您执行

Comment.includes(:user).where("comment.user_id is null")
#=> #<ActiveRecord::Relation [#<Comment id: 4, content: "Hi This is an anonymous comment!", user_id: nil, created_at: "2014-11-12 18:31:02", updated_at: "2014-11-12 18:31:02">]>

它将获取评论用户ID为空的记录,如所示。

此外,includes会将两个表加载到内存中。因此,如果您执行以下操作

comment_1 = Comment.includes(:user).first

comment_1.user.age
#=> 24

正如你所注意到的,comment_1.user.age只是从内存中加载结果,而不会在后台触发数据库查询。

@HunterStevens:是的,它是。 - Aaditi Jain

56

除了性能考虑外,还存在一个功能上的区别。 当您连接评论时,您正在请求具有评论的帖子-默认情况下是内连接。 当您包含评论时,您正在请求所有帖子-外连接。


15

简而言之

对比两种方式:

joins - 用于条件选择记录。

includes - 在结果集的每个成员上使用关联时。

详细版

Joins被用来过滤从数据库中返回的结果集。你可以用它在表上执行集合操作,将其视为执行集合理论的where子句。

Post.joins(:comments)

Post.where('id in (select post_id from comments)')

相同,但是如果有多个评论,使用joins会得到重复的帖子。不过,每篇帖子都会是有评论的帖子。您可以通过distinct来纠正这个问题:

Post.joins(:comments).count
=> 10
Post.joins(:comments).distinct.count
=> 2

相比之下,includes方法只是确保在引用关系时没有额外的数据库查询(这样我们就不会进行n + 1个查询)。

Post.includes(:comments).count
=> 4 # includes posts without comments so the count might be higher.

结论是,在做条件集合操作时请使用joins,而在对集合中的每个成员使用关联时请使用includes


那个 distinct 每次都让我困扰。谢谢! - Ben Hull

4

.joins用于数据库连接,它连接两个或多个表,并从后端(数据库)获取所选数据。

.includes作为数据库的左连接。它加载左侧所有记录,不涉及右侧模型的相关性。它用于急切加载,因为它在内存中加载了所有关联对象。如果我们在包括查询结果上调用关联,则不会在数据库上触发查询,它仅从内存返回数据,因为它已经加载了数据。


0

'joins' 只是用于连接表格,当您在 'joins' 上调用关联时,它会再次触发查询(这意味着会触发多个查询)

lets suppose you have tow model, User and Organisation
User has_many organisations
suppose you have 10 organisation for a user 
@records= User.joins(:organisations).where("organisations.user_id = 1")
QUERY will be 
 select * from users INNER JOIN organisations ON organisations.user_id = users.id where organisations.user_id = 1

it will return all records of organisation related to user
and @records.map{|u|u.organisation.name}
it run QUERY like 
select * from organisations where organisations.id = x then time(hwo many organisation you have)

在这种情况下,SQL 的总数为 11

但是使用 'includes' 将会急切地加载包含的关联并将它们添加到内存中(在第一次加载时加载所有关联),而不会再次触发查询

当您使用 includes 获取记录时,例如 @records= User.includes(:organisations).where("organisations.user_id = 1") 那么查询将会是

select * from users INNER JOIN organisations ON organisations.user_id = users.id where organisations.user_id = 1
and 


 select * from organisations where organisations.id IN(IDS of organisation(1, to 10)) if 10 organisation
and when you run this 

@records.map{|u|u.organisation.name} 不会触发任何查询


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接