MySQL query to see changes during the month of last 3 month - mysql

I want to see changes during the month of last 3 month.
Database:
MySQL:
SELECT DATE_FORMAT(CUR.time, '%Y %b') AS thisTime,
MAX(CUR.value) AS thisValue,
MAX(PRE.value) AS prevValue,
MAX(CUR.value)-MAX(PRE.value) AS compValue
FROM test CUR
INNER JOIN test PRE
ON MONTH(PRE.time) = (SELECT MAX(MONTH(XXX.time))
FROM test XXX
WHERE MONTH(XXX.time) < MONTH(CUR.time))
WHERE CUR.time > DATE_SUB(NOW(), INTERVAL 3 MONTH)
GROUP BY MONTH(CUR.time)
ORDER BY CUR.id
Result:
Can see that there is no January!
I want to see changes during the month of last 3 month like this.
Anyone got ideas?
| 2019 Feb | 50 | 40 | 10 |
| 2019 Jan | 40 | 25 | 15 |
| 2018 Dec | 25 | 20 | 5 |

The problem with trying to use MONTH in a JOIN condition is the wrap around from 12 to 1 at the end of a year. It's easiest just to use the date as a YEARMONTH string:
SELECT DATE_FORMAT(t1.time, '%b %Y') AS thisTime,
MAX(t1.value) AS thisValue,
MAX(t2.value) AS prevValue,
MAX(t1.value) - MAX(t2.value) AS compValue
FROM test t1
JOIN test t2 ON DATE_FORMAT(t2.time, '%Y%m') = (SELECT MAX(DATE_FORMAT(`time`, '%Y%m'))
FROM test t3
WHERE DATE_FORMAT(t3.time, '%Y%m') < DATE_FORMAT(t1.time, '%Y%m'))
GROUP BY thisTime
ORDER BY STR_TO_DATE(CONCAT('1 ', thisTime), '%d %b %Y') DESC
Output:
thisTime thisValue prevValue compValue
Feb 2019 50 40 10
Jan 2019 40 25 15
Dec 2018 25 20 5
Demo on dbfiddle

Instead of trying a complicated join, you can just use sub-queries since the table isn't very complicated. Also just filter for the results on the 2nd of every month since you don't need the 1st from what your expected result shows.
SELECT
DATE_FORMAT(cur.time,'%Y %b') AS thisTime,
cur.value AS thisValue,
(SELECT value FROM test WHERE time = DATE_SUB(cur.time,INTERVAL 1 MONTH)) AS prevValue,
(SELECT cur.value - value FROM test WHERE time = DATE_SUB(cur.time,INTERVAL 1 MONTH)) AS compValue
FROM
test AS cur
WHERE
cur.time >= DATE_SUB(CURDATE(),INTERVAL 3 MONTH)
AND DATE_FORMAT(cur.time,'%d') = 2
Result:
thisTime thisValue prevValue compValue
2019 Feb 50 40 10
2019 Jan 40 25 15
2018 Dec 25 20 5
EDIT
Per Nick's point below, I did assume that the max would always be the 2nd of every month. If that's false and the table snapshot just doesn't happen to show a month where the 1st was the max, then this modification will give the correct max result.
The JOIN of monthMax and the regular test table will guarantee you always pull the max value per month, no matter how many dates there are within that month. Then just use a similar sub-query method as my initial query.
SELECT
DATE_FORMAT(t.time,'%Y %b') AS thisTime,
t.value AS thisValue,
(SELECT MAX(value) FROM test WHERE DATE_FORMAT(time,'%Y-%m') = DATE_FORMAT(DATE_SUB(t.time,INTERVAL 1 MONTH),'%Y-%m')) AS prevValue,
(SELECT t.value - MAX(value) FROM test WHERE DATE_FORMAT(time,'%Y-%m') = DATE_FORMAT(DATE_SUB(t.time,INTERVAL 1 MONTH),'%Y-%m')) AS compValue
FROM
(SELECT DATE_FORMAT(time,'%Y-%m') AS 'timeFix', MAX(value) AS 'maxValue' FROM test GROUP BY timeFix) AS monthMax
JOIN (test AS t) ON (monthMax.maxValue = t.value AND monthMax.timeFix = DATE_FORMAT(t.time,'%Y-%m'))
WHERE
t.time > DATE_SUB(CURDATE(),INTERVAL 3 MONTH)
Result:
thisTime thisValue prevValue compValue
2019 Feb 50 40 10
2019 Jan 40 25 15
2018 Dec 25 20 5

