Check Hall Booking status - sql

I'm making a Hall Booking System, and I've being struggling with the Booking Module
I have 2 tables Halls & Bookings
Halls table With Sample Data
id hallName hallType numSeats status
---------------------------------------------
1 Hall 1 Normal 500 active
2 Hall 2 VIP 30 active
3 Hall 3 VVIP 5 active
4 Hall 4 Normal 60 active
5 Hall 5 Normal 80 active
6 Hall 4+5 Normal 140 active
Booking Table with Sample Data
id custId hallId beginDate endDate time status
-----------------------------------------------------------------
1 1 1 2022-09-26 2022-09-26 morning confirmed
2 6 4 2022-09-26 2022-09-26 evening cancelled
3 4 3 2022-09-26 2022-09-26 full time pending
4 9 4 2022-09-26 2022-09-30 after noon confirmed
Booking time slots are "morning", "after noon", "evening" & "full time"
I have being trying on the Booking validations as well as a report that shows Halls booking status
Edit
After suggestions in the comments I have edited and removed the second part, maybe will post seperately if I can't figure out
Here is what I want to be the result
The records can be filtered by endDate(e.g 2022-09-26)
if Booking exists which is not cancelled for the provided beginDate, the time slot should be labelled as Booked
if a booking does not exist for the provided beginDate or its canclled, the time slot should be labeled as Available
If a full time Booking slot ecists for the provied beginDate, all the time the 3 slots should be labeled as Booked
Here is the twist that may complicate things
As you can see from the Halls table, Hall 6 is a combination of Halls 4 & 5 which means if any of them is booked, Hall 6 should be marked as unavailable or even Booked will be fine.
Overall, here is a sample result based on the above booking table
hallName hallType morning after noon evening
------------------------------------------------------
hall 1 Normal Booked available available
hall 2 VIP available available available
hall 3 VVIP Booked Booked Booked
hall 4 Normal available Booked available
hall 5 Normal available available available
hall 6 Normal available unavailable available
if we take the Booking Table with Sample data, the result should be as shown above
I'm not that much familiar with Joins, merges, etc which are what I'm thinking the solution is. I tried merge with When matched and when Not matched but could not figure it out to work
I then tried left joining the Halls table to the Booking table seems to be the closest so far and the output is clear.
DECLARE #date NVarchar(50) = '2022-09-26'
SELECT h.id, h.hallName, h.hallType, b.time FROM Halls h LEFT JOIN
Bookings b ON b.hallId=h.id WHERE b.beginDate=#date
this returns only Booked halls with beginDate of that
If I drop the WHERE clause, all the 6 halls are returned but time slots which aren't booked are returned as NULL
btw I'm working on the last module and at firts I was working with a single check in date and requirement changed into Range beginDate & endDate & that is when problems arose.

Firstly you need to fix your design how are you intend to store information that Hall 6 is a combine of Hall 4 + 5
One simple way is to add another column in the Halls table that indicate that. Example
create table Halls
(
id int,
hallName varchar(10),
hallType varchar(10),
numSeats int,
status varchar(10),
combine_id int
);
insert into Halls values
(4, 'Hall 4', 'Normal', 60, 'active', 6),
(5, 'Hall 5', 'Normal', 80, 'active', 6),
(6, 'Hall 4+5', 'Normal', 140, 'active', null);
Once that is in-placed, you need to translate the Bookings to handle the combined Halls. This is perform by the CTE BookingData. It will create row for Hall 6 if Hall 4 or Hall 5 is pending or confirmed. And simiarly the other way round. When Hall 6 is Booked, Hall 4 and Hall 5 will be unavailable.
After that it is just simple pivoting of data
The solution:
DECLARE #date date = '2022-09-26';
with BookingData as
(
select b.hallId, b.time, b.status, beginDate
from Bookings b
union all
select hallId = h.combine_id, b.time,
status = case when b.status in ('pending', 'confirmed')
then 'unavailable'
else 'available'
end,
beginDate
from Bookings b
inner join Halls h on b.hallId = h.id
where h.combine_id is not null
union all
select hallId = h.id, b.time,
status = case when b.status in ('pending', 'confirmed')
then 'unavailable'
else 'available'
end,
beginDate
from Bookings b
inner join Halls h on b.hallId = h.combine_id
where h.combine_id is not null
)
SELECT id,
hallName,
hallType,
[morning] = isnull([morning], 'available'),
[afternoon] = isnull([afternoon], 'available'),
[evening] = isnull([evening], 'available')
FROM
(
SELECT h.id, h.hallName, h.hallType, t.timeSlot,
status = case when b.status in ('pending', 'confirmed')
then 'Booked'
when b.status in ('cancelled')
then 'unavailable'
when b.status in ('unavailable')
then b.status
else NULL
end
FROM Halls h
LEFT JOIN BookingData b ON b.hallId = h.id
and b.beginDate = #date
OUTER APPLY
(
select timeSlot = 'morning' where b.time in ('morning', 'full time')
union all
select timeSlot = 'afternoon' where b.time in ('afternoon', 'full time')
union all
select timeSlot = 'evening' where b.time in ('evening', 'full time')
) t
) D
PIVOT
(
MAX (status)
FOR timeSlot in ( [morning], [afternoon], [evening] )
) P
db<>fiddle demo

