Rails避免N + 1查询

时间:2016-11-18 05:59:53

标签: ruby-on-rails postgresql

postlikerscomments个孩子。我想根据它们对帖子进行排序。

class Post < ApplicationRecord
  scope :latest, -> {
    all.sort_by(&:ranking)
  }

  def ranking
    likers.count + comments.count
  end
end

这会调用以下查询:

  Post Load (0.7ms)  SELECT "posts".* FROM "posts"
   (0.4ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 52]]
   (0.4ms)  SELECT COUNT(*) FROM "users" INNER JOIN "user_post_likes" ON "users"."id" = "user_post_likes"."user_id" WHERE "user_post_likes"."post_id" = $1  [["post_id", 52]]
   (0.2ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 53]]
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "user_post_likes" ON "users"."id" = "user_post_likes"."user_id" WHERE "user_post_likes"."post_id" = $1  [["post_id", 53]]

所以我尝试以下方法:

Post.includes(:comments, :likers).all.sort_by(&:ranking)

这会调用以下查询:

  Post Load (0.7ms)  SELECT "posts".* FROM "posts"
  Comment Load (0.4ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (52, 53, 54, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71)
  UserPostLike Load (0.3ms)  SELECT "user_post_likes".* FROM "user_post_likes" WHERE "user_post_likes"."post_id" IN (52, 53, 54, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71)
  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 46
   (0.3ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 52]]
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "user_post_likes" ON "users"."id" = "user_post_likes"."user_id" WHERE "user_post_likes"."post_id" = $1  [["post_id", 52]]
   (0.2ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 53]]
   (0.3ms)  SELECT COUNT(*) FROM "users" INNER JOIN "user_post_likes" ON "users"."id" = "user_post_likes"."user_id" WHERE "user_post_likes"."post_id" = $1  [["post_id", 53]]

为什么会发生这种情况,我该如何解决?

更新

我想出了如何解决它,但是一个非常好的解释的答案会很好:

我必须将count替换为size

初始

class Post < ApplicationRecord
  scope :latest, -> { 
    all.sort_by(&:ranking)
  }

  def ranking
    likers.count + comments.count
  end
end

后:

class Post < ApplicationRecord
  ...

  def ranking
    likers.size + comments.size
  end
end

然后,N+1 Query消失了。我得到的结论是,当你使用counter_cache时,会发生同样的事情。在这种情况下,我没有使用counter_cache,但我仍然必须使用size而不是count。我假设调用count强制Rails调用COUNT SQL查询并调用size使其使用内存中加载的记录。

3 个答案:

答案 0 :(得分:2)

你可以这样使用eager_load

Post.eager_load(:comments, :likers).sort_by(&:ranking)

急切加载使用LEFT OUTER JOIN在单个查询中加载所有关联。

Eager Loading Associations

3 ways to do eager loading (preloading) in Rails 3 & 4

答案 1 :(得分:0)

这里的问题有两个方面:

首先,sort_by立即为我举起一面旗帜: http://apidock.com/ruby/Array/sort%21

这是一个Array方法,意味着您不再构建ActiveRecord查询,而是进行数组转换。

由于您包含commentslikers,因此查询并不尽如人意,但这是另一个问题。

.count的工作方式是预制计数查询SELECT * FROM table

要获得所需的结果,您需要构建自己的计数和排序查询。

看一下这篇文章,希望这会让你更好地了解如何进一步优化这个: Rails 3 ActiveRecord: Order by count on association

答案 2 :(得分:0)

在您的情况下,最好的方法是使用counter_cache likerscomments。 您可以阅读更多细节SHORT ARTICLE。这很简单,也是安全的时间和记忆。

如果您使用counter_cache,则不应向您的数据库发出多个请求。现在你的方法将是:

def ranking
  likers_count + comments_count
end

另一方面,如果您不想向表中添加列,请使用includes

class Post < ApplicationRecord
  scope :latest, -> { 
    includes(:likers, :comments).sort_by(&:ranking)
  }

  def ranking 
    likers.count + comments.count
  end
end

但在这种情况下,每当方法调用时,您都会计算likerscomments