mysql group by month with custom starting day - mysql

Is there a way in mysql to group by month, but with custom starting dates.
Say I want to count logins in a monthly basis, but with the condition that the month starts when a user register.
So for example user A registered on January 30th and user B on January 15th
I should group the logins as follow:
* User A: January 30th - February 28th, March 1st - March 30th, March 31 - April 30 and so on and so forth
* User B: January 15th - February 14th, February 15th - March 14th and so on and so forth
I guess I need to use something like DATE_ADD('2013-01-30', INTERVAL 1 MONTH); but I can not seem to find a way to make the grouping.
UPDATE
#GarethD: You are right that was a typo
In general the month should start at the same day of the next month or the last day of the next month in case that the first is not possible, so if you registered in day 31, the month period would start in day 30 for months that does not have 31 days and the last day of February either 28 or 29
Example:
Given that
id 1 registered on 2012-12-16
id 2 registered on 2013-01-29
and the following table
+----+------------+
| id | date |
+----+------------+
| 1 | 2013-01-15 |
| 1 | 2013-01-16 |
| 1 | 2013-01-17 |
| 1 | 2013-01-17 |
| 2 | 2013-03-20 |
| 2 | 2013-03-21 |
| 2 | 2013-03-28 |
| 2 | 2013-03-29 |
| 2 | 2013-03-30 |
+----+------------+
the results should be
+----+----------------------------+-------+
| id | range | count |
+----+----------------------------+-------+
| 1 | 2012-12-16, 2013-01-15 | 1 |
| 1 | 2013-01-16, 2013-02-15 | 3 |
| 2 | 2013-02-2[8|9], 2013-03-28 | 3 |
| 2 | 2013-03-29, 2013-04-28 | 2 |
+----+----------------------------+-------+
I hope the intent is clearer now.

For the following I am assuming you already have a numbers table, If you don't have a numbers table, then I'd recommend you make one then, but if you don't want to then you can create a number list on the fly
You can get a list of all boundaries by cross joining your userID and registered dates with your numbers table:
SELECT u.ID,
DATE_ADD(RegisteredDate, INTERVAL n.Number MONTH) PeriodStart,
DATE_ADD(RegisteredDate, INTERVAL n.Number + 1 MONTH) PeriodEnd
FROM User u
CROSS JOIN Numbers n;
This gives a table like:
ID PERIODSTART PERIODEND
1 2012-12-16 2012-12-16
2 2013-01-29 2013-01-29
1 2013-01-16 2013-01-16
2 2013-02-28 2013-02-28
Example on SQL Fiddle
You then need to join this to your main table, and do the count:
SELECT u.ID,
u.PeriodStart,
DATE_ADD(PeriodEnd, INTERVAL -1 DAY) PeriodEnd,
COUNT(*) AS `COUNT`
FROM ( SELECT u.ID,
DATE_ADD(RegisteredDate, INTERVAL n.Number MONTH) PeriodStart,
DATE_ADD(RegisteredDate, INTERVAL n.Number + 1 MONTH) PeriodEnd
FROM User u
CROSS JOIN Numbers n
) u
INNER JOIN T
ON T.ID = u.ID
AND T.Date >= u.PeriodStart
AND T.Date < PeriodEnd
GROUP BY u.ID, u.PeriodStart, u.PeriodEnd;
Giving a final result of:
ID PERIODSTART PERIODEND COUNT
1 2012-12-16 2013-01-15 1
1 2013-01-16 2013-02-15 3
2 2013-02-28 2013-03-28 3
2 2013-03-29 2013-04-28 2
Full Example on SQL-Fiddle
You can obviously concatenate your period start and end dates to make a 'range' string, but this is probably best handled in your application layer.
EDIT
This can be achieved with no subqueries which is likely to perform better:
SELECT u.ID,
DATE_ADD(u.RegisteredDate, INTERVAL n.Number MONTH) PeriodStart,
DATE_ADD(DATE_ADD(u.RegisteredDate, INTERVAL n.Number + 1 MONTH), INTERVAL -1 DAY) PeriodEnd,
COUNT(*) AS `COUNT`
FROM User u
CROSS JOIN Numbers n
INNER JOIN T
ON T.ID = u.ID
AND T.Date >= DATE_ADD(u.RegisteredDate, INTERVAL n.Number MONTH)
AND T.Date < DATE_ADD(u.RegisteredDate, INTERVAL n.Number + 1 MONTH)
GROUP BY u.ID, u.RegisteredDate, n.Number;
Example with no subquery on SQL-Fiddle
EDIT 2
This will get you all periods for all users up until the current period (i.e. where today falls within the date range)
SELECT u.ID,
DATE_ADD(u.RegisteredDate, INTERVAL n.Number MONTH) PeriodStart,
DATE_ADD(DATE_ADD(u.RegisteredDate, INTERVAL n.Number + 1 MONTH), INTERVAL -1 DAY) PeriodEnd,
COUNT(T.ID) AS `COUNT`
FROM User u
CROSS JOIN Numbers n
LEFT JOIN T
ON T.ID = u.ID
AND T.Date >= DATE_ADD(u.RegisteredDate, INTERVAL n.Number MONTH)
AND T.Date < DATE_ADD(u.RegisteredDate, INTERVAL n.Number + 1 MONTH)
WHERE DATE_ADD(u.RegisteredDate, INTERVAL n.Number + 1 MONTH) <= CURRENT_TIMESTAMP
GROUP BY u.ID, u.RegisteredDate, n.Number;
Example on SQL Fiddle