Your question is overtly complicated due to the fact the table does not implement normalization properly for "combined halls." We will need to separate Hall 4+5 using string operation and then store the extracted information somewhere. Here is the query that I could think off:
with
composite_halls as
(select *,
substring(hallName, charindex(' ', hallName) + 1,
charindex('+', hallName) - charindex(' ', hallName) - 1) as id1,
right(hallName, len(hallName) - charindex('+', hallName)) as id2,
(null) as id3
from halls where charindex('+', hallName) > 0),
singular_halls as
(select *,
(select ch.id2 from composite_halls as ch where ch.id1 = h.id) as id1,
(select ch.id1 from composite_halls as ch where ch.id2 = h.id) as id2,
(select ch.id from composite_halls as ch where ch.id1 = h.id or ch.id2 = h.id) as id3
from halls as h where charindex('+', hallName) = 0),
all_halls as (
select * from singular_halls
union
select * from composite_halls),
valid_bookings as (
select * from bookings where status = 'confirmed'
and beginDate >= '2022-09-26' and endDate <= '2022-09-26')
select *,
(case when exists (
select *
from valid_bookings as vb
where (time = 'morning' or time = 'full time')
and (vb.hallId = h.id
or (vb.hallId = h.id1 and h.id3 is null)
or (vb.hallId = h.id2 and h.id3 is null)
or vb.hallId = h.id3))
then 'booked'
else 'available'
end) as morning
from all_halls as h
You can try it on fiddle: https://dbfiddle.uk/baQyI1Y7
It looks somewhat detestable and I believe someone else can write shorter queries.
The query above uses 4 CTEs with 3 of them deals with extracting the ids of relevant "combined halls." composite_halls will extract the numbers from hallName and puts them into id1 and id2. singular_halls will put its counterpart's id in either id1 or id2 and its composite in id3. all_halls merges the two result into one. valid_bookings filters bookings for given timeframe and only confirmed bookings are taken.
The next step, it checks if any bookings' hallId correspond to any of id, id1, id2, id3 within the all_halls entries. So far it only handles morning bookings and only provide booked and available status. You can expand it to provide unavailable by adding more case when to specifically handle id1 up to id3. You can handle after noon and evening can be done the same way by changing the time parameter within the case when clause.
I don't think the query above is performant enough for a live application. Especially the fact that we operate on row-by-row basis to extract the relevant halls from the "combined" one.
I believe things would be much easier should you normalize the table a bit more. I feel its like doing excel using TSQL which doesn't seem right.

Related

Count records using Date filter related tables SQL Server

