I have this query where I calculated cumulative sum. Now, I need to calculate reverse cumulative sum for the same variable
SELECT t1.date, t1.ant, t1.hab,
(#csum:= #csum + t1.hab) as cumulative_hab
from(
SELECT date,
ant,
sum(num_habit) as hab
from xxxxxxxxxx
WHERE date BETWEEN CURDATE() - INTERVAL 5 DAY AND CURDATE()
group by ant) AS t1
,(select #csum := 0) vars
order by t1.ant
My table look like this
date ant hab cumulative_hab
24-05-2020 0 382,000 382,000
24-05-2020 1 28,000 410,000
24-05-2020 2 26,000 436,000
24-05-2020 3 11,000 447,000
24-05-2020 4 29,000 476,000
24-05-2020 6 6,000 482,000
24-05-2020 7 12,000 494,000
28-05-2020 8 50,000 544,000
24-05-2020 12 5,000 549,000
24-05-2020 13 6,000 555,000
I would like another column with reverse running sum (reverse cumulative sum), the first value is calculated 555 - 382
date ant hab cumulative_hab reverse_cum_hab
24-05-2020 0 382,000 382,000 555,000
24-05-2020 1 28,000 410,000 173,000,
24-05-2020 2 26,000 436,000 145,000
24-05-2020 3 11,000 447,000 119,000
24-05-2020 4 29,000 476,000 108,000
24-05-2020 6 6,000 482,000 79,000
24-05-2020 7 12,000 494,000 73,000
28-05-2020 8 50,000 544,000 61,000
24-05-2020 12 5,000 549,000 11,000
24-05-2020 13 6,000 555,000 6,000
As a starter: if you are running MySQL 8.0, you can do this easily with window functions:
select
date,
ant,
sum(num_habit) as hab,
sum(sum(num_habit)) over(order by date) cumulative_hab,
sum(sum(num_habit)) over(order by date desc) reverse_cumulative_hab
from mytable
where date between current_date - interval 5 day and current_date
group by date, ant
order by date
In earlier versions, it is more complicated. I would suggest joining two queries:
select t.*, r.reverse_cumulative_hab
from (
select t.*, #csum := #csum + hab cumulative_hab
from (
select date, ant, sum(num_habit) as hab
from mytable
where date between current_date - interval 5 day and current_date
group by date, ant
order by date
) t
cross join (select #csum := 0) x
) t
inner join (
select t.*, #rcsum := #rcsum + hab reverse_cumulative_hab
from (
select date, ant, sum(num_habit) as hab
from mytable
where date between current_date - interval 5 day and current_date
group by date, ant
order by date desc
) t
cross join (select #rcsum := 0) x
) r on r.date = t.date
order by t.date
This assumes no duplicate ant per date.
It might also be possible to simplify the logic and compute the reverse sum by taking the difference between the cumulative sum and the overall sum:
select t.*, z.total_hab - t.cumulative_hab reverse_cumulative_hab
from (
select t.*, #csum := #csum + hab cumulative_hab
from (
select date, ant, sum(num_habit) as hab
from mytable
where date between current_date - interval 5 day and current_date
group by date, ant
order by date
) t
cross join (select #csum := 0) x
) t
cross join (
select sum(num_habit) as total_hab
from mytable
where date between current_date - interval 5 day and current_date
) z
order by date
Note that these queries are safer than your original code in regard of ordering of the rows: records are ordered in a subquery before the cumulative sum is computed.
Related
I have a table like this two
Table A
date amount B_id
'2020-1-01' 3000000 1
'2019-8-01' 15012 1
'2019-6-21' 90909 1
'2020-1-15' 84562 1
--------
Table B
id type
1 7
2 5
I have to show sum of amount until the last date of each month for the last 12 month.
The query i have prepared is like this..
SELECT num2.last_dates,
(SELECT SUM(amount) FROM A
INNER JOIN B ON A.B_id = B.id
WHERE B.type = 7 AND A.date<=num2.last_dates
),
(SELECT SUM(amount) FROM A
INNER JOIN B ON A.B_id = B.id
WHERE B.type = 5 AND A.date<=num2.last_dates)
FROM
(SELECT last_dates
FROM (
SELECT LAST_DAY(CURDATE() - INTERVAL CUSTOM_MONTH MONTH) last_dates
FROM(
SELECT 1 CUSTOM_MONTH UNION
SELECT 0 UNION
SELECT 2 UNION
SELECT 3 UNION
SELECT 4 UNION
SELECT 5 UNION
SELECT 6 UNION
SELECT 7 UNION
SELECT 8 UNION
SELECT 9 UNION
SELECT 10 UNION
SELECT 11 UNION
SELECT 12 )num
) num1
)num2
ORDER BY num2.last_dates
This gives me the result like this which is exactly how i need it. I need this query to execute faster. Is there any better way to do what i am trying to do?
2019-05-31 33488.69 109.127800
2019-06-30 263.690 1248932.227800
2019-07-31 274.690 131.827800
2019-08-31 627.690 13.687800
2019-09-30 1533.370000 08.347800
2019-10-31 1444.370000 01.327800
2019-11-30 5448.370000 247.227800
2019-12-31 61971.370000 016.990450
2020-01-31 19550.370000 2535.185450
2020-02-29 986.370000 405.123300
2020-03-31 1152.370000 26.793300
2020-04-30 9404.370000 11894.683300
2020-05-31 3404.370000 17894.683300
I'd use conditional aggregation, and pre-aggregate the monthly totals in one pass, instead of doing twenty-six individual passes repeatedly through the same data.
I'd start with something like this:
SELECT CASE WHEN A.date < DATE(NOW()) + INTERVAL -14 MONTH
THEN LAST_DAY( DATE(NOW()) + INTERVAL -14 MONTH )
ELSE LAST_DAY( A.date )
END AS _month_end
, SUM(IF( B.type = 5 , B.amount , NULL)) AS tot_type_5
, SUM(IF( B.type = 7 , B.amount , NULL)) AS tot_type_7
FROM A
JOIN B
ON B.id = A.B_id
WHERE B.type IN (5,7)
GROUP
BY _month_end
(column amount isn't qualified in original query, so just guessing here which table that is from. adjust as necessary. best practice is to qualify all column references.
That gets us the subtotals for each month, in a single pass through A and B.
We can get that query tested and tuned.
Then we can incorporate that as an inline view in an outer query which adds up those monthly totals. (I'd do an outer join, just in case rows are missing, sow we don't wind up omitting rows.)
Something like this:
SELECT d.dt + INTERVAL -i.n MONTH + INTERVAL -1 DAY AS last_date
, SUM(IFNULL(t.tot_type_5,0)) AS rt_type_5
, SUM(IFNULL(t.tot_type_7,0)) AS rt_type_7
FROM ( -- first day of next month
SELECT DATE(NOW()) + INTERVAL -DAY(DATE(NOW()))+1 DAY + INTERVAL 1 MONTH AS dt
) d
CROSS
JOIN ( -- thirteen integers, integers 0 thru 12
SELECT 0 AS n
UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8
UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
) i
LEFT
JOIN ( -- totals by month
SELECT CASE WHEN A.date < DATE(NOW()) + INTERVAL -14 MONTH
THEN LAST_DAY( DATE(NOW()) + INTERVAL -14 MONTH )
ELSE LAST_DAY( A.date )
END AS _month_end
, SUM(IF( B.type = 5 , B.amount , NULL)) AS tot_type_5
, SUM(IF( B.type = 7 , B.amount , NULL)) AS tot_type_7
FROM A
JOIN B
ON B.id = A.B_id
WHERE B.type IN (5,7)
GROUP
BY _month_end
) t
ON t._month_end < d.dt
GROUP BY d.dt + INTERVAL -i.n MONTH + INTERVAL -1 DAY
ORDER BY d.dt + INTERVAL -i.n MONTH + INTERVAL -1 DAY DESC
The design is meant to do one swoop through the A JOIN B set. We're expecting to get about 14 rows back. And we're doing a semi-join, duplicating the oldest months multiple times, so approx . 14 x 13 / 2 = 91 rows, that get collapsed into 13 rows.
The big rock in terms of performance is going to be materializing that inline view query.
This is how I'd probably approach this in MySQL 8 with SUM OVER:
Get the last 12 months.
Use these months to add empty month rows to the original data, as MySQL doesn't support full outer joins.
Get the running totals for all months.
Show only the last twelve months.
The query:
with months (date) as
(
select last_day(current_date - interval 1 month) union all
select last_day(current_date - interval 2 month) union all
select last_day(current_date - interval 3 month) union all
select last_day(current_date - interval 4 month) union all
select last_day(current_date - interval 5 month) union all
select last_day(current_date - interval 6 month) union all
select last_day(current_date - interval 7 month) union all
select last_day(current_date - interval 8 month) union all
select last_day(current_date - interval 9 month) union all
select last_day(current_date - interval 10 month) union all
select last_day(current_date - interval 11 month) union all
select last_day(current_date - interval 12 month)
)
, data (date, amount, type) as
(
select last_day(a.date), a.amount, b.type
from a
join b on b.id = a.b_id
where b.type in (5, 7)
union all
select date, null, null from months
)
select
date,
sum(sum(case when type = 5 then amount end)) over (order by date) as t5,
sum(sum(case when type = 7 then amount end)) over (order by date) as t7
from data
group by date
order by date
limit 12;
Demo: https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=ddeb3ab3e086bfc182f0503615fba74b
I don't know whether this is faster than your own query or not. Just give it a try. (You'd get my query much faster by adding a generated column for last_day(date) to your table and use this. If you need this often, this may be an option.)
You are getting some complicated answers. I think it is easier. Start with knowing we can easily sum for each month:
SELECT SUM(amount) as monthtotal,
type,
MONTH(date) as month,
YEAR(date) as year
FROM A LEFT JOIN B on A.B_id=B.id
GROUP BY type,month,year
From that data, we can use a variable to get running total. Best to do by initializing the variable, but not necessary. We can get the data necessary like this
SET #running := 0;
SELECT (#running := #running + monthtotal) as running, type, LAST_DAY(CONCAT(year,'-',month,'-',1))
FROM
(SELECT SUM(amount) as monthtotal,type,MONTH(date) as month,YEAR(date) as year FROM A LEFT JOIN B on A.B_id=B.id GROUP BY type,month,year) AS totals
ORDER BY year,month
You really need to have a connector that supports multiple statements, or make multiple calls to initialize the variable. Although you can null check the variable and default to 0, you still have an issue if you run the query a second time.
Last thing, if you really want the types to be summed separately:
SET #running5 := 0;
SET #running7 := 0;
SELECT
LAST_DAY(CONCAT(year,'-',month,'-',1)),
(#running5 := #running5 + (CASE WHEN type=5 THEN monthtotal ELSE 0 END)) as running5,
(#running7 := #running7 + (CASE WHEN type=7 THEN monthtotal ELSE 0 END)) as running7
FROM
(SELECT SUM(amount) as monthtotal,type,MONTH(date) as month,YEAR(date) as year FROM A LEFT JOIN B on A.B_id=B.id GROUP BY type,month,year) AS totals
ORDER BY year,month
We still don't show months where there is no data. I'm not sure that is a requirement. But this should only need one pass of table A.
Also, make sure the id on table B is indexed.
I have a table with three columns: planning_start_date - planning_end_date - quantity.
For example I have this data:
planning_start_date | planning_end_date | quantity
2019-03-01 | 2019-03-31 | 1500
I need to split the value 1500 into multiple rows with the adverage per day, so 1500 / 31 days = 48,38 per day.
The expected result should be:
date daily_qty
2019-03-01 | 48,38
2019-03-02 | 48,38
2019-03-03 | 48,38
...
2019-03-31 | 48,38
Anyone with some suggestions?
Should you decide to upgrade to MySQL 8.0, here's a recursive CTE that will generate a list of all the days between planning_start_date and planning_end_date along with the required daily quantity:
WITH RECURSIVE cte AS (
SELECT planning_start_date AS date,
planning_end_date,
quantity / (DATEDIFF(planning_end_date, planning_start_date) + 1) AS daily_qty
FROM test
UNION ALL
SELECT date + INTERVAL 1 DAY, planning_end_date, daily_qty
FROM cte
WHERE date < planning_end_date
)
SELECT `date`, daily_qty
FROM cte
ORDER BY `date`
Demo on dbfiddle
In MySLQ 8+, you can use a recursive CTE like this:
with recursive cte(dte, planning_end_date, quantity, days) as (
select planning_start_date as dte, planning_end_date, quantity, datediff(planning_end_date, planning_start_date) + 1 as days
from t
union all
select dte + interval 1 day as dte, planning_end_date, quantity, days
from cte
where dte < planning_end_date
)
select dte, quantity / days
from cte;
Here is a db<>fiddle.
In earlier versions, you want a numbers table of some sort. For instance, if your table has enough rows, you can just use it:
select (planning_start_date + interval n.n day),
quantity / (datediff(planning_end_date, planning_start_date) + 1)
from t join
(select (#rn := #rn + 1) as n
from t cross join
(select #rn := 0) params
) n
on planning_start_date + interval n.n day <= planning_end_date;
You can use any table that is large enough for n.
I have table as below
name price date
soap 10 2013-09-18
soap 10 2013-09-18
pens 8 2013-09-18
deos 7 2013-09-18
book 13 2013-09-17
book 13 2013-09-17
soap 10 2013-09-17
pens 8 2013-09-17
Based on the above data , i would like to calculate the rank of total selling per item ( based on price as below for today and previous day.
name totalselling date todayrank previousdayrank
soap 20 2013-09-18 1 2
pens 8 2013-09-18 2 2
deos 7 2013-09-18 3 -
if the todays item doesnt exit in yesterday ,then its ranking in previous day should be null.
You will have to use subselects, date intervals, some variables, a case and some brainwork.
Mix all this together and you will get something like this:
SELECT today.*,
CASE
WHEN yesterday.yesterdayrank IS NULL THEN '-'
ELSE yesterday.yesterdayrank
END
FROM (SELECT #i:=#i +1 AS todayrank,
name,
SUM(price) AS price
FROM test,
(SELECT #i:= 0) AS foo
WHERE createdate = DATE (NOW()) - INTERVAL 2 DAY
GROUP BY name
ORDER BY todayrank) today
LEFT JOIN (SELECT #j:= #j +1 AS yesterdayrank,
name,
SUM(price) AS price
FROM test,
(SELECT #j:= 0) AS bar
WHERE createdate = DATE (NOW()) - INTERVAL 3 DAY
GROUP BY name
ORDER BY yesterdayrank) yesterday ON today.name = yesterday.name
I hope this helps.
SELECT a.name,
a.todaysales,
a.date,
a.rank AS ranktoday,
b.rank AS rankyesterday
FROM
(SELECT name, sum(price) AS todaysales, date, #n := #n + 1 AS rank
FROM TABLE , (SELECT #n := 0) alias
WHERE date=curdate() group by 1 order by count(*))a
INNER JOIN
(SELECT name, sum(price) AS todaysales, date, #n := #n + 1 AS rank
FROM TABLE , (SELECT #n := 0) alias
WHERE date=curdate() - interval 1 DAY group by 1 order by count(*))b ON a.name=b.name
I need to get Bth working day in a month when the value of B is entered.
For example, If b=12 in the month of January,2013 the resultant value should be in the date format as '17-01-2013' as the result is calculated
after excluding Saturdays, Sundays & holidays in the month.
I have tried it in SQLserver with the following code & its working fine, but Im finding it difficult to execute it in MySql as some functions are not
available as in Sqlserver.
Declare
#fromDate Date,
#Daydiff int
Set #fromDate ='01 jan 2013'
Set #Daydiff=datediff(day, #fromdate, dateadd(month, 1, #fromdate))
Select * from
(
Select
dateadd(day,DayNo,#fromDate) as Date,
dateName(weekday,(dateadd(day,DayNo,#fromDate))) As WeekDate,
Datename(month,(dateadd(day,DayNo,#fromDate))) as MonthName,
Row_number() Over (partition by (DatePart(month,(dateadd(day,DayNo,#fromDate))))
order by (dateadd(day,DayNo,#fromDate))) as Business_day
from
(Select top (#Daydiff) row_number() over(order by (select 1))-1 as DayNo
from sys.syscolumns a cross join sys.syscolumns b)Dates
Where
dateName(weekday,(dateadd(day,DayNo,#fromDate))) Not In ('Saturday','Sunday') and
dateadd(day,DayNo,#fromDate) Not In (Select hdate from Holidays)
)A
Where Business_day=1
Note
Holidays is the static holidays table which contains list of holidays of 2013
I need a similar instance in Mysql.
Kindly help me with this.
SQLFiddle demo
If you need first day set OFFSET 0 in the end. If the second OFFSET 1, if 15-th set OFFSET 14
select d
FROM
(
SELECT #row := #row + 1 as row,
DATE_ADD('2013-01-01', INTERVAL #row-1 DAY) d
from
(SELECT #row := 0) r,
(
select 1 n
union all
select 2 n
union all
select 3 n
union all
select 4 n
union all
select 5 n
union all
select 6 n
) t1,
(
select 1 n
union all
select 2 n
union all
select 3 n
union all
select 4 n
union all
select 5 n
union all
select 6 n
) t2
) num_seq
where
d<DATE_ADD('2013-01-01', INTERVAL 1 MONTH)
and d not in (select hdate from Holidays )
and DAYNAME(d) not in ('Saturday','Sunday')
order by d
LIMIT 1 OFFSET 20
Version without OFFSET and LIMIT. See the latest where r=1 it is the 1-st day. If you need 15-th day change to where r=15
SQLFiddle demo
select d
from
(
select d,#r := #r + 1 as r
FROM
(SELECT #r := 0) r1,
(
SELECT #row := #row + 1 as row,
DATE_ADD('2013-01-01', INTERVAL #row-1 DAY) d
from
(SELECT #row := 0) r,
(
select 1 n
union all
select 2 n
union all
select 3 n
union all
select 4 n
union all
select 5 n
union all
select 6 n
) t1,
(
select 1 n
union all
select 2 n
union all
select 3 n
union all
select 4 n
union all
select 5 n
union all
select 6 n
) t2
) num_seq
where
d<DATE_ADD('2013-01-01', INTERVAL 1 MONTH)
and d not in (select hdate from Holidays )
and DAYNAME(d) not in ('Saturday','Sunday')
order by d
) rTable
where r=1
how to get the same result when only month and year are passed as parameters. Coz when i checked the code... its working when the date is 1st of the respective month, Like if I enter parameter as '2013-01-01' the result is absolute, but if the date is given as '2013-01-15' the procedure is counting the 1st day 15th and calculating the nth day starting from there.
I have the following data of a particular user -
Table temp -
time_stamp
2015-07-19 10:52:00
2015-07-18 10:49:00
2015-07-12 10:43:00
2015-06-08 12:32:00
2015-06-07 11:33:00
2015-06-06 10:05:00
2015-06-05 04:17:00
2015-04-14 04:11:00
2014-04-02 23:19:00
So the output for the query should be -
Maximum streak = 4, Current streak = 2
Max streak = 4 because of these -
2015-06-08 12:32:00
2015-06-07 11:33:00
2015-06-06 10:05:00
2015-06-05 04:17:00
And current streak is 2 because of these (Assuming today's date is 2015-07-19)-
2015-07-19 10:52:00
2015-07-18 10:49:00
EDIT: I want a simple SQL query for MYSQL
For MAX streak(streak) you can use this, I have use the same query to calculate max streak. This may help you
SELECT *
FROM (
SELECT t.*, IF(#prev + INTERVAL 1 DAY = t.d, #c := #c + 1, #c := 1) AS streak, #prev := t.d
FROM (
SELECT date AS d, COUNT(*) AS n
FROM table_name
group by date
) AS t
INNER JOIN (SELECT #prev := NULL, #c := 1) AS vars
) AS t
ORDER BY streak DESC LIMIT 1;
A general approach with the gaps and islands queries is to tag each row with its rank in the data and with its rank in the full list of dates. The clusters will all have the same difference.
Caveats: I don't know if this query will be efficient. I don't remember if MySQL allows for scalar subqueries. I didn't look up the way to calculate a day interval in MySQL.
select user_id, max(time_stamp), count(*)
from (
select
t.user_id, t.time_stamp,
(
select count(*)
from T as t2
where t2.user_id = t.user_id and t2.time_stamp <= t.time_stamp
) as rnk,
number of days from t.time_stamp to current_date as days
from T as t
) as data
group by usr_id, days - rnk