依靠has_many关系排序

Qqw*_*qwy 12 ruby mysql activerecord ruby-on-rails ruby-on-rails-4

这是我经常遇到的问题.关于这个问题有一些类似的问题,但是没有一个是非常完整的(并且它们可能已经过时了,因为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)
Run Code Online (Sandbox Code Playgroud)

缺点:为每个用户创建一个DataBase请求,使得此解决方案非常慢.

2)直接使用SQL代码加入(参见例如这个问题和答案)

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

这将运行DataBase中的所有排序逻辑.然而:

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

3)直接使用SQL与外部联接(例如,请参阅此问题和答案)

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")
Run Code Online (Sandbox Code Playgroud)

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

  • 更多特定于数据库的代码作为#2,甚至更难阅读.

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

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

这非常快速且可读.缺点是我们只能在我们定义了一个新字段后使用它User.对于许多情况,这是可以接受的,但是更难以灵活,因为需要更改用户表,以便按照我们可能想要创建前五的关联工作.此外,由于这是一个缓存字段,因此存在不会触发字段更新的数据库操作.

有没有更好(可读和有效)的方法来实现这一目标?优选使用内置ActiveRecord方法的东西.

Dav*_*dge 5

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

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

在查找具有最多帖子数量的五个用户时,这在数据库术语中非常有效,因为它只需要扫描posts表的user_id列上的索引,因此对于非常大的数据集会有好处.它也是非常"干净"的Rails/ActiveRecord代码,应该与数据库无关.

如果以后计数顺序返回用户是关键的,那么一旦识别出这五个,就可以使用效率较低的排序方法,或者可以在ruby中使用密钥的检索顺序来对返回的用户进行排序.

  • 嗯它肯定是有效的,但它与用户模型应用任何条件(例如活动/非活动)以及使用多个订单条件(例如按帖子数量排序,然后是评论数量)不相容,所以它不是适用于所有情况. (2认同)

Dav*_*dge 1

这是一个值得一看的方法:

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

在连接中需要一点手动操作,但是如果您知道至少有五个用户有帖子,或者不想列出任何没有帖子的用户,那么您可以使用常规连接:

User.joins(:posts).
     group(:id).
     order("count(*) desc").
     limit(5)
Run Code Online (Sandbox Code Playgroud)

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

select ...
from   users ...
order by (select count(*) from posts where posts.user_id = users.id)
Run Code Online (Sandbox Code Playgroud)

ps 在 PostgreSQL 上测试。ID 列上的 GROUP BY 在 Oracle 上肯定不起作用,不确定其他的。