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.
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`
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`
- The
includes_count
method is included only inActiveRecord::Relation
objects, which means you cannot execute it straight on a model. As a workaround, supply the methodscoped
before executingincludes_count
:Blog.scoped.includes_count(:posts)