Order by the sum of an associations property - ruby-on-rails

I have a Department model with an expenses association:
class Department < ActiveRecord::Base
has_many :expenses
end
class Expense < ActiveRecord::Base
belongs_to :department
end
An expense has an amount property:
e = Expense.new
e.amount = 119.50
I want 2 queries now:
list all departments, ordered by SUM of expenses.
same as #1, but grouped by month i.e. jan, feb, march, ...

For #1, the following code will get you the department ids sorted by sum of expenses:
Expense.select('department_id, sum(amount) as total').group('department_id').order('total desc')
Here is a sample code on how to use the returned objects:
Expense.select('department_id, sum(amount) as total').group('department_id').order('total desc').each { |dep| print "Department ID: #{dep.department_id} | Total expense: #{dep.total}\n" }
This will print something like:
Department ID: 2 | Total expense: 119.50
Department ID: 1 | Total expense: 54.34
Department ID: 10 | Total expense: 23.43
For #2, you can similarly add the month grouping along with the sum:
Expense.select('department_id, extract(month from created_at) as month, sum(amount) as total').group('department_id, month').order('month asc, total desc')
Again, a sample code to demonstrate how to use it:
Expense.select('department_id, extract(month from created_at) as month, sum(amount) as total').group('department_id, month').order('month asc, total desc').each { |dep| print "Department ID: #{dep.department_id} | Month: #{dep.month} | Total expense: #{dep.total}\n" }
This will will print something like:
Department ID: 2 | Month: 1 | Total expense: 119.50
Department ID: 1 | Month: 1 | Total expense: 54.34
Department ID: 10 | Month: 1 | Total expense: 23.43
Department ID: 1 | Month: 2 | Total expense: 123.45
Department ID: 2 | Month: 2 | Total expense: 76.54
Department ID: 10 | Month: 2 | Total expense: 23.43
... and so on.
Of course, once you have the department Ids, you can use Department.find() to get the rest of information. I believe ActiveRecord does not support getting at the same time all the Department fields directly without using raw SQL.
EDIT ----
If you want to include the department fields you can either:
1 - Load them in separate queries like:
Expense.select('department_id, sum(amount) as total').group('department_id').order('total desc').each do |department_expense|
# In department_expense you have :department_id and :total
department = Department.find(department_expense.department_id)
# In department now you have the rest of fields
# Do whatever you have to do with this row of department + expense
# Example
print "Department #{department.name} from #{department.company}: $#{department_expense.total}"
end
Advantage: Using ActiveRecord SQL abstractions is nice and clean.
Drawback: You are doing a total of N+1 queries, where N is the number of departments, instead of a single query.
2 - Load them using raw SQL:
Department.select('*, (select sum(amount) from expenses where department_id = departments.id) as total').order('total desc').each do |department|
# Now in department you have all department fields + :total which has the sum of expenses
# Do whatever you have to do with this row of department + expense
# Example
print "Department #{department.name} from #{department.company}: $#{department.total}"
end
Advantage: You are doing a single query.
Drawback: You are losing the abstraction that ActiveRecord is providing to you from SQL.
Both will print:
Department R&D from Microsoft: $119.50
Department Finance from Yahoo: $54.34
Department Facilities from Google: $23.43

Related

PSQL: find invoice that was created in less than 8 months of another invoice

Table: Invoice
user_id
created_at
The goal is to find the invoice, that was created in less than 8 months of another invoice, both belonging to the same user.
Exemple:
Invoice 2: created_at: 2020-12-01 user_id: 1
Invoice 1: created_at: 2020-08-01 user_id: 1
Invoice 4: created_at: 2020-12-01 user_id: 2
Invoice 3: created_at: 2019-12-01 user_id: 2
Invoice 7: created_at: 2019-09-01 user_id: 3
Invoice 6: created_at: 2019-07-01 user_id: 3
Invoice 5: created_at: 2019-06-01 user_id: 3
Query should return Invoice 2, because another one (Invoice 1) has been created in less that 8 months before Invoice 1.
Query should also return Invoice 7, because another one (Invoice 6) has been created in less that 8 months before Invoice 7.
Query should also return Invoice 6, because another one (Invoice 5) has been created in less that 8 months before Invoice 6.
So far:
Invoice.where(created_at: 8.months.ago..Time.now).group_by(&:user_id).select { |k, v| v.size > 1 }
select * from invoices where created_at BETWEEN ('2020-04-21 14:05:57.009849' AND '2020-12-21 14:05:57.009949')
The problem with my approach is that it find only for last 8 months, not in general. And it uses ruby+sql, SQL only can be better.
This should work:
WITH cte AS (
SELECT user_id
, invoice
, created_at
, LAG(created_at) OVER (PARTITION BY user_id ORDER BY created_at) previous
FROM invoice
)
SELECT *
FROM cte
WHERE AGE(created_at, previous) <= INTERVAL '8 months';
I didn't quite understood requirements, do you need to return all invoices or only belonging to specific user?
If for some specific user, maybe try something like this?
def invoices_in_last_8_months(user, other_invoice)
Invoice.where(user: user, created_at: 8.months.ago..other_invoice.created_at)
end

