Group and count in Rails - ruby-on-rails

I know I've seen this before but I can't find anything now. I want to group a query by a certain column and be able to display how many are in each group. I got the first part down:
#line_items = #project.line_items.all(:group => "device_id")
This is for my line item index view, which is just a table displaying the line items. How do I make a column in that table for "count" now that the line items are grouped by device?

You can do count on line_items which will return you an ordered hash of device_id and count.
#project.line_items.group(:device_id).count

hash of devise_id as key and associated records count
#project.line_items.group(:device_id).count

I think you can try this as well.
#project.line_items.group(:device_id).pluck("device_id, count(device_id)")
^^ This gives array of arrays with elements 'device_id and count'

Just add a :select option:
#line_items = #project.line_items.all(
:group => "device_id",
:select => "device_id, COUNT(*) as count"
)
Then each #line_item will have a count attribute.

something like
User.all(:joins => :comments, :select => "users.*, count(comments.id) as comments_count", :group => "users.id")
might also work...

After this commit:
https://github.com/rails/rails/commit/a1c05dd8b9bd3623289d3aa73dda2943d620cc34
there's a new way to do the same thing:
#project.line_items.count(:group => LineItem.arel_table[:device_id])

For only count pluck would be faster here rather than group
#project.line_items.pluck(:device_id).count
#project.line_items.pluck(:device_id).uniq.count

Related

How to make a join between 2 tables in Rails that returns the data of both tables

