Resume of product query - sql

I have the following schema table:
I have three activerecord models with their associations. I am struggling with a query which will show the following information for each product:
Product Name, Money Total, Quantity Sold Total
It should also take account on the status of the order that the product_line are associated with, which it should be equal to "successful".
I also want a second one query which it will show the above but it will have restriction based on the month (based on the orders.created_at column). For example if I want the sales for January of this product.
Product Name, Total Money so far, Quantity total, Month
I managed to create something but I think it isn't very optimized and I used ruby's group_by which it is doing many additional queries on the view. I would appreciate how you usually start thinking about creating a query like that.
Update
I think I almost managed to solve the first query and it is the following:
products = Product.joins(:product_lines).select("products.name, SUM(product_lines.quantity) as sum_amount, SUM(product_lines.quantity*products.price) as money_total"),group("products.id")
I tried to split each columns separately and find out how I could calculate it. I haven't take into account the order status though.
The associations are the following:
ProbudtLine
class ProductLine < ActiveRecord::Base
belongs_to :order
belongs_to :cart
belongs_to :product
end
Product
class Product < ActiveRecord::Base
has_many :product_lines
end
Order
class Order < ActiveRecord::Base
has_many :product_lines, dependent: :destroy
end

I finally did it.
First query:
#best_products_so_far = Product.joins(product_lines: :order)
.select("products.*, SUM(product_lines.quantity) as amount_total, SUM(product_lines.quantity*products.price) as money_total")
.where("orders.status = 'successful'")
.group("products.id")
Second query:
#best_products_this_month = Product.joins(product_lines: :order)
.select("products.*, SUM(product_lines.quantity) as amount_total, SUM(product_lines.quantity*products.price) as money_total")
.where("orders.status = 'successful'")
.where("extract(month from orders.completed_at) = ?", Date.today.strftime("%m"))
.group("products.id")

Related

Rails select by number of associated records

I have following models in my rails app:
class Student < ApplicationRecord
has_many :tickets, dependent: :destroy
has_and_belongs_to_many :articles, dependent: :destroy
class Article < ApplicationRecord
has_and_belongs_to_many :students, dependent: :destroy
class Ticket < ApplicationRecord
belongs_to :student, touch: true
I need to extract all Students who has less than articles and I need to extract all Students who's last ticket title is 'Something'.
Everything I tried so far takes a lot of time. I tried mapping and looping through all Students. But I guess what I need is a joined request. I am looking for the most efficient way to do it, as database I am working with is quite large.
go with #MCI's answer for your first question. But a filter/select/find_all or whatever (although I havn't heared about filter method in ruby) through students record takes n queries where n is the number of student records (called an n+1 query).
studs = Student.find_by_sql(%{select tmp.id from (
select student_id as id from tickets where name='Something' order by tickets.created_at desc
) tmp group by tmp.id})
You asked
"I need to extract all Students who has less than articles". I'll presume you meant "I need to extract all Students who have less than X articles". In that case, you want group and having https://guides.rubyonrails.org/active_record_querying.html#group.
For example, Article.group(:student_id).having('count(articles.id) > X').pluck(:student_id).
To address your second question, you can use eager loading https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations to speed up your code.
result = students.filter do |student|
students.tickets.last.name == 'Something'
end
Here association is HABTM so below query should work
x = 10
Student.joins(:articles).group("articles_students.student_id").having("count(articles.id) < ?",x)

Select users whose last associated object was created less than x days before

