ActiveRecord Query to get a SUM across tables - ruby-on-rails

I have the following models:
# == Schema Information
#
# Table name: quotes
#
# id :integer not null, primary key
# bound_rate_id :integer
class Quote < ActiveRecord::Base
#snip
end
# == Schema Information
#
# Table name: rates
#
# id :integer not null, primary key
# quoted_premium :integer
class Rate < ActiveRecord::Base
#snip
end
I want to create a query that would calculate the same thing as this loop:
sum = 0
for quote in Quote.all
rate = Rate.find(quote.bound_rate_id)
sum += rate.quoted_premium
end
How would I do this using ActiveRecord's query interface? (I am using Rails 4.)
EDIT: I already have ActiveRecord instances from previous queries over Quote, so I would prefer to have my query start from the quotes table and join to the rates table, not the other way around. Like this:
some_quotes = Quote.where(:some_other_property, my_param);
sum_of_rates = some_quotes.?????

Try this out
sum = Rate.where(:id => Quote.pluck(:bound_rate_id).compact).sum(:quoted_premium)
After adding relations try this out
sum = Quote.joins(:rate).sum('rates.quoted_premium') # it will get sum of all query's quoted_premium
To get sum of some specific add where clause
sum = Quote.joins(:rate).where(:bound_rate_id => [list of Rate ids]).sum('rates.quoted_premium')
If you get a Mysql2::Error: Unknown column 'rates.bound_rate_id' in 'on clause' error, specify how ActiveRecord should put together the join
sum = Quote.joins('INNER JOIN rates ON quotes.bound_rate_id = rates.id').sum('rates.quoted_premium')

Related

How to use SQL using Active Record

I am trying to optimise the performance of a query in Active Record. Previously I would do two SQL queries and it should be possible to do it in one.
These are the tables that I am running the queries on:
# Table name: notifications
#
# id :integer not null, primary key
# content :text(65535)
# position :integer
# Table name: dismissed_notifications
#
# id :integer not null, primary key
# notification_id :integer
# user_id :integer
This is the existing query:
where.not(id: user.dismissed_notifications.pluck(:id))
which produces:
SELECT `dismissed_notifications`.`id` FROM `dismissed_notifications` WHERE `dismissed_notifications`.`user_id` = 655
SELECT `notifications`.* FROM `notifications` WHERE (`notifications`.`id` != 1)
This is the SQL I would like to get, which returns the same records:
select *
from notifications n
where not exists(
select 1
from dismissed_notifications dn
where dn.id = n.id
and dn.user_id = 655)
You can write not exists Query like below
where('NOT EXISTS (' + user.dismissed_notifications.where('dismissed_notifications.id = notifications.id').to_sql + ')')
OR
There is also another way to reduce the number of queries is use select instead of pluck, it will create sub-query instead pulling records from database. Rails ActiveRecord Subqueries
where.not(id: user.dismissed_notifications.select(:id))
Which will generate below SQL query
SELECT `notifications`.*
FROM `notifications`
WHERE (
`notifications`.`id` NOT IN
(SELECT `dismissed_notifications`.`id`
FROM `dismissed_notifications`
WHERE `dismissed_notifications`.`user_id` = 655
)
)

How to get duplicate month and year matched record in rails?

