Rails Preload、 Eagerload、 Includes 和 Joins

zhaowenchina · 2014年03月14日 · 最后由 wenjiachengy 回复于 2016年07月21日 · 9953 次阅读
本帖已被设为精华帖!

首发: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 会执行一次新的查询。

赞,写的挺好。

如上所说,具体在 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,请教为什么会这样呢?我哪里出错了吗?

#3 楼 @seeyoup

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

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

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

刚开始没注意关系。

灰常好

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

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

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

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

#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 给找出来

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

#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 的问题

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

赞!对我有帮助。

#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【1 对 n 关系的 1】的表 include 会用 where in 处理不需要用 join(两条 SQL),where 条件是 n 的表会用 join 表处理(一条 SQL)——新手

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