Table with inconsistent starttime and stoptime - sql

I have a table that contains a date, a starttime, and a stoptime. I have several problems that I don't know how to solve for. Each row contains either a starttime OR a stoptime (not both). While this itself is not a problem, I need to calculate the runtime by date. Also, there are instances of multiple starttimes before there is a stoptime registered. Assume the following:
Date, Starttime, Stoptime
4/1/2016, 23:00:00, NULL
4/2/2016, NULL, 03:00:00
4/2/2016, 05:00:00, NULL
4/2/2016, 07:00:00, NULL
4/2/2016, NULL, 08:00:00
4/2/2016, 10:00:00, NULL
4/2/2016, NULL, 10:15:00
I need the output to be:
4/1/2016, 01:00:00
4/2/2016, 06:15:00
I have tried a few things, with very poor results. Can any experts out there solve this problem?

Here's one way how you could handle this:
Setup:
CREATE TABLE #Table1
([Date] date, [Starttime] time, [Stoptime] time)
;
INSERT INTO #Table1
([Date], [Starttime], [Stoptime])
VALUES
('2016-04-01', '23:00:00', NULL),
('2016-04-02', NULL, '00:00:00'),
('2016-04-02', '00:00:00', NULL),
('2016-04-02', NULL, '03:00:00'),
('2016-04-02', '05:00:00', NULL),
('2016-04-02', '07:00:00', NULL),
('2016-04-02', NULL, '08:00:00'),
('2016-04-02', '10:00:00', NULL),
('2016-04-02', NULL, '10:15:00')
;
SQL:
select
convert(date, StartTime),
convert(time, dateadd(second, sum(datediff(second, StartTime, EndTime)),0))
from (
select
min(Time) as StartTime,
min(case when Type = 0 then Time end) as EndTime
from
(
select
sum(Type) over (order by Time asc, Type Asc) as GRP, *
from (
select
isnull(lag(Type) over (order by Time asc, Type Asc), -1) as LagType, *
from (
select
case when Starttime is NULL then 0 else 1 end as Type,
case when Starttime is NULL
then convert(datetime, [Date]) + convert(datetime, Stoptime)
else convert(datetime, [Date]) + convert(datetime, Starttime) end as Time
from
#Table1
) A
) B
where Type != 1 or LagType != 1
) C
group by GRP
) D
group by convert(date, StartTime)
Result:
2016-04-01 01:00:00
2016-04-02 06:15:00
This requires that you have start and stop record in your data for each of the days, so you'll need to use union or something like that to add that to your initial data.
The innermost select generates typing (0/1) for the rows and calculates the date + start / end time into a single column. The next one adds TypeLag to the data and it's used to remove the duplicate start records. The next select has a running total over Type, so that start and end records belonging together will have a unique group number. The rest will just pick the start time + earliest end time from each group and calculate the durations.

Related

How count of numbers id for a period of time