I want to fetch duplicate month of record from table
I have leaves table inside of start_date column have many records my db
https://imgur.com/a/jdyxTo1
example I have 3 duplicate record in table
return duplicate record#08.01.2019,31.01.2019,25.01.2019
How to do this ?
class LeaveController < ApplicationController
def new
#your code here
end
end
My model name Leave my table name leaves
On SQLite, you can't. The supported SQL features are really minimal, you'd better have a local postgres/mysql server - the same server and version you will be using in production.
On a real SQL database you have the EXTRACT(YEAR_MONTH from date) function, and you can use it with GROUP BY.
This could be the proper SQL, you can use it with plain Leave.connection.execute(...):
SELECT
GROUP_CONCAT(`leaves`.`id` SEPARATOR ',') AS ids,
GROUP_CONCAT(`leaves`.`start_date` SEPARATOR ',') AS start_dates
FROM `leaves`
GROUP BY EXTRACT(YEAR_MONTH from `leaves`.`start_date`)
HAVING COUNT(`leaves`.`id`) > 1
With the data in the image, you will have the following result:
ids | start_dates
------------------+---------------------------------
5,6,8 | 2019-01-08,2019-01-31,2019-01-25
1,2,3,4 | ...
and there will be no entry for leaves that don't share month with others.
You cannot find duplicate dates by month when you're trying to find for all months in a single array
The below can give you an array of objects where your month number and duplicate dates for that month number will be there
For MySQL / PostgresQL
class LeaveController < ApplicationController
def new
#result = []
12.times do |i|
month_data = {
month_number: i,
duplicate_dates: Leave.where("extract(month from start_date) = ?", i).pluck(:start_date)
}
#result.push month_data
end
#Print and see the result
p #result
end
end
Update for SQLite
Since you're using sqlite, and the above syntax is not supported, here is a way for the same:
# In your model you can write a class method so you don't have to write all that
# logic in your controller and make it reusable
class Leave < ActiveRecord::Base
def self.duplicate_dates(current_month)
query = "SELECT * FROM leaves WHERE strftime('%m', start_date) = #{current_month}"
self.find_by_sql query
end
end
class LeaveController < ApplicationController
def new
#result = []
('01'..'12').to_a.each do |i|
# Get all the records with duplicate month
duplicate_data = Leave.duplicate_dates(i)
# The above will return a regular array and not an activerecord association,
# so pluck can't be used on it
# To get only the dates you can use map
duplicate_dates = duplicate_data.map {|i| i.start_date}
month_data = {
month_number: i,
duplicate_dates: duplicate_dates
}
#result.push month_data
end
#Print and see the result
p #result
end
end

Ordering results by boolean attribute equals false

I want write properly query to get the most hated news.
Current query:
#most_hated_news = News.joins(:likes).where('likes.like = ?', false).order('likes.like DESC').first
Schema of likes table:
# Table name: likes
#
# id :integer not null, primary key
# like :boolean
# person_id :integer not null
# news_id :integer not null
I want to get news, that has most likes equal false.
The problem is, that query doesn't care ordering likes with false value and return news with most positive likes.
It return news with 3 positive(most) likes and 1 negative. I have in database news with 2 negative(most).
How to write correctly?
I would try something like this:
#most_hated_news = News.joins(:likes)
.where('likes.like = ?', false)
.group('news.id')
.order('COUNT(likes.id) DESC').first

What is the best way to keep track of product sales in an application and pick top selling?

