翻译 Rails Scopes 预加载

justin for Beansmile · 2015年07月03日 · 最后由 justin 回复于 2015年08月23日 · 4898 次阅读
本帖已被设为精华帖!

前几天在rubyweekly里面看到一篇文章,感觉写得不错。拿出来分享一下。原文更精彩

scope 会造成N+1查询

作为一个Rails开发者,我们经常使用scope来做查询,以简化你的代码,如:

class Review < ActiveRecord::Base
  belongs_to :restaurant

  scope :positive, -> { where("rating > 3.0") }
end
irb(main):001:0> Restaurant.first.reviews.positive.count
  Restaurant Load (0.4ms)  SELECT  `restaurants`.* FROM `restaurants`  ORDER BY `restaurants`.`id` ASC LIMIT 1
   (0.6ms)  SELECT COUNT(*) FROM `reviews` WHERE `reviews`.`restaurant_id` = 1 AND (rating > 3.0)
=> 5

但是,当你一不小心,这将严重的影响你应用的性能。 为什么呢?因为使用scope进行定义的查询并不会被预加载。 假设你要查询一些restaurants所有positive reviews

irb(main):001:0> restauraunts = Restaurant.first(5)
irb(main):002:0> restauraunts.map do |restaurant|
irb(main):003:1*   "#{restaurant.name}: #{restaurant.reviews.positive.length} positive reviews."
irb(main):004:1> end
  Review Load (0.6ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 1 AND (rating > 3.0)
  Review Load (0.5ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 2 AND (rating > 3.0)
  Review Load (0.7ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 3 AND (rating > 3.0)
  Review Load (0.7ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 4 AND (rating > 3.0)
  Review Load (0.7ms)  SELECT `reviews`.* FROM `reviews` WHERE `reviews`.`restaurant_id` = 5 AND (rating > 3.0)
=> ["Judd's Pub: 5 positive reviews.", "Felix's Nightclub: 6 positive reviews.", "Mabel's Burrito Shack: 7 positive reviews.", "Kendall's Burrito Shack: 2 positive reviews.", "Elisabeth's Deli: 15 positive reviews."]

我们可以看到,scope:positive并没有被缓存起来,这明显的是一个N+1查询。

用associations代替scopes

我们可以通过用associations代替scopes,来避免这个问题。请看下面例子:

class Restaurant < ActiveRecord::Base
  has_many :reviews
end

当我们查看这个文档时,我们可以看到,has_many允许我们添加自定义查询

class Restaurant < ActiveRecord::Base
  has_many :reviews
  has_many :positive_reviews, -> { where("rating > 3.0") }, class_name: "Review"
end

但我们可以这样子获取一个restaurant的所有positive_reviews

irb(main):001:0> Restaurant.first.positive_reviews.count
  Restaurant Load (0.2ms)  SELECT  `restaurants`.* FROM `restaurants`  ORDER BY `restaurants`.`id` ASC LIMIT 1
   (0.4ms)  SELECT COUNT(*) FROM `reviews` WHERE `reviews`.`restaurant_id` = 1 AND (rating > 3.0)
=> 5

然后我们就可以通过include来预加载这个关联关系了

irb(main):001:0> restauraunts = Restaurant.includes(:positive_reviews).first(5)
  Restaurant Load (0.3ms)  SELECT  `restaurants`.* FROM `restaurants`  ORDER BY `restaurants`.`id` ASC LIMIT 5
  Review Load (1.2ms)  SELECT `reviews`.* FROM `reviews` WHERE (rating > 3.0) AND `reviews`.`restaurant_id` IN (1, 2, 3, 4, 5)
irb(main):002:0> restauraunts.map do |restaurant|
irb(main):003:1*   "#{restaurant.name}: #{restaurant.positive_reviews.length} positive reviews."
irb(main):004:1> end
=> ["Judd's Pub: 5 positive reviews.", "Felix's Nightclub: 6 positive reviews.", "Mabel's Burrito Shack: 7 positive reviews.", "Kendall's Burrito Shack: 2 positive reviews.", "Elisabeth's Deli: 15 positive reviews."]

至此,6个Sql查询变成2个

消除重复

现在我们定义了一个scope:positive和一个关系has_many :positive_reviews,我们可以看到,它们是重复的。我们可以简单的消除这个DRY.

class Review < ActiveRecord::Base
  belongs_to :restaurant

  scope :positive, -> { where("rating > 3.0") }
end

class Restaurant < ActiveRecord::Base
  has_many :reviews
  has_many :positive_reviews, -> { positive }, class_name: "Review"
end

至此,我们可以知道scope虽然好用,但是当你发现的代码出现以上问题时,简单的修改,能减少许多sql查询

文章没有逐字翻译,如有问题,麻烦指出。

共收到 21 条回复

这个很赞

很不错。。。。

https://ruby-china.org/topics/26245

个人喜欢适时抛弃AR自己写一些SQL XD

很赞

@PlayMonkey Rails有数据库的适配器,写SQL可能需要保证切换数据库的时候也适用。

scope的 where 语句最好用 arel 重写,会比较通用

赞一个

@justin 翻得很好! 原文中的链接和后续的comments都推荐看一下。

大赞!

学习 :plus1:

scope 会造成N+1查询 这个不是因为没有includes(: reviews) ,然后遍历restauraunts 导致的吗?

和scope有什么关系???

#13楼 @xu_xiang_yang 这里调用的是

Restaurant.first.reviews.positive.count

includes(:reviews) 只会预加载所有的reviews,而不是带有条件rating > 3.0的reviews

这里讲的就是把查询条件转成关联关系

has_many :positive_reviews, -> { positive }, class_name: "Review"

方便使用

Restaurant.includes(:positive_reviews)

不是说有的情况下N+1的效率比一次性查出来的效率要高些的么

#15楼 @ywjno 这个应该只是在数据量比较少的情况下吧?

#16楼 @justin 可是我在一个几百万数据的情况真的出现这个情况了,打log发现瓶颈在查询数据的sql上,时间花10多秒手动处理这些数据时间可忽略,然后在循环里面的话整个速度比之前快了n倍画面几乎秒开,,,(检索条件已加索引

#17楼 @ywjno 这么神奇,可以把大概的情况描述一下,分享分享。

19楼 已删除
20楼 已删除
21楼 已删除
22楼 已删除
23楼 已删除
24楼 已删除
25楼 已删除
26楼 已删除

#17楼 @ywjno 求分享,很好奇这种情况

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册