翻译 Rails Scopes 预加载

justin for Beansmile · July 03, 2015 · Last by justin replied at August 23, 2015 · 6598 hits
Topic has been selected as the excellent topic by the admin.

前几天在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查询

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

这个很赞

很不错。。。。

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 Floor has deleted
20 Floor has deleted
21 Floor has deleted
22 Floor has deleted
23 Floor has deleted
24 Floor has deleted
25 Floor has deleted
26 Floor has deleted

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

You need to Sign in before reply, if you don't have an account, please Sign up first.