I'm trying to calculates the number of reports (report_user table) per user between two date (Calendar table) and by day worked (agenda_user table).
Here is the diagram of my tables:
Calendar table :
DATE Year Month
---------------------------------
2020-01-01 2020 1
2020-01-02 2020 1
2020-01-03 2020 1
2020-01-04 2020 1
AGENDA_USER table :
ID_USER DATE Value
---------------------------------
1 2020-01-01 1
2 2020-01-01 1
1 2020-01-02 0
2 2020-01-02 1
User table :
ID_USER Name
-------------------------
1 Jack
2 Robert
Report_Result table :
ID_USER Date Result
-----------------------------------
1 2020-01-01 good
1 2020-01-01 good
2 2020-01-01 bad
2 2020-01-01 good
2 2020-01-02 good
2 2020-01-02 good
Result I'm trying to find with an SQL query
ID_USER Date Number of report Day work report/work
---------------------------------------------------------------------------
1 2020-01-01 2 1 2/1 = 2
2 2020-01-01 2 1 1
1 2020-01-02 0 0 0
2 2020-01-02 2 1 2
SELECT
REPORT_USER.ID_USER,
COUNT(ID_USER) AS result
FROM [DB].[dbo].REPORT_USER AS report,
JOIN [DB].[dbo].[USER] AS [USER]
ON [USER].ID_USER = report.ID_USER
JOIN [DB].[dbo].AGENDA_USER AS agenda
ON agenda.ID_USER = report.ID_USER
WHERE CAST(agenda.[Date] AS DATE) >= '2020-09-01'
AND CAST(agenda.[Date] AS DATE) <= '2021-07-28'
AND [USER].ID_user = 1167
GROUP BY
report.ID_VENDEUR;
I'm not entirely sure I understand your problem, but I think I'm reasonably close so here is a start, point out my invalid assumptions and we can refine. More data, particularly in Agenda and Reports would really help. An explanation is below (plus see the comment in the code).
The overall flow is to generate a list of days/people you want to report on (cteUserDays), generate a list of how many reports each user generated on each date (cteReps), generate a list of who worked on what days (cteWork), and then JOIN all 3 parts together using a LEFT OUTER JOIN so the report covers all workers on all days.
EDIT: Add cteRepRaw where DATETIME is converted to DATE and "bad" reports are filtered out. Grouping and counting still happens in cteReps, but joining to cteUserDays is not there because it was adding 1 to count if there was a NULL.
DECLARE #Cal TABLE (CalDate DATETIME, CalYear int, CalMonth int)
DECLARE #Agenda TABLE (UserID int, CalDate DATE, AgendaVal int)
DECLARE #User TABLE (UserID int, UserName nvarchar(50))
DECLARE #Reps TABLE (UserID int, CalDate DATETIME, RepResult nvarchar(50))
INSERT INTO #Cal(CalDate, CalYear, CalMonth)
VALUES ('2020-01-01', 2020, 1), ('2020-01-02', 2020, 1), ('2020-01-03', 2020, 1), ('2020-01-04', 2020, 1)
INSERT INTO #Agenda(UserID, CalDate, AgendaVal)
VALUES (1, '2020-01-01', 1), (2, '2020-01-01', 1), (1, '2020-01-02', 0), (2, '2020-01-02', 1)
INSERT INTO #User (UserID , UserName )
VALUES (1, 'Jack'), (2, 'Robert')
INSERT INTO #Reps (UserID , CalDate , RepResult )
VALUES (1, '2020-01-01', 'good'), (1, '2020-01-01', 'good')
, (2, '2020-01-01', 'bad'), (2, '2020-01-01', 'good')
, (2, '2020-01-02', 'good'), (2, '2020-01-02', 'good')
; with cteUserDays as (
--First, you want zeros in your table where no reports are, so build a table for that
SELECT CONVERT(DATE, D.CalDate) as CalDate --EDIT add CONVERT here
, U.UserID FROM #Cal as D CROSS JOIN #User as U
WHERE D.CalDate >= '2020-01-01' AND D.CalDate <= '2021-07-28'
--EDIT Watch the <= date here, a DATE is < DATETIME with hours of the same day
), cteRepRaw as (--EDIT Add this CTE to convert DATETIME to DATE so we can group on it
--Map the DateTime to a DATE type so we can group reports from any time of day
SELECT R.UserID
, CONVERT(DATE, R.CalDate) as CalDate --EDIT add CONVERT here
, R.RepResult
FROM #Reps as R
WHERE R.RepResult='good' --EDIT Add this test to only count good ones
), cteReps as (
--Get the sum of all reports for a given user on a given day, though some might be missing (fill 0)
SELECT R.UserID , R.CalDate , COUNT(*) as Reports --SUM(COALESCE(R.RepResult, 0)) as Reports
FROM cteRepRaw as R--cteUserDays as D
--Some days may have no reports for that worker, so use a LEFT OUTER JOIN
--LEFT OUTER JOIN cteRepRaw as R on D.CalDate = R.CalDate AND D.UserID = R.UserID
GROUP BY R.UserID , R.CalDate
) , cteWork as (
--Unclear what values "value" in Agenda can take, but assuming it's some kind of work
-- unit, like "hours worked" or "shifts" so add them up
SELECT A.UserID , A.CalDate, SUM(A.AgendaVal) as DayWork FROM #Agenda as A
WHERE A.CalDate >= '2020-01-01' AND A.CalDate <= '2021-07-28'
GROUP BY A.CalDate, A.UserID
)
SELECT D.UserID , D.CalDate, COALESCE(R.Reports, 0) as Reports, W.DayWork
--NOTE: While it's probably a mistake to credit a report to a day a worker had
--no shifts, it could happen and would throw an error so check
, CASE WHEN W.DayWork > 0 THEN R.Reports / W.DayWork ELSE 0 END as RepPerWork
FROM cteUserDays as D
LEFT OUTER JOIN cteReps as R on D.CalDate=R.CalDate AND R.UserID = D.UserID
LEFT OUTER JOIN cteWork as W on D.UserID = W.UserID AND D.CalDate = W.CalDate
ORDER BY CalDate , UserID
First, as per the comments in your OP "Agenda" represents when the user is working, you don't say how it's structured so I'll assume it can have multiple entries for a given person on a given day (i.e. a 4 hour shift and an 8 hour shift) so I'll add them up to get total work (cteWork). I also assume that if somebody didn't work, you can't have a report for them. I check for this, but normally I'd expect your data validator to pre-screen those out.
Second, I'll assume reports are 1 per record, and a given user can have multiple per day. You have that in your given, but it's important to this solution so I'm restating in case somebody else reads this later.
Third, I assume you want all days reported for all users, I assure this by generating a CROSS join between users and days (cteUserDays)

