Includes Count
This gem adds an includes_count method to active record queries, which adds the count of an association to a relation using a simple SELECT SQL query in a similar way as the includes method does, only that retrieving counts instead of the full records collection.
This gem has been tested with ActiveRecord version 3.1.3.
Usage
For example, in the following model:
class Blog
has_many :posts
has_many :comments, :through => :posts
end
class Post
belongs_to :blog
has_many :comments
end
class Comment
belongs_to :post
end
It is possible to retrieve the number of posts in every blog with the command:
blogs_with_posts_count = Blog.scoped.includes_count(:posts)
This will issue a simple SELECT query retrieving all counts and assigning them in memory, thus not requiring an INNER JOIN that could be expensive to handle in the database:
SELECT SQL_NO_CACHE posts.blog_id, COUNT(id) AS posts_count
FROM `posts`
WHERE `posts`.`blog_id` IN (1, 2, 3, 4, 5, 6, 7, 8)
GROUP BY `posts`.`blog_id`
The count is projected to a field named by default association_name_count:
blogs_with_posts_count.map(&:posts_count)
The name of the method can be changed by supplying the count_name option:
blogs_with_posts_count = Blog.scoped.includes_count(:posts, :count_name => 'number_of_posts')
blogs_with_posts_count.map(&:posts_count)
The execution of the count is delayed until execution of the query, as happens with the includes clause, so further clauses, such as where, can be set to the relation:
latest_blogs_with_posts_count = Blog.scoped.includes_count(:posts).where('updated_at > ?', 1.week.ago)
This will retrieve only the blogs that have been updated since 1 week ago, along with their counts. Supposing there are only two blogs that match that condition (ids 3 and 5), the SELECT query issued will be the following:
SELECT SQL_NO_CACHE posts.blog_id, COUNT(id) AS posts_count
FROM `posts`
WHERE `posts`.`blog_id` IN (3, 5)
GROUP BY `posts`.`blog_id`
Conditions can be specified on the included association (using a string, a hash or a proc), in order to filter which records are to be counted:
blogs_with_rails_posts_count = Blog.scoped.includes_count(:posts, :count_name => 'rails_posts_count', :conditions => "category = 'rails'")
SELECT SQL_NO_CACHE posts.blog_id, COUNT(id) AS posts_count
FROM `posts`
WHERE `posts`.`blog_id` IN (3, 5)
AND `posts`.`category` = 'rails'
GROUP BY `posts`.`blog_id`
Through Associations
The includes_count method also supports through associations, and issues as many SELECT queries as needed to navigate the hierarchy and obtain the specified counts.
blogs_with_comments_count = Blog.scoped.includes_count(:comments)
SELECT SQL_NO_CACHE `posts`.*
FROM `posts`
WHERE `posts`.`blog_id` IN (1, 2, 3, 4, 5, 6, 7, 8)
SELECT SQL_NO_CACHE `comments`.post_id, COUNT(id) AS comments_count
FROM `comments`
WHERE `comments`.`post_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
GROUP BY `comments`.`post_id`
As usual, the value can be accessed via the method named after the association, and overridden via count_name:
blogs_with_posts_count.map(&:comments_count)
It is also possible to specify conditions at any of the intermediate associations in the through association:
blogs_with_comments_count_from_rails_posts = Blog.scoped.includes_count(:comments, :through_options => { :posts => { :conditions => "category = 'rails'"} })
SELECT SQL_NO_CACHE `posts`.*
FROM `posts`
WHERE `posts`.`blog_id` IN (1, 2, 3, 4, 5, 6, 7, 8)
AND `posts`.`category` = 'rails'
SELECT SQL_NO_CACHE `comments`.post_id, COUNT(id) AS comments_count
FROM `comments`
WHERE `comments`.`post_id` IN (5, 6, 10, 11, 12)
GROUP BY `comments`.`post_id`
Known Issues
- The
includes_countmethod is included only inActiveRecord::Relationobjects, which means you cannot execute it straight on a model. As a workaround, supply the methodscopedbefore executingincludes_count:Blog.scoped.includes_count(:posts)