Related

MySQL Union without Union

I would like to know how many users joined each day over the last 7 days. Something that looks like this
| day | count |
| 6/19 | 53 |
| 6/18 | 23 |
| 6/17 | 55 |
| 6/16 | 153 |
| 6/15 | 93 |
| 6/14 | 86 |
I would write a query like this:
SELECT SUBDATE(CURRENT_DATE(), INTERVAL 0 DAY) as `day`, count(*) as count FROM my_table WHERE DATE(created_at) = SUBDATE(CURRENT_DATE(), INTERVAL 0 DAY)
UNION ALL
SELECT SUBDATE(CURRENT_DATE(), INTERVAL 1 DAY) as `day`, count(*) as count FROM my_table WHERE DATE(created_at) = SUBDATE(CURRENT_DATE(), INTERVAL 1 DAY)
UNION ALL
SELECT SUBDATE(CURRENT_DATE(), INTERVAL 2 DAY) as `day`, count(*) as count FROM my_table WHERE DATE(created_at) = SUBDATE(CURRENT_DATE(), INTERVAL 2 DAY)
But imagine that I CANNOT use UNION ALL or UNION and it MUST be in this same table format. How would an SQL noob do this?
Thanks
The WHERE clause of your query should have the condition that created_at is greater than or equal to the current date minus 6 days and then group by date:
SELECT DATE(created_at) day, COUNT(*) count
FROM my_table
WHERE created_at >= SUBDATE(CURRENT_DATE, INTERVAL 6 DAY)
GROUP BY day

How to exclude weekends and holidays dates and find an expected date in MySQL?

