I am working with SQL Server statements and have one table like:
| item | value | parentItem |
+------+-------+------------+
| 1 | 2test | 2 |
| 2 | 3test | 3 |
| 3 | 4test | 4 |
| 5 | 1test | 1 |
| 6 | 3test | 3 |
| 7 | 2test | 2 |
And I would like to get the below result using a SQL Server statement:
| item1 | value1 |
+-------+--------------------------+
| 1 | /4test/3test/2test |
| 2 | /4test/3test |
| 3 | /4test |
| 5 | /4test/3test/2test/1test |
| 6 | /4test/3test |
| 7 | /4test/3test/2test |
I didn't figure out the correct SQL to get all the values for all the ids according to parentItem.
I have tried this SQL :
with all_path as
(
select item, value, parentItem
from table
union all
select a.item, a.value, a.parentItem
from table a, all_path b
where a.item = b.parentItem
)
select
item as item1,
stuff(select '/' + value
from all_path
order by item asc
for xml path ('')), 1, 0, '') as value1
from
all_path
But got the "value1" column in result like
/4test/4test/4test/3test/3test/3test/3test/2test/2test/2test/2test
Could you please help me with that? Thanks a lot.
based on the expected output you gave, use the recursive part to concatenate the value
;with yourTable as (
select item, value, parentItem
from (values
(1,'2test',2)
,(2,'3test',3)
,(3,'4test',4)
,(5,'1test',1)
,(6,'3test',3)
,(7,'2test',2)
)x (item,value,parentItem)
)
, DoRecursivePart as (
select 1 as Pos, item, convert(varchar(max),value) value, parentItem
from yourTable
union all
select drp.pos +1, drp.item, convert(varchar(max), yt.value + '/' + drp.value), yt.parentItem
from yourTable yt
inner join DoRecursivePart drp on drp.parentItem = yt.item
)
select drp.item, '/' + drp.value
from DoRecursivePart drp
inner join (select item, max(pos) mpos
from DoRecursivePart
group by item) [filter] on [filter].item = drp.item and [filter].mpos = drp.Pos
order by item
gives
item value
----------- ------------------
1 /4test/3test/2test
2 /4test/3test
3 /4test
5 /4test/3test/2test/1test
6 /4test/3test
7 /4test/3test/2test
Here's the sample data
drop table if exists dbo.test_table;
go
create table dbo.test_table(
item int not null,
[value] varchar(100) not null,
parentItem int not null);
insert dbo.test_table values
(1,'test1',2),
(2,'test2',3),
(3,'test3',4),
(5,'test4',1),
(6,'test5',3),
(7,'test6',2);
Here's the query
;with recur_cte(item, [value], parentItem, h_level) as (
select item, [value], parentItem, 1
from dbo.test_table tt
union all
select rc.item, tt.[value], tt.parentItem, rc.h_level+1
from dbo.test_table tt join recur_cte rc on tt.item=rc.parentItem)
select rc.item,
stuff((select '/' + cast(parentItem as varchar)
from recur_cte c2
where rc.item = c2.item
order by h_level desc FOR XML PATH('')), 1, 1, '') [value1]
from recur_cte rc
group by item;
Here's the results
item value1
1 4/3/2
2 4/3
3 4
5 4/3/2/1
6 4/3
7 4/3/2
The code below is working now in the VIEW based on Windows Authentication, users should able to see all the data that they own and data of those reports to them direct or indirect. Now another WHERE clause needed to handle the additional result of data that giving to the user in the Authorize column.
SAMPLE DATA: Table TORGANIZATION_HIERARCHY
ManagerID | ManagerEmail | Email | EmployeeID | Authorize | Level
---------------------------------------------------------------------------------
NULL | NULL | user0##abc.com | 1 | NULL | 0
1 | user0##abc.com | user1##abc.com | 273 | NULL | 1
273 | user1##abc.com | user2##abc.com | 16 | NULL | 2
273 | user1##abc.com | SJiang##abc.com | 274 | NULL | 2
273 | user1##abc.com | SAbbas#abc.com | 285 | user2##abc.com; user3#abc.com | 2
285 | SAbbas#abc.com | LTsoflias#abc.com | 286 | NULL | 3
274 | SJiang##abc.com | MBlythe#abc.com | 275 | NULL | 3
274 | SJiang##abc.com | LMitchell#abc.com | 276 | NULL | 3
16 | JWhite#abc.com | user3#abc.com | 23 | NULL | 3
SAMPLE DATA: Table TRANS
Email | Destination_account | Customer_service_rep_code
-----------------------------------------------------------
SAbbas#abc.com | Philippines | 12646
Junerk#abc.com | Canada | 95862
LTsoflias#abc.com | Italy | 98524
user2##abc.com | Italy | 29185
user3##abc.com | Brazil | 58722
The bottom query is working when user SAbbas#abc.com (285) log in. It can see all the data of EmployeeID 285 and 286. I need add another where statement that user (SAbbas#abc.com) authorized to see to see in column Authorize. So the result user SAbbas#abc.com should see EmployeeID 285, 286, 16, 23.
WITH CTE
AS (SELECT OH.employeeid,
OH.managerid,
OH.email AS EMPEMAIL,
1 AS level
FROM TORGANIZATION_HIERARCHY OH
WHERE OH.[email] = (SELECT SYSTEM_USER) --Example SAbbas#abc.com
UNION ALL
SELECT CHIL.employeeid,
CHIL.managerid,
CHIL.email,
level + 1
FROM TORGANIZATION_HIERARCHY CHIL
JOIN CTE PARENT
ON CHIL.[managerid] = PARENT.[employeeid]),
ANOTHERCTE
AS (SELECT
T.[email],
T.[destination_account],
T.[customer_service_rep_code]
FROM [KGFGJK].[DBO].[TRANS] AS T)
SELECT *
FROM ANOTHERCTE
INNER JOIN CTE
ON CTE.empemail = ANOTHERCTE.[email];
This will give you what you need based on column Authorize. The result should be 16 and 23
Select b.employeeid from TORGANIZATION_HIERARCHY a inner join TORGANIZATION_HIERARCHY b
on a.Authorize like '%' + b.Email + '%'
where a.Email = 'SAbbas#abc.com'
Let me know
Complete Solution:
For you to be able to see user3#abc.com, I had to correct the email in 6the table #TRANS. You worte in there user3##abc.com instead of user3#abc.com. # and not ##.
the code is below for your tests. After you can replace with you table names
IF OBJECT_ID('tempdb..#TORGANIZATION_HIERARCHY') IS NOT NULL DROP TABLE #TORGANIZATION_HIERARCHY;
select NULL as ManagerID ,NULL as ManagerEmail ,'user0##abc.com' as Email ,1 as EmployeeID ,NULL as Authorize , 0 as Level into #TORGANIZATION_HIERARCHY
union select 1 ,'user0##abc.com', 'user1##abc.com' ,273 ,NULL , 1
union select 273 ,'user1##abc.com', 'user2##abc.com' ,16 ,NULL , 2
union select 273 ,'user1##abc.com', 'SJiang##abc.com' ,274 ,NULL , 2
union select 273 ,'user1##abc.com', 'SAbbas#abc.com' ,285 ,'user2##abc.com; user3#abc.com' , 2
union select 285 ,'SAbbas#abc.com', 'LTsoflias#abc.com' ,286 ,NULL , 3
union select 274 ,'SJiang##abc.com', 'MBlythe#abc.com' ,275 ,NULL , 3
union select 274 ,'SJiang##abc.com', 'LMitchell#abc.com' ,276 ,NULL , 3
union select 16 ,'JWhite#abc.com', 'user3#abc.com' ,23 ,NULL , 3
--select * from #TORGANIZATION_HIERARCHY
IF OBJECT_ID('tempdb..#TRANS') IS NOT NULL DROP TABLE #TRANS;
select 'SAbbas#abc.com' as Email , 'Philippines' as Destination_account , 12646 as Customer_service_rep_code into #TRANS
union select 'Junerk#abc.com' , 'Canada' , 95862
union select 'LTsoflias#abc.com', 'Italy' , 98524
union select 'user2##abc.com' , 'Italy' , 29185
union select 'user3#abc.com' , 'Brazil' , 58722
;WITH CTE
AS (SELECT OH.employeeid,
OH.managerid,
OH.Authorize,
OH.email AS EMPEMAIL,
1 AS [level]
FROM #TORGANIZATION_HIERARCHY OH
WHERE OH.[email] = (SELECT 'SAbbas#abc.com') --Example
UNION ALL
SELECT CHIL.employeeid,
CHIL.managerid,
CHIL.Authorize,
CHIL.email,
CHIL.[level] + 1
FROM #TORGANIZATION_HIERARCHY CHIL
JOIN CTE PARENT
ON CHIL.[managerid] = PARENT.[employeeid]),
ANOTHERCTE
AS (SELECT
T.[email],
T.[destination_account],
T.[customer_service_rep_code]
FROM #TRANS AS T)
SELECT *
FROM ANOTHERCTE
RIGHT JOIN
(
select a.EmployeeID, a.ManagerID, a.Authorize, a.Email as empemail, a.[level] From CTE INNER JOIN #TORGANIZATION_HIERARCHY a on lower(CTE.Authorize) like '%' + lower(a.Email) + '%'
union
select * From CTE
) CTE
ON CTE.empemail = ANOTHERCTE.[email]
order by [level]
Output:
I have 2 tables
Table1
+---------+--------+-------+----+
| CALDATE | GROOMS | ROOMS | fn |
+---------+--------+-------+----+
| 1/5/18 | 15 | 17 | A12|
| 1/5/18 | 0 | 0 | A12|
| 1/6/18 | 0 | 0 | B34|
| 1/6/18 | 75 | 77 | B34|
| 1/7/18 | 123 | 125 | C56|
| 1/7/18 | 0 | 0 | C56|
+---------+--------+-------+----+
-
Table2
+----------+--------+----+
| ROOMDATE | pickup | FN |
+----------+--------+----+
| 1/5/18 | 0 | A12|
| 1/5/18 | 2 | A12|
| 1/5/18 | 1 | A12|
| 1/5/18 | 7 | A12|
| 1/6/18 | 2 | B34|
| 1/6/18 | 1 | B34|
| 1/6/18 | 13 | B34|
| 1/7/18 | 3 | C56|
| 1/7/18 | 0 | C56|
| 1/7/18 | 12 | C56|
+----------+--------+----+
Querying each I use
Select caldate as date, sum(grooms) as g, sum (rooms) as r
from Table1
and
Select roomdate as date, sum(pickup) as p
from Table2
These each give me the info I'm expecting, however when I try and join them things get wonky. I was hoping for something like
Select caldate as date,
sum(grooms) as g,
sum(rooms) as r,
sum(pickup) as p
from Table1
inner join table2 on table1.fn = table2.fn
But that returns way too high of each.
How do I join these queries so that I get my expected output of
+--------+-----+-----+----+----+
| Date | g | r | p | fn |
+--------+-----+-----+----+----+
| 1/5/18 | 15 | 17 | 10 | A12|
| 1/6/18 | 75 | 77 | 16 | B34|
| 1/7/18 | 123 | 125 | 15 | C56|
+--------+-----+-----+----+----+
Each row in your first table will match with each available row in the other table based on your join predicate. Take fn = A12 for example: since you have 2 rows in table1 and 4 rows in table2, you will end up with (4x2) 8 rows in your result set. That will cause your sums to be higher than they should be.
One way to fix this is to use derived tables to get your sums, then join them together:
SELECT t1.date, g, r, p, t1.fn
FROM (SELECT fn, caldate as date, sum(grooms) as g, sum (rooms) as r
FROM Table1
GROUP BY fn, caldate) t1
JOIN (SELECT fn, roomdate as date, sum(pickup) as p
FROM Table2
GROUP BY fn, roomdate) t2 on t1.fn = t2.fn
This makes sure there is one row returned from each table before the join.
You should be Grouping by 'caldate'. This way you will only be getting the sums per date.
Select caldate as date,
sum(grooms) as g,
sum(rooms) as r,
sum(pickup) as p
from Table1
inner join table2 on table1.fn = table2.fn
group by caldate
The reason that you get more rows than expected is because of join condition. if you want to find grooms and rooms for each fn on each day you have add date to join condition as well:
Select table1.caldate as date,
sum(grooms) as g,
sum(rooms) as r,
sum(pickup) as p
from table1
inner join table2 on table1.fn = table2.fn
and table1.caldate = table2.roomdate
Here's an answer that allows you to group by FN and date:
select format(caldate, 'M/d/yyyy') as date, sum(grooms) as g, sum(rooms) as r, t2.p, t1.fn
from Table1 t1
inner join (
select fn, roomdate, sum(pickup) as p from Table2 group by fn, roomdate
)t2 on t1.fn = t2.fn and t1.caldate = t2.roomdate
group by t1.caldate, t1.fn, t2.p
I created some sample data with DML statements so you can test grouping by different combinations of FN and date, and the output:
declare #t1 table (caldate datetime, grooms int, rooms int, fn varchar(3))
declare #t2 table (roomdate datetime, pickup int, fn varchar(3))
insert into #t1 select '1/5/18', 15, 17,'A12'
insert into #t1 select '1/5/18', 0, 0,'A12'
insert into #t1 select '1/6/18', 0, 0,'B34'
insert into #t1 select '1/6/18', 75, 77,'B34'
insert into #t1 select '1/7/18',123,125,'C56'
insert into #t1 select '1/8/18',100,200,'C56' -- changed to 1/8/18, changed vals
insert into #t2 select '1/5/18', 0 ,'A12'
insert into #t2 select '1/5/18', 2 ,'A12'
insert into #t2 select '1/5/18', 1 ,'A12'
insert into #t2 select '1/5/18', 7 ,'A12'
insert into #t2 select '1/6/18', 2 ,'B34'
insert into #t2 select '1/6/18', 1 ,'B34'
insert into #t2 select '1/6/18',13 ,'B34'
insert into #t2 select '1/7/18', 3 ,'C56'
insert into #t2 select '1/7/18', 0 ,'C56'
insert into #t2 select '1/8/18',12 ,'C56' -- changed to 1/8/18
select format(caldate, 'M/d/yyyy') as date, sum(grooms) as g, sum(rooms) as r, t2.p, t1.fn
from #t1 t1
inner join (
select fn, roomdate, sum(pickup) as p from #t2 group by fn, roomdate
)t2 on t1.fn = t2.fn and t1.caldate = t2.roomdate
group by t1.caldate, t1.fn, t2.p
Output:
date g r p fn
1/5/2018 15 17 10 A12
1/6/2018 75 77 16 B34
1/7/2018 123 125 3 C56
1/8/2018 100 200 12 C56
If you don't also join by date, then adding different combinations of caldate/fn give you duplicates. You must mean to join by the date as well, right?
CODE:
CREATE TABLE #Temp1 (CoachID INT, BusyST DATETIME, BusyET DATETIME)
CREATE TABLE #Temp2 (CoachID INT, AvailableST DATETIME, AvailableET DATETIME)
INSERT INTO #Temp1 (CoachID, BusyST, BusyET)
SELECT 1,'2016-08-17 09:12:00','2016-08-17 10:11:00'
UNION
SELECT 3,'2016-08-17 09:30:00','2016-08-17 10:00:00'
UNION
SELECT 4,'2016-08-17 12:07:00','2016-08-17 13:10:00'
INSERT INTO #Temp2 (CoachID, AvailableST, AvailableET)
SELECT 1,'2016-08-17 09:07:00','2016-08-17 11:09:00'
UNION
SELECT 2,'2016-08-17 09:11:00','2016-08-17 09:30:00'
UNION
SELECT 3,'2016-08-17 09:24:00','2016-08-17 13:08:00'
UNION
SELECT 1,'2016-08-17 11:34:00','2016-08-17 12:27:00'
UNION
SELECT 4,'2016-08-17 09:34:00','2016-08-17 13:00:00'
UNION
SELECT 5,'2016-08-17 09:10:00','2016-08-17 09:55:00'
--RESULT-SET QUERY GOES HERE
DROP TABLE #Temp1
DROP TABLE #Temp2
DESIRED OUTPUT:
CoachID CanCoachST CanCoachET NumOfCoaches
1 2016-08-17 09:12:00.000 2016-08-17 09:24:00.000 2 --(ID2 = 2,5)
1 2016-08-17 09:24:00.000 2016-08-17 09:30:00.000 3 --(ID2 = 2,3,5)
1 2016-08-17 09:30:00.000 2016-08-17 09:34:00.000 1 --(ID2 = 5)
1 2016-08-17 09:34:00.000 2016-08-17 09:55:00.000 2 --(ID2 = 4,5)
1 2016-08-17 09:55:00.000 2016-08-17 10:00:00.000 1 --(ID2 = 4)
1 2016-08-17 10:00:00.000 2016-08-17 10:11:00.000 2 --(ID2 = 3,4)
3 2016-08-17 09:30:00.000 2016-08-17 09:34:00.000 1 --(ID2 = 5)
3 2016-08-17 09:34:00.000 2016-08-17 09:55:00.000 2 --(ID2 = 4,5)
3 2016-08-17 09:55:00.000 2016-08-17 10:00:00.000 1 --(ID2 = 4)
4 2016-08-17 12:07:00.000 2016-08-17 12:27:00.000 2 --(ID2 = 1,3)
4 2016-08-17 12:27:00.000 2016-08-17 13:08:00.000 1 --(ID2 = 3)
4 2016-08-17 13:08:00.000 2016-08-17 13:10:00.000 0 --(No one is available)
GOAL:
Consider #Temp1 as the table of team coaches (ID1) and their meeting times (ST1 = Meeting Start Time and ET1 = Meeting End Time).
Consider #Temp2 as the table of team coaches (ID2) and their total available times (ST2 = Available Start Time and ET2 = Available End Time).
Now, the goal is to find all possible coaches from #Temp2 who are available to coach during the meeting time of the coaches from #Temp1.
So for example, For the coach ID1 = 1, who is busy between 9:12 and 10:11 (data can span across multiple days, if that info matters), we have
coach ID2 = 2 and 5 that can coach between 9:12 and 9:24
, coach ID2 = 2,3, and 5 that can coach between 9:24 and 9:30
, coach ID2 = 5 that can coach between 9:30 and 9:34
, coach ID2 = 4 and 5 that can coach between 9:34 and 9:55
, coach ID2 = 4 that can coach between 9:55 and 10:00
, and coach ID2 = 3 and 4 that can coach between 10:00 and 10:11 (note how ID 3, although available in #Temp2 table between 9:24 and 13:08, it can't coach for ID1 = 1 between 9:24 and 10:00 because its also busy between 9:30 and 10:00.
My effort so far: Only dealing with breaking #Temp1's time bracket so far. Still need to figure out A) how to remove that non-busy time window from the output B) add a field/map it to right T1's CoachIDs.
;WITH ED
AS (SELECT BusyET, CoachID FROM #Temp1
UNION ALL
SELECT BusyST, CoachID FROM #Temp1
)
,Brackets
AS (SELECT MIN(BusyST) AS BusyST
,( SELECT MIN(BusyET)
FROM ED e
WHERE e.BusyET > MIN(BusyST)
) AS BusyET
FROM #Temp1 T
UNION ALL
SELECT B.BusyET
,e.BusyET
FROM Brackets B
INNER JOIN ED E ON B.BusyET < E.BusyET
WHERE NOT EXISTS (
SELECT *
FROM ED E2
WHERE E2.BusyET > B.BusyET
AND E2.BusyET < E.BusyET
)
)
SELECT *
FROM Brackets
ORDER BY BusyST;
I think I need to join on comparing ST/ET dates between two tables where IDs don't match each other. But I'm having trouble figuring out how to actually get only the meeting time window and unique count.
Updated with better schema/data-set. Also note, even though CoachID 4 is not "scheduled" to be available, he's still listed as busy for that last few minutes. And there can be scenario where no one else is available to work during that time, in which case, we can return 0 cnt record (or not return it if it's really complicated).
Again, the goal is to find count and combination of all available CoachIDs and their Available time window that can coach for the CoachIDs listed in the busy table.
Updated with more sample description matching sample data.
The query in this answer was inspired by the Packing Intervals by Itzik Ben-Gan.
At first I didn't understand the full complexity of the requirements and assumed that intervals in Table1 and Table2 don't overlap. I assumed that the same coach can't be busy and available at the same time.
It turns out that my assumption was wrong, so the first variant of the query that I'm leaving below has to be extended with preliminary step that subtracts all intervals stored in Table1 from intervals stored in Table2.
It uses the similar idea. Each start of the "available" interval is marked with +1 EventType and end of the "available" interval is marked with -1 EventType. For "busy" intervals the marks are reversed. "Busy" interval starts with -1 and ends with +1. This is done in C1_Subtract.
Then running total tells us where the "truly" available intervals are (C2_Subtract). Finally, CTE_Available leaves only "truly" available intervals.
Sample data
I added few rows to illustrate what happens if no coaches are available. I also added CoachID=9, which is not in the initial results of the first variant of the query.
CREATE TABLE #Temp1 (CoachID INT, BusyST DATETIME, BusyET DATETIME);
CREATE TABLE #Temp2 (CoachID INT, AvailableST DATETIME, AvailableET DATETIME);
-- Start time is inclusive
-- End time is exclusive
INSERT INTO #Temp1 (CoachID, BusyST, BusyET) VALUES
(1, '2016-08-17 09:12:00','2016-08-17 10:11:00'),
(3, '2016-08-17 09:30:00','2016-08-17 10:00:00'),
(4, '2016-08-17 12:07:00','2016-08-17 13:10:00'),
(6, '2016-08-17 15:00:00','2016-08-17 16:00:00'),
(9, '2016-08-17 15:00:00','2016-08-17 16:00:00');
INSERT INTO #Temp2 (CoachID, AvailableST, AvailableET) VALUES
(1,'2016-08-17 09:07:00','2016-08-17 11:09:00'),
(2,'2016-08-17 09:11:00','2016-08-17 09:30:00'),
(3,'2016-08-17 09:24:00','2016-08-17 13:08:00'),
(1,'2016-08-17 11:34:00','2016-08-17 12:27:00'),
(4,'2016-08-17 09:34:00','2016-08-17 13:00:00'),
(5,'2016-08-17 09:10:00','2016-08-17 09:55:00'),
(7,'2016-08-17 15:10:00','2016-08-17 15:20:00'),
(8,'2016-08-17 15:15:00','2016-08-17 15:25:00'),
(7,'2016-08-17 15:40:00','2016-08-17 15:55:00'),
(9,'2016-08-17 15:05:00','2016-08-17 15:07:00'),
(9,'2016-08-17 15:40:00','2016-08-17 16:55:00');
Intermediate results of CTE_Available
+---------+-------------------------+-------------------------+
| CoachID | AvailableST | AvailableET |
+---------+-------------------------+-------------------------+
| 1 | 2016-08-17 09:07:00.000 | 2016-08-17 09:12:00.000 |
| 1 | 2016-08-17 10:11:00.000 | 2016-08-17 11:09:00.000 |
| 1 | 2016-08-17 11:34:00.000 | 2016-08-17 12:27:00.000 |
| 2 | 2016-08-17 09:11:00.000 | 2016-08-17 09:30:00.000 |
| 3 | 2016-08-17 09:24:00.000 | 2016-08-17 09:30:00.000 |
| 3 | 2016-08-17 10:00:00.000 | 2016-08-17 13:08:00.000 |
| 4 | 2016-08-17 09:34:00.000 | 2016-08-17 12:07:00.000 |
| 5 | 2016-08-17 09:10:00.000 | 2016-08-17 09:55:00.000 |
| 7 | 2016-08-17 15:10:00.000 | 2016-08-17 15:20:00.000 |
| 7 | 2016-08-17 15:40:00.000 | 2016-08-17 15:55:00.000 |
| 8 | 2016-08-17 15:15:00.000 | 2016-08-17 15:25:00.000 |
| 9 | 2016-08-17 16:00:00.000 | 2016-08-17 16:55:00.000 |
+---------+-------------------------+-------------------------+
Now we can use these intermediate results of CTE_Available instead of #Temp2 in the first variant of the query. See detailed explanations below the first variant of the query.
Full query
WITH
C1_Subtract
AS
(
SELECT
CoachID
,AvailableST AS ts
,+1 AS EventType
FROM #Temp2
UNION ALL
SELECT
CoachID
,AvailableET AS ts
,-1 AS EventType
FROM #Temp2
UNION ALL
SELECT
CoachID
,BusyST AS ts
,-1 AS EventType
FROM #Temp1
UNION ALL
SELECT
CoachID
,BusyET AS ts
,+1 AS EventType
FROM #Temp1
)
,C2_Subtract AS
(
SELECT
C1_Subtract.*
,SUM(EventType)
OVER (
PARTITION BY CoachID
ORDER BY ts, EventType DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
AS cnt
,LEAD(ts)
OVER (
PARTITION BY CoachID
ORDER BY ts, EventType DESC)
AS NextTS
FROM C1_Subtract
)
,CTE_Available
AS
(
SELECT
C2_Subtract.CoachID
,C2_Subtract.ts AS AvailableST
,C2_Subtract.NextTS AS AvailableET
FROM C2_Subtract
WHERE cnt > 0
)
,CTE_Intervals
AS
(
SELECT
TBusy.CoachID AS BusyCoachID
,TBusy.BusyST
,TBusy.BusyET
,CA.CoachID AS AvailableCoachID
,CA.AvailableST
,CA.AvailableET
-- max of start time
,CASE WHEN CA.AvailableST < TBusy.BusyST
THEN TBusy.BusyST
ELSE CA.AvailableST
END AS ST
-- min of end time
,CASE WHEN CA.AvailableET > TBusy.BusyET
THEN TBusy.BusyET
ELSE CA.AvailableET
END AS ET
FROM
#Temp1 AS TBusy
CROSS APPLY
(
SELECT
TAvailable.*
FROM
CTE_Available AS TAvailable
WHERE
-- the same coach can't be available and busy
TAvailable.CoachID <> TBusy.CoachID
-- intervals intersect
AND TAvailable.AvailableST < TBusy.BusyET
AND TAvailable.AvailableET > TBusy.BusyST
) AS CA
)
,C1 AS
(
SELECT
BusyCoachID
,AvailableCoachID
,ST AS ts
,+1 AS EventType
FROM CTE_Intervals
UNION ALL
SELECT
BusyCoachID
,AvailableCoachID
,ET AS ts
,-1 AS EventType
FROM CTE_Intervals
UNION ALL
SELECT
CoachID AS BusyCoachID
,CoachID AS AvailableCoachID
,BusyST AS ts
,+1 AS EventType
FROM #Temp1
UNION ALL
SELECT
CoachID AS BusyCoachID
,CoachID AS AvailableCoachID
,BusyET AS ts
,-1 AS EventType
FROM #Temp1
)
,C2 AS
(
SELECT
C1.*
,SUM(EventType)
OVER (
PARTITION BY BusyCoachID
ORDER BY ts, EventType DESC, AvailableCoachID
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
- 1 AS cnt
,LEAD(ts)
OVER (
PARTITION BY BusyCoachID
ORDER BY ts, EventType DESC, AvailableCoachID)
AS NextTS
FROM C1
)
SELECT
BusyCoachID AS CoachID
,ts AS CanCoachST
,NextTS AS CanCoachET
,cnt AS NumOfCoaches
FROM C2
WHERE ts <> NextTS
ORDER BY BusyCoachID, CanCoachST
;
Final result
+---------+-------------------------+-------------------------+--------------+
| CoachID | CanCoachST | CanCoachET | NumOfCoaches |
+---------+-------------------------+-------------------------+--------------+
| 1 | 2016-08-17 09:12:00.000 | 2016-08-17 09:24:00.000 | 2 |
| 1 | 2016-08-17 09:24:00.000 | 2016-08-17 09:30:00.000 | 3 |
| 1 | 2016-08-17 09:30:00.000 | 2016-08-17 09:34:00.000 | 1 |
| 1 | 2016-08-17 09:34:00.000 | 2016-08-17 09:55:00.000 | 2 |
| 1 | 2016-08-17 09:55:00.000 | 2016-08-17 10:00:00.000 | 1 |
| 1 | 2016-08-17 10:00:00.000 | 2016-08-17 10:11:00.000 | 2 |
| 3 | 2016-08-17 09:30:00.000 | 2016-08-17 09:34:00.000 | 1 |
| 3 | 2016-08-17 09:34:00.000 | 2016-08-17 09:55:00.000 | 2 |
| 3 | 2016-08-17 09:55:00.000 | 2016-08-17 10:00:00.000 | 1 |
| 4 | 2016-08-17 12:07:00.000 | 2016-08-17 12:27:00.000 | 2 |
| 4 | 2016-08-17 12:27:00.000 | 2016-08-17 13:08:00.000 | 1 |
| 4 | 2016-08-17 13:08:00.000 | 2016-08-17 13:10:00.000 | 0 |
| 6 | 2016-08-17 15:00:00.000 | 2016-08-17 15:10:00.000 | 0 |
| 6 | 2016-08-17 15:10:00.000 | 2016-08-17 15:15:00.000 | 1 |
| 6 | 2016-08-17 15:15:00.000 | 2016-08-17 15:20:00.000 | 2 |
| 6 | 2016-08-17 15:20:00.000 | 2016-08-17 15:25:00.000 | 1 |
| 6 | 2016-08-17 15:25:00.000 | 2016-08-17 15:40:00.000 | 0 |
| 6 | 2016-08-17 15:40:00.000 | 2016-08-17 15:55:00.000 | 1 |
| 6 | 2016-08-17 15:55:00.000 | 2016-08-17 16:00:00.000 | 0 |
| 9 | 2016-08-17 15:00:00.000 | 2016-08-17 15:10:00.000 | 0 |
| 9 | 2016-08-17 15:10:00.000 | 2016-08-17 15:15:00.000 | 1 |
| 9 | 2016-08-17 15:15:00.000 | 2016-08-17 15:20:00.000 | 2 |
| 9 | 2016-08-17 15:20:00.000 | 2016-08-17 15:25:00.000 | 1 |
| 9 | 2016-08-17 15:25:00.000 | 2016-08-17 15:40:00.000 | 0 |
| 9 | 2016-08-17 15:40:00.000 | 2016-08-17 15:55:00.000 | 1 |
| 9 | 2016-08-17 15:55:00.000 | 2016-08-17 16:00:00.000 | 0 |
+---------+-------------------------+-------------------------+--------------+
I'd recommend to create the following indexes to avoid some Sorts in the execution plan.
CREATE UNIQUE NONCLUSTERED INDEX [IX_CoachID_BusyST] ON #Temp1
(
CoachID ASC,
BusyST ASC
);
CREATE UNIQUE NONCLUSTERED INDEX [IX_CoachID_BusyET] ON #Temp1
(
CoachID ASC,
BusyET ASC
);
CREATE UNIQUE NONCLUSTERED INDEX [IX_CoachID_AvailableST] ON #Temp2
(
CoachID ASC,
AvailableST ASC
);
CREATE UNIQUE NONCLUSTERED INDEX [IX_CoachID_AvailableET] ON #Temp2
(
CoachID ASC,
AvailableET ASC
);
On real data, though, the bottleneck may be somewhere else, which may depend on the data distribution. The query is rather complicated and tuning it without real data would be too much guesswork.
First variant of the query
Run the query step-by-step, CTE-by-CTE and examine intermediate results to undestand how it works.
CTE_Intervals gives us a list of available intervals that intersect with busy intervals.
C1 puts both start and end times in the same column with the corresponding EventType. This will help us track when an interval starts or ends.
A running total of EventType gives the count of available coaches. C1 unions busy coaches into the mix to properly count cases when no coach is available.
WITH
CTE_Intervals
AS
(
SELECT
TBusy.CoachID AS BusyCoachID
,TBusy.BusyST
,TBusy.BusyET
,CA.CoachID AS AvailableCoachID
,CA.AvailableST
,CA.AvailableET
-- max of start time
,CASE WHEN CA.AvailableST < TBusy.BusyST
THEN TBusy.BusyST
ELSE CA.AvailableST
END AS ST
-- min of end time
,CASE WHEN CA.AvailableET > TBusy.BusyET
THEN TBusy.BusyET
ELSE CA.AvailableET
END AS ET
FROM
#Temp1 AS TBusy
CROSS APPLY
(
SELECT
TAvailable.*
FROM
#Temp2 AS TAvailable
WHERE
-- the same coach can't be available and busy
TAvailable.CoachID <> TBusy.CoachID
-- intervals intersect
AND TAvailable.AvailableST < TBusy.BusyET
AND TAvailable.AvailableET > TBusy.BusyST
) AS CA
)
,C1 AS
(
SELECT
BusyCoachID
,AvailableCoachID
,ST AS ts
,+1 AS EventType
FROM CTE_Intervals
UNION ALL
SELECT
BusyCoachID
,AvailableCoachID
,ET AS ts
,-1 AS EventType
FROM CTE_Intervals
UNION ALL
SELECT
CoachID AS BusyCoachID
,CoachID AS AvailableCoachID
,BusyST AS ts
,+1 AS EventType
FROM #Temp1
UNION ALL
SELECT
CoachID AS BusyCoachID
,CoachID AS AvailableCoachID
,BusyET AS ts
,-1 AS EventType
FROM #Temp1
)
,C2 AS
(
SELECT
C1.*
,SUM(EventType)
OVER (
PARTITION BY BusyCoachID
ORDER BY ts, EventType DESC, AvailableCoachID
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
- 1 AS cnt
,LEAD(ts)
OVER (
PARTITION BY BusyCoachID
ORDER BY ts, EventType DESC, AvailableCoachID)
AS NextTS
FROM C1
)
SELECT
BusyCoachID AS CoachID
,ts AS CanCoachST
,NextTS AS CanCoachET
,cnt AS NumOfCoaches
FROM C2
WHERE ts <> NextTS
ORDER BY BusyCoachID, CanCoachST
;
DROP TABLE #Temp1;
DROP TABLE #Temp2;
Result
I've added comments for each line with IDs of available coaches that were counted.
Now I understand why my initial result was not the same as your expected result.
+---------+---------------------+---------------------+--------------+
| CoachID | CanCoachST | CanCoachET | NumOfCoaches |
+---------+---------------------+---------------------+--------------+
| 1 | 2016-08-17 09:12:00 | 2016-08-17 09:24:00 | 2 | 2,5
| 1 | 2016-08-17 09:24:00 | 2016-08-17 09:30:00 | 3 | 2,3,5
| 1 | 2016-08-17 09:30:00 | 2016-08-17 09:34:00 | 2 | 3,5
| 1 | 2016-08-17 09:34:00 | 2016-08-17 09:55:00 | 3 | 3,4,5
| 1 | 2016-08-17 09:55:00 | 2016-08-17 10:11:00 | 2 | 3,4
| 3 | 2016-08-17 09:30:00 | 2016-08-17 09:34:00 | 2 | 1,5
| 3 | 2016-08-17 09:34:00 | 2016-08-17 09:55:00 | 3 | 1,4,5
| 3 | 2016-08-17 09:55:00 | 2016-08-17 10:00:00 | 2 | 1,4
| 4 | 2016-08-17 12:07:00 | 2016-08-17 12:27:00 | 2 | 3,1
| 4 | 2016-08-17 12:27:00 | 2016-08-17 13:08:00 | 1 | 3
| 4 | 2016-08-17 13:08:00 | 2016-08-17 13:10:00 | 0 | none
| 6 | 2016-08-17 15:00:00 | 2016-08-17 15:10:00 | 0 | none
| 6 | 2016-08-17 15:10:00 | 2016-08-17 15:15:00 | 1 | 7
| 6 | 2016-08-17 15:15:00 | 2016-08-17 15:20:00 | 2 | 7,8
| 6 | 2016-08-17 15:20:00 | 2016-08-17 15:25:00 | 1 | 8
| 6 | 2016-08-17 15:25:00 | 2016-08-17 15:40:00 | 0 | none
| 6 | 2016-08-17 15:40:00 | 2016-08-17 15:55:00 | 1 | 7
| 6 | 2016-08-17 15:55:00 | 2016-08-17 16:00:00 | 0 | none
+---------+---------------------+---------------------+--------------+
This query will do the calculations:
SELECT TT1.ID1
, case when TT2.ST2 < TT1.ST1 THEN TT1.ST1 ELSE TT2.ST2 END
, case when TT2.ET2 > TT1.ET1 THEN TT1.ET1 ELSE TT2.ET2 END
, COUNT(distinct TT2.id2)
FROM #Temp1 TT1 INNER JOIN #Temp2 TT2
ON TT1.ET1 > TT2.ST2 AND TT1.ST1 < TT2.ET2 AND TT1.ID1 <> TT2.ID2
GROUP BY TT1.ID1
, case when TT2.ST2 < TT1.ST1 THEN TT1.ST1 ELSE TT2.ST2 END
, case when TT2.ET2 > TT1.ET1 THEN TT1.ET1 ELSE TT2.ET2 END
However, the result will include the slots where coaches cal fill in for the full time slot, e.g for the Coach 1 there will be three slots: from 9:00 to 9:30 with substitute coach #2, from 9:30 to 10:00 substitute coach #4 and the timeslot from 9:00 to 10:00 with substitute coaches #3 and #4. Here is the whole result:
ID1
----------- ----------------------- ----------------------- -----------
1 2016-08-17 09:00:00.000 2016-08-17 09:30:00.000 1
1 2016-08-17 09:00:00.000 2016-08-17 10:00:00.000 2
1 2016-08-17 09:30:00.000 2016-08-17 10:00:00.000 1
3 2016-08-17 09:30:00.000 2016-08-17 10:00:00.000 3
4 2016-08-17 12:00:00.000 2016-08-17 12:30:00.000 1
4 2016-08-17 12:00:00.000 2016-08-17 13:00:00.000 1
As best I can tell, what you're looking for is something like this:
;WITH CTE AS (
SELECT ID1, ST1, DATEADD(MINUTE, 30, ST1) ET1
FROM #Temp1
UNION ALL
SELECT C.ID1, C.ET1, DATEADD(MINUTE, 30, C.ET1)
FROM CTE C
JOIN #Temp1 T ON T.ID1 = C.ID1
WHERE T.ET1 >= DATEADD(MINUTE, 30, C.ET1))
SELECT *
FROM CTE C
OUTER APPLY (
SELECT COUNT(*) ID2Cnt
FROM #Temp2 T
WHERE ST2 <= C.ST1
AND ET2 >= C.ET1
AND ID2 <> C.ID1
AND NOT EXISTS (
SELECT 1
FROM CTE
WHERE ID1 = T.ID2
AND ST1 <= C.ST1
AND ET1 >= C.ET1)) T
ORDER BY ID1, ST1;
The CTE will split your #Temp1 coaches up into half hour slots, and then I'm assuming you want to find all the people in #Temp2 who aren't the same ID and have a shift that starts earlier or at the same time and ends after or at the same time... NOTE: I'm assuming blocks can only be half an hour here.
EDIT: Never mind... I just realised you also want to discount the busy people in #Temp1 from the result set so I added a not exists clause in the apply...
I recommend using the concept of an interval / timeslot table.
Another way of explaining it is consider a "Time Dimension Table"
Define all your times, and then record your facts with references to the time intervals at the granularity you care about. Because you had times ending in 7 and 11 minutes I chose 1 minute intervals, though I recommend 15-30 minute intervals.
By doing this it makes it easy to join / compare the tables.
Consider design / implementation below:
-- dimension table
-- drop table #intervals
create table #intervals(intervalId int identity(1,1) not null primary key clustered,intervalStartTime datetime unique)
declare #s datetime, #e datetime, #i int
set #s = '2016-08-16'
set #e = '2016-08-18'
set #i = 1
while (#s <= #e )
begin
insert into #intervals(intervalStartTime) values(#s)
set #s = dateadd(minute, #i, #s)
end
-- fact table:
-- drop table #Fact
create table #Fact(intervalId int, coachid int, isBusy int default(0) , isAvailable int default(0))
-- record every coach's times
insert into #Fact(coachid,intervalId)
select distinct c.coachid, i.intervalId
from
(
select distinct coachid from #temp1
union
select distinct coachid from #temp2
) c cross join #intervals i
-- record free / busy info
update f set isbusy = 1
from #intervals i inner join #fact f on i.intervalId = f.intervalId
inner join #temp1 t on f.coachid = t.coachid and i.intervalStartTime between t.BusyST and t.BusyET
-- record free / busy info
update f set isAvailable = 1
from #intervals i inner join #fact f on i.intervalId = f.intervalId
inner join #temp2 t on f.coachid = t.coachid and i.intervalStartTime between t.AvailableST and t.AvailableET
-- construct your query to find common times,etc
select * from #intervals i inner join #Fact f on i.intervalId = f.intervalId
-- example result showing # of coaches available vs free
select i.intervalId, i.intervalStartTime, sum(isBusy) as coachesBusy, sum(isAvailable) as coachesAvailable
from #intervals i inner join #Fact f on i.intervalId = f.intervalId
group by i.intervalId, i.intervalStartTime
having sum(isBusy) < sum(isAvailable)
you can then look for common or unique interval ids however you need.
let me know if you require additional clarification.
I'm using a little numbers table ... you don't need something for dates, just numbers. What I am building here is smaller than what you would use in a real scenario.
CREATE TABLE dbo.Numbers (Num INT PRIMARY KEY CLUSTERED);
WITH E1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS t(N))
,E2 AS (SELECT N = 1 FROM E1 AS a, E1 AS b)
,E4 AS (SELECT N = 1 FROM E2 AS a, E2 AS b)
,cteTally AS (SELECT N = 0 UNION ALL
SELECT N = ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4)
INSERT INTO dbo.Numbers (Num)
SELECT N FROM cteTally;
Pleae note the #startDate below ... it is artificially close to the dates you're dealing with and in a real prod scenario you would have that date be earlier to go along with your larger Numbers table.
Here is the solution to your problem and it will work with older SQL Server versions (as well as the 2012 you have tagged):
DECLARE #startDate DATETIME = '20160817';
WITH cteBusy AS
(
SELECT num.Num
, busy.CoachID
FROM #Temp1 AS busy
JOIN dbo.Numbers AS num
ON num.Num >= DATEDIFF(MINUTE, #startDate, busy.BusyST)
AND num.Num < DATEDIFF(MINUTE, #startDate, busy.BusyET)
)
, cteAvailable AS
(
SELECT num.Num
, avail.CoachID
FROM #Temp2 AS avail
JOIN dbo.Numbers AS num
ON num.Num >= DATEDIFF(MINUTE, #startDate, avail.AvailableST)
AND num.Num < DATEDIFF(MINUTE, #startDate, avail.AvailableET)
LEFT JOIN cteBusy AS b
ON b.Num = num.Num
AND b.CoachID = avail.CoachID
WHERE b.Num IS NULL
)
, cteGrouping AS
(
SELECT b.Num
, b.CoachID
, NumOfCoaches = COUNT(a.CoachID)
FROM cteBusy AS b
LEFT JOIN cteAvailable AS a
ON a.Num = b.Num
GROUP BY b.Num, b.CoachID
)
, cteFinal AS
(
SELECT cte.Num
, cte.CoachID
, cte.NumOfCoaches
, block = cte.Num - ROW_NUMBER() OVER(PARTITION BY cte.CoachID, cte.NumOfCoaches ORDER BY cte.Num)
FROM cteGrouping AS cte
)
SELECT cte.CoachID
, CanCoachST = DATEADD(MINUTE, MIN(cte.Num), #startDate)
, CanCoachET = DATEADD(MINUTE, MAX(cte.Num) + 1, #startDate)
, cte.NumOfCoaches
FROM cteFinal AS cte
GROUP BY cte.CoachId, cte.NumOfCoaches, cte.block
ORDER BY cte.CoachID, CanCoachST;
Enjoy!
I believe the following query will work, however I can make no promises on performance.
CREATE TABLE #Temp1 (CoachID INT, BusyST DATETIME, BusyET DATETIME)
CREATE TABLE #Temp2 (CoachID INT, AvailableST DATETIME, AvailableET DATETIME)
INSERT INTO #Temp1 (CoachID, BusyST, BusyET)
SELECT 1,'2016-08-17 09:12:00','2016-08-17 10:11:00'
UNION
SELECT 3,'2016-08-17 09:30:00','2016-08-17 10:00:00'
UNION
SELECT 4,'2016-08-17 12:07:00','2016-08-17 13:10:00'
INSERT INTO #Temp2 (CoachID, AvailableST, AvailableET)
SELECT 1,'2016-08-17 09:07:00','2016-08-17 11:09:00'
UNION
SELECT 2,'2016-08-17 09:11:00','2016-08-17 09:30:00'
UNION
SELECT 3,'2016-08-17 09:24:00','2016-08-17 13:08:00'
UNION
SELECT 1,'2016-08-17 11:34:00','2016-08-17 12:27:00'
UNION
SELECT 4,'2016-08-17 09:34:00','2016-08-17 13:00:00'
UNION
SELECT 5,'2016-08-17 09:10:00','2016-08-17 09:55:00'
;WITH WorkScheduleWithID -- Select work schedule (#Temp2 – available times) and generate ID for each schedule entry.
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY [CoachID]) AS [ID]
,[WS].[CoachID]
,[WS].[AvailableST] AS [Start]
,[WS].[AvailableET] As [End]
FROM #Temp2 [WS]
), SchedulesIntersect -- Determine where work schedule and meeting schedule (busy times) intersect.
AS
(
SELECT [ID]
,[CoachID]
,[Start]
,[End]
,[IntersectTime]
,SUM([Availability]) OVER (PARTITION BY [ID] ORDER BY [IntersectTime]) AS GroupID
FROM (
SELECT [WS].[ID]
,[WS].[CoachID]
,[WS].[Start]
,[WS].[End]
,[MS1].[BusyST] AS [IntersectTime]
,0 AS [Availability]
FROM WorkScheduleWithID [WS]
INNER JOIN #Temp1 [MS1] ON ([MS1].[CoachID] = [WS].[CoachID])
AND
( ([MS1].[BusyST] > [WS].[Start]) AND ([MS1].[BusyST] < [WS].[End]) ) -- Meeting start contained with in work schedule
UNION ALL
SELECT [WS].[ID]
,[WS].[CoachID]
,[WS].[Start]
,[WS].[End]
,[MS2].[BusyET] AS [IntersectTime]
,1 AS [Availability]
FROM WorkScheduleWithID [WS]
INNER JOIN #Temp1 [MS2] ON ([MS2].[CoachID] = [WS].[CoachID])
AND
( ([MS2].BusyET > [WS].[Start]) AND ([MS2].BusyET < [WS].[End]) ) -- Meeting end contained with in work schedule
) Intersects
),ActualAvailability -- Determine actual availability of each coach based on work schedule and busy time.
AS
(
SELECT [ID]
,[CoachID]
,(
CASE
WHEN [GroupID] = 0 THEN [Start]
ELSE MIN([IntersectTime])
END
) AS [Start]
,(
CASE
WHEN ( ([GroupID] > 0) AND (MIN([IntersectTime]) = MAX([IntersectTime])) ) THEN [End]
ELSE MAX([IntersectTime])
END
) AS [End]
FROM SchedulesIntersect
GROUP BY [ID], [CoachID], [Start], [End], [GroupID]
UNION ALL
SELECT [ID]
,[CoachID]
,[Start]
,[End]
FROM WorkScheduleWithID WS
WHERE WS.ID NOT IN (SELECT ID FROM SchedulesIntersect)
),TimeIntervals -- Determine time intervals for which each coach’s availability will be checked against.
AS
(
SELECT DISTINCT *
FROM (
SELECT MS.CoachID
,MS.BusyST
,MS.BusyET
,(
CASE
WHEN AC.Start < MS.BusyST THEN MS.BusyST
ELSE AC.Start
END
) AS [TS]
FROM #Temp1 MS
LEFT OUTER JOIN ActualAvailability AC ON (AC.CoachID <> MS.CoachID)
AND
(
( (MS.[BusyST] <= AC.[Start]) AND (MS.[BusyET] >= AC.[End]) ) OR -- Meeting covers entire work schedule
( (MS.[BusyST] > AC.[Start]) AND (MS.[BusyET] < AC.[End]) ) OR -- Meeting is contained with in work schedule
( (MS.[BusyST] < AC.[Start]) AND (MS.[BusyET] > AC.[Start]) AND ([MS].[BusyET] < AC.[End]) ) OR -- Meeting ends within work schedule (partial overlap)
( (MS.[BusyST] > AC.[Start]) AND (MS.[BusyST] < AC.[End]) AND ([MS].[BusyET] > AC.[End]) ) -- Meeting starts within work schedule (partial overlap)
)
UNION ALL
SELECT MS.CoachID
,MS.BusyST
,MS.BusyET
,(
CASE
WHEN AC.[End] > MS.BusyET THEN MS.BusyET
ELSE AC.[End]
END
) AS [TS]
FROM #Temp1 MS
LEFT OUTER JOIN ActualAvailability AC ON (AC.CoachID <> MS.CoachID)
AND
(
( (MS.[BusyST] <= AC.[Start]) AND (MS.[BusyET] >= AC.[End]) ) OR -- Meeting covers entire work schedule
( (MS.[BusyST] > AC.[Start]) AND (MS.[BusyET] < AC.[End]) ) OR -- Meeting is contained with in work schedule
( (MS.[BusyST] < AC.[Start]) AND (MS.[BusyET] > AC.[Start]) AND ([MS].[BusyET] < AC.[End]) ) OR -- Meeting ends within work schedule (partial overlap)
( (MS.[BusyST] > AC.[Start]) AND (MS.[BusyST] < AC.[End]) AND ([MS].[BusyET] > AC.[End]) ) -- Meeting starts within work schedule (partial overlap)
)
) Intervals
),AvailableCoachTimeSegments -- Determine each coach’s availability against each time interval being checked.
AS
(
SELECT ROW_NUMBER() OVER (PARTITION BY TI.CoachID ORDER BY TI.Start, AT.CoachID) AS RankAsc
,ROW_NUMBER() OVER (PARTITION BY TI.CoachID ORDER BY TI.[End] DESC, AT.CoachID DESC) AS RankDesc
,TI.CoachID
,TI.BusyST
,TI.BusyET
,TI.Start
,TI.[End]
,AT.CoachID AS AvailableCoachID
,AT.Start AS AvailableStart
,AT.[End] AS AvailableEnd
,(
CASE
WHEN (MIN(TI.[Start]) OVER (PARTITION BY TI.CoachID)) <> TI.BusyST THEN 1
ELSE 0
END
) AS StartIncomplete
,(
CASE
WHEN (MAX(TI.[End]) OVER (PARTITION BY TI.CoachID)) <> TI.BusyET THEN 1
ELSE 0
END
) AS EndIncomplete
FROM (
SELECT CoachID
,BusyST
,BusyET
,TS AS [Start]
,LEAD(TS, 1, TS) OVER (PARTITION BY CoachID ORDER BY TS) AS [End]
FROM TimeIntervals
) TI
LEFT OUTER JOIN ActualAvailability AT ON
(
( (AT.[Start] <= TI.[Start]) AND (AT.[End] >= TI.[End]) ) OR -- Coach availability covers entire time segment
( (AT.[Start] > TI.[Start]) AND (AT.[End] < TI.[End]) ) OR -- Coach availability is contained within the time segment
( (AT.[Start] < TI.[Start]) AND (AT.[End] > TI.[Start]) AND (AT.[End] < TI.[End]) ) OR -- Coach availability ends within the time segment (partial overlap)
( (AT.[Start] > TI.[Start]) AND (AT.[Start] < TI.[End]) AND (AT.[End] > TI.[End]) ) -- Coach availability starts within the time segment (partial overlap)
)
)
-- Final result
SELECT CoachID
,BusyST
,BusyET
,Start AS CanCoachST
,[End] AS CanCoachET
,COUNT(AvailableCoachID) AS NumOfCoaches
,ISNULL(STUFF((
SELECT TOP 100 PERCENT ', ' + CAST(AvailableCoach.AvailableCoachID AS VARCHAR(MAX))
FROM AvailableCoachTimeSegments AvailableCoach
WHERE (AvailableCoach.CoachID = Results.CoachID AND AvailableCoach.Start = Results.Start AND AvailableCoach.[End] = Results.[End])
ORDER BY AvailableCoach.AvailableCoachID
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,''), '(No one is available)') AS AvailableCoaches
FROM AvailableCoachTimeSegments Results
WHERE [Start] <> [End]
GROUP BY CoachID, BusyST, BusyET, Start, [End], StartIncomplete, EndIncomplete
UNION ALL -- Add any missing time segments at the start of the busy time or end of the busy time.
SELECT CoachID
,BusyST
,BusyET
,(
CASE
WHEN StartIncomplete = 1 THEN BusyST
WHEN EndIncomplete = 1 THEN MAX([End])
ELSE Start
END
) AS CanCoachST
,(
CASE
WHEN StartIncomplete = 1 THEN Start
WHEN EndIncomplete = 1 THEN BusyET
ELSE [End]
END
) AS CanCoachET
,0 AS NumOfCoaches
,'(No one is available)' AS AvailableCoaches
FROM AvailableCoachTimeSegments Results
WHERE [Start] <> [End] AND ( (StartIncomplete = 1 AND RankAsc = 1) OR (EndIncomplete = 1 AND RankDesc = 1) )
GROUP BY CoachID, BusyST, BusyET, Start, [End], StartIncomplete, EndIncomplete
ORDER BY CoachID, CanCoachST
DROP TABLE #Temp1
DROP TABLE #Temp2
This is your expected result that is taking into consideration of the Busy Coaches that overlap the Available Coaches.
| CoachID | CanCoachST | CanCoachET | NumOfCoaches | CanCoach |
|---------|------------------|------------------|--------------|----------|
| 1 | 2016-08-17 09:12 | 2016-08-17 09:24 | 2 | 2, 5 |
| 1 | 2016-08-17 09:24 | 2016-08-17 09:30 | 3 | 2, 3, 5 |
| 1 | 2016-08-17 09:30 | 2016-08-17 09:34 | 1 | 5 |
| 1 | 2016-08-17 09:34 | 2016-08-17 09:55 | 2 | 4, 5 |
| 1 | 2016-08-17 09:55 | 2016-08-17 10:00 | 1 | 4 |
| 1 | 2016-08-17 10:00 | 2016-08-17 10:11 | 2 | 3, 4 |
| 3 | 2016-08-17 09:30 | 2016-08-17 09:34 | 1 | 5 |
| 3 | 2016-08-17 09:34 | 2016-08-17 09:55 | 2 | 4, 5 |
| 3 | 2016-08-17 09:55 | 2016-08-17 10:00 | 1 | 4 |
| 4 | 2016-08-17 12:07 | 2016-08-17 12:27 | 2 | 1, 3 |
| 4 | 2016-08-17 12:27 | 2016-08-17 13:08 | 1 | 3 |
| 4 | 2016-08-17 13:08 | 2016-08-17 13:10 | 0 | NULL |
#Temp1 as Busy Coaches:
| CoachID | BusyST | BusyET |
|---------|------------------|------------------|
| 1 | 2016-08-17 09:12 | 2016-08-17 10:11 |
| 3 | 2016-08-17 09:30 | 2016-08-17 10:00 |
| 4 | 2016-08-17 12:07 | 2016-08-17 13:10 |
#Temp2 as Available Coaches:
| CoachID | AvailableST | AvailableET |
|---------|------------------|------------------|
| 1 | 2016-08-17 09:07 | 2016-08-17 11:09 |
| 1 | 2016-08-17 11:34 | 2016-08-17 12:27 |
| 2 | 2016-08-17 09:11 | 2016-08-17 09:30 |
| 3 | 2016-08-17 09:24 | 2016-08-17 13:08 |
| 4 | 2016-08-17 09:34 | 2016-08-17 13:00 |
| 5 | 2016-08-17 09:10 | 2016-08-17 09:55 |
The script below is a bit long.
;
with
st
(
CoachID,
CanCoachST
)
as
(
select
bound.CoachID,
s.BusyST
from
#Temp1 as s
cross apply
(
select
b.CoachID,
b.BusyST,
b.BusyET
from
#Temp1 as b
where 1 = 1
and s.BusyST between b.BusyST and b.BusyET
)
as bound
union all
select
bound.CoachID,
s.BusyET
from
#Temp1 as s
cross apply
(
select
b.CoachID,
b.BusyST,
b.BusyET
from
#Temp1 as b
where 1 = 1
and s.BusyET between b.BusyST and b.BusyET
and s.CoachID != b.CoachID
)
as bound
union all
select
bound.CoachID,
s.AvailableST
from
#Temp2 as s
cross apply
(
select
b.CoachID,
b.BusyST,
b.BusyET
from
#Temp1 as b
where 1 = 1
and s.AvailableST between b.BusyST and b.BusyET
)
as bound
union all
select
bound.CoachID,
s.AvailableET
from
#Temp2 as s
cross apply
(
select
b.CoachID,
b.BusyST,
b.BusyET
from
#Temp1 as b
where 1 = 1
and s.AvailableET between b.BusyST and b.BusyET
and s.CoachID != b.CoachID
)
as bound
),
d as
(
select distinct
CoachID,
CanCoachST
from
st
),
r as
(
select
row_number() over (order by CoachID, CanCoachST) as RowID,
CoachID,
CanCoachST
from
d
),
rng as
(
select
r1.RowID,
r1.CoachID,
r1.CanCoachST,
case when r1.CoachID = r2.CoachID
then r2.CanCoachST else t.BusyET end as CanCoachET
from
r as r1
left join
r as r2
on
r1.RowID = r2.RowID - 1
left join
#Temp1 as t
on
t.CoachID = r1.CoachID
),
c as
(
select
rng.RowID,
rng.CoachID,
rng.CanCoachST,
rng.CanCoachET,
t2.CoachID as CanCoachID
from
rng
cross join
#Temp1 as t1
cross join
#Temp2 as t2
where 1 = 1
and t2.CoachID != rng.CoachID
and t2.AvailableST <= rng.CanCoachST
and t2.AvailableET >= rng.CanCoachET
),
b as
(
select
rng.RowID,
rng.CoachID,
rng.CanCoachST,
rng.CanCoachET,
t1.CoachID as BusyCoachID
from
rng
cross join
#Temp1 as t1
where 1 = 1
and t1.CoachID != rng.CoachID
and t1.BusyST <= rng.CanCoachST
and t1.BusyET >= rng.CanCoachET
),
e as
(
select
c.RowID,
c.CoachID,
c.CanCoachST,
c.CanCoachET,
c.CanCoachID
from
c
except
select
b.RowID,
b.CoachID,
b.CanCoachST,
b.CanCoachET,
b.BusyCoachID
from
b
),
f as
(
select
rng.RowID,
rng.CoachID,
rng.CanCoachST,
rng.CanCoachET,
e.CanCoachID
from
rng
left join
e
on
e.RowID = rng.RowID
)
select
f.CoachID,
f.CanCoachST,
f.CanCoachET,
count(f.CanCoachID) as NumOfCoaches,
stuff
(
(
select ', ' + cast(f1.CanCoachID as varchar(5))
from f as f1 where f1.RowID = f.RowID
for xml path('')
),
1, 2, ''
)
as CanCoach
from
f
group by
f.RowID,
f.CoachID,
f.CanCoachST,
f.CanCoachET
order by
1, 2