mysql - Calculating profit of sales between dates - mysql

I have a table that looks like this:
+--------+---------------------+-------+--------+-----------+
| PartNo | Date | Inv | Retail | Wholesale |
+--------+---------------------+-------+--------+-----------+
| 1 | 2018-05-12 00:00:00 | 15 | $100 | $90 |
| 2 | 2018-05-12 00:00:00 | 20 | $200 | $150 |
| 3 | 2018-05-12 00:00:00 | 25 | $300 | $200 |
| 1 | 2018-05-13 00:00:00 | 10 | $95 | $90 |
| 2 | 2018-05-14 00:00:00 | 15 | $200 | $150 |
| 3 | 2018-05-14 00:00:00 | 20 | $300 | $200 |
+--------+---------------------+-------+--------+-----------+
And I want it to look like this with a Mysql query:
+--------+------+--------+
| PartNo | Sold | Profit |
+--------+------+--------+
| 1 | 5 | $25 |
| 2 | 5 | $250 |
| 3 | 5 | $500 |
+--------+------+--------+
I need to group by PartNo while calculating the difference between totals and profits over a date range.
The unit profit has to be calculated by subtracting the wholesale from retail on the last day (or record) of the date range.
I feel like this should be easy but the differences over the date ranges are confusing me and handling records within the date range that don't start or end exactly on the date range input are losing me.
Any help would be super appreciated.
Thank you.

You can look up the situation at the start and at the end of the period If no start situation is found, assume no stock. If no end situation is found, that means no sales during the period.
For example for the period starting 2018-05-13 and ending 2018-05-14:
select parts.PartNo
, coalesce(FirstSale.Total, 0) - coalesce(LastSale.Total, FirstSale.Total, 0) as Sold
, (coalesce(FirstSale.Total, 0) - coalesce(LastSale.Total, FirstSale.Total, 0)) *
coalesce(LastSale.Retail - LastSale.Wholesale, 0) as Profit
from (
select PartNo
, max(case when Date < '2018-05-13' then Date end) as FirstEntry
, max(case when Date <= '2018-05-14' then Date end) as LastEntry
from Sales
group by
PartNo
) parts
left join
Sales FirstSale
on FirstSale.PartNo = parts.PartNo
and FirstSale.Date = parts.FirstEntry
left join
Sales LastSale
on LastSale.PartNo = parts.PartNo
and LastSale.Date = parts.LastEntry
Example at SQL Fiddle.

SELECT c.partno as partno,MAX(c.inv)-MIN(c.inv) as sold,SUM(CASE WHEN c.date = c.last_date THEN profit else 0 END)*(MAX(c.inv)-MIN(c.inv)) as profit
FROM (SELECT partno,date,inv,retail-wholesale as profit,MAX(date) OVER (partition by partno) AS last_date FROM test1)c
GROUP BY c.partno
ORDER BY c.partno;
Using the window function, first append a new column to track the max date for each partno. So the inner query inside FROM will produce rows like these with one column added to the the original dataset,
| 1 | 2018-05-12 00:00:00 | 15 | $100 | $90 | **2018-05-13 00:00:00** |
The highlighted field is the one added to the dataset which is the last date in the date range for that part number!
Now from this result, we can pull out profit by checking for the row in which date column is equal to the new column we appended, which is essentially calculating the profit for the last date by subtracting wholesale from retail and multiplying with items sold.
PS : The logic for items sold is grouping by partno and subtracting MIN(Inv) from MAX(Inv)
Link to SQL Fiddle

Related

MySQL: Create running sum for all dates in range?

