Rails: group by the count of a column - sql

I'm not sure how to best express this question.
Let's say I have a UserSkill, which belongs_to :user and belongs_to :skill. I have a collection of Skills, and from those I have an array of skill_ids, say with .map(&:id).
I can easily use this array to do an IN type query like UserSkill.where(skill_id: skill_ids).
But I want to find the users that have the most skills from my input.
I tried writing this naively as UserSkill.where(skill_id: skill_ids).group("user_skills.user_id").order("count(user_skills.user_id) desc"), but that has a syntax error.
To further clarify, let's say we have User id: 1 and User id: 2. Our result from UserSkill.where(skill_id: skill_ids) is the following:
UserSkill user_id: 1, skill_id: 1
UserSkill user_id: 1, skill_id: 2
UserSkill user_id: 2, skill_id: 2
The result I'd be looking for would be:
User id: 1
User id: 2
What's the right query for this? And how should I be phrasing this question to begin with?

Assuming a has_many association from User to UserSkill, you could try
User.joins(:user_skills).
group("users.id").
order("COUNT(users.id) DESC").
merge(UserSkill.where(skill_id: skill_ids))

In SQL I might write this:
select users.*
from users
join user_skills on users.id = user_skills.user_id
where
user_skills.skill id in (1,2,3)
group by users.id
order by count(*) desc, users.id asc
limit 5
Which might look like this:
User.joins("user_skills on users.id = user_skills.user_id").
where("user_skills.skill_id" => skill_ids).
group("users.id").
order("count(*) desc").
limit(5)

Related

Rails 4 ActiveRecord: Order recrods by attribute and association if it exists