Related

MySQL Select data from table with dates between in reverse of interval 7 days

I have a MySQL requirement to select data from a table based on a start date and end date and group it by weekly also selecting the data in reverse order by date. Assume that, I have chosen the start date as 1st November and the end date as 04 December. Now, I would like to fetch the data as 04 December to 28 November, 27 November to 20 November, 19 November to 12 November and so on and sum the value count for that week.
Given an example table,
id
value
created_at
1
10
2021-10-11
2
13
2021-10-17
3
11
2021-10-25
4
8
2021-11-01
5
1
2021-11-10
6
4
2021-11-18
7
34
2021-11-25
8
17
2021-12-04
Now the result should be like 2021-12-04 to 2021-11-28 as one week, following the same in reverse order and summing the column value data for that week. I have tried in the query to add the interval of 7 days after the end date but it didn't work.
SELECT count(value) AS total, MIN(R.created_at)
FROM data_table AS D
WHERE D.created_at BETWEEN '2021-11-01' AND '2021-12-04' - INTERVAL 7 DAY ORDER BY D.created_at;
And it's also possible to have the last week may have lesser than 7 days.
Expected output:
end_interval
start_interval
total
2021-12-04
2021-11-27
17
2021-11-27
2021-11-20
34
2021-11-20
2021-11-13
4
2021-11-13
2021-11-06
1
2021-11-06
2021-10-30
8
2021-10-30
2021-10-25
11
Note that the last week is only 5 days depending upon the selected from and end dates.
One option to address this problem is to
generate a calendar of all your intervals, beginning from last date till first date, with a split of your choice, using a recursive query
joining back the calendar with the original table
capping start_interval at your start_date value
aggregating values for each interval
You can have three variables to be set, to customize your date intervals and position:
SET #start_date = DATE('2021-10-25');
SET #end_date = DATE('2021-12-04');
SET #interval_days = 7;
Then use the following query, as already described:
WITH RECURSIVE cte AS (
SELECT #end_date AS end_interval,
DATE_SUB(#end_date, INTERVAL #interval_days DAY) AS start_interval
UNION ALL
SELECT start_interval AS end_interval,
GREATEST(DATE(#start_date), DATE_SUB(start_interval, INTERVAL #interval_days DAY)) AS start_interval
FROM cte
WHERE start_interval > #start_date
)
SELECT end_interval, start_interval, SUM(_value) AS total
FROM cte
LEFT JOIN tab
ON tab.created_at BETWEEN start_interval AND end_interval
GROUP BY end_interval, start_interval
Check the demo here.

Write Query to display look like in image

The table provided shows all new users signing up on a specific date in the format YYYY-MM-DD.
Your query should output the change from one month to the next. Because the first month has no preceding month, your output should skip that row. Your output should look like the following table.
My table data
Table data:
ID DateJoined
1 2017-01-06
2 2017-01-12
3 2017-01-16
4 2017-01-25
5 2017-02-05
6 2017-02-07
7 2017-02-21
8 2017-03-05
9 2017-03-07
10 2017-03-14
11 2017-03-16
12 2017-03-25
13 2017-03-25
14 2017-03-25
15 2017-03-25
16 2017-03-26
17 2017-04-05
18 2017-04-14
19 2017-04-21
20 2017-05-07
23 2017-05-14
24 2017-05-16
25 2017-05-25
26 2017-05-25
27 2017-05-25
28 2017-05-25
Enter image description here
I want this output:
count all records from every month and subtract it from the next month record.
This is my query:
SELECT
MONTH(L.joindate),
COUNT(L.joindate) - COUNT(R.joindate),
MONTH(R.joindate),
COUNT(R.joindate)
FROM
userlog AS L
LEFT JOIN
userlog AS R
ON MONTH(R.joindate)= (SELECT MIN(MONTH(joindate)) FROM userlog WHERE MONTH(joindate) < MONTH(L.joindate))
GROUP BY (MONTH(L.joindate)),(MONTH(R.joindate));
Use lag(), available in MySQL 8.0:
select date_format(joindate, '%Y-%m-01') joinmonth,
count(*) - lag(count(*), 1, 0) over(order by date_format(joindate, '%Y-%m-01')) m2m
from userlog
group by joinmonth
Note that I changed the logic to truncate dates to the first of month to use date_format().
In earlier versions, you can use a correlated subquery:
select date_format(joindate, '%Y-%m-01') joinmonth,
count(*) - (
select count(*)
from userlog l1
where l1.joindate >= date_format(l.joindate, '%Y-%m-01') - interval 1 month
and l1.joindate < date_format(l.joindate, '%Y-%m-01')
) m2m
from userlog l
group by joinmonth
LIMIT 12 OFFSET 1
You need to use Lag. Also, since it says you need to skip the first row so I have used the not null condition. I believe this query should work.
select
Month,
MonthToMonthChange
from
(
select
m_name as Month,
(total_id - diff) as MonthToMonthChange
from
(
select
total_id,
m_name,
Lag(total_id, 1) OVER(
ORDER BY
m_num ASC
) AS diff
from
(
select
MonthNAME(DateJoined) m_name,
Month(DateJoined) m_num,
count(*) total_id
from
maintable
Group by
m_name,
m_num
) as first_subquery
) as second_subquery
) as final_query
where
MonthToMonthChange IS NOT NULL;
select
MONTHNAME(UL1.DateJoined) as MONTH,
count(UL1.DateJoined) -
(
select count(UL2.DateJoined)
from tablename UL2
where MONTH(UL2.DateJoined )=MONTH(UL1.DateJoined) -1
) as MonthToMonthChange
from tablename UL1
where Month(UL1.DateJoined)!=1
Group by MONTHNAME(UL1.DateJoined)
order by UL1.DateJoined ASC;
https://i.stack.imgur.com/BXXDb.png
I tried this and it worked
select date_format(DateJoined, CONCAT('%M')) as Month,
count(*) - lag(count(*), 1, 0) over(order by date_format(DateJoined, CONCAT('%m'))) MonthToMonthChange
from maintable_OKLOT
group by Month
limit 12 offset 1

adding +1 day to the next date on a join on an incomplete sequence of dates

I have a table which stores an incomplete sequence dates within the year:
eg.
Jan 1
Jan 4
Jan 9
Jan 24
Jan 25
Feb 16
Feb 18
Feb 21
Feb 28
Mar 3
.
.
.
Dec 31
I need to do a self join on the table on the dates column. The date needs to join on date a where date b is + 1 day in the sequence. If the dates were complete, I would do a join such as a.date = b.date but instead of a.date = DATE_ADD(b.date,INTERVAL 1 DAY). But I need to it be something like this a.date = NEXT_DATE_IN_SEQUENCE(b.date), but of course this is not a real function, but I am looking for something like this.
You could use a semi-cartesian product:
SELECT a.date, MIN(b.date)
FROM dates a
INNER JOIN dates b ON b.date > a.date
GROUP BY a.date;
Demo
I would approach this using a correlated subquery:
select t.*,
(select t2.date
from table t2
where t2.date > t.date
order by t2.date
limit 1
) as next_date
from table t;

Employee Year Calculation

I have employees table with date_of_join field
and I have employee_leaves table with the following fields:
employee_id
leave_from
leave_to
total_days
the employee joined on 15 Feb 2011
I want to have a query showing the cound of leaves for every employee years based on his date_of_join
for example, if the employee joined on 15 Feb 2011 then the result will be like this:
Feb 2011 to feb 2012 ---- totals days: 21
Feb 2012 to feb 2013 ---- totals days: 26
Feb 2013 to feb 2014 ---- totals days: 8
where Feb to feb is the employee year so it's from 15 Feb to 14 Feb every year
can anyone help please?
Not having your data it is difficult to test this but I came up with the following based on your description:
SELECT employees.employee_id, DATE_ADD(employees.date_of_join, INTERVAL yrs.years) frm
,DATE_ADD(employees.date_of_join, INTERVAL yrs.years + 1) too,
SUM(employee_leaves.total_days)
FROM employee_leaves
INNER JOIN (SELECT 0 years UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) yrs
ON 1=1
INNER JOIN employees ON employee_leaves.employee_id = employees.employee_id
AND employee_leaves.leave_from BETWEEN DATE_ADD(employees.date_of_join, INTERVAL yrs.years) AND DATE_ADD(employees.date_of_join, INTERVAL yrs.years + 1)
GROUP BY employees.employee_id, DATE_ADD(employees.date_of_join, INTERVAL yrs.years)
;
Probably needs some fiddling, hope this helps.

Insert blank rows in MySQL SELECT statement

I have a query like this:
SELECT COUNT(id), MONTH(date), YEAR(date)
FROM activity
GROUP BY YEAR(date) DESC, MONTH(date) DESC
ORDER BY YEAR(date) DESC, MONTH(date) DESC
Which orders and groups records by month/date. Is there any way I can insert a blank row if a certain month doesn't have a record?
So, instead of this return:
c | M% | Y%
4 | 01 | 2014 # 4 records for Jan 2014
3 | 11 | 2013 # 3 records for Nov 2013
7 | 10 | 2013 # 7 records for Oct 2013
I want to insert months for which no records could be found (Jan 2013 with count = 0), so I can have a neat visualisation of monthly activities.
c | M% | Y%
4 | 01 | 2014
0 | 12 | 2013 # <<<< no records for Dec 2013, but I still want it in array
3 | 11 | 2013
7 | 10 | 2013
Contrary to others, you can make up things in a MySQL query utilizing SQL variables. My inner query creates a baseline date of one month ahead of whatever Now() is. This is joined to the activity table just to get rows to work with. The column is created by just setting the SQL variable equal to one month less than the month result of the previous. In this example, I am doing a limit of 6 so it only goes back 6 months worth of data, but you can change that to however many you care about... as long as there are that many records in the "Activity" table (could be any table as long as it has as many records as you want to create these place-holder records). This creates a result set of a
what I have as "DynamicCalendar". I then use this as the basis to do a left-join to the activity joined by month/year
SELECT
MONTH( DynamicCalendar.GrpDate ) as Mth,
YEAR( DynamicCalendar.GrpDate ) as Yr,
COUNT(activity.id) as Entries
from
( select
#BaseDate := date(date_sub(#BaseDate, interval 1 month)) as GrpDate
from
( select #BaseDate := date_add(Now(), interval 1 month)) sqlvars,
Activity,
limit
6 ) DynamicCalendar
LEFT JOIN Activity
ON MONTH( DynamicCalendar.GrpDate ) = MONTH( Activity.Date )
AND YEAR( DynamicCalendar.GrpDate ) = YEAR( Activity.Date )
group by
MONTH( DynamicCalendar.GrpDate ) as Mth,
YEAR( DynamicCalendar.GrpDate ) as Yr
order by
YEAR( DynamicCalendar.GrpDate ) DESC,
MONTH( DynamicCalendar.GrpDate )