I am working on a task where I have 2 tables. i.e, tickets and holidays.
Now I also have the number of days to complete the tickets. Now I need to find the expected date by excluding holidays(specified in the holidays table) and weekends.
Now I can able to find a date using ticket created date and days to complete the ticket. But unable to calculate the expected due date by removing holidays and weekends.
If ticket expected due date falls under holidays or weekends, we need to shift the expected due date ahead.
And after that we need to compare ticket_closed_date and expected_due_date.
If ticket_closed_date <= expected_due_date then need to return isSlaMet as YES. Else need to return isSlaMet as NO.
Tickets Table
Holidays Table
Example: Generally if a ticket is created on 2nd October,2020 and its days to complete is 3, then the expected due date would be, 5th October and we have a holiday, 5th October. But there are 1 holiday and 2 weekends in between ticket created date and expected due date. i.e, 3rd, 4th and 5th of October . So we need to extend the expected due date by 3 days(because 2 weekends + 1 Holiday). i.e, 8th October. Ticket is closed on 9th October.
Then we need to compare ticket closed date(9th october) and expected due date(8th October) and return isSlaMet as YES.
Expected Input
Tickets Table
--------------------------------------------------------------------------------
tid createdAt apply_sla ticket_closed_date days_to_complete
--------------------------------------------------------------------------------
100 2020-10-02 00:00:00 1 2020-10-09 00:00:00 3
--------------------------------------------------------------------------------
Holidays Table
----------------------------------------------
id holiday_date end_date
----------------------------------------------
20 2020-10-05 2020-10-05
----------------------------------------------
Along with the above holiday, we need to exclude Weekends.
Expected Output
Tickets Table
--------------------------------------------------------------------------------------------------------------------------
tid createdAt apply_sla ticket_closed_date days_to_complete expected_due_date completedIn isSlaMet
--------------------------------------------------------------------------------------------------------------------------
100 2020-10-02 00:00:00 1 2020-10-09 00:00:00 3 2020-10-08 00:00:00 4 NO
--------------------------------------------------------------------------------------------------------------------------
This is the query I have been using so far.
SELECT
`t`.`tid`, `t`.`createdAt`, `t`.`days_to_complete`,
`t`.`ticket_closed_date`,`holidays`.`holiday_date`,
`holidays`.`end_date`, `t.apply_sla`,
IF(ISNULL(`t`.`ticket_closed_date`),
NULL,
IF((`t`.`apply_sla` = 1),
IF(((CAST(`t`.`createdAt` AS DATE) + INTERVAL (`t`.`days_to_complete` + 1) DAY) BETWEEN `holidays`.`holiday_date` AND `holidays`.`end_date`),
IF((CAST(`t`.`ticket_closed_date` AS DATE) <= (`holidays`.`end_date` + INTERVAL `t`.`days_to_complete` DAY)),
'YES',
'NO'),
IF((CAST(`t`.`ticket_closed_date` AS DATE) <= (`t`.`createdAt` + INTERVAL (`t`.`days_to_complete` + 1) DAY)),
'YES',
'NO')),
IF(((TO_DAYS(`t`.`ticket_closed_date`) - TO_DAYS(`t`.`createdAt`)) > (`t`.`days_to_complete` + 1)),
'NO',
'YES')
)
) AS `isSlaMet`
FROM
(`tickets` `t`
LEFT JOIN `holidays` ON (((CAST(`t`.`createdAt` AS DATE) + INTERVAL (`t`.`days_to_complete` + 1) DAY) BETWEEN `holidays`.`holiday_date` AND `holidays`.`end_date`)))
ORDER BY `t`.`tid` DESC;
This is more complicated that it may seem. The simplest approach may be brute force: enumerate all days between the creation and closure date of the ticket with a recursive CTE (available in MySQL 8.0 only), then filter out the weekends and holidays to count the number of SLA days:
with recursive cte_tickets as (
select tid, created_at as dt, ticket_closed_date
from tickets
where apply_sla = 1
union all
select tid, dt + interval 1 day, ticket_closed_date
from cte_tickets
where dt < ticket_closed_date
)
select t.*,
t.created_at
+ interval (t.days_to_complete + sum(weekday(dt) in (5, 6) or h.holiday_date is not null)) day
as expected_due_date,
count(*) - sum(weekday(dt) in (5, 6) or h.holiday_date is not null) - 1 completed_in,
t.ticket_closed_date <= t.created_at
+ interval (t.days_to_complete + sum(weekday(dt) in (5, 6) or h.holiday_date is not null)) day
as is_sla_met
from tickets t
inner join cte_tickets ct on ct.tid = t.tid
left join holidays h on ct.dt between h.holiday_date and h.end_date
group by t.tid
Demo on DB Fiddle:
tid | created_At | apply_sla | ticket_closed_date | days_to_complete | expected_due_date | completed_in | is_sla_met
--: | :------------------ | :-------- | :------------------ | ---------------: | :------------------ | -----------: | ---------:
100 | 2020-10-02 00:00:00 | 1 | 2020-10-09 00:00:00 | 3 | 2020-10-08 00:00:00 | 4 | 0

