Given a class, I would like to return all objects (index) sorted by the number of associations they have with a specific attribute.
For example, let's say User has_many :fish I want to show a list of all users and order them by how many :fish they have that are color: :red (Assuming that's an enum/integer value)
Users with 0 red fish, or 0 fish at all, should still be shown on the table, but sorted to the bottom.
-- User -- | - # of fish - | - # of red fish -
2 | 10 | 10
5 | 25 | 7
6 | 11 | 6
7 | 18 | 5
1 | 27 | 4
3 | 23 | 1
4 | 3 | 0
8 | 0 | 0
So far I've managed to sort the User by the number of fish they have using the left_join gem, but I'm having trouble sorting by an attribute on that model.
This is how I am sorting by the number of fish:
#users = User.left_join(:fish).group("user.id").order("count(user.id) desc")
Any help would be awesome! I'm assuming this is trivial, but I'm also displaying that data as well, so any way to store that data temporarily would be awesome too! (I'm assuming using a select and setting the counts as variables)
EDIT:
Solution, thanks to Anthony E.
#users = User.select("users.*")
.joins("LEFT JOIN fishes ON fishes.user_id = users.id")
.where(fishes: { color: Fish.colors[:red] } )
.group("users.id").order("COUNT(fishes.id) DESC")
.page(params[:page]) // To work with Kaminari/pagination
How about:
User.select("user.*, COUNT(fish.id) AS fish_count")
.joins(:fish)
.where(fish: { color: "red" })
.group("user.id")
.order("fish_count DESC")
The benefit is that you can call fish_count on each object in the returned returned collection because it's in your select
EDIT
To include users with 0 fish, use a LEFT JOIN and modify the where clause:
User.select("user.*, COUNT(fish.id) AS fish_count")
.joins("LEFT JOIN ON fish.user_id = user.id")
.where("fish.color = 'red' OR fish.color IS NULL")
.group("user.id")
.order("fish_count DESC")
Related
| id | user_id | created_at (datetime) |
| 1 | 1 | 17 May 2016 10:31:34 |
| 2 | 1 | 17 May 2016 12:41:54 |
| 3 | 2 | 18 May 2016 01:13:57 |
| 4 | 1 | 19 May 2016 07:21:24 |
| 5 | 2 | 20 May 2016 11:23:21 |
| 6 | 1 | 21 May 2016 03:41:29 |
How can I get the result of unique and latest created_at user_id record, which will be record id 5 and 6 in the above case?
What I have tried so far
So far I am trying to use group_by to return a hash like this:
Table.all.group_by(&:user_id)
#{1 => [record 1, record 2, record 4, record 6], etc}
And select the record with maximum date from it? Thanks.
Updated solution
Thanks to Gordon answer, I am using find_by_sql to use raw sql query in ror.
#table = Table.find_by_sql("Select distinct on (user_id) *
From tables
Order by user_id, created_at desc")
#To include eager loading with find_by_sql, we can add this
ActiveRecord::Associations::Preloader.new.preload(#table, :user)
In Postrgres, you can use DISTINCT ON:
SELECT DISTINCT ON (user_id) *
FROM tables
ORDER BY user_id, created_at DESC;
I am not sure how to express this in ruby.
Table
.select('user_id, MAX(created_at) AS created_at')
.group(:user_id)
.order('created_at DESC')
Notice created_at is passed in as string in order call, since it's a result of aggregate function, not a column value.
1) Extract unique users form the table
Table.all.uniq(:user_id)
2) Find all records of each user.
Table.all.uniq(:user_id).each {|_user_id| Table.where(user_id: _user_id)}
3) Select the latest created
Table.all.uniq(:user_id).each {|_user_id| Table.where(user_id: _user_id).order(:created_at).last.created_at}
4) Return result in form of: [[id, user_id], [id, user_id] ... ]
Table.all.uniq(:user_id).map{|_user_id| [Table.where(user_id: _user_id).order(:created_at).last.id, _user_id]}
This should return [[6,1], [2,5]]
I have a model that stores a lower and upper limit value in each record. The values do not overlap.
Given a number, how do I query the records to see what record contains the range that the number falls into?
I think I could do something like below but I'm wondering is there a cleaner and more efficient answer
#records.each do |r|
if number.between?(r.lower_value, r.upper_value)
desired_record = r.id
break
end
end
Thanks for looking.
Construct a Range and test inclusion of the number with include?.
#records.each do |r|
if (r.lower_value..r.upper_value).include?(number)
desired_record = r.id
break
end
end
If you want to get all records where number is within range:
#records.select {|r| (r.lower_value..r.upper_value).include?(number)}
If you just want the first record:
#records.detect {|r| (r.lower_value..r.upper_value).include?(number)}
If you want to pull only the matching records out of the database:
Model.where("lower_value <= ? AND upper_value >= ?", number, number)
how about using between if your database supports it?
Record.find_by_sql("SELECT * from records where ? BETWEEN lower_value and upper_value", number)
and let the database do the work.
Example in postgresql:
foo_development=# select * from records;
id | lower_value | upper_value
----+-------------+-------------
1 | 1 | 10
2 | 11 | 15
3 | 16 | 120
(3 rows)
select * from records where 100 between lower_value and upper_value;
id | lower_value | upper_value
----+-------------+-------------
3 | 16 | 120
(1 row)
I'm trying to create an inbox for messaging between users.
Here are the following tables:
Messsages
Id | Message_from | message_to | message
1 | 2 | 1 | Hi
2 | 2 | 1 | How are you
3 | 1 | 3 | Hola
4 | 4 | 1 | Whats up
5 | 1 | 4 | Just Chilling
6 | 5 | 1 | Bonjour
Users
Id | Name
1 | Paul
2 | John
3 | Tim
4 | Rob
5 | Sarah
6 | Jeff
I'd like to display an inbox showing the list of users that the person has communicated and the last_message from either users
Paul's Inbox:
Name | user_id | last_message
Sarah| 5 | bonjour
Rob | 4 | Just Chilling
Tim | 3 | Hola
John | 2 | How are you
How do I do this with Active Records?
This should be rather efficient:
SELECT u.name, sub.*
FROM (
SELECT DISTINCT ON (1)
m.message_from AS user_id
, m.message AS last_message
FROM users u
JOIN messages m ON m.message_to = u.id
WHERE u.name = 'Paul' -- must be unique
ORDER BY 1, m.id DESC
) sub
JOIN users u ON sub.user_id = u.id;
Compute all users with the latest message in the subquery sub using DISTINCT ON. Then join to
table users a second time to resolve the name.
Details for DISTINCT ON:
Select first row in each GROUP BY group?
Aside: Using "id" and "name" as column names is not a very helpful naming convention.
How about this:
#received_messages = current_user.messages_to.order(created_at: :desc).uniq
If you want to include messages from the user as well, you might have to do a union query, or two queries, then merge and join them. I'm just guessing with some pseudocode, here, but this should set you on your way.
received_messages = current_user.messages_to
sent_messages = current_user.messages_from
(received_messages + sent_messages).sort_by { |message| message[:created_at] }.reverse
This type of logic is belongs to a model, not the controller, so perhaps you can add this to the message model.
scope :ids_of_latest_per_user, -> { pluck('MAX(id)').group(:user_id) }
scope :latest_per_user, -> { where(:id => Message.latest_by_user) }
Message.latest_per_user
On Rails 4. I am building a contest application. I have three tables relevant to this use case:
Submissions - Attributes include:
:contest_year - The year the submission was created
:category_id - The contest category the submission is assigned to
:division_id - The contest division the submission is assigned to (out of five)
Scores - Attributes include:
:user_id - The ID of the judge who gives the score
:submission_id - The submission ID the score is tied to
:total_score - The integer number the judge provides as a score of the entry; usually out of ten
Judges - Attributes include:
:user_id - The user ID of the judge
:category_id - The category the judge is assigned to score
:division_id - The division the judge is assigned to score
One submission can have many scores (as there are multiple judges assigned to its category/division).
The first thing the app must do is find the submission's combined, final score out of all its child scores the various judges give it. I do this using a calculation in submission.rb:
def calculate_final_score
self.scores.average(:total_score)
end
So basically, it finds all the child scores of the submission and takes the average of those to find the final score. This final score is NOT an updated attribute of the submission table, it is a method calculation result.
Now, here is where my question is. I need to find a way to calculate the submission's ranking compared to other submissions with the SAME :contest_year, :category_id, and :division_id by comparing that above final score calculation. The ranking system MUST give the same rank when submissions have the same final score (a tie).
tl;dr, I need ranking behavior like this (sample submissions table):
----------------------------------------------------------------------
| ID | Contest Year | Division ID | Category ID | Final Score | Rank |
|--------------------------------------------------------------------|
| 1 | 2013 | 1 | 1 | 10 | 1 |
|--------------------------------------------------------------------|
| 2 | 2013 | 1 | 1 | 10 | 1 |
|--------------------------------------------------------------------|
| 3 | 2013 | 1 | 1 | 8 | 2 |
|--------------------------------------------------------------------|
| 4 | 2013 | 1 | 1 | 6 | 3 |
|--------------------------------------------------------------------|
| 4 | 2013 | 1 | 1 | 5 | 4 |
|--------------------------------------------------------------------|
| 5 | 2013 | 2 | 1 | 8 | 1 |
|--------------------------------------------------------------------|
| 6 | 2014 | 1 | 2 | 9 | 1 |
|--------------------------------------------------------------------|
| 7 | 2014 | 1 | 2 | 7 | 2 |
----------------------------------------------------------------------
This ranking information will be placed in my Active Admin submissions' index table page (and also as part of the CSV table output).
I am not much familiarly with Rails (or with Ruby). But in our Django application we had a similar situation where we needed to calculate ranks on some computed data. As we were using PostgreSQL as the DB backend, we used Postges's window functions (http://www.postgresql.org/docs/9.1/static/functions-window.html, http://www.postgresql.org/docs/9.1/static/functions-window.html, http://www.postgresql.org/docs/9.1/static/sql-expressions.html#SYNTAX-WINDOW-FUNCTIONS) for rank calculation. Following is a sample PostgresSQL query which can produce the result required by your question.
sql_query = %Q{SELECT
*, dense_rank() OVER (
PARTITION BY contest_year, category_id, division_id
ORDER BY final_score DESC
)
FROM (
SELECT
Submissions.id,
Submission.contest_year AS contest_year,
Submission.category_id AS category_id,
Submission.division_id AS division_id,
AVG(Scores.total_score) AS final_score
FROM Submissions
INNER JOIN Scores ON (Submissions.id = Scores.submission_id)
GROUP BY
Submissions.id,
Submission.contest_year,
Submission.category_id,
Submission.division_id
) AS FinalScores}
submissions_by_rank = Submission.find_by_sql(sql_query)
Note: You will need to add ORDER BY clause in the query if you want to order the result in a particular fashion.
In Ruby, assuming you have that #calculate_final_score method set up already:
class Submission < ActiveRecord::Base
def self.rank_all_submissions_by_group
keys = [ :contest_year, :category_id, :division_id ]
submission_groups = Submission.all.group_by do |sub|
values = keys.map { |k| sub.send(key) }
Hash[key.zip(values)]
end
return ranked_submissions_by_group = submission_groups.map do |group, submissions|
group[:ranked_submissions] = submissions.map do |s|
[s, s.calculate_final_score]
end.sort_by(&:last)
end
end
end
The data that'll be returned will look like:
[
{
:contest_year => 2013,
:division_id => 1,
:category_id => 1,
:ranked_submissions => [
[ <Submission>, 10 ],
[ <Submission>, 10 ],
[ <Submission>, 8 ],
[ <Submission>, 6 ],
[ <Submission>, 5 ],
],
},
{
:contest_year => 2013,
:division_id => 2,
:category_id => 1,
:ranked_submissions => [
[ <Submission>, 8 ],
],
},
{
:contest_year => 2014,
:division_id => 1,
:category_id => 2,
:ranked_submissions => [
[ <Submission>, 9 ],
[ <Submission>, 7 ],
],
},
]
It won't be very performant though, since it just goes through all submissions for all time, makes a lot of copies (I think?), and I'm not using the right sorting algorithm.
If you have any issues/concerns, let me know, and I can update my answer.
Additional Help
If you're looking for a way to run a raw query in Ruby, consider this trivial example a suggestion:
full_submissions_table_as_hash = ActiveRecord::Base.connection.select_all(<<-mysql)
SELECT *
FROM #{Submissions.table_name}
mysql
Another trivial example showing how the data can be rendered from a controller:
# assuming routes are properly set up to actions in HomeController
class HomeController < ActionController::Base
def ranked_submissions_text
render :text => Submission.rank_all_submissions_by_group
end
def ranked_submissions_json
render :json => Submission.rank_all_submissions_by_group
end
end
I have a table name transactions with following columns
created_at | debit | credit | balance | account_id
2012-2-2 02:22:22 | 3000 | 0 | 0 | 8
2012-2-2 07:22:22 | 500 | 1000 | 0 | 8
2012-2-2 09:22:22 | 0 | 8000 | 0 | 8
2012-3-3 19:17:20 | 1000 | 0 | 0 | 8
2012-3-3 04:02:22 | 0 | 8000 | 0 | 8
Before calculating balance I need to
sort transactions by date (i.e. daily)
then sort by debit (higher debit must come first)
I have a million transactions distinguished by account_id. What is efficient way of sorting in such a scenario ?
Any answer will be appreciated.
This sounds a lot like this question
Transaction.all(:order => "created_at DESC, debit DESC")
This kind of querying is exactly what relational databases are good at and with proper indexing, it should be efficient for you.
For a particular account…
Transaction.find_by_account_id(some_id, :order => "created_at DESC, debit DESC")
If this is setup as a proper ActiveRecord association on the account, you can set the order there so the association is always returned in the preferred order:
class Account
has_many :transactions, :order => "created_at DESC, debit DESC"
end
This way, any time you ask an account for its transactions like Account.find(12345).transactions they'll be returned in the preferred sorting automatically.