Hi I'm working on a project and I have to get the sum of an attribute of a active record collection. So I used:
#total = #records.sum(:cost)
However this gives the wrong value, for example if I have:
#records.each{ |x| puts x.cost}
I get 118.80 and 108.00
but for #total I get 680.40, which obviously isn't the answer, however if I use:
#total = 0
#records.each{ |x| #total = #total + x.cost}
I get the right answer of 226.80
If anyone can help me understand what is going on here it would be greatly appreciated.
Be careful, as a record collection is an instance of ActiveRecord::Associations::CollectionProxy, not an Array. It means that if you call:
#object.collection.sum(:cost)
actually what gets called is this method: http://apidock.com/rails/v4.2.7/ActiveRecord/Calculations/sum
And it will call sum in the SQL database, so the result gets influenced by the parameters of the query, e.g. groups, joins, etc.
While if you want to use Array sum, as in here: http://apidock.com/rails/Enumerable/sum
You would have to make your object Array first, via to_a:
#object.collection.to_a.sum(&:cost)
try this:
pluck the values of attr cost into an array and aggregate their sum
#total = #records.pluck(:cost).sum
Related
I have 3 models. Project, ProjectMaterial, and Material
A Project has_many ProjectMaterials and many Materials through ProjectMaterials.
This is bidirectional, with ProjectMaterial acting as a join table with user-submittable attributes.
I'd like to query the ProjectMaterial model to find the most frequent value of material_id. This way I can use it to find the most frequently used material.
Any help with a query would be greatly appreciated. I'm stuck. Thanks in advance!
You can chain group, count and sort methods on your ActiveRecord query like this:
ProjectMaterial.group(:material_id).count.values.sort.last
The first part ProjectMaterial.group(:material_id).count gives you the hash of each {material_id0 => rows_count0, material_id1 => rows_count1, ...}. Then, you can just get the values of the hash in an array, sort it and get the last item.
One way could be pluck ids to get the array, then count the most frequent.
ids = ProjectMaterial.pluck[:material_id]
For example: Ruby: How to find item in array which has the most occurrences?
Or better, by query to get a hash with counts:
counts = ProjectMaterial.group(:material_id).count
Once you know that you get a hash, you can sort by any ruby method, picking the most frequent or the n most frequent. Example of sorting:
counts.sort_by { |_, v| v }
I have a model Channel. The relating table has several column, for example clicks.
So Channel.all.sum(:clicks) gives me the sum of clicks of all channels.
In my model I have added a new method
def test
123 #this is just an example
end
So now, Channel.first.test returns 123
What I want to do is something like Channel.all.sum(:test) which sums the test value of all channels.
The error I get is that test is not a column, which of course it is not, but I hoped to till be able to build this sum.
How could I achieve this?
You could try:
Channel.all.map(&:test).sum
Where clicks is a column of the model's table, use:
Channel.sum(:clicks)
To solve your issue, you can do
Channel.all.sum(&:test)
But it would be better to try achieving it on the database layer, because processing with Ruby might be heavy for memory and efficiency.
EDIT
If you want to sum by a method which takes arguments:
Channel.all.sum { |channel| channel.test(start_date, end_date) }
What you are talking about here is two very different things:
ActiveRecord::Calculations.sum sums the values of a column in the database:
SELECT SUM("table_name"."column_name") FROM "column_name"
This is what happens if you call Channel.sum(:column_name).
ActiveSupport also extends the Enumerable module with a .sum method:
module Enumerable
def sum(identity = nil, &block)
if block_given?
map(&block).sum(identity)
else
sum = identity ? inject(identity, :+) : inject(:+)
sum || identity || 0
end
end
end
This loops though all the values in memory and adds them together.
Channel.all.sum(&:test)
Is equivalent to:
Channel.all.inject(0) { |sum, c| sum + c.test }
Using the later can lead to serious performance issues as it pulls all the data out of the database.
Alternatively you do this.
Channel.all.inject(0) {|sum,x| sum + x.test }
You can changed the 0 to whatever value you want the sum to start off at.
Lets say I have a model:
class Result < ActiveRecord::Base
attr_accessible :x, :y, :sum
end
Instead of doing
Result.all.find_each do |s|
s.sum = compute_sum(s.x, s.y)
s.save
end
assuming compute_sum is a available method and does some computation that cannot be translated into SQL.
def compute_sum(x,y)
sum_table[x][y]
end
Is there a way to use update_all, probably something like:
Result.all.update_all(sum: compute_sum(:x, :y))
I have more than 80,000 records to update. Each record in find_each creates its own BEGIN and COMMIT queries, and each record is updated individually.
Or is there any other faster way to do this?
If the compute_sum function can't be translated into sql, then you cannot do update_all on all records at once. You will need to iterate over the individual instances. However, you could speed it up if there are a lot of repeated sets of values in the columns, by only doing the calculation once per set of inputs, and then doing one mass-update per calculation. eg
Result.all.group_by{|result| [result.x, result.y]}.each do |inputs, results|
sum = compute_sum(*inputs)
Result.update_all('sum = #{sum}', "id in (#{results.map(&:id).join(',')})")
end
You can replace result.x, result.y with the actual inputs to the compute_sum function.
EDIT - forgot to put the square brackets around result.x, result.y in the group_by block.
update_all makes an sql query, so any processing you do on the values needs to be in sql. So, you'll need to find the sql function, in whichever DBMS you're using, to add two numbers together. In Postgres, for example, i believe you would do
Sum.update_all(sum: "x + y")
which will generate this sql:
update sums set sum = x + y;
which will calculate the x + y value for each row, and set the sum field to the result.
EDIT - for MariaDB. I've never used this, but a quick google suggests that the sql would be
update sums set sum = sum(x + y);
Try this first, in your sql console, for a single record. If it works, then you can do
Sum.update_all(sum: "sum(x + y)")
in Rails.
EDIT2: there's a lot of things called sum here which is making the example quite confusing. Here's a more generic example.
set col_c to the result of adding col_a and col_b together, in class Foo:
Foo.update_all(col_c: "sum(col_a + col_b)")
I just noticed that i'd copied the (incorrect) Sum.all.update_all from your question. It should just be Sum.update_all - i've updated my answer.
I'm completely beginner, just wondering Why not add a self block like below, without adding separate column in db, you still can access Sum.sum from outside.
def self.sum
x+y
end
I have the following rudimentary piece of code in my model to return a random event:
def self.random
Event.first(:offset => rand(Event.count))
end
I would like to modify the function so it returns N number of events.
I'm aware that first can take a number as a parameter, i.e. Event.first(2), but how to combine it with random offset?
I.e. something like this
def self.random(n = 1)
Event.first(n) # and offset??!!
end
Any help would be appreciated!
This should work for returning n random records:
def self.random(n = 1)
Event.order("RANDOM()").limit(n)
end
The limit method is actually called by first when you give it an Integer as the first argument, so you could also use .first(n) instead of .limit(n) if you prefer.
You may need to substitute RAND() instead of RANDOM() depending on your database system, consult the documentation for the exact method.
While Stuarts method works perfectly by itself, it cannot be chained with many other ActiveRecord scopes. I found the other method (using offset) that works with chaining:
def self.random(number = 1)
Event.offset(rand(Event.count - number+1)).first(number)
end
my app has invoices and invoice_items. Each invoice has many invoice_items.
In my invoice_items model, I have a calculation to work out the total:
def total
#total ||= quantity.to_d * price
end
That works fine. What I'm trying to do is calculate the sum of the totals and I'm stuck.
In the console, I've tried this:
invoice = Invoice.first
invoice.invoice_items.sum(:total)
But I get an error saying :total doesn't exist. Which I guess it doesn't.
My question is how I can go about doing this calculation?
-- UPDATE --
I've tried the following as per #paukul's answer:
invoice.invoice_items.sum(&:total)
This gives the error:
ArgumentError: wrong number of arguments (1 for 2)
Thanks
you are calling .sum on the whole invoice_items array (which obviously doesn't know the .total method) and not each individual invoice item.
To make it work you want to use the block version of sum which yields every single item in the array to the block and sums up their results.
with the symbol to proc syntactic sugar, you are almost there.
Try invoice.invoice_items.all.sum(&:total) which is equivalent to invoice.invoice_items.inject(0) { |sum, item| sum + item.total }
see the RDoc for Enumerable#sum