Find all fridays with date in the year 2017

I want a MySQL query that will fetch all Fridays with date for the year 2017.
I know that SQL query for the same is:
SELECT Fridays = DATEADD(yy, DATEDIFF(yy, 0, GETDATE()), n.num)
FROM (SELECT TOP 366 num = ROW_NUMBER() OVER(ORDER BY a.NAME)-1 FROM dbo.syscolumns a, dbo.syscolumns b) n
WHERE DATENAME(weekday, DATEADD(yy, DATEDIFF(yy, 0, GETDATE()), n.num)) = 'Friday'
I am looking for a MySQL alternative for the same.
This is an exact conversion of your sql logic in mysql
SELECT DATE_ADD(MAKEDATE(year(now()),1), INTERVAL #num:=#num+1 DAY)
From (select #num:=-1) num
where DAYNAME(DATE_ADD(MAKEDATE(year(now()),1), INTERVAL #num:=#num+1 DAY))="Friday"
limit 365
Mysql does not have row_number function until version 8. Prior to version 8 row_number can be simulated using variables.
select s.num , date_add(str_to_date(concat('2016','/','12','/','31'),'%Y/%m/%d'), interval s.num day) dt,
dayname(date_add(str_to_date(concat('2016','/','12','/','31'),'%Y/%m/%d'), interval s.num day)) dy
from
(
select #num:=#num + 1 num from information_schema.columns,(select #num:=0) n
limit 35
) s
where dayname(date_add(str_to_date(concat('2016','/','12','/','31'),'%Y/%m/%d'), interval s.num day)) = 'Friday';
information_schema.columns is similar to dbo.syscolumns.
------+------------+--------+
| num | dt | dy |
+------+------------+--------+
| 6 | 2017-01-06 | Friday |
| 13 | 2017-01-13 | Friday |
| 20 | 2017-01-20 | Friday |
| 27 | 2017-01-27 | Friday |
| 34 | 2017-02-03 | Friday |
+------+------------+--------+
5 rows in set, 2 warnings (0.26 sec)
SELECT * FROM table WHERE ([date] BETWEEN date_start and date_end) AND
DAYNAME([date])='Friday'
Source of the query: Get data for every friday between two dates

SQL select all data that past 45 days

I have "alerts" table with date field - targetDate.
I would like to select all data that past 45 days.
I tried the code below but it's not return any results...
SELECT userID, refID, `targetDate`
FROM alerts
WHERE type = 'travelSoon'
AND DATEDIFF( CURDATE( ) , targetDate ) > 45
Table
id userID type refID createDate targetDate lastSendDate sent valid
1 26 travelSoon NO 2018-05-02 13:54:25 0000-00-00 2018-05-02 00:00:00 0 1
2 26 travelSoon NO 2018-05-02 13:55:50 2018-06-01 0000-00-00 00:00:00 0 1
3 26 travelSoon DK 2018-05-02 13:56:12 2018-12-01 0000-00-00 00:00:00 0 1
4 26 travelSoon 2018-05-02 13:59:50 0000-00-00 0000-00-00 00:00:00 0 1
5 26 travelSoon 2018-05-02 14:00:09 2018-08-01 0000-00-00 00:00:00 0 1
6 26 travelSoon DK 2018-05-02 14:00:48 2018-08-01 0000-00-00 00:00:00 0 1
7 26 travelSoon 2018-05-02 16:45:18 2018-05-01 0000-00-00 00:00:00 0 1
8 26 travelSoon RO 2018-05-02 16:45:45 2018-04-01 0000-00-00 00:00:00 0 1
Using DATEDIFF() is a bad idea. It blocks the ability to use indexes, and there is an alternative that doesn't...
SELECT *
FROM alerts
WHERE type = 'travelSoon'
AND targetDate >= DATEADD(DAY, -45, GETDATE()) -- SQL Server
AND targetDate >= CURDATE() - INTERVAL 45 DAY -- MySQL
http://www.sqlfiddle.com/#!9/4ecdc0/6
In MSSQL DATEDIFF(interval, date1, date2) returns interval of date2 - date1.
Interval should be selected from this list:
- year, yyyy, yy = Year
- quarter, qq, q = Quarter
- month, mm, m = month
- dayofyear = Day of the year
- day, dy, y = Day
- week, ww, wk = Week
- weekday, dw, w = Weekday
- hour, hh = hour
- minute, mi, n = Minute
- second, ss, s = Second
- millisecond, ms = Millisecond`
Then use:
SELECT userID, refID, `targetDate`
FROM alerts
WHERE type = 'travelSoon'
AND DATEDIFF(day, targetDate, GETDATE() ) > 45
For MySQL you can use TIMESTAMPDIFF(unit,date1,date2) which returns interval of date1 - date2.
unit can be selected from MICROSECOND (microseconds), SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, or YEAR.
SELECT userID, refID, `targetDate`
FROM alerts
WHERE type = 'travelSoon'
AND TIMESTAMPDIFF(DAY, CURDATE( ), targetDate) > 45
The ANSI Standard syntax would be:
SELECT userID, refID, `targetDate`
FROM alerts
WHERE type = 'travelSoon' AND
targetDate >= CURRENT_DATE - interval '45 day' AND
targetDate <= CURRENT_DATE;
In MySQL (which your syntax suggests:
SELECT userID, refID, `targetDate`
FROM alerts
WHERE type = 'travelSoon' AND
targetDate >= CURRENT_DATE - interval 45 day AND
targetDate <= CURRENT_DATE;
Try this...
SELECT userid, refid, `targetdate`
FROM alerts
WHERE type = 'travelSoon'
AND Datediff(Curdate(), targetDate) < 45 -- or <=45
AND Datediff(Curdate(), targetDate) > 0
Online Demo: http://www.sqlfiddle.com/#!9/4ecdc0/4/0
If you only use Datediff(Curdate(), targetDate) < 45 condition, it may return both past and future dates. Please refer the below table.
Today: May 10, 2018
+----+-------------+
| id | targetDate | DATEDIFF(CURDATE(), targetDate)
+----+-------------+
| 2 | 𝟮𝟬𝟭𝟴-𝟬𝟲-𝟬𝟭 | -22
| 3 | 𝟮𝟬𝟭𝟴-𝟭𝟮-𝟬𝟭 | -205
| 5 | 𝟮𝟬𝟭𝟴-𝟬𝟴-𝟬𝟭 | -83
| 6 | 𝟮𝟬𝟭𝟴-𝟬𝟴-𝟬𝟭 | -83
| 7 | 2018-05-01 | 9
| 8 | 2018-04-01 | 39
+----+-------------+
To avoid this, you can use another condition like this...
Datediff(Curdate(), targetDate) > 0
SELECT userID, refID, `targetDate`
FROM alerts
WHERE type = 'travelSoon'
AND targetDate >= ( CURDATE() - INTERVAL 45 DAY )

MySQL - make a weekly report that includes starting day (twist: week starting wednesday)

I have this kind of table with time based data:
| entity_id | ttime | value |
-------------------------------------------
| 1 | 2014-11-01 00:00:04 | 553 |
| 1 | ... | 600 |
| 2 | ... | 234 |
I want to get the average of the value grouped by week and entity_id. But I would like also the starting day of the week to appear in the results. Additional complexity is that the week starts on wednesday.
I can group by YEAR(ttime + INTERVAL 3 DAY), WEEK(ttime + INTERVAL 3 DAY) but is it possible to print the first day of the group (wednesday) in the results?
Thanks
maybe something like this:
SELECT
`entity_id`,
DATE_SUB(ttime, INTERVAL WEEKDAY(ttime)-2 DAY),
SUM(`value`)
FROM `table`
GROUP BY `entity_id`, YEARWEEK(ttime + INTERVAL 4 DAY)
SqlFiddle
I found this solution:
SELECT
str_to_date(CONCAT(YEAR(ttime + INTERVAL -3 DAY),
WEEK(ttime + INTERVAL -3 DAY), 'Wednesday'), '%X%V %W') as WeekCommencing,
entity_id, AVG(value),
FROM `table`
GROUP BY WeekCommencing, entity_id