20

I looked around and couldn't find any answers to this. All answers involved counts that did not use a GROUP BY.

Background: I have a paginator that will take options for an ActiveRecord.find. It adds a :limit and :offset option and performs the query. What I also need to do is count the total number of records (less the limit), but sometimes the query contains a :group option and ActiveRecord.count tries to return all rows returned by the GROUP BY along with each of their counts. I'm doing this in Rails 2.3.5.

What I want is for ActiveRecord.count to return the number of rows returned by the GROUP BY.

Here is some sample code that demonstrates one instance of this (used for finding all tags and ordering them by the number of posts with that tag):

options = { :select => 'tags.*, COUNT(*) AS post_count',
            :joins => 'INNER JOIN posts_tags',   #Join table for 'posts' and 'tags'
            :group => 'tags.id',
            :order => 'post_count DESC' }

@count = Tag.count(options)

options = options.merge { :offset => (page - 1) * per_page, :limit => per_page }

@items = Tag.find(options)

With the :select option, the Tag.count generates the following SQL:

SELECT count(tags.*, COUNT(*) AS post_count) AS count_tags_all_count_all_as_post_count, tags.id AS tags_id FROM `tags`  INNER JOIN posts_tags  GROUP BY tags.id  ORDER BY COUNT(*) DESC

As you can see it merely wrapped a COUNT() around the 'tags.*, COUNT(*)', and MySQL complains about the COUNT within a COUNT.

Without the :select option, it generates this SQL:

SELECT count(*) AS count_all, tags.id AS tags_id FROM `tags` INNER JOIN posts_tags GROUP BY tags.id ORDER BY COUNT(*)

which returns the whole GROUP BY result set and not the number of rows.

Is there a way around this or will I have to hack up the paginator to account for queries with GROUP BYs (and how would I go about doing that)?

J.Melo
  • 311
  • 1
  • 3
  • 9

5 Answers5

23

Seems like you'd need to handle the grouped queries separately. Doing a count without a group returns an integer, while counting with a group returns a hash:

Tag.count
  SQL (0.2ms)  SELECT COUNT(*) FROM "tags"
 => 37

Tag.count(:group=>"tags.id")
  SQL (0.2ms)  SELECT COUNT(*) AS count_all, tags.id AS tags_id FROM "tags" 
    GROUP BY tags.id
 => {1=>37}
zetetic
  • 45,844
  • 10
  • 106
  • 115
  • 1
    I'll try hijacking queries with a :group => 'column' specified and change it into a :select => 'DISTINCT column'. Let me know if you can see any problems with doing that. – J.Melo Feb 22 '11 at 03:28
  • 1
    Changing the :group => 'column' to :select => 'DISTINCT column' seems to work just fine. – J.Melo Feb 22 '11 at 03:55
17

If you're using Rails 4 or 5 you can do the following as well.

Tag.group(:id).count
Brent Matzelle
  • 3,715
  • 3
  • 24
  • 26
Darwayne
  • 1,330
  • 14
  • 14
9

The workaround for my situation seems to be to replace the :group => 'tags.id' with :select => 'DISTINCT tags.id' in the options hash before executing the count.

count_options = options.clone
count_options.delete(:order)

if options[:group]
  group_by = count_options[:group]
  count_options.delete(:group)
  count_options[:select] = "DISTINCT #{group_by}"
end

@item_count = @type.count(count_options)
J.Melo
  • 311
  • 1
  • 3
  • 9
3

Another (hacky) solution:

selection = Tag.where(...).group(...)
count = Tag.connection.select_value "select count(*) from (" + selection.to_sql + ") as x"
troelskn
  • 107,146
  • 23
  • 127
  • 148
2

If I understand your question correctly, then it should work if you don't use Tag.count at all. Specifying 'COUNT(*) AS post_count' in your select hash should be enough. For example:

@tag = Tag.first(options)
@tag.post_count

As you can see, the post_count value from the query is accessible from the @tag instance. And if you want to get all tags, then perhaps something like this:

@tags = Tag.all(options)
@tags.each do |tag|
  puts "Tag name: #{tag.name} posts: #{tag.post_count}"
end

Update:

Count can be called with which attribute to count and also a parameter :distinct

options = { :select => 'tags.*, COUNT(*) AS post_count',
            :joins => 'INNER JOIN posts_tags',   #Join table for 'posts' and 'tags'
            :group => 'tags.id',
            :order => 'post_count DESC',
            :offset => (page - 1) * per_page,
            :limit => per_page }

@count = Tag.count(:id, :distinct => true, :joins => options[:joins])

@items = Tag.find(options)
DanneManne
  • 20,521
  • 5
  • 53
  • 54
  • What I want to do is count the number of rows. The post_count is just something to be listed next to each tag like you have in your second code snippet. The paginator needs to know how many pages there are going to be so it has to find the number of records the query returns without the limit. The paginator is being reused in many places so I can't insert ad hoc code to fix this, and I would to keep the interface the same. – J.Melo Feb 22 '11 at 02:16
  • Updated my answer with an additional scenario. – DanneManne Feb 22 '11 at 03:02
  • I'm not sure I see how this will help me. My problem is that Tag.count returns a complete resultset because of the GROUP BY clause. I want it to return one integer, which is the count of all the tags, but using the samw query I used to get the tags. – J.Melo Feb 22 '11 at 03:54
  • Well, as you already noticed, when you use the exact same query with the count, it will return a grouped hash instead of an integer. But in reality, the only thing from your original query that needs to be the same is the :joins hash. I'll make an additional update to see if I can clarify. – DanneManne Feb 22 '11 at 04:00
  • Oh, I understand now. I don't want to mess around with the queries too much so that the code will work for any query I throw at it, but I did manage to fix it with a similar idea (see appended edit on question). – J.Melo Feb 22 '11 at 04:12
  • I actually think that my example would work with any query, I use something similar myself. But if you got it to work another way, then stick to it :) – DanneManne Feb 22 '11 at 04:19
  • There are other things that are necessary to keep like :conditions. Not sure of anything else but I'm keeping everything but :group to be safe. – J.Melo Feb 22 '11 at 04:51