I am building a Rails 4.2.7.1 which uses Postgres database and I need to write a feature for certain group of users.
class User < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :user
end
I need to select users from certain location who have exactly one payment and I also need to be able to pick users whose payment created_at attribute is exactly x
I tried
location.users
.without_deleted
.where(num_payments: 1)
.joins(:payments)
.where('payments.user_id = users.id').order('created_at
DESC').where("payments.created_at < ?", Date.today).group('users.id')
but it did not give me expected results.
Thanks!
You should start from User since this is what you want at end, and take joins with payments since you want to query it along.
User.joins(:payments)
.where(location_id: location.id, num_payments: 1)
.where(payments: { created_at: Date.today })

Active Record: Find collection based on sum of two associated tables

I am trying to find the collection of 'underpaid' events in our system. We are running Rails 3.2 using a Postgres database.
The data structure is as follows.
class Event < ActiveRecord::Base
has_many :charges
has_many :transactions
end
class Charge < ActiveRecord::Base
belongs_to :event
end
class Transaction < ActiveRecord::Base
belongs_to :event
end
Underpaid events are defined as those where the total charges are greater than total transactions.
sum(charges.total) > sum(transactions.total)
My SQL skills are poor and I have been trying to execute this using ActiveRecord. This is my latest attempt but it is not bringing back the right collection. In particular it seems to include fully paid events where there was more than one transaction.
Event.joins(:charges,:transactions).group('charges.event_id, transactions.event_id').having("sum(charges.total) > sum(transactions.total)")
Is it possible to achieve this in ActiveRecord and if so, how can I go about it?
Hey I think that in SQL it should be like that
select * from events where
(select sum(charges.total) from charges where charges.event_id = events.id) >
(select sum(transactions.total) from transactions where
transactions.event_id = events.id)
so for now you can build scope like
scope :unpaid, find_by_sql("select * from events where
(select sum(charges.total) from charges where charges.event_id = events.id) >
(select sum(transactions.total) from transactions where
transactions.event_id = events.id)")
I hope it will help!

Alternative to a union with ActiveRecord

I think I want to do a union in Rails, but according to this post rails union hack, how to pull two different queries together unions aren't natively supported in Rails. I'm wondering if there is a better way to approach this problem.
I have table of items, each item has many prices, but I only want to join one price to each item.
To determine the proper price for an item I have two additional foreign keys in the price model: category_id and discount_id. Each could independently declare a price for an item.
Ex.
Item + Category = Price1 and Item + Discount = Price 2
If discount_id matches a passed id I want to exclude the price results FOR THAT ITEM ONLY that match Item + Category. Also I'm trying not to loose lazy loading.
Hopefully the problem is clear! If not I'll try to clarify more, thanks in advance.
Your models would start off looking something like this:
class Price < ActiveRecord::Base
belongs_to :item
belongs_to :category
belongs_to :discount
scope :category, where("prices.category_id IS NOT NULL")
scope :discount, where("prices.discount_id IS NOT NULL")
end
class Item < ActiveRecord::Base
has_many :prices
end
class Category < ActiveRecord::Base
has_many :prices
end
class Discount < ActiveRecord::Base
has_many :prices
end
One way of doing this is to add a class method to Price that encapsulates this logic:
class Price < ActiveRecord::Base
def self.used
discount_items_sql = self.discount.select("prices.item_id").to_sql
where("prices.discount_id IS NOT NULL OR prices.item_id NOT IN (#{discount_items_sql})")
end
end
This is effectively the same as this query:
SELECT * FROM prices
WHERE prices.discount_id IS NOT NULL -- the discount_id is present on this record,
OR prices.item_id NOT IN ( -- or no discount_id is present for this item
SELECT item_id FROM prices WHERE discount_id IS NOT NULL)
You can add these helper methods on your Item model for simplicity:
class Item < ActiveRecord::Base
def category_price
prices.category.first
end
def discount_price
prices.discount.first
end
def used_price
prices.used.first
end
end
Now you can easily get each individual 'type' of price for a single item (will be nil for prices that aren't available):
item.category_price
item.discount_price
item.used_price

Can I push this rails calculation into the database?

I'm trying to increase my app's efficiency by doing work in the database rather than in the app layer, and I'm wondering if I can move this calculation into the database.
Models:
class Offer < ActiveRecord::Base
has_many :lines
has_many :items, :through => :lines
end
class Line < ActiveRecord::Base
belongs_to :offer
belongs_to :item
# also has a 'quantity' attribute (integer)
end
class Item < ActiveRecord::Base
has_many :lines
has_many :offers, :through => :lines
# also has a 'price' attribute (decimal)
end
What I want to do is calculate the price of an offer. Currently I have a price method in the Offer class:
def price
self.lines.inject(0) do |total, line|
total + line.quantity * line.item.price
end
end
I suspect it may be possible to do a Offer.sum calculation instead that would get the answer directly from the DB rather than looping through the records, but the Calculations section of the ActiveRecord query guide doesn't have enough detail to help me out. Anybody?
Thanks!
You're correct that you can do this with sum. Something like this:
class Offer < ActiveRecord::Base
# ...
def price
self.lines.sum 'lines.quantity * items.price', :joins => :item
end
end
When you call e.g. Offer.find( some_id ).price the above will construct a query something like this:
SELECT SUM( lines.quantity * items.price ) AS total
FROM lines
INNER JOIN items ON items.id = lines.item_id
WHERE lines.offer_id = <some_id>
;
Sometimes you're better off with SQL.
SELECT SUM( lines.quantity * items.price ) AS total
FROM offers
INNER JOIN lines ON offers.id = lines.offer_id
INNER JOIN items ON items.id = lines.item_id
WHERE offers.id = 1
;