I have three models that I am having trouble ordering:
User(:id, :name, :email)
Capsule(:id, :name)
Outfit(:id, :name, :capsule_id, :likes_count)
Like(:id, :outfit_id, :user_id)
I want to get all the Outfits that belong to a Capsule and order them by the likes_count.
This is fairly trivial and I can get them like this:
Outfit.where(capsule_id: capsule.id).includes(:likes).order(likes_count: :desc)
However, I then want to also order the outfits so that if a given user has liked it, it appears higher in the list.
Example if I have the following outfit records:
Outfit(id: 1, capsule_id: 2, likes_count: 1)
Outfit(id: 2, capsule_id: 2, likes_count: 2)
Outfit(id: 3, capsule_id: 2, likes_count: 2)
And the given user has only liked outfit with id 3, the returned order should be IDs: 3, 2, 1
I'm sure this is fairly easy, but I can't seem to get it. Any help would be greatly appreciated :)
Postgres SQL with a subquery
SELECT outfits.*
FROM outfits
LEFT OUTER JOIN (SELECT likes.outfit_id, 1 AS weight
FROM likes
WHERE likes.user_id = #user_id) AS user_likes
ON user_likes.outfit_id = outfits.id
WHERE outfits.capsule_id = #capsule_id
ORDER BY user_likes.weight ASC, outfits.likes_count DESC;
Postgres gives NULL values bigger weight when ordering. I am not sure how this would look in Arel query. You can try converting it using this cheatsheets.

Reverse query on a has_many :through

Scenario:
Team
has_many :players, dependent: :destroy
has_many :users, through: :players
Player
belongs_to :team
belongs_to :user
User
So, let's say that i have 4 teams with different users:
Team 1
User 1, User 2
Team 2
User 2, User 3
Team 3
User 1
Team 4
User 2, User 4, User 5
Now, suppose i have the id of two users, (User 1, User 5), and i want to know if there is any team which consists of ONLY these two players. Let's say i have a team that consists of users 1, 2 and 5. The query should not bring this team.
How can i use ActiveRecord semantics in my favor to do this? It is easy to get all players from a team, but i couldn't find a way to do the opposite.
UPDATE: AH! I got it in pure SQL:
users = User.first(2)
Team.joins(:users).group('teams.id').having('SUM( CASE WHEN users.id in (?) THEN 1 ELSE -1 END ) = ?', users, users.count)
Try it and let me know if it works for you (working here: http://sqlfiddle.com/#!17/bb2a9/8 and the same example but with 3 players: http://sqlfiddle.com/#!17/bb2a9/10)
This is not optimized on the DB level as it uses a lot of ruby/rails code, but can you try it?
users = User.first(2)
# find teams with that exact number of players associated
teams = Team.joins(:users).group('teams.id').having('COUNT(users.id) = ?', users.count)
# find players referencing to those teams with other users than the ones specified
players_to_ignore = Player.where(team_id: teams).where('user_id NOT IN (?)', users)
# get Teams where associated players id is not in the previous list
Team.where(id: teams).joins(:players).where('players.id NOT IN (?)', players_to_ignore)
You can use two where clauses:
One for getting all the Teams having exactly two users.
Team.joins(:users).group("teams.id").having("count('distinct users.id') = 2").select("teams.id")
Second for having all Teams with users 1 and 5.
Team.joins(:users).where('users.id in (?)', [1,5]).group("teams.id").having("count('distinct users.id') = 2").select("teams.id")
Intersection of these two should give you what you need.
So to combine it all:
Team.where(id: Team.joins(:users).group("teams.id").having("count('distinct users.id') = 2").select("teams.id")).where(id: Team.joins(:users).where('users.id in (?)', [1,5]).group("teams.id").having("count('distinct users.id') = 2").select("teams.id"))
Team.join(:users).where('users.id in (?)', [1,5]).
select { |team| team.users.map(&:id).sort == [1,5] }
Previous answer (For pre edited question)
Will this works for you?
Team.join(:users).where('users.id in (?)', [1,5])
You can do the same on user model by
# user.rb
has_many :teams, through: :works
has_many :works, foreign_key: :user_id
Responding to your edits & comment
Hacky:
Team.join(:users).where('users.id in (?)', [1,5]).
select { |team| team.users.map(&:id).sort == [1,5] }
Better?
SQL Select only rows where exact multiple relationships exist

rails 3 query with count on nested resource

Consider I have 3 tables: Users (has_many) -> Websites (has_many) -> Visits.
How would one find out the total number of Visits each user has without writing plain sql code?
I have an idea of which I'm not very proud since I let rails do the math instead of mysql:
count = 0
user.websites.each |website|
count += website.visits.count()
I'm new with rails and maybe i'm missing some docs. Is it possible to find out that count just from the query builder?
you can define that the User has many Visits through the Websites like this:
class User < ActiveRecord::Base
has_many :websites
has_many :visits, :through => :websites
end
now, if you do
some_user.visits.count
this sql is executed:
(0.4ms) SELECT COUNT(*) FROM "visits" INNER JOIN "websites" ON "visits"."website_id" = "websites"."id" WHERE "websites"."user_id" = 1
=> 8
That is, ActiveRecord creates the SQL query for you.

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).

Ruby on Rails - How to join two tables?

I have two tables (subjects and pages) in one-to-many relations. I want to add criterias from subjects as well pages to parse a sql, but the progress has been very slow and often times running into problems. I'm brand new in rails, please help.
class Subject < ActiveRecord::Base
has_many :pages
end
class Page < ActiveRecord::Base
belongs_to :subject
end
sample data in subjects, listed three columns below:
id name level
1 'Math' 1
6 'Math' 2
...
Sample data in pages, listed columns below:
id name subject_id
-- -------------------- ----------
2 Addition 1
4 Subtraction 1
5 Simple Multiplication 6
6 Simple Division 6
7 Hard Multiplication 6
8 Hard Division 6
9 Elementary Divsion 1
Given that I don't know the subject.id, I only know the subject name and level, and page name. Here is the sql I want to generate (or something similar that would achieve the same result):
select subjects.id, subjects.name, pages.id, pages.name from subjects, pages
where subjects.id = pages.subject_id
and subjects.name = 'Math'
and subjects.level = '2'
and pages.name like '%Division' ;
I expect to get two rows in the result:
subjects.id subjects.name pages.id pages.name
----------- ------------- -------- -----------
6 Math 6 Simple Division
6 Math 8 Hard Division
This is a very simple sql, but I have not been able to get want I wanted in rails.
Here is my rails console:
>> subject = Subject.where(:name => 'Math', :level => 2)
Subject Load (0.4ms) SELECT `subjects`.* FROM `subjects` WHERE `subjects`.`name` = 'Math' AND `subjects`.`level` = 2
[#<Subject id: 6, name: "Math", position: 1, visible: true, created_at: "2011-12-17 04:25:54", updated_at: "2011-12-17 04:25:54", level: 2>]
>>
>> subject.joins(:pages).where(['pages.name LIKE ?', '%Division'])
Subject Load (4.2ms) SELECT `subjects`.* FROM `subjects` INNER JOIN `pages` ON `pages`.`subject_id` = `subjects`.`id` WHERE `subjects`.`name` = 'Math' AND `subjects`.`level` = 2 AND (pages.name LIKE '%Division')
[#<Subject id: 6, name: "Math", position: 1, visible: true, created_at: "2011-12-17 04:25:54", updated_at: "2011-12-17 04:25:54", level: 2>, #<Subject id: 6, name: "Math", position: 1, visible: true, created_at: "2011-12-17 04:25:54", updated_at: "2011-12-17 04:25:54", level: 2>]
>>
>> subject.to_sql
"SELECT `subjects`.* FROM `subjects` WHERE `subjects`.`name` = 'Math' AND `subjects`.`level` = 2"
>> subject.size
1
>> subject.class
ActiveRecord::Relation
1st statement: subject = Subject.where(:name => 'Math', :level => 2)
2nd statement: subject.joins(:pages).where(['pages.name LIKE ?', '%Division'])
Questions:
the results of the chained sql really returns two rows, but subject.size says only 1?
How do I tell it to return columns from :pages as well?
Why subject.to_sql still shows the sql from statement 1 only, why did it not include the chained sql from statement 2?
Essentially, what do I need to write the statements differently to parse the sql as listed above (or achieve the same result)?
Many thanks.
1) ActiveRecord is going to map your query results to objects not arbitrary returned rows, so because you based the query creation off of the Subject class it is looking at your resulting rows and figures out that it is only referring to 1 unique Subject object, so returns just that single Subject instance.
2) The column data is there, but you are working against what ActiveRecord wants to give you, which is objects. If you would rather have Pages returned, then you need to base the creation of the query on the Page class.
3) You didn't save the results of adding the join(:pages)... back into the subject variable. If you did:
subject = subject.joins(:pages).where(['pages.name LIKE ?', '%Division'])
You would get the full query when running subject.to_sql
4) To get page objects you can do something like this, notice that we are basing it off of the Page class:
pages = Page.joins(:subject).where(['subjects.name = ? AND subjects.level = ? AND pages.name LIKE ?', 'Math', 2, '%Division'])
Then to access the subject name from there for the first Page object returned:
pages[0].subject.name
Which because you have the join in the first, won't result in another SQL query. Hope this helps!