I have a table Service, that stores records of sales and referral types that led to the sale. I need to know the total number of sales that resulted from a given referral type over a range of dates. The relevant data in Service looks like the following:
+------+-------+------------+
| uuid | sr_id | s_saledate |
+------+-------+------------+
| | 1 | 2020-01-01 |
| | 1 | 2020-01-01 |
| | 2 | 2021-01-01 |
| | 2 | 2021-01-01 |
| | 1 | 2021-01-01 |
+------+-------+------------+
I want to count the number of sales for each referral type (sr_id) in a given date range.
If my date range is 2020-01-01 thru 2021-01-01, my output should be something like:
+------+-------+------------+----------------------+
| uuid | sr_id | date | num_sales_as_of_date
+------+-------+-----------------------------------+
| | 1 | 2020-01-01 | 2 |
| | 1 | 2020-01-02 | 2 |
| | 1 | 2020-01-03 | 2 |
........................................................ < many rows for days in range
1 2021-01-01 | 3
| | 2 | 2020-01-01 | 0 |
| | 2 | 2020-01-02 | 0 |
........................................................ < many rows for days in range
| | 2 | 2020-01-01 | 2 |
+------+-------+-----------------------------------+
There should be a row for each referral type on each date in the range.
Right now my query looks like:
SELECT s.sr_id,
s.s_saledate AS date,
Count(s.uuid)
OVER (
partition BY s.sr_id
ORDER BY s.s_saledate) AS num_sales_as_of_date
FROM Service s
How do I get the running sum for each referral type on days that had no Service with that particular referral type id?
*** EDIT FOR CLARIFICATION***
For example, in the first table I give there is no Service row in the Service table with sr_id = '1' AND s_saledate === "2020-01-02". There were two rows from prior days where sr_id = '1'. (2020-01-01). My output row for "2020-01-02" is:
sr_id date num_sales_as_of_date
1 | 2020-01-02 | 2 |
You need to left join your Services table from a table with all the dates in the range and a table with all the referral types, so that you get a row with every combination of date and referral type:
WITH RECURSIVE dates AS (
SELECT date('2020-01-01') AS date
UNION ALL
SELECT dates.date + INTERVAL 1 DAY
FROM dates
WHERE dates.date <= '2020-01-05'
)
SELECT ServiceReferral.sr_id,
dates.date,
Count(s.uuid)
OVER (
partition BY ServiceReferral.sr_id
ORDER BY dates.date) AS num_sales_as_of_date
FROM dates
CROSS JOIN ServiceReferral
LEFT JOIN Service s ON s.s_saledate=dates.date AND s.sr_id=ServiceReferral.sr_id
fiddle
If you do this a lot, it may be more convenient to create an actual table dates with all the dates from 0000-01-01 to 9999-12-31 and use that instead (selecting dates in the desired range in the where clause).

SQL: Get daily/weekly/monthly consumption of electric meter by SQL-Query

I have a MySQL-table (called Item1) with two columns:
"Time" [datetime] and "Value" [float]. Every 5 Minutes the total amount of my electric meter is appended to the data table, like that:
| Time | Value |
| 2018-07-22 21:55:00 | 202660.199951 |
| 2018-07-22 22:00:00 | 202673.899902 |
| 2018-07-22 22:05:00 | 202684.699951 |
| 2018-07-22 22:10:00 | 202691.534534 |
| 2018-07-22 22:15:00 | 202710.334253 |
How can I calculate the power consumption per day / week / month with one sql-query, getting the results as a new table. I tried:
SELECT * FROM Item1 WHERE HOUR(Time)=0 AND MINUTE(Time)=0
which gives me the values every midnight, but how can i subtract these values?
You can use a join:
select date(i.time), i.value, iprev.value,
(i.value - iprev.value) as diff
from item_1 i left join
item_1 iprev
on date(iprev.time) = date(i.time) - interval 1 day and
hour(iprev.time) = 0 and minute(iprev.time) = 0
where hour(i.time) = 0 and minute(i.time) = 0
group by date(i.time), i.value, iprev.value;
You would change the timeframe for iprev to get differences of weeks or months.

MySQL perform subquery if count is zero