I am looking for a way to count the number of id in a period of time every 30 minutes.
I wrote a SQL query, but the result is incorrect
CREATE TABLE [dbo].[#tabl]
(
[Id] [varchar](100) NULL,
[TIMEStart] [datetime] NULL,
[TIMEEnd] [datetime] NULL
) ON [PRIMARY]
INSERT INTO [dbo].[#tabl] VALUES ('1', '2020-04-01 00:05:00.000', '2020-04-01 00:10:00.000')
INSERT INTO [dbo].[#tabl] VALUES ('2', '2020-04-01 00:11:00.000', '2020-04-01 00:29:00.000')
INSERT INTO [dbo].[#tabl] VALUES ('3', '2020-04-01 00:12:00.000', '2020-04-01 00:55:00.000')
WITH CTE AS
(
SELECT
[Id],
DATEADD(MINUTE, (DATEDIFF(MINUTE, [TIMEStart], [TIMEEnd]) / 30) * 30, 0) AS RangeTime
FROM
[dbo].[#tabl]
GROUP BY
[Id], DATEADD(MINUTE, (DATEDIFF(MINUTE, [TIMEStart],[TIMEEnd]) / 30) * 30, 0)
)
SELECT numreq, RangeTime
FROM
(SELECT COUNT(DISTINCT id) AS numreq, RangeTime
FROM CTE
GROUP BY RangeTime) temp
Correct result - table:
numreq RangeTime
-------------------------------------
3 1900-01-01 00:00:00.000
1 1900-01-01 00:30:00.000
Period of time:
1900-01-01 00:00:00.000 - includes 3 id:1, 2, 3
1900-01-01 00:30:00.000 - includes 1 id:1
I think what you need is to create a list of RangeTime values that lie within the range of the TIMEStart and TIMEEnd values in tabl (which you can do with a recursive CTE), then you can JOIN that list back to tabl on an overlapping time range and count the number of rows that overlap each RangeTime:
WITH CTE AS (
SELECT DATEADD(MINUTE,(DATEDIFF(MINUTE, 0, MIN([TIMEStart]))/30)*30,0) as RangeTime,
MAX([TIMEEnd]) AS MaxTime
FROM [dbo].[tabl]
UNION ALL
SELECT DATEADD(MINUTE, 30, RangeTime), MaxTime
FROM CTE
WHERE DATEADD(MINUTE, 30, RangeTime) < MaxTime
)
SELECT RangeTime, COUNT(tabl.Id) AS numreq
FROM CTE
LEFT JOIN tabl ON tabl.TIMEStart < DATEADD(MINUTE, 30, RangeTime)
AND RangeTime <= tabl.TIMEEnd
GROUP BY RangeTime
Output:
RangeTime numreq
2020-04-01T00:00:00Z 3
2020-04-01T00:30:00Z 1
Demo on SQLFiddle
Note: I've presumed you want the actual time for the range, not times starting at the beginning of 1900...

Reading 0.5 value in ROW_NUMBER() and PARTITION BY | SQL Server 2012

I have this question before Looping between 2 case parameters (Date Range and Row Count) | SQL Server 2012 , now I'm thinking about a scenario, what if the value has a 0.5? or .5? Will this work using ROW_NUMBER()?
I'm trying to make this work using only CASE method.
This is my old script:
DECLARE #dbApple TABLE
(
FromDate varchar(30) NOT NULL,
ToDate varchar(30) NOT NULL,
Name varchar(30) NOT NULL,
Count float(30) NOT NULL
)
INSERT INTO #dbApple (FromDate, ToDate, Name, Count)
VALUES ('2019-10-05', '2019-10-09', 'APPLE', '2.5');
(SELECT
CONVERT(date, CONVERT(date, DATEADD(D, VAL.NUMBER, FromDate))) AS Date,
DB.Name,
CASE
WHEN CONVERT(date, CONVERT(date, DATEADD(D, VAL.NUMBER, FromDate))) BETWEEN CONVERT(date, CONVERT(date, DATEADD(D, VAL.NUMBER, FromDate))) AND CONVERT(date, CONVERT(date, DATEADD(D, VAL.NUMBER, ToDate)))
THEN
CASE
WHEN ROW_NUMBER() OVER (PARTITION BY Count, FromDate, ToDate ORDER BY Count) <= Count
THEN (COUNT / COUNT)
END
END AS Count
FROM
#dbApple DB
JOIN
MASTER..SPT_VALUES VAL ON VAL.TYPE = 'P'
AND VAL.NUMBER BETWEEN 0 AND DATEDIFF(D, FromDate, ToDate))
This is the output:
This is my expected output:
Is there a way for this to work? Thank you.
You can greatly simplify your query by noting that VAL.NUMBER is already your row number (just starting at 0 instead of 1). You can then compare your Count value to VAL.NUMBER and if Count - VAL.NUMBER is greater than 1, output 1; if it's greater than 0 output the difference, otherwise output NULL. For this demo query I've simulated your numbers table with a table value constructor:
declare #dbApple TABLE(
FromDate varchar(30) NOT NULL,
ToDate varchar(30) NOT NULL,
Name varchar(30) NOT NULL,
Count float(30) NOT NULL
)
INSERT INTO #dbApple
(FromDate,ToDate,Name,Count) VALUES ('2019-10-05','2019-10-09','APPLE',2.5);
SELECT
CONVERT(date,CONVERT(date,DATEADD(D,VAL.NUMBER,FromDate))) AS Date,
Name,
CASE WHEN Count - VAL.NUMBER > 1 THEN 1
WHEN Count - VAL.NUMBER > 0 THEN Count - VAL.NUMBER
END AS Count
FROM
#dbApple D
JOIN (VALUES (0), (1), (2), (3), (4), (5)) VAL(NUMBER)
ON VAL.NUMBER BETWEEN 0 AND DATEDIFF(D, FromDate, ToDate)
Output:
Date Name Count
2019-10-05 APPLE 1
2019-10-06 APPLE 1
2019-10-07 APPLE 0.5
2019-10-08 APPLE (null)
2019-10-09 APPLE (null)
Demo on dbfiddle

How to Select the first time entry, then only times greater than 5 minutes apart

CREATE TABLE import_time
( date datetime NUll,
time datetime Null,
Employeeid nvarchar(25) Null)
INSERT INTO import_time (date, time, Employeeid)
Values ('2019-05-22 00:00:00.000', '1900-01-01 12:50:12.000', '1234')
, ('2019-05-22 00:00:00.000', '1900-01-01 12:55:00.000', '1234')
, ('2019-05-22 00:00:00.000', '1900-01-01 13:25:12.000', '1234')
, ('2019-05-22 00:00:00.000', '1900-01-01 13:50:12.000', '1234')
How would I only select the first time entered then the times that are greater than 5 minutes apart?
The Query should return 12:50, 13:25 and 13:50 not the 12:55 entry
First add the date and time from previous row to each row.
You can use LED to do that. Hope you have SQL Server 2012 and later
You also should be careful for times around midnight, because for those entries, you may get wrong if you only compare the time.
;with ct as (
select [date], [time], Employeeid
, prev_date = lag([date]) over (partition by Employeeid order by [date], [time])
, prev_time = lag([time]) over (partition by Employeeid order by [date], [time])
from #time
)
select [date], [time], Employeeid
from ct
where prev_date is null or prev_time is null -- this gives you the first entry
or datediff(minute, prev_date + prev_time, date + time) > 5
Result:
You can use NOT EXISTS to check that there's no other row with a lesser or equal time that's within five minutes in the past of the current time, which we get with dateadd().
To decide whether a row isn't the current row you'd need a key. In absence of one in your post I used the undocumented %%physloc%% pseudo column as a surrogate. But undocumented means subject to change without further notice, so you want to replace that.
To fix your split date time you can use +.
For convenience I use a CTE to prepare the original table into a form more usable.
WITH
cte
AS
(
SELECT date,
time,
employeeid,
date + time datetime,
%%physloc%% physloc
FROM import_time
)
SELECT c1.date,
c1.time,
c1.employeeid
FROM cte c1
WHERE NOT EXISTS (SELECT *
FROM cte c2
WHERE c2.employeeid = c1.employeeid
AND c2.physloc <> c1.physloc
AND c2.datetime <= c1.datetime
AND c2.datetime > dateadd(minute, -5, c1.datetime));
db<>fiddle
Try this-
DECLARE #import_time TABLE
(
date datetime NUll,
time datetime Null,
Employeeid nvarchar(25) Null
)
INSERT INTO #import_time (date, time, Employeeid)
Values
('2019-05-22 00:00:00.000', '1900-01-01 12:50:12.000', '1234') ,
('2019-05-22 00:00:00.000', '1900-01-01 12:55:00.000', '1234') ,
('2019-05-22 00:00:00.000', '1900-01-01 13:25:12.000', '1234'),
('2019-05-22 00:00:00.000', '1900-01-01 13:50:12.000', '1234')
SELECT
CONVERT(VARCHAR(5),B.time ,108)
FROM
(
SELECT Employeeid,MIN ([time]) EnrtyTime
FROM #import_time
GROUP BY Employeeid
) A
INNER JOIN #import_time B
ON A.Employeeid = B.Employeeid
AND
(
A.EnrtyTime = B.time
OR
B.time >= DATEADD(MINUTE,5,A.EnrtyTime)
)

Finding the largest time overlap in T-SQL

I'm trying to do this on SQL Server 2008 R2.
I have a table with 4 columns:
parent_id INT
child_id INT
start_time TIME
end_time TIME
You should look at the children as sub-processes that run for the parent program. All these sub-processes are run once every day, and each child run within its given time span. I want to find the largest overlap of time intervals for each parent based on the times of its children, i.e. I want to know the longest possible overlap where all the sub-processes are running. The fact that each time span is repeated every day means that even if child's time interval spans midnight (i.e. 23:00-10:00), it can overlap with a child that only runs in the morning (i.e. 07:00-09:00), because even if they don't overlap on "the first day", they will overlap on all subsequent days.
The output should look like this:
parent_id INT
start_time TIME
end_time TIME
valid BIT
Where valid = 1 if an overlap was found and valid = 0 if no overlap was found.
A couple of important pieces of information:
A time interval can span midnight, i.e. start_time = 23:00 and end_time = 03:00, which is a time interval of 4 hours.
Two time intervals may overlap in two different places, i.e. start_time1 = 13:00, end_time1 = 06:00, start_time2 = 04:00, end_time2 = 14:00. This would give the largest overlap as 04:00 - 06:00 = 2 hours.
There may be no overlap common for the children of a given parent, in which case the out put for that parent would be start_time = NULL, end_time = NULL and valid = 0.
If a child interval spans the whole day, then start_time = NULL and end_time = NULL. This was chosen to avoid having a day as 00:00-24:00, which would slice overlaps crossing midnight in two, i.e. parent 3 below would end up having two overlaps (23:00-24:00 and 00:00 - 004:00), in stead of one (23:00-04:00).
An overlap is only an overlap if the time interval is shared by all the children of a parent.
The time span of one child can never be longer than 24 hours.
Take this example:
parent_id child_id start_time end_time
1 1 06:00 14:00
1 2 13:00 09:00
1 3 07:00 09:00
2 1 12:00 17:00
2 2 09:00 11:00
3 1 NULL NULL
3 2 23:00 04:00
4 1 NULL NULL
4 2 NULL NULL
10 1 06:11 14:00
10 2 06:00 09:00
10 3 05:00 08:44
11 1 11:38 17:00
11 2 09:02 12:11
These data would produce this result set:
parent_id start_time end_time valid
1 07:00 09:00 1
2 NULL NULL 0
3 23:00 04:00 1
4 NULL NULL 1
10 06:11 08:44 1
11 11:38 12:11 1
The overlap for a parent is the time interval that is shared by all its children. So the overlap for parent 10 is found by finding the overlap where all 3 children share time:
Child 1 (06:11-14:00) and 2 (06:00-09:00) overlap from 06:11 to 09:00. This overlap time interval is then applied to child 3 (05:00-08:44), which gives an overlap of 06:11 to 08:44, since this interval is the only interval where all 3 children share common time.
I hope this makes sense.
I can do it with a cursor, but I would really prefer to avoid cursors. I have been wracking my brain about how to do it without cursors, but I have come up short. Is there any way of doing it without cursors?
EDIT: Expanded the text for clause 4, to explain the decision of having a full day be NULL to NULL, in stead of 00:00 to 00:00.
EDIT: Expanded the examples with two more cases. The new cases have parent ID 10 and 11.
EDIT: Inserted explanation of how the overlap for parent 10 is found.
EDIT: Clarified clause 3. Added clauses 5 and 6. Went into detail about what this is all about.
Based on your question, I think your output should be:
parent_id start_time end_time valid
1 07:00 09:00 1
2 NULL NULL 0
3 23:00 04:00 1
4 NULL NULL 1
10 06:11 08:44 1
11 11:38 12:11 1
And here is a set-based solution:
DECLARE #Times TABLE
(
parent_id INT
,child_id INT
,start_time TIME
,end_time TIME
);
INSERT INTO #Times
VALUES
(1, 1, '06:00', '14:00')
,(1, 2, '13:00', '09:00')
,(1, 3, '07:00', '09:00')
,(2, 1, '12:00', '17:00')
,(2, 2, '09:00', '11:00')
,(3, 1, NULL, NULL)
,(3, 2, '23:00', '04:00')
,(4, 1, NULL, NULL)
,(4, 2, NULL, NULL)
,(10, 1, '06:11', '14:00')
,(10, 2, '06:00', '09:00')
,(10, 3, '05:00', '08:44')
,(11, 1, '11:38', '17:00')
,(11, 2, '09:02', '12:11');
DECLARE #Parents TABLE
(
parent_id INT PRIMARY KEY
,ChildCount INT
)
INSERT INTO #Parents
SELECT
parent_id
,COUNT(DISTINCT child_id) AS ChildCount
FROM
#Times
GROUP BY
parent_id
DECLARE #StartTime DATETIME2 = '00:00'
DECLARE #MinutesInTwoDays INT = 2880
DECLARE #Minutes TABLE(ThisMinute DATETIME2 PRIMARY KEY);
WITH
MinutesCTE AS
(
SELECT
1 AS MinuteNumber
,#StartTime AS ThisMinute
UNION ALL
SELECT
NextMinuteNumber
,NextMinute
FROM MinutesCTE
CROSS APPLY (VALUES(MinuteNumber+1,DATEADD(MINUTE,1,ThisMinute))) NextDates(NextMinuteNumber,NextMinute)
WHERE
NextMinuteNumber <= #MinutesInTwoDays
)
INSERT INTO #Minutes
SELECT ThisMinute FROM MinutesCTE M OPTION (MAXRECURSION 2880);
DECLARE #SharedMinutes TABLE
(
ThisMinute DATETIME2
,parent_id INT
,UNIQUE(ThisMinute,parent_id)
);
WITH TimesCTE AS
(
SELECT
Times.parent_id
,Times.child_id
,CAST(ISNULL(Times.start_time,'00:00') AS datetime2) AS start_time
,
DATEADD
(
DAY
,
CASE
WHEN Times.end_time IS NULL THEN 2
WHEN Times.start_time > Times.end_time THEN 1
ELSE 0
END
,CAST(ISNULL(Times.end_time,'00:00') AS datetime2)
) as end_time
FROM
#Times Times
UNION ALL
SELECT
Times.parent_id
,Times.child_id
,DATEADD(DAY,1,CAST(Times.start_time as datetime2)) AS start_time
,DATEADD(DAY,1,CAST(Times.end_time AS datetime2)) AS end_time
FROM
#Times Times
WHERE
start_time < end_time
)
--Get minutes shared by all children of each parent
INSERT INTO #SharedMinutes
SELECT
M.ThisMinute
,P.parent_id
FROM
#Minutes M
JOIN
TimesCTE T
ON
M.ThisMinute BETWEEN start_time AND end_time
JOIN
#Parents P
ON T.parent_id = P.parent_id
GROUP BY
M.ThisMinute
,P.parent_id
,P.ChildCount
HAVING
COUNT(DISTINCT T.child_id) = P.ChildCount
--get results
SELECT
parent_id
,CAST(CASE WHEN start_time = '1900-01-01' AND end_time = '1900-01-02 23:59' THEN NULL ELSE start_time END AS TIME) AS start_time
,CAST(CASE WHEN start_time = '1900-01-01' AND end_time = '1900-01-02 23:59' THEN NULL ELSE end_time END AS TIME) AS end_time
,valid
FROM
(
SELECT
P.parent_id
,MIN(ThisMinute) AS start_time
,MAX(ThisMinute) AS end_time
,CASE WHEN MAX(ThisMinute) IS NOT NULL THEN 1 ELSE 0 END AS valid
FROM
#Parents P
LEFT JOIN
#SharedMinutes SM
ON P.parent_id = SM.parent_id
GROUP BY
P.parent_id
) Results
You may find that the iterative algorithm you have outlined in your question would be more efficient. But I would use a WHILE loop instead of a cursor if you take that approach.
This might be a very verbose method of achieving the desired results, but it works for the given dataset, although it should be tested with larger data.
I've simply joined the table to itself where the parent_id matches and the child_id is different to get all of the combinations of times that might overlap and then performed some DATEDIFF's to calculate the difference, before filtering and grouping the output.
You can run the below in isolation to test and tweak if required:
-- setup initial table
CREATE TABLE #OverlapTable
(
[parent_id] INT ,
[child_id] INT ,
[start_time] TIME ,
[end_time] TIME
);
-- insert dummy data
INSERT INTO #OverlapTable
( [parent_id], [child_id], [start_time], [end_time] )
VALUES ( 1, 1, '06:00', '14:00' ),
( 1, 2, '13:00', '09:00' ),
( 1, 3, '07:00', '09:00' ),
( 2, 1, '12:00', '17:00' ),
( 2, 2, '09:00', '11:00' ),
( 3, 1, NULL, NULL ),
( 3, 2, '23:00', '04:00' ),
( 4, 1, NULL, NULL ),
( 4, 2, NULL, NULL );
-- insert all combinations into a new temp table #Results with overlap calculations
SELECT *
INTO #Results
FROM ( SELECT t1.parent_id ,
t1.start_time ,
t1.end_time ,
t2.start_time AS t2_start_time ,
t2.end_time AS t2_end_time ,
CASE WHEN t1.start_time IS NULL
AND t1.end_time IS NULL THEN 0
WHEN t1.start_time BETWEEN t2.start_time
AND t2.end_time
THEN DATEDIFF(HOUR, t1.start_time, t2.end_time)
WHEN t1.end_time BETWEEN t2.start_time AND t2.end_time
THEN DATEDIFF(HOUR, t2.start_time, t1.end_time)
ELSE NULL
END AS Overlap
FROM #OverlapTable t1
INNER JOIN #OverlapTable t2 ON t2.parent_id = t1.parent_id
AND t2.child_id != t1.child_id
) t
-- SELECT * FROM #Results -- this shows intermediate results
-- filter and group results with the largest overlaps and handle other cases
SELECT DISTINCT
r.parent_id ,
CASE WHEN r.Overlap IS NULL THEN NULL
ELSE CASE WHEN r.start_time IS NULL THEN r.t2_start_time
ELSE r.start_time
END
END start_time ,
CASE WHEN r.Overlap IS NULL THEN NULL
ELSE CASE WHEN r.end_time IS NULL THEN r.t2_end_time
ELSE r.end_time
END
END end_time ,
CASE WHEN r.Overlap IS NULL THEN 0
ELSE 1
END Valid
FROM #Results r
WHERE EXISTS ( SELECT parent_id ,
MAX(Overlap)
FROM #Results
WHERE r.parent_id = parent_id
GROUP BY parent_id
HAVING MAX(Overlap) = r.Overlap
OR ( MAX(Overlap) IS NULL
AND r.Overlap IS NULL
) )
DROP TABLE #Results
DROP TABLE #OverlapTable
Hope that helps.

How could I "auto-rotate" appended records in SQL (1 goes to 2, 2 goes 3, 3 goes to 4, 4 goes back to 1)?

I'm working on a system (ASP.NET/MSSQL/C#) for scheduling restaurant employees.
The problem I'm having is I need to "auto-rotate" the shift "InTimes" every week.
The user needs to be able to copy one day's schedule to the same day next week with all the employee shift times rotated one slot.
For example, in the table below, Monica has the 10:30am shift this Monday, so she would have the 11:00am next week, and Adam would go from 12:00pm to 10:30am.
The time between shifts is not constant, nor is the number of employees on each shift.
Any ideas on how to do this (ideally with SQL statements) would be greatly appreciated.
Please keep in mind I'm a relative novice.
RecordID EmpType Date Day Meal ShiftOrder InTime EmployeeID
1 Server 29-Aug-11 Monday Lunch 1 10:30:00 AM Monica
2 Server 29-Aug-11 Monday Lunch 2 11:00:00 AM Sofia
3 Server 29-Aug-11 Monday Lunch 3 11:30:00 AM Jenny
4 Server 29-Aug-11 Monday Lunch 4 12:00:00 PM Adam
5 Server 29-Aug-11 Monday Dinner 1 4:30:00 PM Adam
6 Server 29-Aug-11 Monday Dinner 2 4:45:00 PM Jenny
7 Server 29-Aug-11 Monday Dinner 3 5:00:00 PM Shauna
8 Server 29-Aug-11 Monday Dinner 4 5:15:00 PM Sofia
10 Server 29-Aug-11 Monday Dinner 5 5:30:00 PM Monica
Somehow an employee would need to get his last (few) shifts
SELECT TOP 3 * FROM shift WHERE EmployeeID LIKE 'monica' ORDER BY [date] DESC
Next he/she would need to enter the time and date offset he would like to work next week, relative to a schedule before.
INSERT INTO shift SELECT
recordID
,[date]
,CASE [Intime]
WHEN [Intime] BETWEEN 00:00 AND 10:00 THEN 'Breakfast'
WHEN [Intime] BETWEEN 10:01 AND 04:29 THEN 'Lunch'
WHEN [Intime] BETWEEN 04:30 AND 23:59 THEN 'Dinner'
END as Meal
,No_idea_how_to_generate_this AS ShiftOrder
,[Intime]
,EmployeeID
FROM (SELECT
NULL as recordID
,DATEADD(DAY, 7+#dateoffset, ls.[date]) as [date]
,CAST(DATEADD(MINUTE, #timeoffset, ls.[time] AS TIME) as [Intime]
,EmployeeId
FROM Shift WHERE recordID = #recordID ) AS subselect
Here:
- #recordID is the record the employee choose as the starting point for the new appointment.
- #dateoffset is the number of days to add the the starting record
- #timeoffset is the number of minutes to add to the starting record
All the rest is determined by the row the user used as the starting point.
Here's what I came up with:
CREATE TABLE #tmp
(
[RecordID] INT ,
[EmpType] VARCHAR(20) ,
[Date] DATE ,
[Day] VARCHAR(10) ,
[Meal] VARCHAR(10) ,
[ShiftOrder] INT ,
[InTime] TIME ,
[EmployeeID] VARCHAR(50)
)
INSERT INTO [#tmp]
( [RecordID] ,
[EmpType] ,
[Date] ,
[Day] ,
[Meal] ,
[ShiftOrder] ,
[InTime] ,
[EmployeeID]
)
VALUES (1,'Server','29-Aug-11','Monday','Lunch',1,'10:30:00 AM','Monica'),
(2,'Server','29-Aug-11','Monday','Lunch',2,'11:00:00 AM','Sofia'),
(3,'Server','29-Aug-11','Monday','Lunch',3,'11:30:00 AM','Jenny'),
(4,'Server','29-Aug-11','Monday','Lunch',4,'12:00:00 PM','Adam'),
(5,'Server','29-Aug-11','Monday','Dinner',1,'4:30:00 PM','Adam'),
(6,'Server','29-Aug-11','Monday','Dinner',2,'4:45:00 PM','Jenny'),
(7,'Server','29-Aug-11','Monday','Dinner',3,'5:00:00 PM','Shauna'),
(8,'Server','29-Aug-11','Monday','Dinner',4,'5:15:00 PM','Sofia'),
(10,'Server','29-Aug-11','Monday','Dinner',5,'5:30:00 PM','Monica');
WITH CountByShift AS (SELECT *, COUNT(1) OVER (PARTITION BY EmpType, [Day], [Meal]) AS [CountByShiftByDayByEmpType]
FROM [#tmp]
),
NewShiftOrder AS (
SELECT *, ([ShiftOrder] + 1) % [CountByShiftByDayByEmpType] AS [NewShiftOrder]
FROM [CountByShift]
)
SELECT [RecordID] ,
[EmpType] ,
[Date] ,
[Day] ,
[Meal] ,
[ShiftOrder] ,
CASE WHEN [NewShiftOrder] = 0 THEN [CountByShiftByDayByEmpType] ELSE [NewShiftOrder] END AS [NewShiftOrder],
[InTime] ,
[EmployeeID]
FROM NewShiftOrder
ORDER BY [RecordID]
You need a table with all of the shifts in it:
create table dbo.Shifts (
[Day] varchar(9) not null,
Meal varchar(6) not null,
ShiftOrder integer not null,
InTime time not null,
constraint PK__dbo_Shifts primary key ([Day], Meal, ShiftOrder)
);
If that table is properly populated you can then run this to get a map of the current Day, Meal, ShiftOrder n-tuple to the next in that Day, Meal pair:
with numbers_per_shift as (
select [Day], Meal, max(ShiftOrder) as ShiftOrderCount
from dbo.Shifts s
group by [Day], Meal
)
select s.[Day], s.Meal, s.ShiftOrder,
s.ShiftOrder % n.ShiftOrderCount + 1 as NextShiftOrder
from dbo.Shifts as s
inner join numbers_per_shift as n
on s.[Day] = n.[Day]
and s.Meal = n.Meal;
For the table to be properly populated each of the shift orders would have to begin with one and increase by one with no skipping or repeating within a Day, Meal pair.
Borrowing most of the #tmp table definition from #Ben Thul, assuming you have an identity field, not assuming you are storing dates and times as dates and times...this should run well over and over, copying the latest date into the following week:
CREATE TABLE #tmp
(
[RecordID] INT ,
[EmpType] VARCHAR(20) ,
[Date] VARCHAR(9) ,
[Day] VARCHAR(10) ,
[Meal] VARCHAR(10) ,
[ShiftOrder] INT ,
[InTime] VARCHAR(11) ,
[EmployeeID] VARCHAR(50)
)
INSERT INTO [#tmp]
( [RecordID] ,
[EmpType] ,
[Date] ,
[Day] ,
[Meal] ,
[ShiftOrder] ,
[InTime] ,
[EmployeeID]
)
VALUES (1,'Server','29-Aug-11','Monday','Lunch',1,'10:30:00 AM','Monica'),
(2,'Server','29-Aug-11','Monday','Lunch',2,'11:00:00 AM','Sofia'),
(3,'Server','29-Aug-11','Monday','Lunch',3,'11:30:00 AM','Jenny'),
(4,'Server','29-Aug-11','Monday','Lunch',4,'12:00:00 PM','Adam'),
(5,'Server','29-Aug-11','Monday','Dinner',1,' 4:30:00 PM','Adam'),
(6,'Server','29-Aug-11','Monday','Dinner',2,' 4:45:00 PM','Jenny'),
(7,'Server','29-Aug-11','Monday','Dinner',3,' 5:00:00 PM','Shauna'),
(8,'Server','29-Aug-11','Monday','Dinner',4,' 5:15:00 PM','Sofia'),
(10,'Server','29-Aug-11','Monday','Dinner',5,' 5:30:00 PM','Monica');
with
Shifts as (
select EmpType, [Day], Meal, ShiftOrder, InTime
from #tmp
where [Date] = (select max(cast([Date] as datetime)) from #tmp)
),
MaxShifts as (
select EmpType, [Day], Meal, max(ShiftOrder) as MaxShiftOrder
from #tmp
where [Date] = (select max(cast([Date] as datetime)) from #tmp)
group by EmpType, [Day], Meal
)
insert into #tmp (EmpType, [Date], [Day], Meal, ShiftOrder, InTime, EmployeeID)
select s.EmpType
, replace(convert(varchar(11), dateadd(dd, 7, cast(a.[Date] as datetime)), 6), ' ', '-') as [Date]
, s.Day
, s.Meal
, s.ShiftOrder
, s.InTime
, a.EmployeeID
from #tmp as a
join MaxShifts as m on a.EmpType = m.EmpType
and a.[Day] = m.[Day]
and a.Meal = m.Meal
join Shifts as s on a.EmpType = s.EmpType
and a.[Day] = s.[Day]
and a.Meal = s.Meal
and 1 + a.ShiftOrder % m.MaxShiftOrder = s.ShiftOrder
where a.[Date] = (select max(cast([Date] as datetime)) from #tmp)
I'm assuming that the schedule is really tied to a meal and weekday in a below answer.
Also I would like to note that ShiftOrder and Day columns should not be columns. Day is obviously determined by Date so it is a total waste of space (computed column OR determine it on the UI side) and ShiftOrder is determined by Date and InTime columns (probably easy to calculate in a query with RANK() function or on the UI side). That said it will make this query a bit easier :)
declare #dt date = cast('29-Aug-11' as date)
/* note: the date above may be passed from UI or it maybe calculated based on getdate() and dateadd function or s.t. like that */
INSERT INTO [table] (EmpType,Date,Day,Meal,ShiftOrder,InTime,EmployeeID)
SELECT t1.EmpType, dateadd(day, 7, t1.date), t1.day, t1.meal, t2.ShiftOrder, t2.InTime, t1.EmployeeID
FROM [table] t1
INNER JOIN [table] t2
ON (t1.Date = t2.Date
and t1.Meal = t2.Meal
and (
t1.ShiftOrder = t2.ShiftOrder + 1
or
(
t1.ShiftOrder = (select max(shiftOrder) from [table] where meal = t1.meal and date =t1.date)
and
t2.ShiftOrder = (select min(shiftOrder) from [table] where meal = t1.meal and date =t1.date)
)
)
)
WHERE t1.Date = #dt
This is a pretty straight-forward set-oriented problem. Aggregations (count(*) and max()) and lookup tables are unnecessary. You can do it with one SQL statement.
The first step (set) is to identity those employees who simply slide down in the schedule.
The next step (set) is to identity those employees who need to "wrap around" to the head of the schedule.
Here's what I came up with:
/* Set up the temp table for demo purposes */
DROP TABLE #tmp
CREATE TABLE #tmp
(
[RecordID] INT ,
[EmpType] VARCHAR(20) ,
[Date] DATE ,
[Day] VARCHAR(10) ,
[Meal] VARCHAR(10) ,
[ShiftOrder] INT ,
[InTime] TIME,
[EmployeeID] VARCHAR(50)
)
INSERT INTO [#tmp]
( [RecordID] ,
[EmpType] ,
[Date] ,
[Day] ,
[Meal] ,
[ShiftOrder] ,
[InTime] ,
[EmployeeID]
)
VALUES (1,'Server','29-Aug-11','Monday','Lunch',1,'10:30:00 AM','Monica'),
(2,'Server','29-Aug-11','Monday','Lunch',2,'11:00:00 AM','Sofia'),
(3,'Server','29-Aug-11','Monday','Lunch',3,'11:30:00 AM','Jenny'),
(4,'Server','29-Aug-11','Monday','Lunch',4,'12:00:00 PM','Adam'),
(5,'Server','29-Aug-11','Monday','Dinner',1,' 4:30:00 PM','Adam'),
(6,'Server','29-Aug-11','Monday','Dinner',2,' 4:45:00 PM','Jenny'),
(7,'Server','29-Aug-11','Monday','Dinner',3,' 5:00:00 PM','Shauna'),
(8,'Server','29-Aug-11','Monday','Dinner',4,' 5:15:00 PM','Sofia'),
(10,'Server','29-Aug-11','Monday','Dinner',5,' 5:30:00 PM','Monica');
/* the "fills" CTE will find those employees who "wrap around" */
;WITH fills AS (
SELECT
[d2].[EmpType],
[d2].[Date],
[d2].[Day],
[d2].[Meal],
1 AS [ShiftOrder],
[d2].[InTime],
[d2].[EmployeeID]
FROM
[#tmp] d1
RIGHT OUTER JOIN
[#tmp] d2 ON
([d1].[Meal] = [d2].[Meal])
AND ([d1].[ShiftOrder] = [d2].[ShiftOrder] + 1)
WHERE
[d1].[EmployeeID] IS NULL
)
INSERT INTO [table] (EmpType,Date,Day,Meal,ShiftOrder,InTime,EmployeeID)
SELECT
[d1].[EmpType],
DATEADD(DAY, 7, [d1].[Date]) AS [Date],
DATENAME(dw,(DATEADD(DAY, 7, [d1].[Date]))) AS [Day],
[d1].[Meal],
[d1].[ShiftOrder],
[d1].[InTime],
ISNULL([d2].[EmployeeID], [f].[EmployeeID]) AS [EmployeeID]
FROM
[#tmp] d1
LEFT OUTER JOIN
[#tmp] d2 ON
([d1].[Meal] = [d2].[Meal]) AND ([d1].[ShiftOrder] = [d2].[ShiftOrder] + 1)
LEFT OUTER JOIN
[fills] f ON
([d1].[Meal] = [f].[Meal]) AND ([d1].[ShiftOrder] = [f].[ShiftOrder])
You can use a subquery (for a tutorial on subqueries, see http://www.databasejournal.com/features/mssql/article.php/3464481/Using-a-Subquery-in-a-T-SQL-Statement.htm) to get the last shift time.
After this, its trivial addition and modular division (in case you don't know what that is, have a look at this).
Hope this helped. I'm a bit tired right now, so I can't provide you with an example.
I'm a SQL programmer and DBA for 20 yrs now. With that said, business logic this complex should be in the C# part of the system. Then the TDD built application can handle the inevitable changes, and still be refactor-able and correct.
My recommendation is 'push-back'. Your response should be something along the lines of "This isn't just some look-up/fill-in the blank logic. This kind of complex business logic belongs in the App". It belongs in something that can be unit tested, and will be unit tested every time its changed.
The right answer sometimes is 'No', this is one of them.
How about using a Pivot Table for all employees and then adding shift timings as rows?? Order the names based on Shift for the initial Day.
Something like this..
Date_time Shift_Order Monica Sofia Jenny Adam Shauna
08/29/11 1 10:30AM 11:00AM 11:30AM 12:00PM NULL
08/29/11 2 5:30PM 5:15PM 4:45PM 4:30PM 5:00PM