Model.find(:all, :include => :children, :order => "COUNT(children)")???

Hey all,

Let's say I have 2 models: model Venue has many Fans model Fans has many Venues

In my app, Fans can bookmark their favorite Venues. Le's say I want to list the venues, sorted by the number of Fans that has bookmarked them, in descending order. So, using eager loading:

venues_sorted_by_fans = Venue.find(:all, :include => :fans, :order => 'COUNT ???')

What do I COUNT by in the SQL part? I admit I'm no SQL expert, but I have tried searching for the proper answer. Hopefully someone can help me here.

Thanks! Chad

you were almost there

venues_sorted_by_fans = Venue.find(:all, :include => :fans, :order => ‘COUNT DESC’)

to order by count descending

I'm sorry, I actually meant my models are as such: model Venue HABTM Fans model Fans HABTM Venues

Hmmm that won't sort by the count of fans that are associated with each venue....

I don't believe this can be done in a single query with ActiveRecord (at least in MySQL). The set of options you want would look something like this.

:joins => "LEFT JOIN fans AS fans_to_count ON venues.id = fans_to_count.venue_id" :group => "fans_to_count.venue_id" :order => "COUNT(fans_to_count.id)"

Unfortunately this does not work because the COUNT(fans_to_count.id) is an invalid ORDER BY clause (at least in MySQL). The reason is because the group functions such as COUNT must be in the SELECT clause, not the ORDER clause. But when you are using eager loading (:include), ActiveRecord sets up the SELECT clause explicitly and doesn't use an :select option you pass. There is currently no way to add an additional SELECT component which is all that would be necessary.

Instead you will have to do two queries. First you group the IDs of the venues you want with something like:

venues = Venue.find(:all, :joins => "LEFT JOIN fans AS fans_to_count ON venues.id = fans_to_count.venue_id", :group => "fans_to_count.venue_id", :select => "id,COUNT(fans_to_count.id) AS count", :order => "count DESC")

ids = venues.map(&:id)

Then you do your original query with

:conditions => {:id => ids} # AR automatically converts an array of ids to IN(1,2,3,4) SQL syntax.

They won't come back in order, so you'll have to sort in Ruby. But that will still be faster than the n+1 queries necessary without eager loading. Ugly, I know, but ActiveRecord would need some hacks to get around this. Now that I think about it, I might make a plugin that lets you do something like :additional_select that would let you get around this.

I would do:

Venue.find_by_sql( "SELECT v.* FROM venues v, fans_venues fv WHERE v.id=fv.venue_id GROUP BY v.venue_id ORDER BY count( distinct fv.fan_id ) DESC" ); because i have no idea how to do it w/ the standard find. you could modify from that to take your :include => :fans into account but off the top of my head i don't remember what that sequel looks like...

Chad W wrote:

Unfortunately this does not work because the COUNT(fans_to_count.id) is an invalid ORDER BY clause (at least in MySQL). The reason is because the group functions such as COUNT must be in the SELECT clause, not the ORDER clause. But when you are using eager loading (:include), ActiveRecord sets up the SELECT clause explicitly and doesn't use an :select option you pass. There is currently no way to add an additional SELECT component which is all that would be necessary.

Not true. You can have the count in the order clause with mysql. I do it all the time. Not sure which mysql might have introduced that but it is there.

YES! Perfect! That's exactly what I needed. Thank you SO MUCH!

Chad

At least MySQL 5.0 because it doesn't work in 4.1

To convert to the standard find you just split it up into it's appropriate pieces in the options, :select, :joins, :conditions, :group and :order. However the whole point is use eager loading, and so the GROUP BY and ORDER clauses need to be structured to not clobber the eager loaded rows as this GROUP BY statement will do.

There is no way to replicate the eager loading code manually. It aliases tables as t0, t1, etc and columns as t0_r0, t0_r1 to avoid name collisions, and as far as I know there is no way to hook directly the model building process. That means you are stuck with whatever :select Rails generates for you. You also can't group on the joined table from eager loading because that would destroy the eager loading itself. So you must add your own explicit join with it's own table name to perform the group and count.

The original options I posted are what you need, but as mentioned in my other message, you will need at least MySQL 5 or it will blow up.

Why don't you create a new model for the relationship, and then cache the size of fans.

Then you could do something like

EventBooking.find :all, :include => :fans, :order => "fans_count"

EventBooking would just have id, venue_id, fan_id, and fans_count. Venue use has_many :through to transparently have access to fans.

Pat

That won't work with a limit.