【问题标题】:Order by count on has_many relation根据 has_many 关系按计数排序
【发布时间】:2015-08-10 11:11:02
【问题描述】:

这是我经常遇到的一个问题。关于这个问题有一些类似的问题,但都不是很完整(而且它们可能已经过时,因为 Rails 4 可能引入了有助于解决这个问题的新功能)

让我举一个问题的简单例子和已知的“解决”问题的方法:


假设我有一个User 模型和一个Post 模型,还有一个User has_many :posts

现在,我想要获得帖子最多的前五名用户。

以下是我知道的选项,但它们都有自己的缺点:

1)

users = User.all
@top_users = users.sort {|a,b| a.posts.count <=> b.posts.count}.take(5)

缺点:为每个用户发出一个数据库请求,使得这个解决方案非常慢。

2) 直接使用带有 Join 的 SQL 代码 (例如参见this question and answer

select('users.*, COUNT(posts.id) AS posts_count').joins(:posts).group('users.id').order('posts_count DESC').take(5)

这将运行数据库中的所有排序逻辑。然而:

  • 我们使用了大量特定于 DB 的代码(例如在 PostgreSQL 中,我们需要其他语法)。如果可能,最好使用 ActiveRecord 方法。
  • 使用内部联接意味着永远不会返回没有任何帖子的用户。当我们想要返回没有帖子的用户时,这也是一个问题。

3) 直接将 SQL 与外部联接一起使用(参见例如 this question and answers

User.select("users.*, COUNT(posts.id) as posts_count").joins("LEFT OUTER JOIN posts ON posts.user_id = users.id").group("posts.id").order("posts_count DESC")

这也会返回没有帖子的用户。缺点:

  • 更多特定于 DB 的代码(如 #2),更难阅读。

4) 使用计数器缓存列 (有关此技术的完整说明,请参阅this Railscasts episode

基本上,在 User 上创建一个新列,通过在每次创建或删除新帖子时更改字段中的值来跟踪该用户的当前 posts 计数。

这是非常快速和可读的。缺点是我们只能在User 上定义一个新字段后才能使用它。在许多情况下,这是可以接受的,但更难变得灵活,因为需要更改用户表才能使其在每个关联中工作,我们可能希望为其创建前五名。此外,由于这是一个缓存字段,因此存在不会触发该字段更新的数据库操作。

有没有更好(可读和有效)的方法来完成这个?首选使用内置 ActiveRecord 方法的东西。

【问题讨论】:

  • 还有另一个选项可以添加到您的列表中,但有缺点。您可以将字段 posts_count 添加到 User 模型。如果帖子的添加频率低于选择的前五名用户,那么这可能是一个不错的尝试。
  • In PostgreSQL for example we would need other syntax – 示例 #2 中的 DB 到底是什么?
  • 您可以尝试使用includes(...),而不是显式的LEFT OUTER JOIN,它隐式地执行OUTER JOIN。此外,您在选项 2 和 3 中的查询似乎足够通用,以至于语法可能在您感兴趣的 SQL 实现中是通用的。
  • @Glupo:啊,是的。 cache_counter,对吧?我会将其添加到问题帖子中的可能性中。在许多情况下,这可能是一个很好的解决方案。但是,缺点当然是需要为此更改user 数据库表。

标签: mysql ruby-on-rails ruby ruby-on-rails-4 activerecord


【解决方案1】:

另一种方法,有一些限制可能使其更像是部分解决方案:

User.where(:id => Post.group(:user_id).
                       order("count(*) desc").
                       limit(5).
                       keys)

从数据库的角度来看,这在查找帖子数量最多的五个用户方面非常有效,因为它只需要扫描帖子表的 user_id 列上的索引,因此对于非常大的数据集非常有用。它也是相当“干净”的 Rails/ActiveRecord 代码,实际上应该独立于数据库。

如果按照统计后的顺序返回用户很关键,那么一旦这五个被识别出来,就可以使用效率较低的排序方法,或者可以在 ruby​​ 中使用键的检索顺序对返回的用户进行排序。

【讨论】:

  • 我真的很喜欢这种方法! :-) count(*) 对我来说是新手,在这种情况下似乎非常聪明。
  • 嗯,它确实很有效,但它与将任何条件应用于用户模型(例如活动/不活动)以及使用多个订单条件(例如,按帖子数量排序,然后按数量排序)不兼容cmets),因此并非适用于所有情况。
  • 为什么链中有一个 .count 调用?它不仅会返回不适用于 .keys 的单个值吗?
【解决方案2】:

这里有一个值得一看的方法:

User.joins("left join posts on posts.user_id = users.id").
     group(:id).
     order("count(*) desc").
     limit(5)

加入有点手动,但是如果您知道至少有五个用户有帖子,或者不想列出任何没有帖子的用户,那么您可以使用常规加入:

User.joins(:posts).
     group(:id).
     order("count(*) desc").
     limit(5)

如果您有其他 has_many 连接,则 count(*) 不一定可靠,但在这种情况下,您可能希望生成如下查询:

select ...
from   users ...
order by (select count(*) from posts where posts.user_id = users.id)

附言在 PostgreSQL 上测试。 ID 列上的 GROUP BY 肯定不会在 Oracle 上工作,不确定其他人。

【讨论】:

    【解决方案3】:

    这个选项可能值得研究,没有测试,所以可能需要一些调整。

    class Post < ActiveRecord::Base
      belongs_to :user, counter_cache: true
    end
    

    使用counter_cache,您将在数据库中找到一张表。

    class User < ActiveRecord::Base
      has_many :posts
    
      def self.top_5
        order('post_counts DESC').limit(5)
      end
    end
    

    在用户表中添加posts_count整数列,默认为0。

    class AddPostsCountToUsers < ActiveRecord::Migration
      def change
        add_column :users, :posts_count, :integer, default: 0
      end
    end
    

    如果您的数据库中已经有现有用户。

    您需要在控制台中运行以下命令,或者如果您需要运行几次,则将其设置为 rake 任务:

    User.find_each { |user| User.reset_counters(user.id, :posts) }
    

    【讨论】:

    • 这和方法#4有区别吗?
    【解决方案4】:

    你也可以像下面这样-

    User.joins(:posts).select('users.*, count(*) as posts_count').group('users.id').order('posts_count')
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-04-29
      • 2021-01-25
      • 2011-12-18
      • 1970-01-01
      • 2014-05-07
      • 2017-03-25
      • 2011-10-14
      相关资源
      最近更新 更多