SQL generate free slots on the fly from bookings table? - mysql

I know there's a lot of booking questions on php section.
Belive me when i tell you that I tried most of them, at least those which seems compatible. So let me explain.
I have this appointments table
ID | day | start | end |
----------------------------------
1 | 01-01-2018 | 09:00 | 10:00 |
2 | 01-01-2018 | 10:00 | 13:00 |
3 | 02-01-2018 | 12:00 | 15:00 |
4 | 02-01-2018 | 18:00 | 19:30 |
I was wondering, it possibile with sql to get empty time slots? The result should like something like:
day | start | end
---------------------------
01-01-2018 | 00:00 | 09:00
01-01-2018 | 13:00 | 23:59
02-01-2018 | 00:00 | 12:00
02-01-2018 | 15:00 | 18:00
02-01-2018 | 19:30 | 23:59
The query should contain 2 dates: start_day + end_day
I prepared a fiddle here: https://www.db-fiddle.com/f/6dm8q8UtmDkkkjExYfMEbx/1

MSSQL Version
WITH X AS
(
SELECT ROW_NUMBER() OVER (ORDER BY Day, Start)sq, [Day], [Start], [End]
FROM (
SELECT [Day], [Start], [End]
FROM [appointments]
UNION
SELECT Day, '00:00', '00:00'
FROM [appointments]
UNION
SELECT Day, '23:59', '23:59'
FROM [appointments]
) T1
)
SELECT A.Day, A.[End] AS Start, b.[Start] AS End
FROM x A
JOIN x B
ON A.sq = B.sq -1
AND A.[Day] = B.[Day]
AND A.[End] <> b.[Start]
Mysql 5.7 version
SET #RowNumber = 0;
CREATE TABLE cte
SELECT (#RowNumber := #RowNumber+1) AS Rownumber, Day, Start, End
FROM (
SELECT Day, Start, End
FROM booking
UNION
SELECT Day, '00:00', '00:00'
FROM booking
UNION
SELECT Day, '23:59', '23:59'
FROM booking
) T1
ORDER BY day ASC, Start ASC
;
SELECT A.Day, A.End AS Start, B.Start AS End
FROM cte A
JOIN cte B
ON A.Rownumber = B.Rownumber -1
AND A.Day = B.Day
AND A.End <> B.Start
ORDER BY A.Day asc, A.End asc
Will add a fiddle to demonstrate
https://www.db-fiddle.com/f/6dm8q8UtmDkkkjExYfMEbx/2
Mysql 5.7 including days without bookings
SET #RowNumber = 0;
CREATE TABLE cte
SELECT (#RowNumber := #RowNumber+1) AS Rownumber, Day, Start, End
FROM (
SELECT Day, Start, End
FROM booking
UNION
SELECT Day, '00:00', '00:00'
FROM booking
UNION
SELECT Day, '23:59', '23:59'
FROM booking
) T1
ORDER BY day ASC, Start ASC
;
SELECT DAY, Start, End
FROM(
SELECT A.Day, A.End AS Start, B.Start AS End
FROM cte A
JOIN cte B
ON A.Rownumber = B.Rownumber -1
AND A.Day = B.Day
AND A.End <> B.Start
UNION
SELECT DATE_ADD(A.Day, INTERVAL 1 DAY) AS Day, B.Start AS Start, A.End AS End
FROM cte A
JOIN cte B
ON A.Rownumber = B.Rownumber -1
AND A.Day <> B.Day
)Result
ORDER BY Day ASC, Start ASC
https://www.db-fiddle.com/f/6dm8q8UtmDkkkjExYfMEbx/3

Here is a solution below. Assumed the logic of ID column is identity(1,1), otherwise please generate row_number() for each column first. Syntax might not match with MySql, but you will get the logic and apply same if u like.
CREATE TABLE #result (
[day] date NOT NULL,
[start] time NOT NULL,
[end] time NOT NULL
)
declare #maxid int = (select max(id) from #booking), #counter int = 1, #day date
declare #tempStart time = '00:00', #currentStart time ='00:00'
declare #tempDay date = (select TOP 1 [day] from #booking)
while #counter <= #maxid
begin
set #currentStart = (select start from #booking where id=#counter)
set #day = (select [day] from #booking where id=#counter)
if (#day > DATEADD(day,1,#tempDay))
begin
insert into #result values (DATEADD(day,1,#tempDay),'00:00', '23:00')
set #tempDay = #day
end
if(#tempStart < #currentStart)
begin
insert into #result values (#day, #tempStart, #currentStart)
end
if(#counter = #maxid and #tempStart <> '23:59')
begin
insert into #result values (#day, (select [end] from #booking where id=#counter), '23:59')
end
set #tempStart = (select [end] from #booking where id=#counter)
set #counter = #counter + 1
end
select * from #result
Example for SQL Server: https://rextester.com/live/VBNJ34000

Related

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

MYSQL check if a chain of dates exists

I want to check in MySQL if a chain of dates exists longer than 5. For example:
11-05-2016
12-05-2016
13-05-2016
14-05-2016
15-05-2016
These would be rows in my DB, this would be a chain of 5. I want to make a trigger on my table to protect my data from making chains bigger than 5.
Here's an incomplete answer...
I ain't pretending this is pretty (and it scales poorly too), but this does at least demonstrate a principle that others might build off...
DROP TABLE IF EXISTS my_table;
CREATE TABLE my_table(dt DATE NOT NULL PRIMARY KEY);
INSERT INTO my_table VALUES
('2016-05-01'),
('2016-05-02'),
('2016-05-03'),
('2016-05-05'),
('2016-05-07'),
('2016-05-08'),
('2016-05-09'),
('2016-05-11'),
('2016-05-12'),
('2016-05-13'),
('2016-05-14');
Here we have a streak of 3 days (1st to 3rd), followed by a streak of 1 day (5th), followed by another streak of 3 days (7th to 9th), followed by a 4 day streak (11th to 14th).
So, we should be able to insert a value of '2016-05-15' (because that would produce a 5-day streak), but not '2016-05-10' (because that would make an 8-day streak).
1. 2016-05-10
SET #x = '2016-05-10';
SELECT a.dt start
, MIN(c.dt) end
FROM
( SELECT dt FROM my_table
UNION
SELECT #x
) a
LEFT
JOIN
( SELECT dt FROM my_table
UNION
SELECT #x
) b
ON b.dt = a.dt - INTERVAL 1 DAY
LEFT
JOIN
( SELECT dt FROM my_table
UNION
SELECT #x
) c
ON c.dt >= a.dt
LEFT
JOIN
( SELECT dt FROM my_table
UNION
SELECT #x
) d
ON d.dt = c.dt + INTERVAL 1 DAY
WHERE b.dt IS NULL
AND c.dt IS NOT NULL
AND d.dt IS NULL
GROUP
BY a.dt
HAVING DATEDIFF(end,start)>5;
+------------+------------+
| start | end |
+------------+------------+
| 2016-05-07 | 2016-05-15 |
+------------+------------+
2. 2016-05-15
SET #x = '2016-05-15';
SELECT a.dt start
, MIN(c.dt) end
FROM
( SELECT dt FROM my_table
UNION
SELECT #x
) a
LEFT
JOIN
( SELECT dt FROM my_table
UNION
SELECT #x
) b
ON b.dt = a.dt - INTERVAL 1 DAY
LEFT
JOIN
( SELECT dt FROM my_table
UNION
SELECT #x
) c
ON c.dt >= a.dt
LEFT
JOIN
( SELECT dt FROM my_table
UNION
SELECT #x
) d
ON d.dt = c.dt + INTERVAL 1 DAY
WHERE b.dt IS NULL
AND c.dt IS NOT NULL
AND d.dt IS NULL
GROUP
BY a.dt
HAVING DATEDIFF(end,start)>5;
Empty set (0.03 sec)

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

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

MySQL: Expand date range into new rows

I have a table in MySQL that contains min and max date values for each key:
key | minDate | maxDate
1 2011-01-01 2011-01-10
2 2011-02-13 2011-02-15
3 2011-10-19 2011-12-10
How can I create a new table that contains one row for each date between minDate and maxDate for each of the keys:
key | Date
1 2011-01-01
1 2011-01-02
... ...
1 2011-01-10
2 2011-02-13
2 2011-02-14
2 2011-02-15
3 2011-10-19
... ...
Using an integers table, you can do this:
SELECT "key", minDate + INTERVAL i DAY
FROM mytable
INNER JOIN integers
ON i <= DATEDIFF(maxDate, minDate)
That assumes the "integers" table has its column named "i", of course.
From there you can populate your new table with INSERT INTO ... SELECT.
Using a recursive common table expression (requires mysql 8 or mariadb 10.2+):
with recursive expanded_ranges as (
select id, mindate dt
from ranges
union all
select expanded_ranges.id, expanded_ranges.dt+interval 1 day
from expanded_ranges
join ranges on expanded_ranges.id=ranges.id and dt<maxdate
)
select * From expanded_ranges;
fiddle
-- Below are 2 variables to set start date and end date
set #start_date = '2020-01-01';
set #end_date = '2022-12-31';
-- Below is the recursive CTE which returns all the dates between Start date and End Date
WITH RECURSIVE ListDates(AllDates) AS
(
SELECT #start_date AS DATE
UNION ALL
SELECT DATE_ADD(AllDates, INTERVAL 1 DAY)
FROM ListDates
WHERE AllDates < #end_date
)
SELECT AllDates FROM ListDates
From memory, it could be something like this:
create table #res (
key int,
Date datetime
)
declare #minD datetime, #maxD datetime
select #minD = min(minDate), #maxD = max(maxDate) from tablename
while #minD <= #maxD do
insert into #res
select key, #minD from tablename where #minD >= minDate and #minD <= maxDate
select #minD = dateadd(#minD, interval 1 day)
end while;
select key, Date from #res
drop table #res

Get a list of dates between two dates

Using standard mysql functions is there a way to write a query that will return a list of days between two dates.
eg given 2009-01-01 and 2009-01-13 it would return a one column table with the values:
2009-01-01
2009-01-02
2009-01-03
2009-01-04
2009-01-05
2009-01-06
2009-01-07
2009-01-08
2009-01-09
2009-01-10
2009-01-11
2009-01-12
2009-01-13
Edit: It appears I have not been clear. I want to GENERATE this list. I have values stored in the database (by datetime) but want them to be aggregated on a left outer join to a list of dates as above (I am expecting null from the right side of some of this join for some days and will handle this).
I would use this stored procedure to generate the intervals you need into the temp table named time_intervals, then JOIN and aggregate your data table with the temp time_intervals table.
The procedure can generate intervals of all the different types you see specified in it:
call make_intervals('2009-01-01 00:00:00','2009-01-10 00:00:00',1,'DAY')
.
select * from time_intervals
.
interval_start interval_end
------------------- -------------------
2009-01-01 00:00:00 2009-01-01 23:59:59
2009-01-02 00:00:00 2009-01-02 23:59:59
2009-01-03 00:00:00 2009-01-03 23:59:59
2009-01-04 00:00:00 2009-01-04 23:59:59
2009-01-05 00:00:00 2009-01-05 23:59:59
2009-01-06 00:00:00 2009-01-06 23:59:59
2009-01-07 00:00:00 2009-01-07 23:59:59
2009-01-08 00:00:00 2009-01-08 23:59:59
2009-01-09 00:00:00 2009-01-09 23:59:59
.
call make_intervals('2009-01-01 00:00:00','2009-01-01 02:00:00',10,'MINUTE')
.
select * from time_intervals
.
interval_start interval_end
------------------- -------------------
2009-01-01 00:00:00 2009-01-01 00:09:59
2009-01-01 00:10:00 2009-01-01 00:19:59
2009-01-01 00:20:00 2009-01-01 00:29:59
2009-01-01 00:30:00 2009-01-01 00:39:59
2009-01-01 00:40:00 2009-01-01 00:49:59
2009-01-01 00:50:00 2009-01-01 00:59:59
2009-01-01 01:00:00 2009-01-01 01:09:59
2009-01-01 01:10:00 2009-01-01 01:19:59
2009-01-01 01:20:00 2009-01-01 01:29:59
2009-01-01 01:30:00 2009-01-01 01:39:59
2009-01-01 01:40:00 2009-01-01 01:49:59
2009-01-01 01:50:00 2009-01-01 01:59:59
.
I specified an interval_start and interval_end so you can aggregate the
data timestamps with a "between interval_start and interval_end" type of JOIN.
.
Code for the proc:
.
-- drop procedure make_intervals
.
CREATE PROCEDURE make_intervals(startdate timestamp, enddate timestamp, intval integer, unitval varchar(10))
BEGIN
-- *************************************************************************
-- Procedure: make_intervals()
-- Author: Ron Savage
-- Date: 02/03/2009
--
-- Description:
-- This procedure creates a temporary table named time_intervals with the
-- interval_start and interval_end fields specifed from the startdate and
-- enddate arguments, at intervals of intval (unitval) size.
-- *************************************************************************
declare thisDate timestamp;
declare nextDate timestamp;
set thisDate = startdate;
-- *************************************************************************
-- Drop / create the temp table
-- *************************************************************************
drop temporary table if exists time_intervals;
create temporary table if not exists time_intervals
(
interval_start timestamp,
interval_end timestamp
);
-- *************************************************************************
-- Loop through the startdate adding each intval interval until enddate
-- *************************************************************************
repeat
select
case unitval
when 'MICROSECOND' then timestampadd(MICROSECOND, intval, thisDate)
when 'SECOND' then timestampadd(SECOND, intval, thisDate)
when 'MINUTE' then timestampadd(MINUTE, intval, thisDate)
when 'HOUR' then timestampadd(HOUR, intval, thisDate)
when 'DAY' then timestampadd(DAY, intval, thisDate)
when 'WEEK' then timestampadd(WEEK, intval, thisDate)
when 'MONTH' then timestampadd(MONTH, intval, thisDate)
when 'QUARTER' then timestampadd(QUARTER, intval, thisDate)
when 'YEAR' then timestampadd(YEAR, intval, thisDate)
end into nextDate;
insert into time_intervals select thisDate, timestampadd(MICROSECOND, -1, nextDate);
set thisDate = nextDate;
until thisDate >= enddate
end repeat;
END;
Similar example data scenario at the bottom of this post, where I built a similar function for SQL Server.
For MSSQL you can use this. It is VERY quick.
You can wrap this up in a table valued function or stored proc and parse in the start and end dates as variables.
DECLARE #startDate DATETIME
DECLARE #endDate DATETIME
SET #startDate = '2011-01-01'
SET #endDate = '2011-01-31';
WITH dates(Date) AS
(
SELECT #startdate as Date
UNION ALL
SELECT DATEADD(d,1,[Date])
FROM dates
WHERE DATE < #enddate
)
SELECT Date
FROM dates
OPTION (MAXRECURSION 0)
GO
Edit 2021/01 (Dr. V):
I liked this solution and made it work for mySQL V8. Here is the code, wrapping it into a procedure:
DELIMITER //
CREATE PROCEDURE dates_between (IN from_date DATETIME,
IN to_date DATETIME) BEGIN
WITH RECURSIVE dates(Date) AS
(
SELECT from_date as Date
UNION ALL
SELECT DATE_ADD(Date, INTERVAL 1 day) FROM dates WHERE Date < to_date
)
SELECT DATE(Date) FROM dates;
END//
DELIMITER ;
You can use MySQL's user variables like this:
SET #num = -1;
SELECT DATE_ADD( '2009-01-01', interval #num := #num+1 day) AS date_sequence,
your_table.* FROM your_table
WHERE your_table.other_column IS NOT NULL
HAVING DATE_ADD('2009-01-01', interval #num day) <= '2009-01-13'
#num is -1 because you add to it the first time you use it. Also, you can't use "HAVING date_sequence" because that makes the user variable increment twice for each row.
We had a similar problem with BIRT reports in that we wanted to report on those days that had no data. Since there were no entries for those dates, the easiest solution for us was to create a simple table that stored all dates and use that to get ranges or join to get zero values for that date.
We have a job that runs every month to ensure that the table is populated 5 years out into the future. The table is created thus:
create table all_dates (
dt date primary key
);
No doubt there are magical tricky ways to do this with different DBMS' but we always opt for the simplest solution. The storage requirements for the table are minimal and it makes the queries so much simpler and portable. This sort of solution is almost always better from a performance point-of-view since it doesn't require per-row calculations on the data.
The other option (and we've used this before) is to ensure there's an entry in the table for every date. We swept the table periodically and added zero entries for dates and/or times that didn't exist. This may not be an option in your case, it depends on the data stored.
If you really think it's a hassle to keep the all_dates table populated, a stored procedure is the way to go which will return a dataset containing those dates. This will almost certainly be slower since you have to calculate the range every time it's called rather than just pulling pre-calculated data from a table.
But, to be honest, you could populate the table out for 1000 years without any serious data storage problems - 365,000 16-byte (for example) dates plus an index duplicating the date plus 20% overhead for safety, I'd roughly estimate at about 14M [365,000 * 16 * 2 * 1.2 = 14,016,000 bytes]), a minuscule table in the scheme of things.
Elegant solution using new recursive (Common Table Expressions) functionality in MariaDB >= 10.3 and MySQL >= 8.0.
WITH RECURSIVE t as (
select '2019-01-01' as dt
UNION
SELECT DATE_ADD(t.dt, INTERVAL 1 DAY) FROM t WHERE DATE_ADD(t.dt, INTERVAL 1 DAY) <= '2019-04-30'
)
select * FROM t;
The above returns a table of dates between '2019-01-01' and '2019-04-30'.
Borrowing an idea from this answer, you can set up a table with 0 through 9 and use that to generate your list of dates.
CREATE TABLE num (i int);
INSERT INTO num (i) VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9);
select adddate('2009-01-01', numlist.id) as `date` from
(SELECT n1.i + n10.i*10 + n100.i*100 AS id
FROM num n1 cross join num as n10 cross join num as n100) as numlist
where adddate('2009-01-01', numlist.id) <= '2009-01-13';
This will allow you to generate a list of up to 1000 dates. If you need to go larger, you can add another cross join to the inner query.
For Access (or any SQL language)
Create one table that has 2 fields, we'll call this table tempRunDates:
--Fields fromDate and toDate
--Then insert only 1 record, that has the start date and the end date.
Create another table: Time_Day_Ref
--Import a list of dates (make list in excel is easy) into this table.
--The field name in my case is Greg_Dt, for Gregorian Date
--I made my list from jan 1 2009 through jan 1 2020.
Run the query:
SELECT Time_Day_Ref.GREG_DT
FROM tempRunDates, Time_Day_Ref
WHERE Time_Day_Ref.greg_dt>=tempRunDates.fromDate And greg_dt<=tempRunDates.toDate;
Easy!
Typically one would use an auxiliary numbers table you usually keep around for just this purpose with some variation on this:
SELECT *
FROM (
SELECT DATEADD(d, number - 1, '2009-01-01') AS dt
FROM Numbers
WHERE number BETWEEN 1 AND DATEDIFF(d, '2009-01-01', '2009-01-13') + 1
) AS DateRange
LEFT JOIN YourStuff
ON DateRange.dt = YourStuff.DateColumn
I've seen variations with table-valued functions, etc.
You can also keep a permanent list of dates. We have that in our data warehouse as well as a list of times of day.
CREATE FUNCTION [dbo].[_DATES]
(
#startDate DATETIME,
#endDate DATETIME
)
RETURNS
#DATES TABLE(
DATE1 DATETIME
)
AS
BEGIN
WHILE #startDate <= #endDate
BEGIN
INSERT INTO #DATES (DATE1)
SELECT #startDate
SELECT #startDate = DATEADD(d,1,#startDate)
END
RETURN
END
We used this in our HRMS System you will find it useful
SELECT CAST(DAYNAME(daydate) as CHAR) as dayname,daydate
FROM
(select CAST((date_add('20110101', interval H.i*100 + T.i*10 + U.i day) )as DATE) as daydate
from erp_integers as H
cross
join erp_integers as T
cross
join erp_integers as U
where date_add('20110101', interval H.i*100 + T.i*10 + U.i day ) <= '20110228'
order
by daydate ASC
)Days
This solution is working with MySQL 5.0
Create a table - mytable.
The schema does not material. What matters is the number of rows in it.
So, you can keep just one column of type INT with 10 rows, values - 1 to 10.
SQL:
set #tempDate=date('2011-07-01') - interval 1 day;
select
date(#tempDate := (date(#tempDate) + interval 1 day)) as theDate
from mytable x,mytable y
group by theDate
having theDate <= '2011-07-31';
Limitation:
The maximum number of dates returned by above query will be
(rows in mytable)*(rows in mytable) = 10*10 = 100.
You can increase this range by changing form part in sql:
from mytable x,mytable y, mytable z
So, the range be 10*10*10 =1000 and so on.
You can use this
SELECT CAST(cal.date_list AS DATE) day_year
FROM (
SELECT SUBDATE('2019-01-01', INTERVAL 1 YEAR) + INTERVAL xc DAY AS date_list
FROM (
SELECT #xi:=#xi+1 as xc from
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc1,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc2,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc3,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc4,
(SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4) xc5,
(SELECT #xi:=-1) xc0
) xxc1
) cal
WHERE cal.date_list BETWEEN '2019-01-01' AND '2019-12-31'
ORDER BY cal.date_list DESC;
No temp table | No procedure | No functions
The example below present one month date range - from day 1 to the last day of the same month. You can change the date range by playing with #e `INTERVAL` values.
SET #s := CAST('2021-12-01' AS DATE); -- start at 2021-12-01
SET #e := #s + INTERVAL 1 MONTH - INTERVAL 1 DAY; -- ends at 2021-12-31
WITH RECURSIVE d_range AS
(
SELECT #s AS gdate -- generated dates
UNION ALL
SELECT gdate + INTERVAL 1 day
FROM d_range
WHERE gdate < #e
)
SELECT *
FROM d_range;
Result:
| gdate |
+------------+
| 2021-12-01 |
| 2021-12-02 |
| 2021-12-03 |
| 2021-12-04 |
| 2021-12-05 |
| 2021-12-06 |
| 2021-12-07 |
| 2021-12-08 |
| 2021-12-09 |
| 2021-12-10 |
| 2021-12-11 |
| 2021-12-12 |
| 2021-12-13 |
| 2021-12-14 |
| 2021-12-15 |
| 2021-12-16 |
| 2021-12-17 |
| 2021-12-18 |
| 2021-12-19 |
| 2021-12-20 |
| 2021-12-21 |
| 2021-12-22 |
| 2021-12-23 |
| 2021-12-24 |
| 2021-12-25 |
| 2021-12-26 |
| 2021-12-27 |
| 2021-12-28 |
| 2021-12-29 |
| 2021-12-30 |
| 2021-12-31 |
+------------+
-> 31 rows in set (0.037 sec)
Note. This query uses RECURSION there for it is quite slow 31 rows in set (0.037 sec), when selecting longer date ranges
it will get even slower.
Create a stored procedure which takes two parameters a_begin and a_end.
Create a temporary table within it called t, declare a variable d, assign a_begin to d, and run a WHILE loop INSERTing d into t and calling ADDDATE function to increment the value d. Finally SELECT * FROM t.
I would use something similar to this:
DECLARE #DATEFROM AS DATETIME
DECLARE #DATETO AS DATETIME
DECLARE #HOLDER TABLE(DATE DATETIME)
SET #DATEFROM = '2010-08-10'
SET #DATETO = '2010-09-11'
INSERT INTO
#HOLDER
(DATE)
VALUES
(#DATEFROM)
WHILE #DATEFROM < #DATETO
BEGIN
SELECT #DATEFROM = DATEADD(D, 1, #DATEFROM)
INSERT
INTO
#HOLDER
(DATE)
VALUES
(#DATEFROM)
END
SELECT
DATE
FROM
#HOLDER
Then the #HOLDER Variable table holds all the dates incremented by day between those two dates, ready to join at your hearts content.
I've been fighting with this for quite a while. Since this is the first hit on Google when I searched for the solution, let me post where I've gotten so far.
SET #d := '2011-09-01';
SELECT #d AS d, cast( #d := DATE_ADD( #d , INTERVAL 1 DAY ) AS DATE ) AS new_d
FROM [yourTable]
WHERE #d <= '2012-05-01';
Replace [yourTable] with a table from your database. The trick is that the number of rows in the table you select must be >= the number of dates you want to be returned. I tried using the table placeholder DUAL, but it would only return one single row.
select * from table_name where col_Date between '2011/02/25' AND DATEADD(s,-1,DATEADD(d,1,'2011/02/27'))
Here, first add a day to the current endDate, it will be 2011-02-28 00:00:00, then you subtract one second to make the end date 2011-02-27 23:59:59. By doing this, you can get all the dates between the given intervals.
output:
2011/02/25
2011/02/26
2011/02/27
DELIMITER $$
CREATE PROCEDURE popula_calendario_controle()
BEGIN
DECLARE a INT Default 0;
DECLARE first_day_of_year DATE;
set first_day_of_year = CONCAT(DATE_FORMAT(curdate(),'%Y'),'-01-01');
one_by_one: LOOP
IF dayofweek(adddate(first_day_of_year,a)) <> 1 THEN
INSERT INTO calendario.controle VALUES(null,150,adddate(first_day_of_year,a),adddate(first_day_of_year,a),1);
END IF;
SET a=a+1;
IF a=365 THEN
LEAVE one_by_one;
END IF;
END LOOP one_by_one;
END $$
this procedure will insert all dates from the beginning of the year till now, just substitue the days of the "start" and "end", and you are ready to go!
I needed a list with all months between 2 dates for statistics. The 2 dates are the start and enddate from a subscription.
So the list shows all months and the amount of subscriptions per month.
MYSQL
CREATE PROCEDURE `get_amount_subscription_per_month`()
BEGIN
-- Select the ultimate start and enddate from subscribers
select #startdate := min(DATE_FORMAT(a.startdate, "%Y-%m-01")),
#enddate := max(DATE_FORMAT(a.enddate, "%Y-%m-01")) + interval 1 MONTH
from subscription a;
-- Tmp table with all months (dates), you can always format them with DATE_FORMAT)
DROP TABLE IF EXISTS tmp_months;
create temporary table tmp_months (
year_month date,
PRIMARY KEY (year_month)
);
set #tempDate=#startdate; #- interval 1 MONTH;
-- Insert every month in tmp table
WHILE #tempDate <= #enddate DO
insert into tmp_months (year_month) values (#tempDate);
set #tempDate = (date(#tempDate) + interval 1 MONTH);
END WHILE;
-- All months
select year_month from tmp_months;
-- If you want the amount of subscription per month else leave it out
select mnd.year_month, sum(subscription.amount) as subscription_amount
from tmp_months mnd
LEFT JOIN subscription ON mnd.year_month >= DATE_FORMAT(subscription.startdate, "%Y-%m-01") and mnd.year_month <= DATE_FORMAT(subscription.enddate, "%Y-%m-01")
GROUP BY mnd.year_month;
END
No Function, No Procedures
SET #s := '2020-01-01';
SET #e := #s + INTERVAL 1 YEAR - INTERVAL 1 DAY; -- set end date to select
SELECT CAST((DATE(#s)+INTERVAL (H+T+U) DAY) AS DATE) d -- generate a list of 400 days starting from #s date so #e can not be more then #s +
FROM ( SELECT 0 H
UNION ALL SELECT 100 UNION ALL SELECT 200 UNION ALL SELECT 300
) H CROSS JOIN ( SELECT 0 T
UNION ALL SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 30
UNION ALL SELECT 40 UNION ALL SELECT 50 UNION ALL SELECT 60
UNION ALL SELECT 70 UNION ALL SELECT 80 UNION ALL SELECT 90
) T CROSS JOIN ( SELECT 0 U
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
) U
-- generated lenght of date list can be calculated as fallow 1 * H(3 + 1) * T(9 + 1) * U(9 + 1) = 400
-- it is crucial to preserve the numbering convention as 1, 2 ... 20, 30..., 100, 200, ... to retrieve chronological date selection
-- add more UNION 400, 500, 600, ... H(6 + 1) if you want to select from more than 700 days!
WHERE
(DATE(#s+INTERVAL (H+T+U) DAY)) <= DATE((#e))
ORDER BY d;
UNIONS define the length and range of available dates to elect from. So always make sure to make it log enough.
SELECT 1 * (3 + 1) * (9 + 1) * (9 + 1); -- 400 days starting from from #s
Based on this article
Tested and working as expected
Improving on #brad answer, I used a CTE with a cursor:
DECLARE cursorPeriod CURSOR FOR SELECT * FROM (
WITH RECURSIVE period as
(
SELECT '2019-01-01' as dt
UNION
SELECT DATE_ADD(period.dt, INTERVAL 1 DAY) FROM period WHERE DATE_ADD(period.dt, INTERVAL 1 DAY) <= '2019-04-30'
)
SELECT DATE(dt) FROM period ORDER BY dt ) AS derived_table;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET exit_loop = TRUE;
Portable solution
-- Generate
WITH
tdates AS (
WITH t19 AS (
SELECT 0 i 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 )
SELECT adddate('1970-01-01',t4.i*10000 + t3.i*1000 + t2.i*100 + t1.i*10 + t0.i) datei
FROM t19 AS t0, t19 AS t1, t19 AS t2, t19 AS t3, t19 AS t4
)
select * from tdates
where datei between '2012-02-10' and '2012-02-15'
I am using Server version: 5.7.11-log MySQL Community Server (GPL)
Now we will solve this in a simple way.
I have created a table named "datetable"
mysql> describe datetable;
+---------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+---------+------+-----+---------+-------+
| colid | int(11) | NO | PRI | NULL | |
| coldate | date | YES | | NULL | |
+---------+---------+------+-----+---------+-------+
2 rows in set (0.00 sec)
now, wee will see the inserted records within.
mysql> select * from datetable;
+-------+------------+
| colid | coldate |
+-------+------------+
| 101 | 2015-01-01 |
| 102 | 2015-05-01 |
| 103 | 2016-01-01 |
+-------+------------+
3 rows in set (0.00 sec)
and here our query to fetch records within two dates rather than those dates.
mysql> select * from datetable where coldate > '2015-01-01' and coldate < '2016-01-01';
+-------+------------+
| colid | coldate |
+-------+------------+
| 102 | 2015-05-01 |
+-------+------------+
1 row in set (0.00 sec)
hope this would help many ones.