Mysql : Finding empty time blocks between two dates and times? - mysql

I wanted to find out user's availability from database table:
primary id | UserId | startdate | enddate
1 | 42 | 2014-05-18 09:00 | 2014-05-18 10:00
2 | 42 | 2014-05-18 11:00 | 2014-05-18 12:00
3 | 42 | 2014-05-18 14:00 | 2014-05-18 16:00
4 | 42 | 2014-05-18 18:00 | 2014-05-18 19:00
Let's consider above inserted data is user's busy time, I want to find out free time gap blocks from table between start time and end time.
BETWEEN 2014-05-18 11:00 AND 2014-05-18 19:00;
Let me add here schema of table for avoiding confusion:
Create Table availability (
pid int not null,
userId int not null,
StartDate datetime,
EndDate datetime
);
Insert Into availability values
(1, 42, '2013-10-18 09:00', '2013-10-18 10:00'),
(2, 42, '2013-10-18 11:00', '2013-10-18 12:00'),
(3, 42, '2013-10-18 14:00', '2013-11-18 16:00'),
(4, 42, '2013-10-18 18:00', '2013-11-18 19:00');
REQUIREMENT:
I wanted to find out free gap records like:
'2013-10-27 10:00' to '2013-10-28 11:00' - User is available for 1 hours and
'2013-10-27 12:00' to '2013-10-28 14:00' - User is available for 2 hours and
available start time is '2013-10-27 10:00' and '2013-10-27 12:00' respectively.

Here you go
SELECT t1.userId,
t1.enddate, MIN(t2.startdate),
MIN(TIMESTAMPDIFF(HOUR, t1.enddate, t2.startdate))
FROM user t1
JOIN user t2 ON t1.UserId=t2.UserId
AND t2.startdate > t1.enddate AND t2.pid > t1.pid
WHERE
t1.endDate >= '2013-10-18 09:00'
AND t2.startDate <= '2013-11-18 19:00'
GROUP BY t1.UserId, t1.endDate
http://sqlfiddle.com/#!2/50d693/1

