views:

108

answers:

3

I have three models: User, RaceWeek, Race.

Current associations:

class User < ActiveRecord::Base
  has_many :race_weeks
end

class RaceWeek < ActiveRecord::Base
  belongs_to :user
  has_many :races
end

class Race < ActiveRecord::Base
  belongs_to :race_week
end

So the user_id is a foreign key in RaceWeek and race_week_id is a foreign key in Race.

fastest_time is an attribute of the Race model.

QUESTION: What's the optimal way to retrieve a list of users who have the top X fastest race times?

+1  A: 

Something like:

 races = Race.all(:order => "fastest_time desc", :limit => X, :include => {:race_week => :user})
 users = races.map{|race| race.race_week.user}.uniq

Note: didn't test this.

jrallison
Could you explain how that :include works? I'm not quite sure I get what :race_week => :user is doing.
keruilin
Also, you're right, I do just want to know the list of user(s) with the top X fastest race times (Joe could have the 10 fastest times). However, top X users with the fastest race times (Joe has the fastest time, but then the next 9 distinct users).
keruilin
include is basically just a database optimization. It tells ActiveRecord to go ahead and fetch all the associated race_weeks and users at one time. If you left out the :include, it would still work, just perform a lot more queries.
jrallison
+1  A: 

Given your current model the following should work.

race_weeks = RaceWeek.find_by_sql(["SELECT user_id FROM race_weeks JOIN races ON races.race_week_id = race_weeks.id ORDER BY races.fastest_time desc LIMIT ?", X)
users = User.find(race_weeks.collect(&:user_id).uniq)

I know that it requires two look ups but the second lookup should be very fast since you are only looking up X records by their primary key.

Randy Simon
This would return the top X users with the fastest race times, not necessarily the list of users with the top X fastest race times.
jrallison
I modified my code to satisfy that requirement that I guess I misunderstood.
Randy Simon
Your limit is still limiting the number of users returned, right? This means you will always return X users. The way the question is framed, you want to return X fastest times regardless how many users obtained those fastest times. I.E. User A has the 10 fastest race times. If X = 10, you should only return one user.
jrallison
Not sure why I am struggling to grasp the actual problem. I have updated my answer again. I think that my solution is more efficient because for your solution you have to marshall the Race object as well as the RaceWeek object for each race when all we really want is the User object. In addition, your solution will make 3 SQL queries vs 2 for my solution. That said, I think either solution is probably fine if you are not working with large datasets and the value of X is small.
Randy Simon
Sorry, I didn't mean to make it sound like it was my solution vs. your solution. However, I do agree that the main problem with mine is object instantiation.
jrallison
Oh, nor did I. I just wanted to take the opportunity to talk through options with you as I think this is an interesting problem with several solutions so I felt I should highlight the differences.
Randy Simon
+1  A: 

You can do it like this:

users = User.all(:limit => X, :joins => {:race_weeks => :races}, :order => "reces.fastest_time DESC").uniq

If you have correctly specified has_many :through association, then you could even do it like this:

users = User.all(:limit => X, :joins => :races, :order => "reces.fastest_time DESC").uniq

In this solution, you get what you want with one query, but two joins. And this uniq method is not very good unless you would use small X.

klew
How would I need to modify this if I wanted to include the Race fields in the returned User object so I could access the fastest_time?
keruilin
I think that you need to have correct associations in model and scope races with `:order => 'fastest_time DESC'`. But it will make aditional sql queries. Then you can get it like this: `users.each {|a| puts a.races.fastest.first.fastest_time`
klew