Rails Preload、 Eagerload、 Includes 和 Joins

zhaowenchina · 发布于 2014年3月14日 · 最后由 wenjiachengy 回复于 2016年7月21日 · 4599 次阅读
835
本帖已被设为精华帖!

首发:http://zhaowen.me/blog/2014/03/13/preload-eagerload-includes-and-joins/ 原文:Preload, Eagerload, Includes and Joins

Rails 提供了4种方式来加载关联表的数据。在这篇文章中,我们来分别来看看这些方法。

Preload

preload 使用一条附加的查询语句来加载关联数据。

User.preload(:posts).to_a

# =>
SELECT "users".* FROM "users"
SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" IN (1)

这也是 includes 默认的加载数据的方式。

preload 总是会生成两条 SQL 语句,所以我们不能在 where 条件中使用 posts 表。比如下面的查询就会报错。

User.preload(:posts).where("posts.desc='ruby is awesome'")

# =>
SQLite3::SQLException: no such column: posts.desc: 
SELECT "users".* FROM "users"  WHERE (posts.desc='ruby is awesome')

preload 也可以指定 where 条件。

User.preload(:posts).where("users.name='Neeraj'")

# =>
SELECT "users".* FROM "users"  WHERE (users.name='Neeraj')
SELECT "posts".* FROM "posts"  WHERE "posts"."user_id" IN (3)

Includes

preload 一样,includes 也使用一条附加的查询语句来加载关联数据。

然而,它要比 preload 更聪明一些。我们刚刚看到了使用 preload 无法查询 User.preload(:posts).where("posts.desc='ruby is awesome'")。让我们使用 includes 来试试看。

User.includes(:posts).where('posts.desc = "ruby is awesome"').to_a

# =>
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "posts"."id" AS t1_r0, 
       "posts"."title" AS t1_r1, 
       "posts"."user_id" AS t1_r2, "posts"."desc" AS t1_r3 
FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id" 
WHERE (posts.desc = "ruby is awesome")

如你所见,includes 不再使用两条查询语句,而是使用了单独一条 LEFT OUTER JOIN 语句来获取数据,而且也加载了 where 条件。

所以,includes 在某些场合会从两次查询变成一次查询。默认的简单情况下它会使用两次查询。如果出于某些原因,你想强制其使用单次查询,可以使用 references

User.includes(:posts).references(:posts).to_a

# =>
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "posts"."id" AS t1_r0, 
       "posts"."title" AS t1_r1, 
       "posts"."user_id" AS t1_r2, "posts"."desc" AS t1_r3 
FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"

上面的例子就只执行了一次查询。

Eager load

eager_load 使用 LEFT OUTER JOIN 进行单次查询,并加载所有的关联数据。

User.eager_load(:posts).to_a

# =>
SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, "posts"."id" AS t1_r0, 
       "posts"."title" AS t1_r1, "posts"."user_id" AS t1_r2, "posts"."desc" AS t1_r3 
FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"

这与 includeswhereorder 语句中指定了 posts 表的属性的情况下的单次查询完全相同。

Joins

joins 使用 INNER JOIN 来加载关联数据。

User.joins(:posts)

# =>
SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"

上面的语句不会查询出 posts 表的数据。这个查询还可能会得到重复的结果,我们先创建一些数据。

def self.setup
  User.delete_all
  Post.delete_all

  u = User.create name: 'Neeraj'
  u.posts.create! title: 'ruby', desc: 'ruby is awesome'
  u.posts.create! title: 'rails', desc: 'rails is awesome'
  u.posts.create! title: 'JavaScript', desc: 'JavaScript is awesome'

  u = User.create name: 'Neil'
  u.posts.create! title: 'JavaScript', desc: 'Javascript is awesome'

  u = User.create name: 'Trisha'
end

有了上面的数据后,当我们执行 User.joins(:posts) 后得到的结果为

#<User id: 9, name: "Neeraj">
#<User id: 9, name: "Neeraj">
#<User id: 9, name: "Neeraj">
#<User id: 10, name: "Neil">

要去除重复数据,可以使用 distinct

User.joins(:posts).select('distinct users.*').to_a

另外,如果我们想要得到 posts 表中的属性值,就必须显式地 select 它们。

records = User.joins(:posts).select('distinct users.*, posts.title as posts_title').to_a
records.each do |user|
  puts user.name
  puts user.posts_title
end

值得注意的是,使用 joins 就意味着 user.posts 会执行一次新的查询。

共收到 17 条回复
535