I have setup my databases like this :
Product :
# id :integer not null, primary key
# code :string
# name :string
# category_id :integer
...
Order items :
# id :integer not null, primary key
# order_id :integer
# product_id :integer
# color_id :integer
# qty :integer default("0")
# price :money
...
Order :
# id :integer
# state :string
# placed_on :datetime
...
Now this setup make it really hard for me to pick the best selling products in each week from each category. How can I fix this? Another database to keep track of sales? Please help.
What you basically need is to join categories, products, order_items and orders tables.
Joining can be done with the following code:
rel = Category.joins(products: [order_items: :order])
#=> SELECT "categories".* FROM "categories" INNER JOIN "products" ON "products"."category_id" = "categories"."id" INNER JOIN "order_items" ON "order_items"."product_id" = "products"."id" INNER JOIN "orders" ON "orders"."id" = "order_items"."order_id"
Based on this you can filter on dates interval.
Let's assume we already have d1 and d2 values, which define start and end of the interval we are interested in:
rel = rel.where('orders.placed_on >= ? AND orders.placed_on <= ?', d1, d2)
Now you can aggregate on fields:
result = rel.select('categories.id, categories.name, SUM(order_items.qty) as qty, SUM(order_items.qty * order_items.price) as total')
.group('categories.id, categories.name')
sample = result.first
sample.id # => 1
sample.name # => "Category 1"
sample.qty # => 100.0
sample.total # => 500.0
This looks like a simple database query to me. The following should be the simple and straight-forward steps to accomplish it.
Join the three tables
Filter it by date
Group by product_id
Aggregate the qty
And, sort by aggregated value.
I'm not confident about the method to get the date. Please fill it in yourself in the query below.
SELECT P.id, P.name, P.category_id, SUM(qty) as LastWeekSales
FROM Product as P INNER JOIN Order Items as OI
ON P.id = OI.product_id
INNER JOIN Order as O
ON O.id = OI.order_id
WHERE O.placed_on <= GetTodaysDate() AND O.placed_on > GetOneWeekBacksDate()
GROUPBY P.category_id
ORDERBY WeekSales
All you need to do is, prepare this query in ruby-on-rails. I would do it but I don't know anything about ruby-on-rails.
+1 on taking care of this in the model. Here is some working drop-in code if you need it in the meantime. I'm practicing manipulating hashes tonight in case you couldn't tell, ha.
Add to Order model:
def self.last_week
Order.where(" created_at >= ? ", 1.week.ago.utc)
end
Add to whatever controller:
#qty_hash = category = Hash.new 0;
#output_hash = Hash.new { |hash, key| hash[key] = {} }
#recently_ordered_items = OrderItem.find_all_by_order_id(Order.last_week)
#recently_ordered_items.each { |i| #qty_hash[i.product_id] += i.qty }
#recent_products=Product.find_all_by_id(#qty_hash.keys)
#qty_hash.each do |key, value|
#recent_products.each { |i| category = i.category_id if i.id == key }
#output_hash[category] = #output_hash[category].merge(#qty_hash.slice(key))
end
#output_hash is the output and is in the format:
{1=>{3=>9}, 2=>{4=>8, 6=>5, 7=>4}}
In this case the categories are 1 and 2, product ids are 3 (9 sold), 4 (8 sold), 6 (5 sold), and 7 (4 sold)
Tested and working. Good luck.

grouping with a non-primary key in postgres / activerecord

I have a model Lap:
class Lap < ActiveRecord::Base
belongs_to :car
def self.by_carmodel(carmodel)
scoped = joins(:car_model).where(:car_models => {:name => carmodel})
scoped
end
def self.fastest_per_car
scoped = select("laps.id",:car_id, :time, :mph).group("laps.id", :car_id, :time, :mph).order("time").limit(1)
scoped
end
end
I want to only return the fastest lap for each car.
So, I need to group the Laps by the Lap.car_id and then only return the fastest lap time based on that car, which would determined by the column Lap.time
Basically I would like to stack my methods in my controller:
#corvettes = Lap.by_carmodel("Corvette").fastest_per_car
Hopefully that makes sense...
When trying to run just Lap.fastest_per_car I am limiting everything to 1 result, rather than 1 result per each Car.
Another thing I had to do was add "laps.id" as :id was showing up empty in my results as well. If i just select(:id) it was saying ambiguous
I think a decent approach to this would be to add a where clause based on an efficient SQL syntax for returning the single fastest lap.
Something like this correlated subquery ...
select ...
from laps
where id = (select id
from laps laps_inner
where laps_inner.car_id = laps.car_id
order by time asc,
created_at desc
limit 1)
It's a little complex because of the need to tie-break on created_at.
The rails scope would just be:
where("laps.id = (select id
from laps laps_inner
where laps_inner.car_id = laps.car_id
order by time asc,
created_at desc
limit 1)")
An index on car_id would be pretty essential, and if that was a composite index on (car_id, time asc) then so much the better.
You are using limit which will return you one single value. Not one value per car. To return one car value per lap you just have to join the table and group by a group of columns that will identify one lap (id is the simplest).
Also, you can have a more ActiveRecord friendly friendly with:
class Lap < ActiveRecord::Base
belongs_to :car
def self.by_carmodel(carmodel)
joins(:car_model).where(:car_models => {:name => carmodel})
end
def self.fastest_per_car
joins(:car_model)
.select("laps.*, MIN(car_models.time) AS min_time")
.group("laps.id")
.order("min_time ASC")
end
end
This is what I did and its working. If there is a better way to go about these please post your answer:
in my model:
def self.fastest_per_car
select('DISTINCT ON (car_id) *').order('car_id, time ASC').sort_by! {|ts| ts.time}
end

Resources