I'm trying to join two tables: Rooms and Room_Types (which have a relationship already). The thing is, I'm trying to do something like:
room = Room.all :conditions => ['rooms.id = ?', #room_id],
:joins => :room_type
room.to_json
..and this JSON is being sent to my view.
However, the JSON is only showing the fields of the Room table and is not including the Room_Type fields, and I need both tables' fields in this JSON. How can I accomplish this?
:joins only performs a JOIN. As in SQL, this does not add the JOINed table's columns to the results. If you want to do that you should use :include instead:
rooms = Room.all :conditions => [ 'rooms.id = ?', #room_id ],
:include => :room_type
rooms.to_json
Or, in Rails 3 parlance:
rooms = Room.where(:id => #room_id).include(:room_type).all
rooms.to_json
try
Room.joins(:room_type).where(:id => #room_id).select('room_types.foo as foo, room_types.bar as bar')

Sorting by properties of a has_many association

Suppose Songs have_many Comments; How can I:
Pull a list of all songs from the database sorted by the number of comments they each have? (I.e., the song with the most comments first, the song with the least comments last?)
Same, but sorted by comment creation time? (I.e., the song with the most recently created comment first, the song with the least recently created comment last?)
1) There are a couple of ways to do this, easiest would be counter cache, you do that my creating a column to maintain the count and rails will keep the count up to speed. the column in this case would be comments_count
songs = Song.all(:order => "comments_count DESC")
OR you could do a swanky query:
songs = Song.all(:joins => "LEFT JOIN comments ON songs.id = comments.song_id",
:select => "song.name, count(*)",
:group => "song.name",
:order => "count(*) DESC")
a few caveats with the second method, anything you want to select in the songs you will need to include in the group by statement. If you only need to pull songs with comments then you can:
songs = Song.all(:joins => :comments,
:select => "song.name, count(*)",
:group => "song.name",
:order => "count(*) DESC")
Which looks nicer but because it does an inner join you would not get songs that had no comments
2) just an include/joins
songs = Song.all(:include => :comments, :order => "comment.created_at"
I hope this helps!
If you need to sort by number of comments they have - while you can do it directly using SQL - I strongly recommend you use counter_cache - See: http://railscasts.com/episodes/23-counter-cache-column
Ofter that, just set the order by option of find like so:
Song.all(:order => "comments_count DESC");
This is different in Rails 3 so it depends what you're using.
I would also recommend caching the latest comment created thing on the Song model to make your life easier.
You would do this with an after_save callback on the Comment model with something like:
self.song.update_attributes!({:last_comment_added_at => Time.now.to_s(:db)})

Select, group and sum results from database

I have a database with some fields I'd like to sum. But that's not the big problem, I want to group those fields by the month they were created. ActiveRecord automaticaly created a field named "created_at". So my question; how can I group the result by month, then sum the fields for each month?
Updated with code
#hours = Hour.all(:conditions => "user_id = "+ #user.id.to_s,
:group => "strftime('%m', created_at)",
:order => 'created_at DESC')
This is the code I have now. Managed to group by month, but doesn't manage to sum two of my fields, "mins" and "salary" which I need to sum
You can use active record calculations to do this. Some example code might be
Model.sum(:column_name, :group => 'MONTH("created_at")')
Obviously with the caveat MONTH is mysql specific, so if you were developing on an SQLite database this would not work.
I don't know if there's a SQL query you use to do it (without changing your current table structure). However, you do it with some lines of code.
records = Tasks.find(:conditions => {..})
month_groups = records.group_by{|r| r.created_at.month}
month_groups.each do |month, records|
sum stuff.. blah blah blah..
end
I saw this link on the right side of this question. I assume other databases, besides MySQL have similar functions.
mysql select sum group by date
Fixed it by using :select when getting the query, inputing selects manually
#hours = Hour.all(:conditions => "user_id = "+ #user.id.to_s,
:select => "created_at, SUM(time) time",
:group => "strftime('%m', created_at)",
:order => 'created_at DESC')

Rails, Get a random record when using :group

How do I get a random record when using :group?
#paintings = Painting.all(:group => "user_id", :order => "created_at DESC")
This gives me the latest painting for each user. Now I would like to select a random painting from each user instead of the latest. The order of the paintings should still be the same, so that the user that have been the most active will get his/her random painting displayed first.
painting150 (user1)
painting200 (user2)
painting231 (user3)
Is this possible?
Best regards.
Asbjørn Morell.
This answer is specific to Rails, but since you are using ActiveRecord, I am assuming it should be fine.
unique_paintings = []
#paintings.group_by(&:user_id).each do |user_id, paintings|
unique_paintings << paintings[rand(paintings.size-1)]
end
unique_paintings.sort_by(&:created_at)
The group_by most certainly messes up the created_at sort you did in the query, so I did a sort_by as the last step. You might want to get rid of it in the query since you'll have to do it anyway here.
#painting = #paintings[rand(#paintings.size-1)]
(or paintings.count, dont know the right method yet)
Assuming you have MySQL, you can try:
#paintings = Painting.all(:group => "user_id", :order => "RAND()")
you could do something like this but it will suffer as your number of records grow
#paintings = Painting.find(:all, :order => 'RAND()').map{ |i| i.user_id }.uniq

How to get last N records with activerecord?

With :limit in query, I will get first N records. What is the easiest way to get last N records?
This is the Rails 3 way
SomeModel.last(5) # last 5 records in ascending order
SomeModel.last(5).reverse # last 5 records in descending order
Updated Answer (2020)
You can get last N records simply by using last method:
Record.last(N)
Example:
User.last(5)
Returns 5 users in descending order by their id.
Deprecated (Old Answer)
An active record query like this I think would get you what you want ('Something' is the model name):
Something.find(:all, :order => "id desc", :limit => 5).reverse
edit: As noted in the comments, another way:
result = Something.find(:all, :order => "id desc", :limit => 5)
while !result.empty?
puts result.pop
end
new way to do it in rails 3.1 is SomeModel.limit(5).order('id desc')
For Rails 5 (and likely Rails 4)
Bad:
Something.last(5)
because:
Something.last(5).class
=> Array
so:
Something.last(50000).count
will likely blow up your memory or take forever.
Good approach:
Something.limit(5).order('id desc')
because:
Something.limit(5).order('id desc').class
=> Image::ActiveRecord_Relation
Something.limit(5).order('id desc').to_sql
=> "SELECT \"somethings\".* FROM \"somethings\" ORDER BY id desc LIMIT 5"
The latter is an unevaluated scope. You can chain it, or convert it to an array via .to_a. So:
Something.limit(50000).order('id desc').count
... takes a second.
For Rails 4 and above version:
You can try something like this If you want first oldest entry
YourModel.order(id: :asc).limit(5).each do |d|
You can try something like this if you want last latest entries..
YourModel.order(id: :desc).limit(5).each do |d|
Solution is here:
SomeModel.last(5).reverse
Since rails is lazy, it will eventually hit the database with SQL like: "SELECT table.* FROM table ORDER BY table.id DESC LIMIT 5".
If you need to set some ordering on results then use:
Model.order('name desc').limit(n) # n= number
if you do not need any ordering, and just need records saved in the table then use:
Model.last(n) # n= any number
In my rails (rails 4.2) project, I use
Model.last(10) # get the last 10 record order by id
and it works.
Just try:
Model.order("field_for_sort desc").limit(5)
we can use Model.last(5) or Model.limit(5).order(id: :desc) in rails 5.2
I find that this query is better/faster for using the "pluck" method, which I love:
Challenge.limit(5).order('id desc')
This gives an ActiveRecord as the output; so you can use .pluck on it like this:
Challenge.limit(5).order('id desc').pluck(:id)
which quickly gives the ids as an array while using optimal SQL code.
Let's say N = 5 and your model is Message, you can do something like this:
Message.order(id: :asc).from(Message.all.order(id: :desc).limit(5), :messages)
Look at the sql:
SELECT "messages".* FROM (
SELECT "messages".* FROM "messages" ORDER BY "messages"."created_at" DESC LIMIT 5
) messages ORDER BY "messages"."created_at" ASC
The key is the subselect. First we need to define what are the last messages we want and then we have to order them in ascending order.
If you have a default scope in your model that specifies an ascending order in Rails 3 you'll need to use reorder rather than order as specified by Arthur Neves above:
Something.limit(5).reorder('id desc')
or
Something.reorder('id desc').limit(5)
A simple answer would be:
Model.limit(5).order(id: :desc)
There is a problem with this solution, as id can't be the sole determiner of when a record was created in the time.
A more reliable solution would be:
Model.order(created_at: :desc).limit(5)
As others have pointed out, one can also use Model.last(5). The only gotcha with this is that it returns Array, and not Model::ActiveRecord_Relation.
Add an :order parameter to the query

Resources