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

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.

Related

SQL - Returning combinations of ID from tables if combination not found in another

I have four tables of interest: users, models, questions and labels.
"Labels" contains rows that describes a user's answer to a question on a model. e.g username TEXT, mid INT, qid INT, answer TEXT
I am interested in finding out what model-question pairs the user is still required to provide. A user is asked to provide an answer for every combination of model and questions that appear in their respective tables. So for a given username, I can have rows of model ids and question ids.
Users:
Bacon, XXXX, XXXX, XXXX, ...
Mark, XXXX, XXXX, XXXX, ...
Models:
1, climateOne, XXXX, ....
2, climateTwo, XXXX, ....
3, climateTwo, XXXX, ....
Questions:
1, "Is this a question?"
2, "And another?"
labels:
Bacon, 1, 2, "Yes is was..."
Bacon, 3, 2, "Another what?!"
So the result of asking "what model question pairs has 'Bacon' not completed" would be the return of:
(1,1)(2,1)(2,2)(3,1)
To get all possible combinations of models and questions, use a cross join:
SELECT models.id,
questions.id
FROM models
CROSS JOIN questions
Then filter out those that have been completed:
...
WHERE (models.id, questions.id) NOT IN (SELECT mid, qid
FROM labels);
Thanks go to CL. for the point in the right direction. This code worked for me as I was using python's sqlite3 package. I have included the username specification too. However, I have no clue whether it is efficient or not :)
SELECT m.mid, q.qid
FROM models m CROSS JOIN questions q
WHERE NOT EXISTS(
SELECT 1
FROM labels l
WHERE l.username = ? AND l.mid = m.mid AND l.qid = q.qid
)
Thanks all!

Rails query for associated model with max column value

I have 3 models in rails: Author, Book, and Page. pages belongs to book, books belong to author as so:
class Author < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :author
has_many :pages
end
class Page < ActiveRecord::Base
belongs_to :book
end
the Page model has a column called page_number. I'm using Postgres.
My question is this: Assuming a have an author #author, how do query for all that author's last pages? In other words, I want the last page of each book written by that author. I am trying the following which isn't working:
Page.where(book_id: #author.books.pluck(:id)).select('MAX(page_number), *').group(:book_id)
EDIT
The following 2 lines work, but I would love to learn of a faster/cleaner solution:
all_pages = Page.where(book: #author.books)
last_pages = all_pages.select{ |a| !all_pages.select{ |b| b.book_id == a.book_id}.any?{ |c| c.page_number > a.page_number } }
The most efficient way might be leveraging postgres' window functions
A query like this doesn't fit into the activerecord common use case, so you may have to use find_by_sql, but it may be well worth it.
In your case, grabbing the book ids first may be a good call, as joining or an additional subquery may not be advantageous—your call.
Let's say you have a list of book ids from #author.books.ids. The next thing we want is a list of pages "grouped by" book so we can pluck off the last page for each group. Let 1,2 be the book ids for the author in question.
We can use a window function and the rank function in postgres to create a resultset where pages are ranked over partitions (group) of book. We'll even sort those partitions of pages by the page number so that the maximum page number (last page) is at the top of each partition. The query would look like this:
select
*,
rank() over (
partition by book_id order by page_number desc
) as reverse_page_index
from pages
where book_id in (1,2)
Our imagined pages result set would look like this.
author 1, book 1, page 3, rank 1
author 1, book 1, page 2, rank 2
author 1, book 1, page 1, rank 3
author 1, book 2, page 6, rank 1
author 1, book 2, page 5, rank 2
author 1, book 2, page 4, rank 3
author 1, book 2, page 3, rank 4
author 1, book 2, page 2, rank 5
author 1, book 2, page 1, rank 6
The pages records are partitioned by book, sorted by page number ascending and given a rank amongst their partition.
If we then want only the first ranked (last page) for each book after the window calculation is performed, we can use a sub-select like so:
select *
from
(
select
*,
rank() over (
partition by book_id order by page_number desc
) as reverse_page_index
from pages
where book_id in (1,2)
) as pages
where reverse_page_index = 1;
Where we filter the above imagined result set to just page records where the rank (reverse_page_index) is 1 (i.e. the last page).
And now our result set would be:
author 1, book 1, page 3, rank 1
author 1, book 2, page 6, rank 1
You could also order this resultset by last modified or whatever you need.
Toss that query in a find_by_sql and you'll have some activerecord objects to use.

How to order by largest amount of identical entries with Rails?

I have a survey where users can post answers and since the answers are being saved in the db as a foreign key for each question, I'd like to know which answer got the highest rating.
So if the DB looks somewhat like this:
answer_id
1
1
2
how can I find that the answer with an id of 1 was selected more times than the one with an id of 2 ?
EDIT
So far I've done this:
#question = AnswerContainer.where(user_id: params[:user_id]) which lists the things a given user has voted for, but, obviously, that's not what I need.
you could try:
YourModel.group(:answer_id).count
for your example return something like: {1 => 2, 2 => 1}
You can do group by and then sort
Select answer_id, count(*) as maxsel
From poll
Group by answer_id
Order by maxsel desc
As stated in rails documentation (http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html) when you use group with count, active record "returns a Hash whose keys represent the aggregated column, and the values are the respective amounts"
Person.group(:city).count
# => { 'Rome' => 5, 'Paris' => 3 }

Rails: group by the count of a column

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)

Order a query based on comparison of associated records

class Man
has_many :sons
# id
end
class Son
belongs_to :man
# id, man_id, age
end
I was to retrieve men from the DB and I want them ordered based on the age of their oldest son. Here's an example.
first_man = Man.create
first_man.sons.create(age: 10)
first_man.sons.create(age: 5)
second_man = Man.create
second_man.sons.create(age: 20)
second_man.sons.create(age: 5)
third_man = Man.create
third_man.sons.create(age: 19)
third_man.sons.create(age: 8)
Man.order('[some order]').to_a
=> [second_man, third_man, first_man]
How do I get ActiveRecord to do this?
Edit
I get invalid SQL when I try to do Man.joins(:sons).order("sons.age DESC").uniq.
ActiveRecord::StatementInvalid:
PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
LINE 1: ...sons"."man_id" = "men"."id" ORDER BY sons...
^
: SELECT DISTINCT "men".* FROM "men" INNER JOIN "sons" ON "sons"."man_id" = "men"."id" ORDER BY sons.age DESC LIMIT 15
Not tried it but i guess this should work
Man.includes(:sons).order("sons.age").to_a
Try this
Man.joins(:sons).order('sons.age DESC').uniq
Updated
Maybe this will help but is's ugly
Son.order('age DESC').map(&:man).uniq