I have three tables: monthly_revenue, currencies and foreign_exchange.
monthly_revenue table
|------------------------------------------------------|
| id | product_id | currency_id | value | month | year |
|------------------------------------------------------|
| 1 | 1 | 1 | 100 | 1 | 2015 |
| 2 | 1 | 2 | 125 | 1 | 2015 |
| 3 | 1 | 3 | 115 | 1 | 2015 |
| 4 | 1 | 1 | 100 | 2 | 2015 |
| 5 | 1 | 2 | 125 | 2 | 2015 |
| 6 | 1 | 3 | 115 | 2 | 2015 |
|------------------------------------------------------|
foreign_exchange table
|---------------------------------------|
| id | base | target | rate | rate_date |
|---------------------------------------|
| 1 | GBP | USD | 1.6 |2015-01-01 |
| 2 | GBP | USD | 1.62 |2015-01-15 |
| 3 | GBP | USD | 1.61 |2015-01-31 |
| 4 | EUR | USD | 1.2 |2015-01-01 |
| 5 | EUR | USD | 1.4 |2015-01-15 |
| 6 | EUR | USD | 1.4 |2015-01-31 |
| 7 | GBP | EUR | 1.4 |2015-01-01 |
| 8 | GBP | EUR | 1.45 |2015-01-15 |
| 9 | GBP | EUR | 1.44 |2015-01-31 |
|---------------------------------------|
From this, we can see the average fx rates:
GBP > USD in January is 1.61
EUR > USD in January is 1.33
GBP > EUR in January is 1.43
No rates are available for USD as a base currency, and no rates are available for February.
currencies table
|-----------|
| id | name |
|-----------|
| 1 | GBP |
| 2 | USD |
| 3 | EUR |
|-----------|
What i'm trying to achieve
Each row within the monthly_revenue table can have a different currency_id, as orders are placed is different currencies. I want to see all revenue for a given month, in a common currency. So, rather than looking at all revenue in January in GBP, and then separately looking at all revenue in January in USD, I'd like to get one value for all revenue in January - converted to USD (for example).
This can be calculated for each row, using the following (using January for this example):
revenue value x average fx rate for January between base and target currency
If I have 50 orders in January, in 4 different currencies, this let's me see all revenue in any single currency.
Example - get all revenue in January, in USD
This should return:
|------------------------------------------------------|
| id | product_id | currency_id | value | month | year |
|------------------------------------------------------|
| 1 | 1 | 1 | 100 | 1 | 2015 |
| 2 | 1 | 2 | 125 | 1 | 2015 |
| 3 | 1 | 3 | 115 | 1 | 2015 |
|------------------------------------------------------|
However, rows 1 and 3 are not in USD (these are GBP, and EUR respectively).
What I'd like to see is each row returned with the average FX rate that is being converted to, and a converted column. For example:
|-------------------------------------------------------------------------|
| id | prod_id | currency_id | value | month | year | fx_avg | converted |
|-------------------------------------------------------------------------|
| 1 | 1 | 1 | 100 | 1 | 2015 | 1.61 | 161 |
| 2 | 1 | 2 | 125 | 1 | 2015 | 1 | 125 |
| 3 | 1 | 3 | 115 | 1 | 2015 | 1.33 | 152.95 |
|-------------------------------------------------------------------------|
Where I'm at
I can currently get the basic calculation done using the query below, but a couple of key features are lacking:
If there is no FX rate available (for example for future dates where of course an FX rate isn't available) then the entire row is ignored. What I'd like in this instance is for the latest month's average to be used.
If the calculation is being performed where the target currency is the same as the base currency, the entire row is ignored (as there is no record in the FX table where the base equals the target). In this instance, the rate should be hard defined as 1.
Query so far
SELECT
r.value * IFNULL(AVG(fx.rate),1) as converted, AVG(fx.rate) as averageFx,
r.*, fx.*
FROM
foreign_exchange fx, monthly_revenue r, order_headers h
WHERE
fx.base IN (SELECT name FROM currencies WHERE id = r.currency_id) AND
r.order_header_id = h.id AND
fx.target = 'USD' AND
MONTH(fx.rate_date) = r.month AND
YEAR(fx.rate_date) = r.year AND
r.year = 2015
GROUP BY r.id
ORDER BY month ASC
If there are no records available for FX, it looks like a separate subquery should be performed to get the average of the latest month's rates.
Any input would be appreciated. If any further info is required, please post a comment.
Thanks.
Edit Here is a SQFiddle which has the example schemas and the code which highlights the issue.
Here is an approximation of a function that computes your exchange for a given currency and start of month:
DELIMITER //
CREATE FUNCTION MonthRate(IN _curr CHAR(3) CHARACTER SET ascii,
IN _date DATE)
RETURNS FLOAT
DETERMINISTIC
BEGIN
-- Note: _date must be the first of some month, such as '2015-02-01'
DECLARE _avg FLOAT;
DECLARE _prev FLOAT;
-- First, try to get the average for the month:
SELECT AVG(rate) INTO _avg FROM foreign_exchange
WHERE base = _curr
AND target = 'USD'
AND rate_date >= _date
AND rate_date < _date + INTERVAL 1 MONTH;
IF _avg IS NOT NULL THEN
RETURN _avg;
END;
-- Fall back onto the last rate before the month:
SELECT rate INTO _prev
FROM foreign_exchange
WHERE base = _curr
AND target = 'USD'
AND rate_date < _date
ORDER BY _date
LIMIT 1;
IF _prev IS NOT NULL THEN
RETURN _prev;
END;
SELECT "Could not get value -- ran off start of Rates table";
END;
DELIMITER ;
There are probably syntax errors, etc. But hopefully you can work with it.
It should be easy to call the function from the rest of the code.
For performance, this would be beneficial:
INDEX(base, target, rate_date, rate)
Create a view :
create view avg_rate as
select base, target, year(fx.rate_date) year, month(fx.rate_date) month,
avg(rate) avg_rate
from foreign_exchange group by base, target
Join it twice, once for current month, and once for previous
select r.id, r.month,
r.value * avg(coalesce(cr.avg_rate, pr.avg_rate, 1)) converted,
avg(coalesce(cr.avg_rate, pr.avg_rate), 0) rate
from monthly_revenue r, avg_rate cr, avg_rate pr, order_headers h
where
r.year = 2015 and
cr.year = r.year and cr.month = r.month and cr.target='USD' and
pr.year = r.year and pr.month = r.month - 1 and pr.target='USD' and
r.order_header_id = h.id
group by r.id
order by r.month
Also I personally don't like this way of writing query and prefer to using explicit joins as you group conditions logically and don't have a mess in where clause. i.e.:
...
from monthly_revenue r
inner join order_headers h on r.order_header_id = h.id
left join avg_rate cr on cr.year = r.year and cr.month = r.month and cr.target='USD'
left join avg_rate pr on pr.year = r.year and pr.month = r.month - 1 and pr.target='USD'
where r.year = 2015
http://sqlfiddle.com/#!9/6a41a/1
This fiddle based on your original one but I added some rate values for February and March to test and show how it works.
SELECT t.*,
IF(#first=t.id, #flag := #flag+1,#flag:=1) `flag`,
#first:=t.id
FROM
(SELECT
coalesce(fx.rate,1) `rate`, (r.value * coalesce(fx.rate,1)) as converted,
r.*, fx.base,fx.target, fx.avg_date, fx.rate frate
FROM
monthly_revenue r
LEFT JOIN
currencies
ON r.currency_id = currencies.id
LEFT JOIN
(SELECT AVG(rate) `rate`,
`base`,
`target`,
STR_TO_DATE(CONCAT('1/',MONTH(rate_date),'/',YEAR(rate_date)), '%d/%m/%Y') avg_date
FROM foreign_exchange
GROUP BY `base`, `target`, `avg_date`
) fx
ON currencies.name = fx.base
AND fx.target = 'USD'
AND fx.avg_date <= STR_TO_DATE(CONCAT('1/',r.month,'/',r.year), '%d/%m/%Y')
ORDER BY r.id, fx.avg_date DESC) t
HAVING `flag` = 1
and if you need records just for specific month you can add WHERE before ORDER like this:
WHERE r.month = 1 and r.year = 2015
ORDER BY r.id, fx.avg_date DESC) t
You may test this query on the fiddle link you provided : http://sqlfiddle.com/#!9/33def/2
select id,product_id,currency_id,currency_name,
value,month,year,#prev_fx_avg:=ifnull(fx_avg,#prev_fx_avg) fx_avg,
value*#prev_fx_avg as converted
from (SELECT
r.id,r.product_id,r.currency_id,c.name as currency_name,
r.value,r.month,r.year,if(c.name="USD",1,temp.avg_rate) as fx_avg
FROM
monthly_revenue r
left join currencies c on r.currency_id=c.id
left join
(select base , avg(rate) as avg_rate, MONTH(fx.rate_date) month,
YEAR(fx.rate_date) year
from foreign_exchange fx
where target="USD"
group by base,target,MONTH(fx.rate_date),
YEAR(fx.rate_date)) temp on(r.month=temp.month and r.year=temp.year and c.name=temp.base)
group by r.id
order by r.currency_id,r.month ASC, r.year ASC) final,(select #prev_fx_avg:=-1) temp2;

How can I perform cumulative calculations over a date range in SQL? (e.g. calculation for 01 uses data from 01, 02 uses 01 and 02, and so on)

Context
I would like to perform cumulative calculations over a selected date range. (i.e. 2014-07-01 (henceforth referred to as 01 for simplicity) would perform a calculation on data in 01 only. 02 would perform a calculation using data from 01 and 02. 03 would use data from 01, 02, and 03. And so on.)
Detail
I have a record of every sale made in a store and whether or not a complaint has been made. It is easy enough to generate the following table with the query below -
SELECT
Date,
COUNT(*) AS Sales,
SUM(CASE WHEN ComplaintMade = 'True' THEN 1 ELSE 0 END) AS Complaints
FROM SalesRecords
WHERE Date BETWEEN '2014-07-01' AND '2014-07-05'
GROUP BY Date
ORDER BY Date
--------------------------------------
| Date | Sales | Complaints |
--------------------------------------
| 2014-07-01 | 100 | 2 |
| 2014-07-02 | 150 | 6 |
| 2014-07-03 | 180 | 9 |
| 2014-07-04 | 140 | 10 |
| 2014-07-05 | 300 | 15 |
--------------------------------------
Calculating the average number of Sales per Complaint is easy enough to calculate by extending this query and selecting the following -
COUNT(*)
/
CASE WHEN(SUM(CASE WHEN ComplaintMade = 'True' THEN 1 ELSE 0 END)) = 0
THEN 1
ELSE (SUM(CASE WHEN ComplaintMade = 'True' THEN 1 ELSE 0 END))
END)
AS SalesPerComplaint
(This shows the number of sales per complaint made (Sales / Complaints), or, if no complaints have been made, the number of Sales (Sales/1))
This would display the following table.
----------------------------------------------------------
| Date | Sales | Complaints | SalesPerComplaint |
---------------------------------------------------------|
| 2014-07-01 | 100 | 2 | 50 |
| 2014-07-02 | 150 | 6 | 25 |
| 2014-07-03 | 180 | 9 | 20 |
| 2014-07-04 | 140 | 10 | 14 |
| 2014-07-05 | 300 | 15 | 20 |
---------------------------------------------------------|
What I would like to do is show the cumulative SalesPerComplaint. So, for 2014-07-01, take the Sales for 2014-07-01 divided by the number of Complaints for 2014-07-01. But, for 2014-07-02 take the Sales for 01 and 02, divided by the Complaints for 01 and 02. And for 03 use the data from 01, 02 and 03. And so on.
So, the first couple of rows of the table would look like -
--------------------------------------------------------------------
| Date | Sales | Complaints | CumulativeSalesPerComplaint |
-------------------------------------------------------------------|
| 2014-07-01 | 100 | 2 | 50 |
| 2014-07-02 | 150 | 6 | 31.25 |
-------------------------------------------------------------------|
(The CumulativeSalesPerComplaint for 2014-07-02 is now 31.25 because it is calculated using the Sales from both Dates so far (100 + 150 = 250) divided by the number of Complaints from both Dates so far (2 + 6 = 8) (250/8 = 31.25))
(Please let me know if I could improve this question formatting. I wanted to make this as clear as possible but if I've included too much detail or a confusing structure please let me know and I will gladly improve it. Thank you.)
My sqlfiddle is at sqlfiddle.com/#!2/9e2ad/5
What about this (SqlFiddle)?
In this query, you are joining the distinct dates you have with the data you've already obtained:
SELECT sr.uniqueday Date, qry.Sales, qry.Complaints,
SUM(Sales)/SUM(Complaints) CumulativeSalesPerComplaint
FROM (SELECT DISTINCT(Date) uniqueday FROM SalesRecords) sr
LEFT JOIN (
SELECT
Date,
COUNT(*) AS Sales,
SUM(CASE WHEN ComplaintMade = 'True' THEN 1 ELSE 0 END) AS Complaints,
((COUNT(*)) / (CASE WHEN (SUM( CASE WHEN ComplaintMade = 'True' THEN 1 ELSE 0 END )) = 0
THEN 1
ELSE (SUM( CASE WHEN ComplaintMade = 'True' THEN 1 ELSE 0 END ))
END)
) AS SalesPerComplaint
FROM SalesRecords
WHERE Date BETWEEN '2014-07-01' AND '2014-07-05'
GROUP BY Date
ORDER BY Date DESC
) qry ON qry.Date <= sr.uniqueday
GROUP BY sr.uniqueday
The trick is to join the two tables (one containing only the several days, the other with your data) with a join with the condition "qry.Date <= sr.uniqueday", so for a single uniquedate you are joining all the previous rows.

mysql query get range between date not from table

I have payment table info like this
ID Costumer | start_pay | Payment
1 | 2014-01-01 | 1.500
2 | 2013-12-01 | 900
that information they must pay every month, i want calculating it for range between start_pay to CURDATE
if CURDATE is 2014-03-01 (Y-m-d) the result I want like this
ID Costumer | start_pay | Payment | total_to_pay | month_count
1 | 2014-01-01 | 1.500 | 4.500 | 3
2 | 2013-12-01 | 900 | 3.600 | 4
can i do that with mysql query?
Try
SELECT *,TIMESTAMPDIFF(MONTH, DATE_SUB(start_pay,INTERVAL 1 MONTH), CURDATE()) AS
month_count,Payment * month_count AS total_to_pay FROM TABLE
Note that if the difference is less than a month it will output 0
PERIOD_DIFF is basically made for this type of calculation:
SELECT PERIOD_DIFF(DATE_FORMAT(CURDATE(),'%Y%m'), DATE_FORMAT(start_pay, '%Y%m')) from your table;