【问题标题】:How do I get Rails to eager load counts?如何让 Rails 急切加载计数?
【发布时间】:2011-02-05 18:51:29
【问题描述】:

这与a year and change ago的问题有关。

我举了一个应该开箱即用的问题示例,前提是您有可用的 sqlite3:https://github.com/cairo140/rails-eager-loading-counts-demo

安装说明(主分支)

git clone git://github.com/cairo140/rails-eager-loading-counts-demo.git
cd rails-eager-loading-counts-demo
rails s

我在存储库中有更完整的文章,但我的一般问题是这样的。

我怎样才能使 Rails 急切加载计数以最大限度地减少全面的数据库查询?

n+1 问题出现在您对关联使用 #count 时,尽管已通过 #includes(:associated) 将该关联包含在 ActiveRelation 中。一种解决方法是使用#length,但这只有在它被调用的对象已经加载时才有效,更不用说我怀疑它复制了 Rails 内部已经完成的一些事情。此外,使用#length 的一个问题是,如果一开始就没有加载关联并且只需要计数,它会导致不幸的过载。

来自自述文件:

我们可以通过在帖子数组(见附录)上运行#length 来避开这个问题,该数组已经加载,但如果让 count 也随时可用就更好了。它不仅更加一致;它提供了不一定需要加载帖子的访问路径。例如,如果您有一个无论如何都显示计数的部分,但有一半的时间是在加载帖子的情况下调用该部分,而另一半的时间没有加载,您将面临以下情况:

  • 使用#count
    • n COUNT 帖子已加载时的样式查询
    • n COUNT 未加载帖子时的样式查询
  • 使用#length
    • 已加载帖子时的附加查询为零
    • n * 未加载帖子时的样式查询

在这两个选项之间,没有占优选项。但是最好修改#count 以推迟到#length 或访问以其他方式存储在幕后的长度,以便我们可以有以下场景:

  • 使用修改后的#count
    • 已加载帖子时的附加查询为零
    • n COUNT 未加载帖子时的样式查询

那么这里的正确方法是什么?有什么我忽略的(非常、非常可能)吗?

【问题讨论】:

  • 如果你是性能导向的,考虑counter-cache:asciicasts.com/episodes/23-counter-cache-column
  • 如何“急切地加载”计数?无论如何,计数将需要另一个查询。无论您现在或以后加载计数,您最终都会执行相同数量的查询。
  • 使用常规查询将产生一个类似数组的对象,length 不会导致另一个查询。

标签: ruby-on-rails activerecord eager-loading active-relation


【解决方案1】:

正如@apneadiving 建议的那样,counter_cache 运行良好,因为计数器列会在添加或删除记录时自动更新。所以当你加载父对象时,计数会包含在对象中,而不需要访问其他表。

但是,如果出于某种原因您不喜欢这种方法,您可以这样做:

Post.find(:all,
          :select => "posts.*, count(comments.id) `comments_count`",
          :joins  => "left join comments on comments.post_id = posts.id")

【讨论】:

  • 什么对我有用(Rails 3.2):Post.select('posts.*, (select count(cmets.id) from cmets where cmets.post_id = posts.id) cmets_count')跨度>
  • @JamesRoscoe 您的版本很好,因为它不会在每个帖子中返回多行。或者,您可以将:group => 'posts.id' 添加到我的版本中。
【解决方案2】:

Zubin 的另一种方法:

Post.select('posts.*, count(comments.id) `comments_count`').joins(:comments).group('posts.id')

【讨论】:

    【解决方案3】:

    似乎实现这种功能的最佳方式可能是为您想要的单独的模型和子计数对象创建 SQL 视图(参考:herehere);及其相关的 ActiveRecord 模型。

    您也许可以非常聪明并在原始模型上使用子类化并结合set_table_name :sql_view_name 来保留对象上的所有原始方法,甚至可能保留它们的一些关联。

    例如,假设我们要在您的示例中添加“Post.has_many :cmets”,就像上面@Zubin 的回答一样;那么一个人可能会这样做:

       class CreatePostsWithCommentsCountsView < ActiveRecord::Migration
          def self.up
            #Create SQL View called posts_with_comments_counts which maps over 
            # select posts.*, count(comments.id) as comments_count from posts 
            #   left outer join comments on comments.post_id = posts.id 
            #   group by posts.id
            # (As zubin pointed out above.) 
            #*Except* this is in SQL so perhaps we'll be able to do further 
            # reducing queries against it *as though it were any other table.*
          end    
       end
    
       class PostWithCommentsCount < Post         #Here there be cleverness.
                                                  #The class definition sets up PWCC 
                                                  # with all the regular methods of 
                                                  # Post (pointing to the posts table
                                                  # due to Rails' STI facility.)
    
        set_table_name :posts_with_comment_counts #But then we point it to the 
                                                  # SQL view instead.
                                                  #If you don't really care about
                                                  # the methods of Post being in PWCC
                                                  # then you could just make it a 
                                                  # normal subclass of AR::Base.
       end
    
       PostWithCommentsCount.all(:include => :user)  #Obviously, this sort of "upward
         # looking" include is best used in big lists like "latest posts" rather than
         # "These posts for this user." But hopefully it illustrates the improved 
         # activerecordiness of this style of solution.
       PostWithCommentsCount.all(:include => :comments) #And I'm pretty sure you 
         # should be able to do this without issue as well. And it _should_ only be 
         # the two queries.
    

    【讨论】:

      【解决方案4】:

      我设置了一个小 gem,它向 ActiveRecord 添加了一个 includes_count 方法,它使用 SELECT COUNT 来获取关联中的记录数,而不使用可能很昂贵的 JOIN(取决于具体情况) .

      https://github.com/manastech/includes-count

      希望对你有帮助!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-01-07
        相关资源
        最近更新 更多