赞,写的挺好。

96

如上所说,具体在rails4中rails console实践了下,发现下面不一致 执行

User.includes(:topics).where('users.id=1').to_a

输出:

User Load (1.0ms)  SELECT `users`.* FROM `users` WHERE (users.id=1)
Topic Load (1.0ms)  SELECT `topics`.* FROM `topics` WHERE `topics`.`user_id` IN (1)
[#<User id: 1, username: "zjg", password: "123456", email: "234@qq.com", reg_date: "2014-04-09 01:37:00", login_date: "2014-04-09 01:37:00", avatar_path: "", point: 2, memo: "232323232323", created_at: "2014-04-09 01:37:49", updated_at: "2014-04-09 01:37:49">]

并不是使用LEFT OUTER JOIN,请教为什么会这样呢?我哪里出错了吗?

96

#3楼 @seeyoup

所以,includes 在某些场合会从两次查询变成一次查询。默认的简单情况下它会使用两次查询。如果出于某些原因,你想强制其使用单次查询,可以使用 reference。

我也试了一下同样也是两条查询

96

#4楼 @leozwa 又测试下,发现如果是一对多,比如user.includes(:topics)就会是两条sql,如果topic.inclues(:user)多对一的时候(user has_many:topics,topic belongs_to :user),就会是join

刚开始没注意关系。

594

灰常好

8969

为什么我这边reference一旦使用就报以上错误,不知道大家遇到过咩

835

#7楼 @zhq_zhq 看了一下,references 方法貌似是从 Rails 4.0 开始才加入的 http://apidock.com/rails/ActiveRecord/QueryMethods/references

594

推荐那些想了解更多细节的 通宵们 阅读 官方文档 http://guides.rubyonrails.org/active_record_querying.html

8969

@zhaowenchina 不可能就为了这个原因而去升级rails吧

96

#3楼 @seeyoup 关键点是在where,而不是includes,你的查询条件和@zhaowenchina的条件不一样。

  • 你的查询语句:User.includes(:post).where('users.id=1').to_a 查询的主体是users表,查询条件是users.id,所以不需要进行LEFT JOIN 就可以把users给找出来,includes只是在找出user的基础上,同时把post加载到内存里面.

  • 楼主的例子,User.includes(:post).where('posts.desc = XXX').to_a 查询主体是users,但是查询条件是posts表里面的属性,这个时候就需要LEFT JOIN才能把users给找出来

5489

#11楼 @pishilong include可以解决N+1query的问题,注意这个in(1),当在遍历user时,使用user.posts时,不会做额外的检索。但是如果在开始include时,where条件字句中有post表的条件,就会执行一次查询。

96

#13楼 @rubyu2 你好,谢谢回复我,但是:

  • 你似乎理解错了N+1问题了,使用user.posts不会做额外的检索,是因为在includes之后,Post集中做了一次查询,放到缓存了,Post.where('user_id in (?)', user_ids)。
  • 不太明白你说的但是如果在开始include时,where条件字句中有post表的条件,就会执行一次查询。是什么意思,不管有没有post表的条件,Post都会进行查询的,N+1问题是,每一个user.posts时,Post都会查一次,所以需要n次查询,而用了include之后,变成了上面所说的1次查询。
  • 另外,N+1和我回@seeyoup 所说的是两码事,是说有没有left join的问题
5489

#14楼 @pishilong 我说的也是“不会做额外的检索”,我只是补充了下你的说法。 但是如果在开始include时,where条件字句中有post表的条件,就会执行一次查询。 意思就是,如果条件中没有post表的where字句就是一次sql查询,如果有,就是两条sql查询。

96

赞!对我有帮助。

96

#3楼 @seeyoup 我觉得是,User.includes(:topics).where('users.id=1'),where条件是('users.id=1'),这个可以直接取得users表的结果集,然后查topics表时使用where in,这样不需要join表,这属于简单的情况(使用两条sql更为合适)。你可以是试下User.includes(:topics).where(topics.XXX) 【前提是user和topics是1对N关系】(这属于复杂情况),这样无可避免join表。include存在的意义应该:默认的简单情况下它会使用两次查询,复杂的情况使用join表进行一次查询,这样更加灵活。总结来说where条件是1的表include会用where in处理不需要用join(两条SQL),where条件是n的表会用join表处理(一条SQL)——新手

3211 kamiiyu 浅谈 ActiveRecord 的 N + 1 查询问题 中提及了此贴 2月23日 00:04
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册