Datediff on 2 rows of a table with a condition

My data looks like the following
TicketID OwnedbyTeamT Createddate ClosedDate
1234 A
1234 A 01/01/2019 01/05/2019
1234 A 10/05/2018 10/07/2018
1234 B 10/04/2019 10/08/2018
1234 finance 11/01/2018 11/11/2018
1234 B 12/02/2018
Now, I want to calculate the datediff between the closeddates for teams A, and B, if the max closeddate for team A is greater than max closeddate team B. If it is smaller or null I don't want to see them. So, for example,I want to see only one record like this :
TicketID (Datediff)result-days
1234 86
and for another tickets, display the info. For example, if the conditions aren't met then:
TicketID (Datediff)result-days
2456 -1111111
Data sample for 2456:
TicketID OwnedbyTeamT Createddate ClosedDate
2456 A
2456 A 10/01/2019 10/05/2019
2456 B 08/05/2018 08/07/2018
2456 B 06/04/2019 06/08/2018
2456 finance 11/01/2018 11/11/2018
2456 B 12/02/2018
I want to see the difference in days between 01/05/2019 for team A, and
10/08/2018 for team B.
Here is the query that I wrote, however, all I see is -1111111, any help please?:
SELECT A.incidentid,
( CASE
WHEN Max(B.[build validation]) <> 'No data'
AND Max(A.crfs) <> 'No data'
AND Max(B.[build validation]) < Max(A.crfs) THEN
Datediff(day, Max(B.[build validation]), Max(A.crfs))
ELSE -1111111
END ) AS 'Days-CRF-diff'
FROM (SELECT DISTINCT incidentid,
Iif(( ownedbyteam = 'B'
AND titlet LIKE '%Build validation%' ), Cast(
closeddatetimet AS NVARCHAR(255)), 'No data') AS
'Build Validation'
FROM incidentticketspecifics) B
INNER JOIN (SELECT incidentid,
Iif(( ownedbyteamt = 'B'
OR ownedbyteamt =
'Finance' ),
Cast(
closeddatetimet AS NVARCHAR(255)), 'No data') AS
'CRFS'
FROM incidentticketspecifics
GROUP BY incidentid,
ownedbyteamt,
closeddatetimet) CRF
ON A.incidentid = B.incidentid
GROUP BY A.incidentid
I hope the following answer will be of help.
With two subqueries for the two teams (A and B), the max date for every Ticket is brought. A left join between these two tables is performed to have these information in the same row in order to perform DATEDIFF. The last WHERE clause keeps the row with the dates greater for A team than team B.
Please change [YourDB] and [MytableName] in the following code with your names.
--Select the items to be viewed in the final view along with the difference in days
SELECT A.[TicketID],A.[OwnedbyTeamT], A.[Max_DateA],B.[OwnedbyTeamT], B.[Max_DateB], DATEDIFF(dd,B.[Max_DateB],A.[Max_DateA]) AS My_Diff
FROM
(
--The following subquery creates a table A with the max date for every project for team A
SELECT [TicketID]
,[OwnedbyTeamT]
,MAX([ClosedDate]) AS Max_DateA
FROM [YourDB].[dbo].[MytableName]
GROUP BY [TicketID],[OwnedbyTeamT]
HAVING [OwnedbyTeamT]='A')A
--A join between view A and B to bring the max dates for every project
LEFT JOIN (
--The max date for every project for team B
SELECT [TicketID]
,[OwnedbyTeamT]
,MAX([ClosedDate]) AS Max_DateB
FROM [YourDB].[dbo].[MytableName]
GROUP BY [TicketID],[OwnedbyTeamT]
HAVING [OwnedbyTeamT]='B')B
ON A.[TicketID]=B.[TicketID]
--Fill out the rows on the max dates for the teams
WHERE A.Max_DateA>B.Max_DateB
You might be able to do with a PIVOT. I am leaving a working example.
SELECT [TicketID], "A", "B", DATEDIFF(dd,"B","A") AS My_Date_Diff
FROM
(
SELECT [TicketID],[OwnedbyTeamT],MAX([ClosedDate]) AS My_Max
FROM [YourDB].[dbo].[MytableName]
GROUP BY [TicketID],[OwnedbyTeamT]
)Temp
PIVOT
(
MAX(My_Max)
FOR Temp.[OwnedbyTeamT] in ("A","B")
)PIV
WHERE "A">"B"
Your sample query is quite complicated and has conditions not mentioned in the text. It doesn't really help.
I want to calculate the datediff between the closeddates for teams A, and B, if the max closeddate for team A is greater than max closeddate team B. If it is smaller or null I don't want to see them.
I think you want this per TicketId. You can do this using conditional aggregation:
SELECT TicketId,
DATEDIFF(day,
MAX(CASE WHEN OwnedbyTeamT = 'B' THEN ClosedDate END),
MAX(CASE WHEN OwnedbyTeamT = 'A' THEN ClosedDate END) as diff
)
FROM incidentticketspecifics its
GROUP BY TicketId
HAVING MAX(CASE WHEN OwnedbyTeamT = 'A' THEN ClosedDate END) >
MAX(CASE WHEN OwnedbyTeamT = 'B' THEN ClosedDate END)

