2
rails 3.2

Invoice.sum(:amount)

Accurately returns the sum of all values in the amount column.

If I do the following:

invoices = Invoice.all

And then:

invoices.sum(:amount)

I get the following error:

NoMethodError: undefined method `+'

Is there a way to do this?

EastsideDev
  • 6,257
  • 9
  • 59
  • 116

3 Answers3

3

If you are certain that there is not a single amount == nil then what you tried, should work. If you have a single nil column this would result in NoMethodError: undefined method '+' for nil. In this case you can either do invoices.map(&:amount).compact.sum or invoices.sum { |i| i.amount || 0 }

Kkulikovskis
  • 2,028
  • 16
  • 28
3

This is because #sum is an ActiveRecord method. What it is essentially doing is building the aggregation into your database query like so (in SQL):

SELECT SUM(name_of_column_to_sum) FROM table_name;

If you want to return the values and store them in a variable (#all),

SELECT * FROM table_name;

and THEN sum them, you are no longer working with an ActiveRecord query, but rather an ActiveRecord relation (an array-like object that is returned from the ActiveRecord query). So, at that point you would need to use vanilla Ruby to sum up. You could use #map and then #sum, but it would be faster and cleaner to just use #inject, like so:

invoices.inject(0){ |sum, invoice| sum + invoice.amount }
sammms
  • 668
  • 8
  • 24
  • 1
    Used this to get rid of N+1 query's I was have. Thank you for posting this BTW. Was wondering if you know if this is a proper way to get rid of the N+1? Looking at my logs it does do that but I'm unaware if it is the "proper way". – Chrismisballs Aug 12 '22 at 19:35
  • @Chrismisballs the "proper" way is often different depending on your use case, however, a good rule of thumb is this.. Avoid loading things into memory that you don't have to. In this case, `.all` is building a whole bunch of activerecord objects in memory just to iterate over one value. It's smarter to use the `.sum` activerecord method in this case than to use `.inject`. [For more on that check out this post](https://stackoverflow.com/questions/41449617/why-is-sum-so-much-faster-than-inject) – sammms Sep 19 '22 at 18:57
1

You have to use map function, like this:

invoices.map(&:amount).sum
Gustavo Fe
  • 64
  • 6
  • This is iterating twice over the same array, which is not optimal. It will work, but also does not address the initial question which is 'why' doesn't `invoices.sum(:amount)` work. It's because the OP is mixing up activerecord and vanilla ruby. See my answer for details. – sammms Dec 29 '18 at 05:09
  • @Gustavo Fe If I need sum two colums like invoices.map(&:amount + &:fees ).sum is it possible? – urjit on rails Oct 23 '20 at 09:48