ActiveRecord calculating multiple averages - ruby-on-rails

I have a scores table with both score and time per user. I want to calculate both averages grouped by user. I can successfully calculate one of them but not sure how to do both at once.
#scores = SpellingScore.where(:user_id => users).average(:score, :group => :user)
Will produce sql like the following
SELECT AVG(`spelling_scores`.`score`) AS average_score, user_id AS user_id
FROM `spelling_scores`
WHERE (`spelling_scores`.`user_id` IN (78767, 78772, 78775)) GROUP BY user_id
I know how to do it in SQL but can't work out the ActiveRecord way.
This is what I want to do...
SELECT AVG(`spelling_scores`.`score`) AS average_score, AVG(`spelling_scores`.`time`) AS average_time, user_id AS user_id
FROM `spelling_scores`
WHERE (`spelling_scores`.`user_id` IN (78767, 78772, 78775)) GROUP BY user_id
Cheers,
Tim

Thanks for your help Macarthy.
I ended up doing it this way
SpellingScore.select("AVG(`spelling_scores`.`score`) AS average_score,
AVG(`spelling_scores`.`time`) AS average_time, COUNT(*) AS question_count, user_id AS
user_id").where(:user_id => users).group(:user_id).includes(:user).order('users.last')
At least I've retained some ActiveRecord chaining.

Just use SQL. Forget the ActiveRecord way, SQL is better for something like this. If you want to keep your logic in your model just create a new method in your model

Wouldn't this work SpellingScore.group(:user).average(:score)
Can't test this without a schema though

Related

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.

Setting attributes on active record join tables

I have a standard has_many through association in active record with an added "quantity" attribute on the join table.
When using this syntax to add an association, how do I set a value for the quantity attribute in the join table.
order.products << Product.find(params[:product_id])
The only way I have been able to accomplish this is by creating the "join object" directly like this:
order.product_orders.create(:product => Product.find(params[:product_id], :quantity => 5)
The second approach kind of bugs me because I am addressing the join directly.
Is there an alternate/better way to approach this?
Thanks!
I think your better approach is 1. You have to separete product and order item.

How to touch multiple records in ActiveRecord?

I'd like to update the updated_at for a few records:
users = User.in_mailing_list
users.update_all(:updated_at => Time.now)
Is there a shortcut for the purpose, say something like a users.touch_all method?
Not sure if rhernando's answer works in older versions of Ruby, but this is a much clearer method in my opinion and works in Ruby 2+
users.each(&:touch)
NB. As mentioned in the comments this will cause N requests as opposed to using update_all which would do it in a single command.
You can do it like this:
User.update_all({updated_at: Time.now}, {id: user_ids})
Note: The braces are required, otherwise it tries to set updated_at and id, instead of updating updated_at where id is in user_ids
Cheers!
If you need touch ActiveRelaton records you have to use update_all method. It touches multiple records in a single transaction:
User.update_all(updated_at: Time.current)
User.where(active: true).update_all(active: false)
But if you have Array of records, in this case, you use only each with update
users.each { |user| user.update(active: true) }
the disadvantage of this case: for each user will be a separate transaction
Not sure since when but the solution is much simpler:
users = User.in_mailing_list
users.touch_all
Or if you don't want to instantiate the user records:
users = User.in_mailing_list.touch_all
This should do it:
User.update(users, :updated_at => Time.now)

Rails ActiveRecord query - Best possible way to implement the query inside the model

SELECT user_id, investments, expenditures
FROM ( SELECT user_id, SUM(amount) investments
FROM transactions
where beneficiary_id is not null
group by user_id) A JOIN ( SELECT beneficiary_id, SUM(amount) expenditures
FROM transactions
where beneficiary_id is not null
group by beneficiary_id) B
ON A.user_id = B.beneficiary_id"
What is the best possible way with which an active record method can be written using find method ? The idea here is to use less SQL and more rails active record methods. I know this can be implemented with find_by_sql but looking for a more rails way of doing this.
Transactions is a model with the following structure
txndate:datetime
user_id:integer
beneficiary_id:integer
amount:decimal, :precision => 14, :scale => 2
Any help will be greatly appreciated.
I'm not sure, and little bit confused, but maybe you need just scopes?
http://edgerails.info/articles/what-s-new-in-edge-rails/2010/02/23/the-skinny-on-scopes-formerly-named-scope/index.html

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

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)

Resources