Querying a log table with nonlinear data

I'm trying to run some queries on a log-style table, which contains a bunch of nonlinear data. I have the following schema:
Signouts
+------------+----------------+------------+----------+
| signout_id | environment_id | date_start | date_end |
+------------+----------------+------------+----------+
| int | int | datetime | datetime |
+------------+----------------+------------+----------+
Environments
+-----+---------+
| id | name |
+-----+---------+
| int | varchar |
+-----+---------+
Signouts is the log table (and I say "log table" because records are never updated, only marked as "disabled" and added anew). When a user signs out an environment, their chosen start and end time is entered into the signouts table. Currently, to see if an environment is signed out, I simply check if the current date falls between date_start and date_end. If another user wants to sign out that environment, the minimum time they can choose is the ending date of the current signout.
I have a new challenge now, though. I now need to implement a reservation system. All of a sudden, dates can be anywhere in the future, and an environment can be reserved at any time. Now I need to know when an environment can still be signed out, and what those minimum (and now maximum) values are!
I've gotten it down to this naive plan, but I'm having trouble getting it into SQL:
get all signouts where start < curdate & end > curdate
if there is no current signout, get the min start of all signouts where start > curdate
if there is a signout, get the max end
Here is the closest I've gotten, among many other scrapped queries:
SELECT s.date_start_unavailable, s.date_available, e.id AS environment_id
FROM Environments AS e
LEFT OUTER JOIN (
SELECT TOP (100) PERCENT signout_id, environment_id, username, date_start, date_end, project, notes, in_use, max(date_end) as date_available, min(date_start) as date_start_unavailable
FROM dbo.Signouts
WHERE date_end >= GETDATE()
GROUP BY signout_id, environment_id, username, date_start, date_end, project, notes, in_use
ORDER BY date_start DESC
) AS s ON s.environment_id = e.id
This almost works. date_start_unavailable is the time at which the system becomes unavailable for a signout, and dave_available is the time when there are no more signouts. This still has problems, however; someone could reserve an environment years into the future for a month, and normal users wouldn't be able to see that most of the time is unallocated. I'll have to find a way to restrict this, but I can worry about that later.
The signouts last for arbitrary, user-entered amounts of time, otherwise implementing a time-block system would be trivial. If anybody can offer some DBA wisdom, it would be much appreciated!
Setting up my test-environment like this:
create table environment (id int, name varchar(255));
insert into environment values (1, 'DVD');
insert into environment values (2, 'BluRay');
create table signout (id int, environment_id int, date_start date, date_end date);
insert into signout values (1, 1, '01.11.2015', '09.11.2015');
insert into signout values (2, 1, '10.11.2015', '12.11.2015');
insert into signout values (3, 1, '01.12.2015', '24.12.2015');
insert into signout values (4, 2, '01.12.2015', '02.12.2015');
insert into signout values (5, 2, '04.12.2015', '07.12.2015');
insert into signout values (6, 2, '11.12.2015', '13.12.2015');
insert into signout values (7, 2, '14.12.2015', '23.12.2015');
Now, selecting the booked times is trivial:
select e.name, s.date_start d_start, s.date_end d_end, 'booked' as d_status FROM
signout s inner join environment e ON e.id = s.environment_id
Bu what about free times? These would be the times where no booking exists - so you join the table with itself in a given order:
select e.name, dateadd(DAY, 1, s.date_end) d_start,
COALESCE(dateadd(day, -1, s2.date_start), '31.12.2025') d_end, 'free'
FROM signout s
OUTER APPLY (
SELECT TOP 1 date_start, date_end from signout sx
WHERE sx.environment_id = s.environment_id
AND sx.date_start > s.date_end ORDER BY sx.date_start
) s2
inner join environment e ON e.id = s.environment_id
WHERE (s2.date_end is NULL OR s2.date_start > dateadd(DAY, 1, s.date_end))
Now UNION these together and add sort according to environment and date:
select e.name, s.date_start d_start, s.date_end d_end, 'booked' as d_status FROM
signout s inner join environment e ON e.id = s.environment_id
AND s.date_start > getdate()
UNION
select e.name, dateadd(DAY, 1, s.date_end) d_start,
COALESCE(dateadd(day, -1, s2.date_start), '31.12.2025') d_end, 'free'
FROM signout s
OUTER APPLY (
SELECT TOP 1 date_start, date_end from signout sx
WHERE sx.environment_id = s.environment_id
AND sx.date_start > s.date_end ORDER BY sx.date_start
) s2
inner join environment e ON e.id = s.environment_id
WHERE (s2.date_end is NULL OR s2.date_start > dateadd(DAY, 1, s.date_end))
AND s.date_start > getdate()
ORDER BY 1, 2
Here's what this gets me:
BluRay 2015-12-01 2015-12-02 booked
BluRay 2015-12-03 2015-12-03 free
BluRay 2015-12-04 2015-12-07 booked
BluRay 2015-12-08 2015-12-10 free
BluRay 2015-12-11 2015-12-13 booked
BluRay 2015-12-14 2015-12-23 booked
BluRay 2015-12-24 2025-12-31 free
DVD 2015-11-10 2015-11-12 booked
DVD 2015-11-13 2015-11-30 free
DVD 2015-12-01 2015-12-24 booked
DVD 2015-12-25 2025-12-31 free

