ActiveRecord counting with conditions over two tables - sql

After watching, the latest RailsCasts with the include and joins information, I know I can do what I am doing in a much more efficient way. I have a very simple User model and a Status model. The User belongs to Status and the Status has many Users. I need to count how many users have a specific kind of status, this creates a new SQL count query for every single status and I know that this is not a good way to do it. It looks like this right now.
statuses = Status.all
statuses.each do |status|
status.users.count
end
I end up with 4 queries of:
SELECT count(*) AS count_all FROM "users" WHERE ("users".status_id = 1)
SELECT count(*) AS count_all FROM "users" WHERE ("users".status_id = 2)
It goes on like that for as many different statuses as exist in the database. The big problem is that now I need to filter by another association as well which is Organization. So I need to find the count for all users who have a certain status in a certain organization. This ends up quadrupling the amount of queries I am making and feels horrible. I'm not sure what kind of join I could use to cut down on this or what I could possibly do to fix this. Thanks for the help :)

Ok so I am answering my own question, just in case anyone has the same issue.
Status.all(:joins => :users,
:select => "statuses.*, count(users.id) as users_count",
:group => "statuses.id")
This returns every status that has users and the count of the users as users_count. In order to further refine the query and only count users that belong to a certain organization the query changes to this.
Status.all(:joins => :users,
:select => "statuses.*, count(users.id) as users_count",
:conditions => {:users => {:organization_id => ORG_ID_HERE}},
:group => "statuses.id")
I hope this helps anyone with the same issue and thanks to Eimantas, and Ryan Bates(RailCasts).

You can try plain SQL:
SELECT s.name,COUNT(u.id) AS users_count FROM statuses s, users u WHERE s.id=u.status_id GROUP BY s.id;

Out of interest, what does your Status model contain? Does it really need to be its own model? I'm guessing here, but you may want to consider implementing a finite state machine. There are a number of rails plugins that make it easy to implement an FSM e.g. acts_as_state_machine

Related

Rails: Getting all has_many objects associated with an active relation of parents

I have what I suspect is a basic question. In my rails app, One user has many scores. Is there a way to get all the scores of an active relation of users. I can do user.scores (obviously) but if users is a group of users I need to do something like users.scores. This is clearly not correct.
You can use
Score.where(user: users)
This will construct sql like
select * from scores where user_id in (select *.id from users where ..)
Did you try the following?
#users.collect(&:scores).flatten!

ActiveRecord - Retrieve one record for each association

Merchant has_many Shops
Shop belongs_to Merchant
i.e. One merchant (Starbucks) can have many shops locations.
I'm using Gecoder to get the nearby shops, e.g. #shops = Shop.near("Times Square").
I would like to return only 1 record for each merchant only. I.e. #shops only contain 1 Starbucks, 1 Subway, but is a collection.
Sorry I've been Googling and searching on SO to no avail. Perhaps I'm not using the right word to describe what I want. Thanks in advance.
To answer what you should be googling for, joined or combined queries within a scope will probably solve what you are looking to do. If you build a scope with :or logic combining queries, one each for each shop, limited to the first record, you should get what you are looking for.
I won't pretend that I understand Geocoder or advanced scopes enough to do this, but I found an example that shows this approach in another problem:
named_scope :or, lambda { |l, r| {
:conditions =>
"annotations.id IN (#{l.send(:construct_finder_sql,{:select => :id})}) or " +
"annotations.id IN (#{r.send(:construct_finder_sql,{:select => :id})})"
}}
This comes from this SO question: Combine two named scopes with OR (instead of AND)
Hope this helps you find the solution.
I googled a bit more and stumbled on group by for SQL.
If I have 4 shops belonging to 2 merchants near a location called "Raffles Place", within 1 kilometer.
Calling Shop.near("Raffles Place",1) returns 4 shops.
If I add a group to Shop.near("Raffles Place",1).group(:merchant_id), only 2 shops are returned.
This can be used with other conditions too, such as Shop.where(:featured=>true).group(:merchant_id) to only show 1 shop per featured merchant.

Ruby on Rails Query Count

I'm developing a site where users can post opinions, as well as rate opinion up or down.
In turn, I need to find a way to count opinion that have been rated up or down.
I'm currently working on those which have been rated up. I have a many-to-many relationship between the following entities: Opinion and Rating.
The two entities are joined together by a table called OpinionRatings. Below is the query I have come up with so far.
#topUpSize = OpinionRating.find(
:first,
:select => "count(opinion_ratings.opinion_id) as count",
:joins => "inner join ratings on opinion_ratings.rating_id=ratings.id",
:group => "opinion_ratings.opinion_id",
:having => ["ratings.up=?", true]
)
The problem I have is I currently have two separate test opinions that have been rated up so the count that should be displayed is two, however, the count that is displayed is one. I'm not sure why this is happening.
Any help would be appreciated.

Rails3: left join aggregate count - how to calculate?

In my application Users register for Events, which belong to a Stream. The registrations are managed in the Registration model, which have a boolean field called 'attended'.
I'm trying to generate a leaderboard and need to know: the total number of registrations for each user, as well as a count for user registrations in each individual event stream.
I'm trying this (in User.rb):
# returns an array of users and their attendence count
def self.attendance_counts
User.all(
:select => "users.*, sum(attended) as attendance_count",
:joins => 'left join `registrations` ON registrations.user_id = users.id',
:group => 'registrations.user_id',
:order => 'attendance_count DESC'
)
end
The generated SQL works for just returning the total attended count for each user when I run it in the database, but all that gets returned is the User record in Rails.
I'm about to give up and hardcode a counter_cache for each stream (they are fairly fixed) into the User table, which gets manually updated whenever the attended attribute changes on a Registration model save.
Still, I'm really curious as to how to perform a query like this. It must come up all the time when calculating statistics and reports on records with relationships.
Your time and consideration is much appreciated. Thanks in advance.
Firstly as a couple of points on style and rails functions to help you with building DB queries.
1) You're better writing this as a scope rather than a method i.e.
scope attendance_counts, select("users.*, sum(attended) as attendance_count").joins(:registrations).group('registrations.user_id').order('attendance_count DESC')
2) It's better not to call all/find/first on the query you've built up until you actually need it (i.e. in the controller or view). That way if you decide to implement action / fragment caching later on the DB query won't get called if the cached action / fragment is served to the user.
3) Rails has a series of functions to help with aggregating db data. for example if you only wanted a user's id and the sum of attended you could use something like the following code:
Registrations.group(:user_id).sum(:attended)
Other functions include count, avg, minimum, maximum
Finally in answer to your question, rails will create an attribute for you to access the value of any custom fields you have in the select part of your query. e.g.
#users = User.attendance_counts
#users[0].attendance_count # The attendance count for the first user returned by the query

Rails include with options

Is it possible to limit an ActiveRecord :include to say only pull one record?
Item.find(:all,
:include => [ :external_ratings, :photos => LIMIT 1 ])
I have a list of items, and each item has between 5 and 15 photos. I want to load a photo id into memory, but I don't need all of them. I just want to preview the first one.
Is there a way to do this with ActiveRecord?
I don't believe it is possible to do it directly, but as finding one record is only a single query, why not do it outside the include?
e.g
first_photo = Photo.find(records.first.photo_id)
After your main find
I'm not going to investigate the detailed query, but I'm thinking find_by_sql is in order for this particular case.