Ruby on Rails: how do I sort with two columns using ActiveRecord? - ruby-on-rails

I want to sort by two columns, one is a DateTime (updated_at), and the other is a Decimal (Price)
I would like to be able to sort first by updated_at, then, if multiple items occur on the same day, sort by Price.

In Rails 4 you can do something similar to:
Model.order(foo: :asc, bar: :desc)
foo and bar are columns in the db.

Assuming you're using MySQL,
Model.all(:order => 'DATE(updated_at), price')
Note the distinction from the other answers. The updated_at column will be a full timestamp, so if you want to sort based on the day it was updated, you need to use a function to get just the date part from the timestamp. In MySQL, that is DATE().

Thing.find(:all, :order => "updated_at desc, price asc")
will do the trick.
Update:
Thing.all.order("updated_at DESC, price ASC")
is the Rails 3 way to go. (Thanks #cpursley)

Active Record Query Interface lets you specify as many attributes as you want to order your query:
models = Model.order(:date, :hour, price: :desc)
or if you want to get more specific (thanks #zw963 ):
models = Model.order(price: :desc, date: :desc, price: :asc)
Bonus: After the first query, you can chain other queries:
models = models.where('date >= :date', date: Time.current.to_date)

Actually there are many ways to do it using Active Record. One that has not been mentioned above would be (in various formats, all valid):
Model.order(foo: :asc).order(:bar => :desc).order(:etc)
Maybe it's more verbose, but personally I find it easier to manage.
SQL gets produced in one step only:
SELECT "models".* FROM "models" ORDER BY "models"."etc" ASC, "models"."bar" DESC, "models"."foo" ASC
Thusly, for the original question:
Model.order(:updated_at).order(:price)
You need not declare data type, ActiveRecord does this smoothly, and so does your DB Engine

Model.all(:order => 'updated_at, price')

None of these worked for me!
After exactly 2 days of looking top and bottom over the internet, I found a solution!!
lets say you have many columns in the products table including: special_price and msrp. These are the two columns we are trying to sort with.
Okay, First in your Model
add this line:
named_scope :sorted_by_special_price_asc_msrp_asc, { :order => 'special_price asc,msrp asc' }
Second, in the Product Controller, add where you need to perform the search:
#search = Product.sorted_by_special_price_asc_msrp_asc.search(search_params)

Related

How sorting works in ruby on rails? when there are multiple fields to sort in a single query

I have a model with the fields price, min_price,max_price, discount,in my product table. if I want to execute ascending descending orders, how that will get executed when we apply for an order on multiple fields. for example like below.
#products = Product.order("price asc").order("min_price desc").order("max_price asc").order("updated_at asc") (Query might be wrong but for reference im adding)
will it order as per the order sequence ?
If you append .to_sql to that, it will show the generated SQL so you can investigate yourself.
I tried a similar query:
Book.select(:id).order("id asc").order("pub_date desc").to_sql
=> "SELECT \"books\".\"id\" FROM \"books\" ORDER BY id asc, pub_date desc"
You might instead:
Book.select(:id).order(id: :asc, pub_date: :desc).to_sql
=> "SELECT \"books\".\"id\" FROM \"books\" ORDER BY \"books\".\"id\" ASC, \"books\".\"pub_date\" DESC"
... which you see adds the table name in, so is more reliable when if you are accessing multiple tables

Rails order query method for multiple and child attributes

While the new rails syntax implements an attribute that chooses the order direction as a symbol with colons,
I have not found a way to enact this syntaxic method for the following old-school style of ordering, when invoking multiple attributes where at least one is from a child table
#items = Item.eager_load(:itemoptions)
.order('model_id ASC, itemoptions.modeloption_id ASC')
.where( [...])
.all
how can this be achieved with the new approach?
As you're joining both tables, you need to prefix the columns you're using with their corresponding table names.
The only arguments for direction that order is able to receive are [:asc, :desc, :ASC, :DESC, "asc", "desc", "ASC", "DESC"], so that forces you to pass the columns as hash keys in the form { "table_name.column_name" => <direction> }.
Try with:
Item.eager_load(:itemoptions)
.where( [...])
.order('items.id' => :ASC, 'itemoptions.modeloption_id' => :ASC)
You don't need the all method there.
The error message that I receive says that it's possible to use Arel's sql escape to do this. Currently I am able to do something like Item.eager_load(:itemoptions).order(Arel.sql("model_id ASC, itemoptions.modeloption_id ASC"))
Also I know not part of the question, but I don't think you need the .all at the end

How to get a most recent value group by year by using SQL

I have a Company model that has_many Statement.
class Company < ActiveRecord::Base
has_many :statements
end
I want to get statements that have most latest date field grouped by fiscal_year_end field.
I implemented the function like this:
c = Company.first
c.statements.to_a.group_by{|s| s.fiscal_year_end }.map{|k,v| v.max_by(&:date) }
It works ok, but if possible I want to use ActiveRecord query(SQL), so that I don't need to load unnecessary instance to memory.
How can I write it by using SQL?
select t.username, t.date, t.value
from MyTable t
inner join (
select username, max(date) as MaxDate
from MyTable
group by username
) tm on t.username = tm.username and t.date = tm.MaxDate
For these kinds of things, I find it helpful to get the raw SQL working first, and then translate it into ActiveRecord afterwards. It sounds like a textbook case of GROUP BY:
SELECT fiscal_year_end, MAX(date) AS max_date
FROM statements
WHERE company_id = 1
GROUP BY fiscal_year_end
Now you can express that in ActiveRecord like so:
c = Company.first
c.statements.
group(:fiscal_year_end).
order(nil). # might not be necessary, depending on your association and Rails version
select("fiscal_year_end, MAX(date) AS max_date")
The reason for order(nil) is to prevent ActiveRecord from adding ORDER BY id to the query. Rails 4+ does this automatically. Since you aren't grouping by id, it will cause the error you're seeing. You could also order(:fiscal_year_end) if that is what you want.
That will give you a bunch of Statement objects. They will be read-only, and every attribute will be nil except for fiscal_year_end and the magically-present new field max_date. These instances don't represent specific statements, but statement "groups" from your query. So you can do something like this:
- #statements_by_fiscal_year_end.each do |s|
%tr
%td= s.fiscal_year_end
%td= s.max_date
Note there is no n+1 query problem here, because you fetched everything you need in one query.
If you decide that you need more than just the max date, e.g. you want the whole statement with the latest date, then you should look at your options for the greatest n per group problem. For raw SQL I like LATERAL JOIN, but the easiest approach to use with ActiveRecord is DISTINCT ON.
Oh one more tip: For debugging weird errors, I find it helpful to confirm what SQL ActiveRecord is trying to use. You can use to_sql to get that:
c = Company.first
puts c.statements.
group(:fiscal_year_end).
select("fiscal_year_end, MAX(date) AS max_date").
to_sql
In that example, I'm leaving off order(nil) so you can see that ActiveRecord is adding an ORDER BY clause you don't want.
for example you want to get all statements by start of the months you should use this
#companey = Company.first
#statements = #companey.statements.find(:all, :order => 'due_at, id', :limit => 50)
then group them as you want
#monthly_statements = #statements.group_by { |statement| t.due_at.beginning_of_month }
Building upon Bharat's answer you can do this type of query in Rails using find_by_sql in this way:
Statement.find_by_sql ["Select t.* from statements t INNER JOIN (
SELECT fiscal_year_end, max(date) as MaxDate GROUP BY fiscal_year_end
) tm on t.fiscal_year_end = tm.fiscal_year_end AND
t.created_at = tm.MaxDate WHERE t.company_id = ?", company.id]
Note the last where part to make sure the statements belong to a specific company instance, and that this is called from the class. I haven't tested this with the array form, but I believe you can turn this into a scope and use it like this:
# In Statement model
scope :latest_from_fiscal_year, lambda |enterprise_id| {
find_by_sql[..., enterprise_id] # Query above
}
# Wherever you need these statements for a particular company
company = Company.find(params[:id])
latest_statements = Statement.latest_from_fiscal_year(company.id)
Note that if you somehow need all the latest statements for all companies then this most likely leave you with a N+1 queries problem. But that is a beast for another day.
Note: If anyone else has a way to have this query work on the association without using the last where part (company.statements.latest_from_year and such) let me know and I'll edit this, in my case in rails 3 it just pulled em from the whole table without filtering.

Rails 3. How to perform a "where" query by a virtual attribute?

I have two models: ScheduledCourse and ScheduledSession.
scheduled_course has_many scheduled_sessions
scheduled_session belongs_to scheduled_course
ScheduledCourse has a virtual attribute...
def start_at
s = ScheduledSession.where("scheduled_course_id = ?", self.id).order("happening_at ASC").limit(1)
s[0].happening_at
end
... the start_at virtual attribute checks all the ScheduledSessions that belongs to the ScheduledCourse and it picks the earliest one. So start_at is the date when the first session happens.
Now I need to write in the controller so get only the records that start today and go into the future. Also I need to write another query that gets only past courses.
I can't do the following because start_at is a virtual attribute
#scheduled_courses = ScheduledCourse.where('start_at >= ?', Date.today).page(params[:page])
#scheduled_courses = ScheduledCourse.where('start_at <= ?', Date.today)
SQLite3::SQLException: no such column: start_at: SELECT "scheduled_courses".* FROM "scheduled_courses" WHERE (start_at >= '2012-03-13') LIMIT 25 OFFSET 0
You can't perform SQL queries on columns that aren't in the database. You should consider making this a real database column if you intend to do queries on it instead of a fake column; but if you want to select items from this collection, you can still do so. You just have to do it in Ruby.
ScheduledCourse.page(params).find_all {|s| s.start_at >= Date.today}
Veraticus is right; You cannot use virtual attributes in queries.
However, I think you could just do:
ScheduledCourse.joins(:scheduled_sessions).where('scheduled_courses.happening_at >= ?', Date.today)
It will join the tables together by matching ids, and then you can look at the 'happening_at' column, which is what your 'start_at' attribute really is.
Disclaimer: Untested, but should work.
I wonder if this would be solved by a subquery ( the subquery being to find the earliest date first). If so, perhaps the solution here might help point in a useful direction...

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