ActiveRecord Query. Group by multiple columns from multiple tables - sql

Attendance.joins(:user).group(:email, :name).count
The query above gives me the below results. "User 1" had 2 attendances and "User 2" had 6 attendance, plus I have access to the user name and email.
{["email#example.com", "User 1"]=>2, ["email2#example.com", "User 2"]=>6]}
This is great except I'd really like the User.id to be included as well. I can't group by the ID because then each record will be unique instead of being grouped by email and name. The below is what i'd like:
{[1, "email#example.com", "User 1"]=>2, [2, "email2#example.com", "User 2"]=>6]}
Any help would be appreciated.
Answer
Attendance.joins(:user).group(:email, :name, 'attendances.user_id').count

Attendance.joins(:user).group(:email, :name, 'attendances.user_id').count

# collect required fields and group by email, name
grouped_attendance = Attendance.joins(:user).pluck(:id, :email, :name).group_by{|r| [r[1], r[2]]}
# calculate the counts
attendance_counts = grouped_attendance.each{|k,v| grouped_attendance[k] = v.count}
Depending on your setup, but above should be fast enough as you are not instantiating ActiveRecord objects.

Related

query include methods rails

I have a Student model and that model has_many subscriptions. I need to return all students who have subscriptions. this object must contain only the student's name, email, phone and status, in addition to the subscription id and recurrence_code. I thought something more or less like this but I can't succeed in the query:
students = Student.all.includes(:subscriptions)
students.each do |student|
student.select(:id, :name, :email, :phone, :status, subscription: {
methods: [:id, :recurrency_code]}
end
This is a classic inner join scenario
class Student < ApplicationRecord
has_many :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :student
end
I find it helpful to break these problems into steps:
"Only Students where a Subscription record is present" is a standard inner join:
Student.joins(:subscriptions).uniq
"object must contain only the student's name, email, phone and status"
Student.joins(:subscriptions).select(:name, :email, :phone, :status).uniq
"in addition to the subscription id and recurrence_code"
students = Student.joins(:subscriptions)
.select(
'students.name, students.email,'\
'students.phone, students.status, '\
'subscriptions.id as subscription_id,'\
'subscriptions.recurrence_code as subscription_recurrence_code'
)
A few notes:
1. Using select with joins
#vee's SO Answer here points out:
If the column in select is not one of the attributes of the model on which the select is called on then those columns are not displayed. All of these attributes are still contained in the objects within AR::Relation and are accessible as any other public instance attributes.
This means if you load an individual record (e.g. students.first), you will only see the Student attributes by default. But you can still access the Subscription attributes by the as name you set in the query. E.g.:
students.first.subscription_recurrence_code
2. Use .uniq to eliminate duplicates.
Because of the has_many relationship, the query Student.joins(:subscriptions) will return a record for each subscription, which means each student will appear in the result the same number of times as they have subscriptions. Calling .uniq (short for unique) will remove duplicates from the result.
I'm agree with the Chiperific response, but I disagree to use the uniq method because it doesn't call the 'DISTINCT' in the SQL query.
Rails: uniq vs. distinct
For me it's better to use distinct. So the query could be as this:
Student.joins(:subscriptions).distinct.select(
:name, :email, :phone, :status,
'subscriptions.id AS subscription_id',
'subscriptions.recurrence_code'
)

Rails Select one random listings for premium users

In my rails app, I have Users and Listings. The Listings belong to a User. Listing has user_id and its filled with users id who is creating the listing.
A user can be a premium user, gold user or silver user.
What I want is for each premium user, select one random listing to show in premium listings.
I can do it in O(n**2) time or n+1 query as follow:
users_id = User.where(:role => "premium").pluck[:id]
final_array = Array.new
users_id.each do |id|
final_array << Listing.where(:user_id => id).sample(1)
end
final_array
Is there a better way of doing this?
You could try this:
listings = Listing.select(
<<~SQL
DISTINCT ON (users.id) users.id,
listings.*,
row_number() OVER (PARTITION BY users.id ORDER BY random())
SQL
)
.joins(:user)
.includes(:user)
.where(users: { role: :premium })
It gives a random Listing for every premium user.
It produces the only request to db and also it won't make an extra request for getting listing's user, so you are free to do something like this:
listings.each do |listing|
p listing.user
end
random_user_listings = []
User.includes(:listings).where(role: "premium").find_each do |user|
random_user_listings << user.listings.sample(1)
end
random_user_listings
To avoid N+1 query you need to combine them, perform query one time like this:
list = Listing.includes(:user).where(:role => "premium").sample(1)
Feel free to deal with list instead of Listing. Because now you're dealing with variable, not Query.
ids = list.pluck(:user_id).uniq
Getting array of ids like above and doing further steps as you did (but with list, not Listing)
Need to be noticed that, when you deal with Model you're dealing with QUERY. Avoiding doing that in loop statement.

Count total number of objects in list ordered by the number of associated objects

I have two models
class User
has_many :subscriptions
end
and
class Subscription
belongs_to :user
end
one one of my pages I would like to display a list of all users ordered by the number of subscriptions each user has. I am not to good with sql queries but I think that
list = Users.all.joins(:subscriptions).group("user.id").order("count(subscriptions.id) DESC")
dose the job. Now to my problem, when I try to count the total number of objects in list, using list.count, I get a hash with user.id and subscription count, like this
{11 => 5,
8 => 7,
1 => 11,
...}
not the total number of users in list.. .count works fine if I have a list sorted by for example user name (which is in the user table). I would really like to use .count since it in a module for pagination thats in a gem but any ideas is great!
Thanks!
We can just use a single query to finish this:
User.joins("LEFT OUTER JOIN ( SELECT user_id, COUNT(*) as num_subscriptions
FROM subscriptions
GROUP BY user_id
) AS temp
ON temp.user_id = users.id")
.order("temp.num_subscriptions DESC")
Basically, my idea is to try to query the number of subscription for each user_id in the subquery, then join with User. I used LEFT OUTER JOIN, because there will be several users which don't have any subscriptions
Improve option: You can define a scope inside User, it would be more beautiful for later usage:
user.rb
class User < ActiveRecord::Base
has_many :subscriptions
scope :sorted_by_num_subscriptions, -> {
joins("LEFT OUTER JOIN ( SELECT user_id, COUNT(*) as num_subscriptions
FROM subscriptions
GROUP BY user_id
) AS temp
ON temp.user_id = users.id")
.order("temp.num_subscriptions DESC")
}
end
Then just use it:
User.sorted_by_num_subscriptions
When grouping, the count method changes it's behavior and indeed, instead of returning the total count of records, it returns a hash of the counts for each group (see the docs for more info). So what you get with list.count is simply a hash of the subscription counts for each user.
So, your query is correct and all you need is to sum up the individual counts in the groups. This can be done simply by:
total_count = list.count.values.sum
If it is the pagination code that calls just a bare count that makes the issue, usually the pagination code is able to accept a parameter with total count. For example, will_paginate accepts the total_entries parameter, so you should be able to pass it the total count like this:
list.paginate(page: 2, total_entries: list.count.values.sum)

Active Record find using conditional based on multi level association's attribute

I am getting missing FROM clause error (shown below)
I want to find the entries whose associated reports' city is the same as the users' city, thus only showing relavant entries to users. So, if a user belongs to a city that a report does, the user should be shown the entries from that report.
I tried a few things, one being this scope in the Entry model:
scope :report_in_city, lambda { |user| joins(:report).where("report.city_id = ?", user.city_id) }
And if I call Entry.report_in_city(user)
I get this error:
SELECT "entries".* FROM "entries" INNER JOIN "reports" ON "reports"."id" = "entries"."report_id" WHERE (report.city_id = 1)
ActiveRecord::StatementInvalid: PG::Error: ERROR: missing FROM-clause entry for table "report"
LINE 1: ... ON "reports"."id" = "entries"."report_id" WHERE (report.cit...
^
: SELECT "entries".* FROM "entries" INNER JOIN "reports" ON "reports"."id" = "entries"."report_id" WHERE (report.city_id = 1)
I have a few models set up like this:
class Report
belongs_to user
belongs_to city
has_many entries
class Entry
belongs_to report
class User
has_many reports
belongs_to city
class City
has_many users
has_many reports
I am new to sql and would appreciate any advice on this query!
Since that string condition "report.city_id = ?" goes into the SQL, you have to use the table names. Rails is cute about going back and forth between plural and singular, but SQL only knows the table name reports. You can see that in the generated SQL like ON "reports"."id" = "entries"."report_id". The table name is plural.
Try this:
"reports.city_id = ?"
or the Rails way:
joins(:report).where(:report => { :city_id => user.city_id })

Rails 3 Order Records By Grand-child Count

I'm trying to do some fairly complicated record sorting that I was having a bit of trouble with. I have three models:
class User < ActiveRecord::Base
has_many :registers
has_many :results, :through => :registers
#Find all the Users that exist as registrants for a tournament
scope :with_tournament_entrees, :include => :registers, :conditions => "registers.id IS NOT NULL"
end
Register
class Register < ActiveRecord::Base
belongs_to :user
has_many :results
end
Result
class Result < ActiveRecord::Base
belongs_to :register
end
Now on a Tournament result page I list all users by their total wins (wins is calculated through the results table). First thing first I find all users who have entered a tournament with the query:
User.with_tournament_entrees
With this I can simply loop through the returned users and query each individual record with the following to retrieve each users "Total Wins":
user.results.where("win = true").count()
However I would also like to take this a step further and order all of the users by their "Total Wins", and this is the best I could come up with:
User.with_tournament_entrees.select('SELECT *,
(SELECT count(*)
FROM results
INNER JOIN "registers"
ON "results"."register_id" = "registers"."id"
WHERE "registers"."user_id" = "users.id"
AND (win = true)
) AS total_wins
FROM users ORDER BY total_wins DESC')
I think it's close, but it doesn't actually order by the total_wins in descending order as I instruct it to. I'm using a PostgreSQL database.
Edit:
There's actually three selects taking place, the first occurs on User.with_tournament_entries which just performs a quick filter on the User table. If I ignore that and try
SELECT *, (SELECT count(*) FROM results INNER JOIN "registers" ON "results"."register_id" = "registers"."id" WHERE "registers"."user_id" = "users.id" AND (win = true)) AS total_wins FROM users ORDER BY total_wins DESC;
it fails in both PSQL and the ERB console. I get the error message:
PGError: ERROR: column "users.id" does not exist
I think this happens because the inner-select occurs before the outer-select so it doesn't have access to the user id before hand. Not sure how to give it access to all user ids before than inner select occurs but this isn't an issue when I do User.with_tournament_entires followed by the query.
In your SQL, "users.id" is quoted wrong -- it's telling Postgres to look for a column named, literally, "users.id".
It should be "users"."id", or, just users.id (you only need to quote it if you have a table/column name that conflicts with a postgres keyword, or have punctuation or something else unusual).