RAILS: How to select fields from associated table with grouping and create a hash from result?

I would like to create an active record query and store the result in a hash which includes summary information from associated tables as follows.
Here is the tables and associations:
Post belongs_to: category
Category has_many: posts
Here I would like to count the # of posts in each category and create a summary table as follows (with the SQL query for the desired table):
select c.name, count(p.id) from posts a left join categories c on p.category_id = c.id where p.status = 'Approved' group by (c.name) order by (c.name);
Category | count
---------------+-------
Basketball | 2
Football | 3
Hockey | 4
(3 rows)
Lastly I would like to store the result in a hash as follows:
summary_hash = { 'Basketball' => 2, 'Football' => 3, 'Hockey' => 4 }
I will appreciate if you can guide me how to write the active record query and store the result in the hash.
Try
Post.where(status: 'Approved').joins(:category).
select("categories.name").group("categories.name").count

Fetching rows based on integer interval of a column

I am using rails 5.0.0 and postgresql
I have 3 tables namely orders, cart_items and products. The relation among them is as follows :
orders has_many cart_items
cart_item belongs_to product
Products table contains the price of the product.
What I want to do is fetch the number of orders between the price range of 2500 like orders which contain products worth between 0-2500, 2500-5000 and so on.
Thanks in advance.
The concept here is to group each orders by their total_price divided by 2500 (we're speaking about integer division here), so you will be able to group by their "sliced category".
Because:
300 / 2500 = 0
400 / 2500 = 0
2500 / 2500 = 1
3000 / 2500 = 1
3000 / 2500 = 1
4999 / 2500 = 1
5000 / 2500 = 2
etc. It's the result of this integer division that will create what I called earlier the "sliced category" (1 => 0-2500, 2 => 2500-5000, 3 => 5000-7500, etc). Since your total price may be decimal or floats, I "round" the value and cast them with ::integer.
The query can be something like this:
SELECT count(*), (sliced_2500_category + 1) * 2500 AS sliced_2500_group
FROM (
SELECT c.order_id, ROUND(SUM(price))::integer / 2500 AS sliced_2500_category
FROM cart_items c
INNER JOIN products p ON c.product_id = p.id
GROUP BY c.order_id
) orders_by_2500_category
GROUP BY sliced_2500_category;
Make sure your cart_items also belongs_to :orders
and product model has_many cart_items
I'm guessing you want to select the invoices/carts which are in the range of 0-2500 and so on..
Raw SQL:
SELECT order_id, sum(price)
FROM cart_items c INNER JOIN products p
ON c.product_id = p.id
GROUP_BY order_id
HAVING sum(price) BETWEEN 0 AND 2500
Required statement for controller:
CartItem.joins(:product)
.select(:order_id, 'sum(price)')
.group(:order_id)
.having('sum(price) BETWEEN 0 AND ?', params[:your_upper_limit_variable])

creating weekly report in ruby on rails that cover empty date with 0

I have a sale table, consist of 5 columns
id, sale_code, total_sale, created_at, updated_at
I want to make a weekly report like this
date | total_sale
2016-09-27 | 1000
2016-09-28 | 0
2016-09-29 | 500
and when there is no product being sold that day, in the report still showed it with 0 value.
Is there any way to do this? Thanks in advance
You can go through each day of the current week and count all sales of the day.
It could look like something like that:
today = Date.today
week_days = (today.at_beginning_of_week..today.at_end_of_week).map
week_sales = Sales.where("created_at > ? AND created_at < ?", week_days.first.beginning_of_day, week_days.last.end_of_day)
p "date | total_sale"
week_days.each do |day|
day_sales = week_sales.count{|sale| sale.created_at > day.beginning_of_day && sale.created_at < day.end_of_day}
p "#{day.strftime("%Y-%m-%d")} | #{day_sales}"
end

Rails ActiveRecord distinct count of attribute greater than number of occurrences

I have a a table author_comments with a fields author_name, comment and brand id.
I would like to get the number (count) of records where the author has more than N (2) records for a given brand.
For example,
author_comments
author_name comment brand
joel "loves donuts" 1
joel "loves cookies" 1
joel "loves oranges" 1
fred "likes bananas" 2
fred "likes tacos" 2
fred "likes chips" 2
joe "thinks this is cool" 1
sally "goes to school" 1
sally "is smart" 1
sally "plays soccer" 1
In this case my query should return 2 for brand 1 and 1 for brand 2.
I'm interested in the best performing option here, not getting all the records from the db and sorting through them in ruby, I can do this. I'm looking for best way using active record constructs or sql.
Update:
Here is the SQL:
SELECT author_name, COUNT(*) AS author_comments
FROM fan_comments
WHERE brand_id =269998788
GROUP BY author_name
HAVING author_comments > 2;
Should I just do find_by_sql?
You can define the same query using active record constructions:
FanComments.all(
:select => 'author_name, count(*) as author_comments',
:group => 'author_name',
:having => 'author_comments > 2') # in rails 2
or:
FanComments.
select('author_name, count(*) as author_comments').
group('author_name').
having('author_comments > 2') # in rails 3
FanComment.group(:author_name).count

Resources