Getting ranges that are not in database

I want to get all times that an event is not taking place for each room. The start of the day is 9:00:00 and end is 22:00:00.
What my database looks like is this:
Event EventStart EventEnd Days Rooms DayStarts
CISC 3660 09:00:00 12:30:00 Monday 7-3 9/19/2014
MATH 2501 15:00:00 17:00:00 Monday:Wednesday 7-2 10/13/2014
CISC 1110 14:00:00 16:00:00 Monday 7-3 9/19/2014
I want to get the times that aren't in the database.
ex. For SelectedDate (9/19/2014) the table should return:
Room FreeTimeStart FreeTimeEnd
7-3 12:30:00 14:00:00
7-3 16:00:00 22:00:00
ex2. SelectedDate (10/13/2014):
Room FreeTimeStart FreeTimeEnd
7-2 9:00:00 15:00:00
7-2 17:00:00 22:00:00
What I have tried is something like this:
select * from Events where ________ NOT BETWEEN eventstart AND eventend;
But I do not know what to put in the place of the space.
This was a pretty complex request. SQL works best with sets, and not looking at line by line. Here is what I came up with. To make it easier to figure out, I wrote it as a series of CTE's so I could work through the problem a step at a time. I am not saying that this is the best possible way to do it, but it doesn't require the use of any cursors. You need the Events table and a table of the room names (otherwise, you don't see a room that doesn't have any bookings).
Here is the query and I will explain the methodology.
DECLARE #Events TABLE (Event varchar(20), EventStart Time, EventEnd Time, Days varchar(50), Rooms varchar(10), DayStarts date)
INSERT INTO #Events
SELECT 'CISC 3660', '09:00:00', '12:30:00', 'Monday', '7-3', '9/19/2014' UNION
SELECT 'MATH 2501', '15:00:00', '17:00:00', 'Monday:Wednesday', '7-2', '10/13/2014' UNION
SELECT 'CISC 1110', '14:00:00', '16:00:00', 'Monday', '7-3', '9/19/2014'
DECLARE #Rooms TABLE (RoomName varchar(10))
INSERT INTO #Rooms
SELECT '7-2' UNION
SELECT '7-3'
DECLARE #SelectedDate date = '9/19/2014'
DECLARE #MinTimeInterval int = 30 --smallest time unit room can be reserved for
;WITH
D1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
),
D2(N) AS (SELECT 1 FROM D1 a, D1 b),
D4(N) AS (SELECT 1 FROM D2 a, D2 b),
Numbers AS (SELECT TOP 3600 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS Number FROM D4),
AllTimes AS
(SELECT CAST(DATEADD(n,Numbers.Number*#MinTimeInterval,'09:00:00') as time) AS m FROM Numbers
WHERE DATEADD(n,Numbers.Number*#MinTimeInterval,'09:00:00') <= '22:00:00'),
OccupiedTimes AS (
SELECT e.Rooms, ValidTimes.m
FROM #Events E
CROSS APPLY (SELECT m FROM AllTimes WHERE m BETWEEN CASE WHEN e.EventStart = '09:00:00' THEN e.EventStart ELSE DATEADD(n,1,e.EventStart) END and CASE WHEN e.EventEnd = '22:00:00' THEN e.EventEnd ELSE DATEADD(n,-1,e.EventEnd) END) ValidTimes
WHERE e.DayStarts = #SelectedDate
),
AllRoomsAllTimes AS (
SELECT * FROM #Rooms R CROSS JOIN AllTimes
), AllOpenTimes AS (
SELECT a.*, ROW_NUMBER() OVER( PARTITION BY (a.RoomName) ORDER BY a.m) AS pos
FROM AllRoomsAllTimes A
LEFT OUTER JOIN OccupiedTimes o ON a.RoomName = o.Rooms AND a.m = o.m
WHERE o.m IS NULL
), Finalize AS (
SELECT a1.RoomName,
CASE WHEN a3.m IS NULL OR DATEDIFF(n,a3.m, a1.m) > #MinTimeInterval THEN a1.m else NULL END AS FreeTimeStart,
CASE WHEN a2.m IS NULL OR DATEDIFF(n,a1.m,a2.m) > #MinTimeInterval THEN A1.m ELSE NULL END AS FreeTimeEnd,
ROW_NUMBER() OVER( ORDER BY a1.RoomName ) AS Pos
FROM AllOpenTimes A1
LEFT OUTER JOIN AllOpenTimes A2 ON a1.RoomName = a2.RoomName and a1.pos = a2.pos-1
LEFT OUTER JOIN AllOpenTimes A3 ON a1.RoomName = a3.RoomName and a1.pos = a3.pos+1
WHERE A2.m IS NULL OR DATEDIFF(n,a1.m,a2.m) > #MinTimeInterval
OR
A3.m IS NULL OR DATEDIFF(n,a3.m, a1.m) > #MinTimeInterval
)
SELECT F1.RoomName, f1.FreeTimeStart, f2.FreeTimeEnd FROM Finalize F1
LEFT OUTER JOIN Finalize F2 ON F1.Pos = F2.pos-1 AND f1.RoomName = f2.RoomName
WHERE f1.pos % 2 = 1
In the first several lines, I create temp variables to simulate your tables Events and Rooms.
The variable #MinTimeInterval determines what time interval the room schedules can be on (every 30 min, 15 min, etc - this number needs to divide evenly into 60).
Since SQL cannot query data that is missing, we need to create a table that holds all of the times that we want to check for. The first several lines in the WITH create a table called AllTimes which are all the possible time intervals in your day.
Next, we get a list of all of the times that are occupied (OccupiedTimes), and then LEFT OUTER JOIN this table to the AllTimes table which gives us all the available times. Since we only want the start and end of each free time, create the Finalize table which self joins each record to the previous and next record in the table. If the times in these rows are greater than #MinTimeInterval, then we know it is either a start or end of a free time.
Finally we self join this last table to put the start and end times in the same row and only look at every other row.
This will need to be adjusted if a single row in Events spans multiple days or multiple rooms.
Here's a solution that will return the "complete picture" including rooms that aren't booked at all for the day in question:
Declare #Date char(8) = '20141013'
;
WITH cte as
(
SELECT *
FROM -- use your table name instead of the VALUES construct
(VALUES
('09:00:00','12:30:00' ,'7-3', '20140919'),
('15:00:00','17:00:00' ,'7-2', '20141013'),
('14:00:00','16:00:00' ,'7-3', '20140919')) x(EventStart , EventEnd,Rooms, DayStarts)
), cte_Days_Rooms AS
-- get a cartesian product for the day specified and all rooms as well as the start and end time to compare against
(
SELECT y.EventStart,y.EventEnd, x.rooms,a.DayStarts FROM
(SELECT #Date DayStarts) a
CROSS JOIN
(SELECT DISTINCT Rooms FROM cte)x
CROSS JOIN
(SELECT '09:00:00' EventStart,'09:00:00' EventEnd UNION ALL
SELECT '22:00:00' EventStart,'22:00:00' EventEnd) y
), cte_1 AS
-- Merge the original data an the "base data"
(
SELECT * FROM cte WHERE DayStarts=#Date
UNION ALL
SELECT * FROM cte_Days_Rooms
), cte_2 as
-- use the ROW_NUMBER() approach to sort the data
(
SELECT *, ROW_NUMBER() OVER(PARTITION BY DayStarts, Rooms ORDER BY EventStart) as pos
FROM cte_1
)
-- final query: self join with an offest of one row, eliminating duplicate rows if a room is booked starting 9:00 or ending 22:00
SELECT c2a.DayStarts, c2a.Rooms , c2a.EventEnd, c2b.EventStart
FROM cte_2 c2a
INNER JOIN cte_2 c2b on c2a.DayStarts = c2b.DayStarts AND c2a.Rooms =c2b.Rooms AND c2a.pos = c2b.pos -1
WHERE c2a.EventEnd <> c2b.EventStart
ORDER BY c2a.DayStarts, c2a.Rooms

Finding overlapping dates

I have a set of Meeting rooms and meetings in that having start date and end Date. A set of meeting rooms belong to a building.
The meeting details are kept in MeetingDetail table having a startDate and endDate.
Now I want to fire a report between two time period say reportStartDate and reportEndDate, which finds me the time slots in which all the meeting rooms are booked for a given building
Table structure
MEETING_ROOM - ID, ROOMNAME, BUILDING_NO
MEETING_DETAIL - ID, MEETING_ROOM_ID, START_DATE, END_DATE
The query has to be fired for reportStartDate and REportEndDate
Just to clarify further, the aim is to find all the time slots in which all the meeting rooms were booked in a given time period of reportStartDate and reportEndDate
For SQL Server 2005+ you could try the following (see note at the end for mysql)
WITH TIME_POINTS (POINT_P) AS
(SELECT DISTINCT START_DATE FROM MEETING_DETAIL
WHERE START_DATE > #reportStartDate AND START_DATE < #reportEndDate
UNION SELECT DISTINCT END_DATE FROM MEETING_DETAIL
WHERE END_DATE > #reportStartDate AND END_DATE < #reportEndDate
UNION SELECT #reportEndDate
UNION SELECT #reportStartDate),
WITH TIME_SLICE (START_T, END_T) AS
(SELECT A.POINT_P, MIN(B.POINT_P) FROM
TIMEPOINTS A
INNER JOIN TIMEPOINTS B ON A.POINT_P > B.POINT_P
GROUP BY A.POINT_P),
WITH SLICE_MEETINGS (START_T, END_T, MEETING_ROOM_ID, BUILDING_NO) AS
(SELECT START_T, END_T, MEETING_ROOM_ID, BUILDING_NO FROM
TIME_SLICE A
INNER JOIN MEETING_DETAIL B ON B.START_DATE <= A.START_T AND B.END_DATE >= B.END_T
INNER JOIN MEETING_ROOM C ON B.MEETING_ROOM_ID = C.ID),
WITH SLICE_COUNT (START_T, END_T, BUILDING_NO, ROOMS_C) AS
(SELECT START_T, END_T, BUILDING_NO, COUNT(MEETING_ROOM_ID) FROM
SLICE_MEETINGS
GROUP BY START_T, END_T, BUILDING_NO),
WITH ROOMS_BUILDING (BUILDING_NO, ROOMS_C) AS
(SELECT BUILDING_NO, COUNT(ID) FROM
MEETING_ROOM
GROUP BY BUILDING_NO)
SELECT B.BUILDING_NO, A.START_T, A.END_T
FROM SLICE_COUNT A.
INNER JOIN ROOMS_BUILDING B WHERE A.BUILDING_NO = B.BUILDING_NO AND B.ROOMS_C = A.ROOMS_C;
what it does is (each step corresponds to each CTE definition above)
Get all the time markers, i.e. end or start times
Get all time slices i.e. the smallest unit of time between which there is no other time marker (i.e. no meetings start in a time slice, it's either at the beginning or at the end of a time slice)
Get meetings for each time slice, so now you get something like
10.30 11.00 Room1 BuildingA
10.30 11.00 Room2 BuildingA
11.00 12.00 Room1 BuildingA
Get counts of rooms booked per building per time slice
Filter out timeslice-building combinations that match the number of rooms in each building
Edit
Since mysql doesn't support the WITH clause you'll have to construct views for each (of the 5) WITH clases above. everything else would remain the same.
After reading your comment, I think I understand the problem a bit better. As a first step I would generate a matrix of meeting rooms and time slots using cross join:
select *
from (
select distinct start_date
, end_date
from #meeting_detail
) ts
cross join
#meeting_room mr
Then, for each cell in the matrix, add meetings in that timeslot:
left join
#meeting_detail md
on mr.id = md.meeting_room_id
and ts.start_date < md.end_date
and md.start_date < ts.end_date
And then demand that there are no free rooms. For example, by saying that the left join must succeed for all rooms and time slots. A left join succeeds if any field is not null:
group by
mr.building_no
, ts.start_date
, ts.end_date
having max(case when md.meeting_room_id is null
then 1 else 0 end) = 0
Here's a complete working example. It's written for SQL Server, and the table variables (#meeting_detail) won't work in MySQL. But the report generating query should work in most databases:
set nocount on
declare #meeting_room table (id int, roomname varchar(50),
building_no int)
declare #meeting_detail table (meeting_room_id int,
start_date datetime, end_date datetime)
insert #meeting_room (id, roomname, building_no)
select 1, 'Kitchen', 6
union all select 2, 'Ballroom', 6
union all select 3, 'Conservatory', 7
union all select 4, 'Dining Room', 7
insert #meeting_detail (meeting_room_id, start_date, end_date)
select 1, '2010-08-01 9:00', '2010-08-01 10:00'
union all select 1, '2010-08-01 10:00', '2010-08-01 11:00'
union all select 2, '2010-08-01 10:00', '2010-08-01 11:00'
union all select 3, '2010-08-01 10:00', '2010-08-01 11:00'
select mr.building_no
, ts.start_date
, ts.end_date
from (
select distinct start_date
, end_date
from #meeting_detail
) ts
cross join
#meeting_room mr
left join
#meeting_detail md
on mr.id = md.meeting_room_id
and ts.start_date < md.end_date
and md.start_date < ts.end_date
group by
mr.building_no
, ts.start_date
, ts.end_date
having max(case when md.meeting_room_id is null
then 1 else 0 end) = 0
This prints:
building_no start end
6 2010-08-01 10:00:00.000 2010-08-01 11:00:00.000