Using your data, the easiest way is to list the hours when someone is free. The following gets a list of hours when someone is available:
select (StartTime + interval n.n hour) as FreeHour
from (select cast('2014-05-18 11:00' as datetime) as StartTime,
cast('2014-05-18 19:00' as datetime) as EndTime
) var join
(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
) n
on StartTime + interval n.n hour <= EndTime
where not exists (select 1
from availability a
where StartTime + interval n.n hour < a.EndDate and
StartTime + interval n.n hour >= a.StartDate
);
EDIT:
The general solution to your problem requires denormalizing the data. The basic query is:
select thedate, sum(isstart) as isstart, #cum := #cum + sum(isstart) as cum
from ((select StartDate as thedate, 1 as isstart
from availability a
where userid = 42
) union all
(select EndDate as thedate, -1 as isstart
from availability a
where userid = 42
) union all
(select cast('2014-05-18 11:00' as datetime), 0 as isstart
) union all
(select cast('2014-05-18 19:00' as datetime), 0 as isstart
)
) t
group by thedate cross join
(select #cum := 0) var
order by thedate
You then just choose the values where cum = 0. The challenge is getting the next date from this list. In MySQL that is a real pain, because you cannot use a CTE or view or window function, so you have to repeat the query. This is why I think the first approach is probably better for your situation.

The core query can be this. You can dress it up as you like, but I'd handle all that stuff in the presentation layer...
SELECT a.enddate 'Available From'
, MIN(b.startdate) 'To'
FROM user a
JOIN user b
ON b.userid = a.user
AND b.startdate > a.enddate
GROUP
BY a.enddate
HAVING a.enddate < MIN(b.startdate)
For times outside the 'booked' range, you have to extend this a little with a UNION, or again handle the logic at the application level

Related

Find gaps in SQL between datetime Ranges within a startdatetime and enddatetime value

im looking for a SQL Query that can deliver me all of the free time Intervals between a given Range for a Table with two datetime Columns (DATE_FROM, DATE_TILL). As a Requirement: All other entries are not overlapped and the only acceptable distance between each interval is 1 Second.
I have found a Solution but this doesnt fill all my Requirements, specially the one where i want to put a given start and end datetime to calculate the missing Intervals if given.
Here is my datatable:
ROW_ID LOCATION_ID DATE_FROM DATE_TILL
1 193 2019-02-01 00:00:00 2019-12-31 23:59:59
2 193 2020-02-01 00:00:00 2020-12-31 23:59:59
3 193 2021-01-01 00:00:00 2021-12-31 23:59:59
4 193 2022-01-01 00:00:00 2022-12-31 23:59:59
5 204 2020-01-01 00:00:00 2021-12-31 23:59:59
And this is my SQL Query, which is from another Solution in this Plattform where i made some requirements changes.
SELECT DATE_ADD(DATE_TILL,INTERVAL 1 SECOND) AS GAP_FROM, DATE_SUB(DATE_FROM,INTERVAL 1 SECOND) AS GAP_TILL
FROM
(
SELECT DISTINCT DATE_FROM, ROW_NUMBER() OVER (ORDER BY DATE_FROM) RN
FROM overlappingtable T1
WHERE
LOCATION_ID = 193 AND
NOT EXISTS (
SELECT *
FROM overlappingtable T2
WHERE T1.DATE_FROM > T2.DATE_FROM AND T1.DATE_FROM < T2.DATE_TILL
)
) T1
JOIN (
SELECT DISTINCT DATE_TILL, ROW_NUMBER() OVER (ORDER BY DATE_TILL) RN
FROM overlappingtable T1
WHERE
LOCATION_ID = 193 AND
NOT EXISTS (
SELECT *
FROM overlappingtable T2
WHERE T1.DATE_TILL > T2.DATE_FROM AND T1.DATE_TILL < T2.DATE_TILL
)
) T2
ON T1.RN - 1 = T2.RN
WHERE
DATE_ADD(DATE_TILL,INTERVAL 1 SECOND) < DATE_FROM
This Query delivers me this result:
GAP_FROM GAP_TILL
2020-01-01 00:00:00 2020-01-31 23:59:59
Which is great, this is the free Interval that i have to deliver between entries that have their ranges and dont overlap.
But I want to set in this Query two Parameters for The Main Range for this entries. One for the startdate and the other for enddate. For this example:
startdate = '2019-01-01 00:00:00'
enddate = '9999-12-31 23:59:59'
For LOCATION_ID = 193 i am missing the gap between the startdate('2019-01-01 00:00:00') and the first DATE_FROM for the first entry('2019-02-01 00:00:00').
The result that i would like to deliver should look like this for LOCATION_ID = 193:
GAP_FROM GAP_TILL
2019-01-01 00:00:00 2019-01-31 23:59:59
2020-01-01 00:00:00 2020-01-31 23:59:59
2023-01-01 00:00:00 9999-12-31 23:59:59
Im really new at SQL and could understand this Query, but i can't develop this further to set these Main Ranges and deliver the missing gaps.
Thanks in Advance
For clarity I would recommend to find the initial gaps, the middle ones, and the ending ones in separate CTEs, as shown below in the b, m, and e CTEs. Then, a simple UNION ALL can combine all of them:
with
p (loc_id, start_date, end_date) as (
select 193, '2019-01-01 00:00:00', '9999-12-31 23:59:59'
),
r as (
select location_id, date_from,
date_add(date_till, interval 1 second) as date_till,
lead(date_from) over(partition by location_id order by date_from) as next_from
from overlappingtable t
cross join p
where t.location_id = p.loc_id
),
b as (
select p.start_date as gap_from, r.date_from as gap_till
from (select * from r order by date_from limit 1) r
cross join p
where p.start_date < r.date_from
),
m as (
select date_till, next_from
from r
where date_till < next_from
),
e as (
select r.date_till, p.end_date
from (select * from r order by date_till desc limit 1) r
cross join p
where r.date_till < p.end_date
)
select * from b
union all select * from m
union all select * from e
order by gap_from
Result:
gap_from gap_till
-------------------- -------------------
2019-01-01 00:00:00 2019-02-01 00:00:00
2020-01-01 00:00:00 2020-02-01 00:00:00
2023-01-01 00:00:00 9999-12-31 23:59:59
See running example atDB Fiddle.
The initial CTE p includes the parameters of the query (loc_id, start_date, end_date) and is added for clarity.
You could join to a sub-query with the start & end datetimes.
Then compare to the previous & next datetimes per location_id.
The previous or next datetimes can be found via the LAG & LEAD functions.
WITH CTE_UNDERLAPS AS
(
SELECT t.*
, LAG(DATE_TILL) OVER (PARTITION BY LOCATION_ID ORDER BY DATE_FROM, DATE_TILL) AS PREV_DATE_TILL
, LEAD(DATE_FROM) OVER (PARTITION BY LOCATION_ID ORDER BY DATE_FROM, DATE_TILL) AS NEXT_DATE_FROM
, l.*
FROM overlappingtable t
JOIN (
SELECT
CAST('2019-01-01 00:00:00' AS DATETIME) AS START_DATETIME
, CAST('9999-12-31 23:59:59' AS DATETIME) AS END_DATETIME
) l ON DATE_FROM >= START_DATETIME AND DATE_TILL <= END_DATETIME
)
SELECT LOCATION_ID
, COALESCE(DATE_ADD(PREV_DATE_TILL,INTERVAL 1 SECOND), START_DATETIME) AS DATE_FROM
, DATE_SUB(DATE_FROM,INTERVAL 1 SECOND) AS DATE_TILL
FROM CTE_UNDERLAPS
WHERE COALESCE(DATE_ADD(PREV_DATE_TILL,INTERVAL 1 SECOND), START_DATETIME) < DATE_FROM
UNION
SELECT LOCATION_ID
, DATE_ADD(DATE_TILL,INTERVAL 1 SECOND) AS DATE_FROM
, COALESCE(DATE_SUB(NEXT_DATE_FROM,INTERVAL 1 SECOND), END_DATETIME) AS DATE_TILL
FROM CTE_UNDERLAPS
WHERE DATE_ADD(DATE_TILL,INTERVAL 1 SECOND) < COALESCE(NEXT_DATE_FROM, END_DATETIME)
ORDER BY LOCATION_ID, DATE_FROM, DATE_TILL
LOCATION_ID
DATE_FROM
DATE_TILL
193
2019-01-01 00:00:00
2019-01-31 23:59:59
193
2020-01-01 00:00:00
2020-01-31 23:59:59
193
2023-01-01 00:00:00
9999-12-31 23:59:59
204
2019-01-01 00:00:00
2019-12-31 23:59:59
204
2022-01-01 00:00:00
9999-12-31 23:59:59
Demo on db<>fiddle here

How do I create multiple views in the same query in SQL and then join them?

This is the query that I am using.
I need to join the three views to calculate the monthly total revenue.
How should I proceed?
With Txn as(
Select DATE_FORMAT(DATE_ADD(createdAt, interval 330 MINUTE), '%y-%m') as Month, Sum(netPrice/100) as TransactionRevenue from transactions
group by Month)
With Leaves as(
Select DATE_FORMAT(DATE_ADD(createdAt, interval -1 MONTH), '%y-%m') as Month, sum(amount/100) as LeaveRevenue from driverPaymentTransactions
group by Month)
With Sxn as(
Select DATE_FORMAT(DATE_ADD(createdAt, interval 330 MINUTE ), '%y-%m') as Month, sum(amount/100) as SubscribedRevenue from subscribedDriversDailyRevenues
group by MONTH)
Select * from Txn t
join Leaves l on t.Month = l.month
join Sxn s on t.month = s.month
With Txn as(
Select DATE_FORMAT(DATE_ADD(createdAt, interval 330 MINUTE), '%y-%m') as Month, Sum(netPrice/100) as TransactionRevenue from transactions
group by Month),
Leaves as(
Select DATE_FORMAT(DATE_ADD(createdAt, interval -1 MONTH), '%y-%m') as Month, sum(amount/100) as LeaveRevenue from driverPaymentTransactions
group by Month),
Sxn as(
Select DATE_FORMAT(DATE_ADD(createdAt, interval 330 MINUTE ), '%y-%m') as Month, sum(amount/100) as SubscribedRevenue from subscribedDriversDailyRevenues
group by MONTH)
Select * from Txn t
join Leaves l on t.Month = l.month
join Sxn s on t.month = s.month
You need to "join" the subqueries
CREATE VIEw myview
AS (With Txn as(
Select DATE_FORMAT(DATE_ADD(createdAt, interval 330 MINUTE), '%y-%m') as Month, Sum(netPrice/100) as TransactionRevenue from transactions
group by Month)
, Leaves as(
Select DATE_FORMAT(DATE_ADD(createdAt, interval -1 MONTH), '%y-%m') as Month, sum(amount/100) as LeaveRevenue from driverPaymentTransactions
group by Month)
, Sxn as(
Select DATE_FORMAT(DATE_ADD(createdAt, interval 330 MINUTE ), '%y-%m') as Month, sum(amount/100) as SubscribedRevenue from subscribedDriversDailyRevenues
group by MONTH)
Select * from Txn t
join Leaves l on t.Month = l.month
join Sxn s on t.month = s.month)
Do not join when you need to "pull" some different measures to the common attribute. Use union all, where you do not need to care about the most complete source of group values:
create table t1 as
select 1 as id, 10 as val union all
select 1, 20 union all
select 2, 30 union all
select 3, 49
create table t2 as
select 1 as id, 10 as val union all
select 3, 20 union all
select 3, 30 union all
select 5, 49
create table t3 as
select 4 as id, 10 as val union all
select 6, 20 union all
select 2, 30 union all
select 3, 49
with u as (
select
id
, val as t1_val
, cast(null as decimal) as t2_val
, cast(null as decimal) as t3_val
from t1
union all
select
id
, null as t1_val
, val as t2_val
, null as t3_val
from t2
union all
select
id
, null as t1_val
, null as t2_val
, val as t3_val
from t3
)
select
id
, sum(t1_val) as t1_val
, sum(t2_val) as t2_val
, sum(t3_val) as t3_val
from u
group by id
id | t1_val | t2_val | t3_val
-: | -----: | -----: | -----:
1 | 30 | 10 | null
2 | 30 | null | 30
3 | 49 | 50 | 49
5 | null | 49 | null
4 | null | null | 10
6 | null | null | 20
db<>fiddle here

Mysql Select Query to find items also partially free between two given dates

I am in need of listing free items between two given dates.
I have in table A items and in table B their occupancies.
When I search for the free items between two dates, I need to list also items partially free.
Tabel A:
| ID | ITEM_NAME |
| -------- | -------------- |
| 1 | Item1 |
| 2 | Item2 |
| 3 | Item3 |
Table B:
|id_item |occupancy_start_date |occupancy_end_date|
|---------------------------------------------------------|
|1 |2021-07-24 |2021-08-06 |
|2 |2021-07-24 |2021-07-31 |
|3 |2021-07-29 |2021-08-03 |
While I search for free items between 2021-07-24 and 2021-08-06, I must get Item2 and Item3.
Item2 is free from 2021-08-01 till 2021-08-06
Item3 is free from 2021-07-24 till 2021-07-29
Item3 is free from 2021-08-04 till 2021-08-06
(Practically I must find free slots of dates between two given dates by the user)
Can you guys help me? Thank you.
Here's another starting point (using the fiddle generously provided by others - pushed up to a more recent version) ... I'll post a more complete answer next week, if no one's beaten me to it...
WITH RECURSIVE cte AS (
SELECT '2021-07-24' dt
UNION ALL
SELECT dt + INTERVAL 1 DAY
FROM cte
WHERE dt < '2021-08-06')
SELECT x.dt
, a.*
FROM cte x
JOIN TableA a
LEFT
JOIN TableB b
ON b.id_item = a.id
AND x.dt BETWEEN b.occupancy_start_date AND b.occupancy_end_date
WHERE b.id_item IS NULL
Without window function :
There is a way to extract by dividing into three parts: "From start date", "Till end date" and "Others", and filter the rows by each query, like this:
SET #sdate = DATE'2021-07-24';
SET #edate = DATE'2021-08-06';
-- From start date
SELECT
b.id_item id_item,
#sdate from_date,
DATE_ADD(MIN(b.occupancy_start_date), INTERVAL -1 DAY) to_date
FROM TableB b
GROUP BY b.id_item
HAVING from_date <= to_date
-- Till end date
UNION ALL
SELECT
b.id_item id_item,
DATE_ADD(MAX(b.occupancy_end_date), INTERVAL 1 DAY) from_date,
#edate to_date
FROM TableB b
GROUP BY b.id_item
HAVING from_date <= to_date
-- Others
UNION ALL
SELECT
b1.id_item,
DATE_ADD(b1.occupancy_end_date, INTERVAL 1 DAY),
DATE_ADD(b2.occupancy_start_date, INTERVAL -1 DAY)
FROM TableB b1 INNER JOIN TableB b2
ON b1.id_item=b2.id_item AND
b1.occupancy_end_date < b2.occupancy_start_date AND
-- Exclude inappropriate rows
NOT EXISTS (SELECT 1 FROM TableB b WHERE
b1.id_item = b.id_item AND (
( b1.occupancy_end_date < b.occupancy_start_date AND
b.occupancy_start_date < b2.occupancy_start_date) OR
( b1.occupancy_end_date < b.occupancy_end_date AND
b.occupancy_end_date < b2.occupancy_start_date) ) )
ORDER BY 1,2
;
DB Fiddle
With window function :
If MySQL8, you can use the LAG (or LEAD) function, like this:
SET #sdate = DATE'2021-07-24';
SET #edate = DATE'2021-08-06';
SELECT
a.ITEM_NAME,
w.id_item,
w.from_date,
w.to_date
FROM (
-- Non-free period(s) exist
SELECT * FROM (
SELECT
id_item,
DATE_ADD(LAG(occupancy_end_date, 1)
OVER (PARTITION BY id_item ORDER BY occupancy_end_date),
INTERVAL 1 DAY) from_date,
DATE_ADD(occupancy_start_date, INTERVAL -1 DAY) to_date
FROM TableB
WHERE #sdate < occupancy_end_date AND occupancy_start_date < #edate
) t
WHERE from_date <= to_date
-- From start date
UNION ALL
SELECT
id_item,
#sdate from_date,
DATE_ADD(MIN(occupancy_start_date), INTERVAL -1 DAY) to_date
FROM TableB
GROUP BY id_item
HAVING from_date <= to_date
-- Till end date
UNION ALL
SELECT
id_item,
DATE_ADD(MAX(occupancy_end_date), INTERVAL 1 DAY) from_date,
#edate to_date
FROM TableB
GROUP BY id_item
HAVING from_date <= to_date
-- No occupancy
UNION ALL
SELECT
ID,
#sdate from_date,
#edate to_date
FROM TableA a
WHERE NOT EXISTS (SELECT 1 FROM TableB b WHERE a.ID=b.id_item)
) w
INNER JOIN TableA a ON w.id_item=a.ID
ORDER BY
w.id_item,
w.from_date
;
DB Fiddle

How to find gaps in Date of the same group of data - mySQL

I have a list of dates that have gaps for an execution of a promotion.
id promotion_name period value
8495115 Cash_Discount1 2016-11-01 10.00
8495116 Cash_Discount1 2016-12-01 20.00
8491724 Cash_Discount1 2017-01-01 10.00
8479109 Cash_Discount1 2017-02-01 20.00
8459125 Cash_Discount1 2017-03-01 40.00
8491649 Cash_Discount1 2017-06-01 30.00
8491648 Cash_Discount1 2017-07-01 50.00
8491647 Cash_Discount1 2017-08-01 70.00
8491646 Cash_Discount1 2017-09-01 80.00
The period in the above table denotes the start date of the execution of the promotion and it runs for a month.
Hence, the first row means the cash discount runs from 1-11-2016 till
30-11-2016.
I need the same data in the below mentioned format.
promotion_name start_date end_date value
Cash_Discount1 2016-11-01 2017-03-31 100.00
Cash_Discount1 2017-06-01 2017-09-30 230.00
Whenever there is a gap, that has to appear as a separate row. can somebody please help me with this as any number of self joins is not giving me the result.
i have tried using this for starters but, i am nowhere close to the result.
SELECT p.id
, p.promotion_name
, p.period AS start_date
, q.period AS end_date
, p.value AS spend
FROM table p
LEFT
JOIN table q
ON p.id = q.id
AND p.promotion_name = q.promotion_name
AND p.period = DATE_SUB(q.period,INTERVAL 1 MONTH)
I am in a situation where i do not know what to search for.
Any Help is appreciated.
select
ts.promotion_name,
min(t.period) as date_start,
max(t.period) as date_end,
sum(t.value) as spend
from mytable ts
left join mytable t1
on t1.promotion_name = ts.promotion_name
and t1.period = ts.period - interval 1 month
join mytable t
on t.promotion_name = ts.promotion_name
and t.period >= ts.period
and t.period <= (
select min(t2.period)
from mytable t2
left join mytable t3
on t3.promotion_name = t2.promotion_name
and t3.period = t2.period + interval 1 month
where t2.promotion_name = ts.promotion_name
and t2.period >= ts.period
and t3.period is null
)
where ts.promotion_name = 'Cash_Discount1'
and t1.period is null
group by ts.id
order by date_start
Demo: https://www.db-fiddle.com/f/7Xx5bSQmRUGzUCLyB4rKBf/1
The idea of that query is first to find the first row of a consecutive group with
select ts.*
from mytable ts
left join mytable t1
on t1.promotion_name = ts.promotion_name
and t1.period = ts.period - interval 1 month
where ts.promotion_name = 'Cash_Discount1'
and t1.period is null
Demo: https://www.db-fiddle.com/f/fKaNQPkgZqbTfY3Je6Kz4/0
and get the last date of the group with a subquery
select min(t2.period)
from mytable t2
left join mytable t3
on t3.promotion_name = t2.promotion_name
and t3.period = t2.period + interval 1 month
where t2.promotion_name = ts.promotion_name
and t2.period >= ts.period
and t3.period is null
Demo: https://www.db-fiddle.com/f/hSNZwjDuL1nf2NzPoms5JG/0
Then you need another join with a range condition
and t.period >= ts.period -- start_date
and t.period <= (<subquery>) -- end_date
and use aggregation over the matching rows.

selecting between date range and forcing empty rows to be 0?

I have a table that looks like
expires | value
-------------------
2011-06-15 | 15
2011-06-15 | 15
2011-06-25 | 15
2011-07-15 | 15
2011-07-15 | 15
2011-07-25 | 15
2011-08-15 | 15
2011-08-15 | 15
2011-08-25 | 15
I want to run a query that will spit out
June | 45
July | 45
August | 45
So my query is
SELECT SUM(amount) AS `amount`,
DATE_FORMAT(expires , '%M') AS `month`
FROM dealDollars
WHERE DATE(expires) BETWEEN DATE(NOW())
AND LAST_DAY(DATE(NOW()+INTERVAL 3 MONTH))
GROUP BY MONTH(expires)
Which works fine. But with the result, if there were no rows in say July, July would not show up.
How can I force July to show up with 0 as its value?
There is no easy way to do this. One possible way is to have a table called months:
Which will have 12 rows: (January, February, ..., December)
You can left join the Months table with the query you have to get the desired output.
The general consensus is that you should just create a table of month names. What follows is a silly solution which can serve as a workaround.
You'll have to work on the specifics yourself, but have you looked at sub-queries in the from clause?
Basically, it would be something like this:
SELECT NVL(B.amount, 0) as `amount`, A.month as `month`
FROM (SELECT 'January' as `month`
UNION SELECT 'February' as `month`
UNION SELECT 'March' as `month`...
UNION SELECT 'DECEMBER' as `month`) as A
LEFT JOIN
(SELECT SUM(amount) AS `amount`,
DATE_FORMAT(expires , '%M') AS `month`
FROM dealDollars
WHERE
DATE(expires) BETWEEN
DATE(NOW()) AND
LAST_DAY(DATE(NOW()+INTERVAL 3 MONTH))
GROUP BY MONTH(expires)) as B
ON (A.MONTH = B.MONTH)
Crazy, no?
MySQL doesn't have recursive functionality, so you're left with using the NUMBERS table trick -
Create a table that only holds incrementing numbers - easy to do using an auto_increment:
DROP TABLE IF EXISTS `example`.`numbers`;
CREATE TABLE `example`.`numbers` (
`id` int(10) unsigned NOT NULL auto_increment,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Populate the table using:
INSERT INTO NUMBERS
(id)
VALUES
(NULL)
...for as many values as you need. In this case, the INSERT statement needs to be run at least 3 times.
Use DATE_ADD to construct a list of days, increasing based on the NUMBERS.id value:
SELECT x.dt
FROM (SELECT DATE(DATE_SUB(CURRENT_DATE(), INTERVAL (n.id - 1) MONTH)) AS dt
FROM numbers n
WHERE DATE(DATE_SUB(CURRENT_DATE(), INTERVAL (n.id - 1) MONTH)) BETWEEN CURRENT_DATE()
AND LAST_DAY(CURRENT_DATE() +INTERVAL 3 MONTH)) ) x
Use an OUTER JOIN to get your desired output:
SELECT x.dt,
COUNT(*) AS total
FROM (SELECT DATE(DATE_SUB(CURRENT_DATE(), INTERVAL (n.id - 1) MONTH)) AS dt
FROM numbers n
WHERE DATE(DATE_SUB(CURRENT_DATE(), INTERVAL (n.id - 1) MONTH)) BETWEEN CURRENT_DATE()
AND LAST_DAY(CURRENT_DATE() +INTERVAL 3 MONTH)) ) x
LEFT JOIN YOUR_TABLE y ON y.date = x.dt
GROUP BY x.dt
ORDER BY x.dt
Why Numbers, not Dates?
Simple - dates can be generated based on the number, like in the example I provided. It also means using a single table, vs say one per data type.
select MONTHNAME(expires) as month_name,sum(`value`) from Table1
group by month_name order by null;
fiddle