Creating time slots in SQL Server - sql-server

We are trying to do an analysis of how long our staff have been working on a hourly basis for trending and forecasting purposes.
We have both the clock in (SHIFTA_Start) and clock out (SHIFTA_End) of the employees.
Then we did a datepart into 4 sections:
Start Time_Hour
Start Time_min
End Time_Hour
End Time_min
[I have included the current output I have, and the desired outcome I hope to get in this image]
http://i.stack.imgur.com/1uhq0.png
Given start time and end time:
e.g.
Start time – 940am (0940)
End time – 615pm (1815)
It can populate very well in the respective hour slots as it is straightforward.
However if the employee work overnight, given start time and end time:
e.g.
Start time – 930pm (2130)
End time – 7am (0700)
The hour slots cannot be populated.
To make it short, this is part of my case statement from 0hr to 1hr
SELECT
--b.*,
b.EMPLOYEENAME,
B.DEPARTMENT,
CONVERT(datetime, LEFT(b.SHIFTA_start,17),103) AS SHIFTA_start,
CONVERT(datetime, LEFT(b.ShiftA_End,17),103) as ShiftA_End,
b.StartTime_HOUR,
b.StartTime_min,
b.EndTime_HOUR,
b.EndTime_min,
CASE WHEN b.[0H_START] < b.[0H_END] THEN b.[0H_START] ELSE b.[0H_END] END AS [0],
CASE WHEN b.[1H_START] < b.[1H_END] THEN b.[1H_START] ELSE b.[1H_END] END AS [1]
from
(
/*Step 2 - calculating minutes from starttime and endtime */
select a.*,
/**Calculating the number of minutes worked from start_time MIN **/
CASE WHEN a.StartTime_HOUR = 0 and a.[0] = 1 AND a.StartTime_min !=0 THEN cast(cast((60-a.StartTime_min) as decimal(10,2))/60 as decimal(10,2)) ELSE a.[0] END AS [0H_START],
CASE WHEN a.StartTime_HOUR = 1 and a.[1] = 1 AND a.StartTime_min !=0 THEN cast(cast((60-a.StartTime_min) as decimal(10,2))/60 as decimal(10,2)) ELSE a.[1] END AS [1H_START],
/**Calculating the number of minutes worked from END_time MIN **/
CASE WHEN a.EndTime_HOUR = 0 and a.[0] = 1 AND a.EndTime_min !=0 THEN cast(cast((a.EndTime_min) as decimal(10,2))/60 as decimal(10,2)) ELSE a.[0] END AS [0H_END],
CASE WHEN a.EndTime_HOUR = 1 and a.[1] = 1 AND a.EndTime_min !=0 THEN cast(cast((a.EndTime_min) as decimal(10,2))/60 as decimal(10,2)) ELSE a.[1] END AS [1H_END]
from
(--Step 1:
/*to determine 1 or 0 using the start and end hour
If time falls in the respective hour = 1
if time doesnt fall in the respective hours = 0*/
SELECT
[EMPLOYEENAME],
[DEPARTMENT],
[SHIFTA_start],
CASE WHEN [SHIFTA_START] !='' OR SHIFTA_START != NULL THEN CONVERT(datetime, LEFT([SHIFTA_START],17),103) ELSE NULL END AS SHIFTA_START_con,
CASE WHEN [SHIFTA_START] !='' OR SHIFTA_START != NULL THEN DATEPART(hh,CONVERT(datetime, LEFT([SHIFTA_START],17),103)) ELSE NULL END AS StartTime_HOUR,
CASE WHEN [SHIFTA_START] !='' OR SHIFTA_START != NULL THEN DATEPART(mi,CONVERT(datetime, LEFT([SHIFTA_START],17),103)) ELSE NULL END AS StartTime_min,
[SHIFTA_end],
CASE WHEN [SHIFTA_END] !='' OR SHIFTA_end != NULL THEN CONVERT(datetime, LEFT([SHIFTA_END],17),103) ELSE NULL END AS SHIFTA_END_con,
CASE WHEN [SHIFTA_END] !='' OR SHIFTA_end != NULL THEN DATEPART(hh,CONVERT(datetime, LEFT([SHIFTA_end],17),103)) ELSE NULL END AS EndTime_HOUR,
CASE WHEN [SHIFTA_END] !='' OR SHIFTA_end != NULL THEN DATEPART(mi,CONVERT(datetime, LEFT([SHIFTA_end],17),103)) ELSE NULL END AS EndTime_min,
CASE WHEN [SHIFTA_START] !='' AND 0 BETWEEN DATEPART(hh,CONVERT(datetime, LEFT([SHIFTA_START],17),103)) AND DATEPART (hh,CONVERT(datetime, LEFT([SHIFTA_end],17),103)) THEN 1 ELSE 0 END AS [0],
CASE WHEN [SHIFTA_START] !='' AND 1 BETWEEN DATEPART(hh,CONVERT(datetime, LEFT([SHIFTA_START],17),103)) AND DATEPART (hh,CONVERT(datetime, LEFT([SHIFTA_end],17),103)) THEN 1 ELSE 0 END AS [1]
from [DatabaseTable].[dbo].[ATTENDANCE]
where ShiftA_Start != '' and ShiftA_End !='' and shiftA_start != shiftA_End
)a
)b
This is the output #Mike
http://i.stack.imgur.com/laSKX.png
My current SQL Statement is
DECLARE #WORKINGHOURS TABLE (
ID INT IDENTITY(1,1) NOT NULL,
SHIFTA_START DATETIME NOT NULL,
SHIFTA_END DATETIME NOT NULL
);
WITH WORKINGHOURS AS (
SELECT TOP 1000 ID,
-- flatten the first hour to remove the minutes and get the initial current hour
DATEADD(hour, DATEDIFF(hour, 0, CONVERT(datetime, LEFT(SHIFTA_start,17),103)), 0) AS currentHour,
CONVERT(datetime, LEFT(SHIFTA_start,17),103) AS [SHIFTA_START],
CONVERT(datetime, LEFT(SHIFTA_END,17),103) AS [SHIFTA_END],
DATEPART(hour, CONVERT(datetime, LEFT(SHIFTA_start,17),103)) AS HourOrdinal,
-- determine how much of the first hour is applicable. if it is minute 0 then the whole hour counts
CAST(CASE DATEPART(minute, CONVERT(datetime, LEFT(SHIFTA_start,17),103))
WHEN 0 THEN 1.0
ELSE (60 - DATEPART(minute, CONVERT(datetime, LEFT(SHIFTA_start,17),103))) / 60.0
END AS DECIMAL(5,3)) AS HourValue
FROM [TableName].[dbo].[Attendance]
UNION ALL
SELECT ID,
-- add an hour to the currentHour each time the recursive CTE is called
DATEADD(hour, 1, currentHour) AS currentHour,
CONVERT(datetime, LEFT(SHIFTA_start,17),103) AS [SHIFTA_START],
CONVERT(datetime, LEFT(SHIFTA_END,17),103) AS [SHIFTA_END],
DATEPART(hour, DATEADD(hour, 1, currentHour)) AS hourOrdinal,
CAST(CASE
-- when this is the last time period determine the amount of the hour that is applicable
WHEN DATEADD(hour, 2, currentHour)
> CONVERT(datetime, LEFT(SHIFTA_END,17),103)
THEN DATEPART(minute, CONVERT(datetime, LEFT(SHIFTA_END,17),103)) / 60.0
ELSE 1
END AS DECIMAL(5,3)) AS HourValue
FROM WORKINGHOURS
-- contine recursion until the next hour is after the ShiftEnd
WHERE DATEADD(hour, 1, currentHour) < CONVERT(datetime, LEFT(SHIFTA_END,17),103)
)
SELECT *
FROM (
SELECT ID,
CONVERT(datetime, LEFT(SHIFTA_start,17),103) AS [SHIFTA_START],
CONVERT(datetime, LEFT(SHIFTA_END,17),103) AS [SHIFTA_END],
HourValue,
HourOrdinal
FROM WORKINGHOURS
) AS t
PIVOT (
SUM(HourValue)
FOR HourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt
OPTION (MAXRECURSION 0);

this should gives you what you wanted.
;
with cte as
(
select *,
hr_st1 = case when datepart(hour, SHIFTA_Start) < datepart(hour, SHIFTA_End)
then datepart(hour, SHIFTA_Start)
else 0
end,
hr_en1 = datepart(hour, SHIFTA_End),
hr_st2 = case when datepart(hour, SHIFTA_Start) > datepart(hour, SHIFTA_End)
then datepart(hour, SHIFTA_Start)
end,
hr_en2 = case when datepart(hour, SHIFTA_Start) > datepart(hour, SHIFTA_End)
then 23
end
from #shift s
)
select *,
CASE WHEN 0 BETWEEN hr_st1 and hr_en1
OR 0 BETWEEN hr_st2 and hr_en2
THEN 1
ELSE 0 END AS [0],
CASE WHEN 1 BETWEEN hr_st1 and hr_en1
OR 1 BETWEEN hr_st2 and hr_en2
THEN 1
ELSE 0 END AS [1],
CASE WHEN 2 BETWEEN hr_st1 and hr_en1
OR 2 BETWEEN hr_st2 and hr_en2
THEN 1
ELSE 0 END AS [2],
CASE WHEN 3 BETWEEN hr_st1 and hr_en1
OR 3 BETWEEN hr_st2 and hr_en2
THEN 1
ELSE 0 END AS [3]
from cte

Because of the need to get a column for each hour this is a good candidate for using the TSQL PIVOT operator. Pivot takes rows of data and turns it into columns.
The first step is to create all of our rows: one for each hour and also calculate how much of that hour was applicable. I've done this by using a recursive CTE to generate all of the numbers from 0 to 23. A recursive CTE is a query that keeps calling itself until the anchor condition is met. The anchor condition is the WHERE clause of the 2nd SQL statement in the UNION.
Then I join on that using the ShiftStart_Hour and ShiftStart_End columns but you need to take into account when one of them is on the next day (ShiftStart_Hour > ShiftEndHour):
INNER JOIN hourOrdinals AS h ON (
t.ShiftStart_Hour < t.ShiftEnd_Hour
AND h.hr BETWEEN t.ShiftStart_Hour AND t.ShiftEnd_Hour
) OR (
t.ShiftStart_Hour > t.ShiftEnd_Hour
AND (
h.hr BETWEEN 0 AND t.ShiftEnd_Hour
OR
h.hr BETWEEN t.ShiftStart_Hour AND 23
)
)
That join creates a row for each hour. Now we need to calculate how much of that hour was applicable using a CASE statement in the SELECT clause:
CAST(CASE h.hr
WHEN ShiftStart_Hour THEN (60 - ShiftStart_Minute) / 60.0
WHEN ShiftEnd_Hour THEN ShiftEnd_Minute / 60.0
ELSE 1.0
END AS DECIMAL(5,3)) AS hourValue
After that we can finally use the PIVOT operator to get the data in the final format we're looking for. The full working example is:
/* CREATE TEST TABLE & DATA */
CREATE TABLE #dataTable (
RowID INT IDENTITY(1,1) NOT NULL,
ShiftStart DATETIME NOT NULL,
ShiftStart_Hour AS DATEPART(hour, ShiftStart),
ShiftStart_Minute AS DATEPART(minute, ShiftStart),
ShiftEnd DATETIME NOT NULL,
ShiftEnd_Hour AS DATEPART(hour, ShiftEnd),
ShiftEnd_Minute AS DATEPART(minute, ShiftEnd)
);
INSERT INTO #dataTable (
ShiftStart,
ShiftEnd
)
VALUES (
'2015-08-18 07:00:00',
'2015-08-18 21:00:00'
),(
'2015-08-20 09:40:00',
'2015-08-20 18:15:00'
),(
'2015-08-20 21:30:00',
'2015-08-21 07:00:00'
),(
'2015-08-25 11:00:00',
'2015-08-27 11:00:00'
);
/* END OF TEST DATA CREATION */
WITH hourOrdinals AS (
SELECT 0 AS hr
UNION ALL
SELECT hr+1
FROM hourOrdinals
WHERE hr<23
)
SELECT *
FROM (
SELECT RowId,
ShiftStart,
ShiftEnd,
h.hr AS hourOrdinal,
CAST(CASE h.hr
WHEN ShiftStart_Hour THEN (60 - ShiftStart_Minute) / 60.0
WHEN ShiftEnd_Hour THEN ShiftEnd_Minute / 60.0
ELSE 1.0
END AS DECIMAL(5,3)) AS hourValue
FROM #dataTable AS t
INNER JOIN hourOrdinals AS h ON (
t.ShiftStart_Hour < t.ShiftEnd_Hour
AND h.hr BETWEEN t.ShiftStart_Hour AND t.ShiftEnd_Hour
) OR (
t.ShiftStart_Hour > t.ShiftEnd_Hour
AND (
h.hr BETWEEN 0 AND t.ShiftEnd_Hour
OR
h.hr BETWEEN t.ShiftStart_Hour AND 23
)
)
) AS shiftHours
PIVOT (
SUM(hourValue)
FOR hourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt;
DROP TABLE #dataTable;
But that's not good enough...
There is a problem with this method. Because we're only actually looking at the hours and not the dates it isn't going to get the right answer if some super hard worker goes on a 24+ hour shift. But there is a better way.
Instead of using the recursive CTE to generate the hours we want to match against we can use it to iterate all of the hours between the ShiftStart and ShiftEnd values. While at the same time calculating how much of each hour is applicable. In this case the anchor condition has to do with the actual data from the table instead of just simple arithmetic. Then just PIVOT again to get the result set in the correct format.
Note: This query likely won't scale for more than a few thousand rows. Date range filtering and other limits on the dataset should help with that. But if you need to process more than that you will need to persist these calculations somewhere.
This example works for shifts spanning multiple days:
/* CREATE TEST TABLE AND DATA */
DECLARE #dataTable TABLE (
RowID INT IDENTITY(1,1) NOT NULL,
ShiftStart DATETIME NOT NULL,
ShiftEnd DATETIME NOT NULL
);
INSERT INTO #dataTable (
ShiftStart,
ShiftEnd
)
VALUES (
'2015-08-18 07:00:00',
'2015-08-18 21:00:00'
),(
'2015-08-20 09:40:00',
'2015-08-20 18:15:00'
),(
'2015-08-20 21:30:00',
'2015-08-21 07:00:00'
),(
'2015-08-25 11:00:00',
'2015-08-27 11:00:00'
),(
'2015-08-01 12:00:00',
'2015-08-02 06:00:00'
),(
'2015-08-02 12:15:00',
'2015-08-04 07:45:00'
),(
'2015-08-11 12:00:00',
'2015-08-11 12:59:00'
),(
'1900-01-01 12:00:00',
'1900-01-01 12:00:00'
),(
'2015-08-11 12:00:00',
'2015-08-11 12:15:00'
);
/* END OF TEST DATA CREATION */
WITH shiftHours AS (
SELECT RowID,
-- flatten the first hour to remove the minutes and get the initial current hour
DATEADD(hour, DATEDIFF(hour, 0, ShiftStart), 0) AS currentHour,
ShiftStart,
ShiftEnd,
DATEPART(hour, ShiftStart) AS hourOrdinal,
-- determine how much of the first hour is applicable. if it is minute 0 then the whole hour counts
CAST(CASE
WHEN DATEADD(hour, DATEDIFF(hour, 0, ShiftStart), 0) = DATEADD(hour, DATEDIFF(hour, 0, ShiftEnd), 0) THEN DATEDIFF(minute, ShiftStart, ShiftEnd) / 60.0
WHEN DATEPART(minute, ShiftStart) = 0 THEN 1.0
ELSE (60 - DATEPART(minute, ShiftStart)) / 60.0
END AS DECIMAL(5,3)) AS hourValue
FROM #dataTable AS t
UNION ALL
SELECT RowID,
-- add an hour to the currentHour each time the recursive CTE is called
DATEADD(hour, 1, currentHour) AS currentHour,
ShiftStart,
ShiftEnd,
DATEPART(hour, DATEADD(hour, 1, currentHour)) AS hourOrdinal,
CAST(CASE
-- when this is the last time period determine the amount of the hour that is applicable
WHEN DATEADD(hour, 2, currentHour) > ShiftEnd THEN DATEPART(minute, ShiftEnd) / 60.0
ELSE 1
END AS DECIMAL(5,3)) AS hourValue
FROM shiftHours
-- contine recursion until the next hour is after the ShiftEnd
WHERE DATEADD(hour, 1, currentHour) < ShiftEnd
)
SELECT *
FROM (
SELECT RowID,
ShiftStart,
ShiftEnd,
hourValue,
hourOrdinal
FROM shiftHours
) AS t
PIVOT (
SUM(hourValue)
FOR hourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt
OPTION (MAXRECURSION 0);
Data type conversion issues...
This answer has already gone way beyond what is a good fit for StackOverflow but let's just iron out the last detail. Your table stores dates as strings. That is a terrible design decision that will cause you lots of pain when trying to do reports from this database.
I can't see how your strings are formatted so I've just made the assumption that your conversion function is correct: CONVERT(datetime, LEFT(b.SHIFTA_start,17),103) If it isn't working then none of this code will work and you'll need to fix the two calls in table expression x below. I've also made the assumption that the table name you've given in your updated query above is correct. If not you'll need to fix that in the same table expression. It should be obvious what needs to be changed.
WITH shiftHours AS (
SELECT RowID,
-- flatten the first hour to remove the minutes and get the initial current hour
DATEADD(hour, DATEDIFF(hour, 0, ShiftStart), 0) AS currentHour,
ShiftStart,
ShiftEnd,
DATEPART(hour, ShiftStart) AS hourOrdinal,
-- determine how much of the first hour is applicable. if it is minute 0 then the whole hour counts
CAST(CASE
WHEN DATEADD(hour, DATEDIFF(hour, 0, ShiftStart), 0) = DATEADD(hour, DATEDIFF(hour, 0, ShiftEnd), 0) THEN DATEDIFF(minute, ShiftStart, ShiftEnd) / 60.0
WHEN DATEPART(minute, ShiftStart) = 0 THEN 1.0
ELSE (60 - DATEPART(minute, ShiftStart)) / 60.0
END AS DECIMAL(5,3)) AS hourValue
FROM (
-- use a ROW_NUMBER() to generate row IDs for the shifts to ensure each row is unique once it gets to the pivot
SELECT ROW_NUMBER() OVER(ORDER BY ShiftStart, ShiftEnd) AS RowID,
ShiftStart,
ShiftEnd
FROM (
-- this is where the data gets pulled from the source table and where the data types are converted from string to DATETIME
SELECT CONVERT(DATETIME, LEFT(SHIFTA_start, 17), 103) AS ShiftStart,
CONVERT(DATETIME, LEFT(SHIFTA_end, 17), 103) AS ShiftEnd
FROM WORKINGHOURS
-- this is also where you would add any filtering from the source table such as date ranges
) x
) AS y
UNION ALL
SELECT RowID,
-- add an hour to the currentHour each time the recursive CTE is called
DATEADD(hour, 1, currentHour) AS currentHour,
ShiftStart,
ShiftEnd,
DATEPART(hour, DATEADD(hour, 1, currentHour)) AS hourOrdinal,
CAST(CASE
-- when this is the last time period determine the amount of the hour that is applicable
WHEN DATEADD(hour, 2, currentHour) > ShiftEnd THEN DATEPART(minute, ShiftEnd) / 60.0
ELSE 1
END AS DECIMAL(5,3)) AS hourValue
FROM shiftHours
-- contine recursion until the next hour is after the ShiftEnd
WHERE DATEADD(hour, 1, currentHour) < ShiftEnd
)
SELECT *
FROM (
SELECT RowID,
ShiftStart,
ShiftEnd,
hourValue,
hourOrdinal
FROM shiftHours
) AS t
PIVOT (
SUM(hourValue)
FOR hourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt
OPTION (MAXRECURSION 0);

You are making this way harder than it needs to be. Use the DateTime as is and use a calculated column to give you minutes worked (or hours).
create table WorkTimes
(
Id int not null identity,
SHIFTA_Start DateTime not null,
SHIFTA_End DateTime,
MinutesWorked AS CASE WHEN SHIFTA_End IS NULL THEN NULL
ELSE DATEDIFF(MINUTE, SHIFTA_Start, SHIFTA_End)
END,
constraint WorkTimes_Check_Shift
check (SHIFTA_End IS NULL OR SHIFTA_Start < SHIFTA_End),
constraint WorkTimes_Check_Shift_Too_Long
check (SHIFTA_End IS NULL OR datediff(hour, SHIFTA_Start, SHIFTA_End) < 22)
)
So the MinutesWorked will always be correct. No need to have code to account for daylight savings time, year change, working overnight, etc.
Now that you have the correct data type you can calculate what you want.
declare #start datetime = '4/27/2016 22:10', #stop datetime = '4/28/2016 05:20'
select h.[Hour],
round(cast(case when #start > h.hour then 60 - DATEPART(MINUTE, #start)
when #stop < h.hour then DATEPART(minute, #stop)
else 60 end as decimal(5, 2)) / 60, 2) PercentHour
from [dbo].HoursBetween(#start, #stop) h
order by h.[Hour]
This creates:
The function I used is:
create function [dbo].[HoursBetween](#Start datetime, #Stop datetime)
returns #Hours TABLE
(
[Hour] DateTime
)
begin
declare #temp datetime
set #Start = cast(CONVERT(VARCHAR(13), #Start, 120) + ':00' as datetime)
set #temp = cast(CONVERT(VARCHAR(13), #Stop, 120) + ':00' as datetime)
if(#temp <> #Stop)
set #Stop = DATEADD(hour, 1, #temp)
while(#Start <= #Stop)
begin
insert into #Hours
values(#Start)
set #Start = DATEADD(hour, 1, #Start)
end
return;
end
GO

Related

How do I can divide a day in 30 minutes intervals in SQL server for a single date or multiple days like a month [duplicate]

I have below query and i want to get datetime in 30 min intervals between 2 datetime. Basicly I got it, but is limitited and wouln't return al results if the timediff is over 24 hrs.
For example:
#DateTime1 = 24/11/2016 18:00:00
#DateTime2 = 25/11/2016 06:00:00
Result: (in format "dd-HH:mm")
24-18:00
24-18:30
24-19:00
24-19:30
24-20:00
...
...
25-05:00
25-05:30
25-06:00
What I've tried.
SELECT number, DATEADD(MINUTE, number, #DateTime1) AS DateTimeLine, DATEPART(DAY, DATEADD(MINUTE, number, #DateTime1)) AS Days, DATEPART(MONTH,
DATEADD(MINUTE, number, #DateTime1)) AS Months, DATEPART(YEAR, DATEADD(MINUTE, number, #DateTime1)) AS Years, DATEPART(HOUR, DATEADD(MINUTE,
number, #DateTime1)) AS Hours, DATEPART(MINUTE, DATEADD(MINUTE, number, #DateTime1)) AS Minute, CAST(DATEADD(MINUTE, number, #DateTime1)
AS DATE) AS Date, CAST(DATEADD(MINUTE, number, #DateTime1) AS TIME) AS Time
FROM master.dbo.spt_values
WHERE (type = 'P') AND (DATEPART(MINUTE, DATEADD(MINUTE, number, #DateTime1)) = 30 OR DATEPART(MINUTE, DATEADD(MINUTE, number, #DateTime1)) = 0) AND (DATEADD(MINUTE, number, #DateTime1) <= #DateTime2)
ORDER BY number
A tally table is a great way to deal with this type of thing. I keep one in a view to avoid using spt_values.
create View [dbo].[cteTally] as
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select N from cteTally
Then your code becomes really simple too. A small amount of datemath and voila.
declare #DateTime1 datetime = '2016/11/24 18:00:00'
, #DateTime2 datetime = '2016/11/25 06:00:00'
select FORMAT(DATEADD(minute, (t.N - 1) * 30, #DateTime1), 'dd-HH:mm')
from cteTally t
where t.N <= (DATEDIFF(hour, #DateTime1, #DateTime2) * 2) + 1
I have a TVF which generates dynamic date/time ranges. It is faster than a recursive cte, and I think more flexible. You pass the date range, desired DatePart, and increment.
Declare #DateTime1 DateTime = '2016-11-24 18:00:00'
Declare #DateTime2 DateTime = '2016-11-25 06:00:00'
Select Format(RetVal,'dd-HH:mm') from [dbo].[udf-Range-Date](#DateTime1,#DateTime2,'MI',30)
Returns
24-18:00
24-18:30
24-19:00
24-19:30
24-20:00
24-20:30
24-21:00
24-21:30
24-22:00
24-22:30
24-23:00
24-23:30
25-00:00
....
25-04:30
25-05:00
25-05:30
25-06:00
The UDF if needed
CREATE FUNCTION [dbo].[udf-Range-Date] (#R1 datetime,#R2 datetime,#Part varchar(10),#Incr int)
Returns Table
Return (
with cte0(M) As (Select 1+Case #Part When 'YY' then DateDiff(YY,#R1,#R2)/#Incr When 'QQ' then DateDiff(QQ,#R1,#R2)/#Incr When 'MM' then DateDiff(MM,#R1,#R2)/#Incr When 'WK' then DateDiff(WK,#R1,#R2)/#Incr When 'DD' then DateDiff(DD,#R1,#R2)/#Incr When 'HH' then DateDiff(HH,#R1,#R2)/#Incr When 'MI' then DateDiff(MI,#R1,#R2)/#Incr When 'SS' then DateDiff(SS,#R1,#R2)/#Incr End),
cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a, cte1 b, cte1 c, cte1 d, cte1 e, cte1 f, cte1 g, cte1 h ),
cte3(N,D) As (Select 0,#R1 Union All Select N,Case #Part When 'YY' then DateAdd(YY, N*#Incr, #R1) When 'QQ' then DateAdd(QQ, N*#Incr, #R1) When 'MM' then DateAdd(MM, N*#Incr, #R1) When 'WK' then DateAdd(WK, N*#Incr, #R1) When 'DD' then DateAdd(DD, N*#Incr, #R1) When 'HH' then DateAdd(HH, N*#Incr, #R1) When 'MI' then DateAdd(MI, N*#Incr, #R1) When 'SS' then DateAdd(SS, N*#Incr, #R1) End From cte2 )
Select RetSeq = N+1
,RetVal = D
From cte3,cte0
Where D<=#R2
)
/*
Max 100 million observations -- Date Parts YY QQ MM WK DD HH MI SS
Syntax:
Select * from [dbo].[udf-Range-Date]('2016-10-01','2020-10-01','YY',1)
Select * from [dbo].[udf-Range-Date]('2016-01-01','2017-01-01','MM',1)
*/
using a recursive Common Table Expression [CTE] is one pretty clean method. For the formatting I am showing FORMAT() from SQL-Server 2012+ you may consider using DATEPART etc to do it though as FORMAT() can have performance impact.
I do agree with #RossBush's comment if you do things like this a lot generating a calendar (dates) and a time dimensions is very helpful for these purposes.
DECLARE #DateTime1 DATETIME = '2016/11/24 18:00:00'
DECLARE #DateTime2 DATETIME = '2016/11/25 06:00:00'
;WITH cte30MinIncrements AS (
SELECT #DateTime1 as DT
UNION ALL
SELECT DATEADD(MINUTE,30,DT)
FROM
cte30MinIncrements
WHERE DATEADD(MINUTE,30,DT) <= #DateTime2
)
SELECT
*
,FORMAT(DT,'dd-HH:mm') as Formated
FROM
cte30MinIncrements
Please see if this works.
declare #DateTime1 DateTime = '2016-11-24 18:00:00'
declare #DateTime2 DateTime = '2016-11-25 18:00:00'
declare #Interval DateTime = #DateTime1
declare #vartmptable table(DT DateTime)
While (#Interval < #DateTime2)
begin
--select #Interval, FORMAT(#Interval,'dd-HH:mm')
insert into #vartmptable select #Interval
set #Interval = DATEADD(mi,30,#Interval)
end
select FORMAT(DT,'dd-HH:mm') from #vartmptable
What about this? You can use variables/ fixed values as necessary.
WITH CTE_Numbers
AS (
SELECT n = 1
UNION ALL
SELECT n + 1
FROM CTE_Numbers
WHERE n < 100
)
SELECT FORMAT(DATEADD(mi, n * 30, '2016/11/03'),'dd-HH:mm')
FROM CTE_Numbers

Count number of occurrences per hour between two dates SQL Server

I've created a temp table #MB that has a record ID (119 rows), start and end date (partial screenshot of the list is below):
I'm trying to get a count of occurrences that happened each hour for each record ID during the start and end date (or number of occurrences each hour when ID was active between two dates).
I've used this code:
SELECT *
FROM
(SELECT
ISNULL(CAST(part AS VARCHAR(5)), 'Total') AS part,
COUNT(*) AS part_count
FROM
(SELECT DATEPART([HOUR], [Start]) AS part
FROM #MB) grp
GROUP BY
GROUPING SETS((part),())
) pre
PIVOT
(MAX(part_count)
FOR part IN ([0], [1], [2], [3], [4], [5], [6], [7], [8],
[9], [10], [11], [12], [13], [14], [15], [16],
[17], [18], [19], [20], [21], [22], [23], Total)
) pvt;
but it counts only records based on the start date (don't count each hour between two dates) and I stuck on how to generate occurrences per hour for each ID between two dates that I can later use to pre-aggregate and pivot.
first, you need to generate the list of rows for each hour
here i am using a recursive cte query to do it
; with MB as
(
select ID, [Start], [End], [Date] = [Start]
from #MB
union all
select ID, [Start], [End], [Date] = dateadd(hour, 1, convert(date, c.[Date]))
from MB c
where dateadd(hour, 1, c.[Date]) < [End]
)
select *
from MB
so in your pivot query , just change to this
; with MB as
(
select ID, [Start], [End], [Date] = [Start]
from #MB
union all
select ID, [Start], [End], [Date] = DATEADD(HH,DATEPART(HH,[Start]),CAST(CAST([Start] AS DATE) AS DATETIME))
from MB c
where dateadd(hour, 1, c.[Date]) < [End]
)
SELECT *
FROM (
SELECT ISNULL(CAST(part AS VARCHAR(5)), 'Total') AS part,
COUNT(*) AS part_count
FROM (
SELECT DATEPART([HOUR], [Date]) AS part
FROM MB -- changed to the cte
) grp
GROUP BY
GROUPING SETS((part),())
) pre
PIVOT (MAX(part_count) FOR part IN (
[0],[1],[2],[3],[4],[5],[6],[7],[8],
[9],[10],[11],[12],[13],[14],[15],[16],
[17],[18],[19],[20],[21],[22],[23], Total)) pvt;

Find gaps in timesheet data between certain hours

I am trying to find gaps in timesheets between the hours of 8AM and 6PM. I am able to find the gaps for records that are logged, but I cannot figure out how to determine if a record was "missed" - meaning if they started at 8:30 AM, I cannot figure out how to identify the 30 minute gap between 8AM and 8:30 AM (ie. they started work late).
In below example, I can find the two gaps between 12 and 12:30 pm, but not the 8am-8:30am gap and 5:30 to 6pm gap on 5/8, and 8am-8:30am gap on 5/10.
Any ideas to point me in the right direction on how I could approach this?
drop table #time;
create table #time (
TimesheetId int not null
, StartTime datetime not null
, EndTIme datetime not null
);
insert into #time (TimesheetId, StartTime, EndTime)
values (210, '2017-05-08 05:30:00.000', '2017-05-08 06:30:00.000')
, (210, '2017-05-08 06:30:00.000', '2017-05-08 08:30:00.000')
, (210, '2017-05-08 08:30:00.000', '2017-05-08 12:00:00.000')
, (210, '2017-05-08 12:30:00.000', '2017-05-08 18:30:00.000')
, (210, '2017-05-09 08:30:00.000', '2017-05-09 12:00:00.000')
, (210, '2017-05-09 12:30:00.000', '2017-05-09 17:30:00.000')
, (210, '2017-05-09 22:30:00.000', '2017-05-10 05:30:00.000')
, (210, '2017-05-10 08:30:00.000', '2017-05-10 18:00:00.000')
;
; with t1 as (
SELECT TimesheetId
, StartTime
, lag(EndTime) OVER (PARTITION BY TimesheetId ORDER BY StartTime) AS prev_endtime
FROM #time
where datepart(HH, StartTime) <= 18
and datepart(HH, EndTime) >= 8
)
select prev_endtime as gapStart
, StartTime as gapEnd
from t1
where StartTime <> prev_endtime
and cast(prev_endtime as date) = cast(StartTime as date)
;
WITH
a AS(SELECT DATEADD(hh, DATEDIFF(dd, 0, StartTime) * 24 + 8, 0) t,
TimesheetId FROM #time),
b AS(SELECT * FROM #time UNION ALL SELECT TimesheetId, t, t FROM a UNION ALL
SELECT TimesheetId, DATEADD(hh, 10, t), DATEADD(hh, 10, t) FROM a),
c AS(SELECT TimesheetId,
LAG(EndTime) OVER (
PARTITION BY TimesheetId ORDER BY StartTime
) prev_fin,
StartTime
FROM b),
d AS(SELECT *, DATEADD(hh, DATEDIFF(dd, 0, prev_fin) * 24 + 8, 0) beg,
DATEADD(hh, DATEDIFF(dd, 0, prev_fin) * 24 + 18, 0) fin
FROM c)
SELECT TimesheetId, prev_fin, StartTime
FROM d
WHERE prev_fin < StartTime AND
((prev_fin >= beg AND prev_fin < fin) OR
(StartTime > beg AND StartTime <= fin));
Check it on rextester.com.
You can use this to insert a record and then use what you have
Or you could use a UNION
select distinct t1.TimesheetId, dateadd(hh, 8, cast(CONVERT(date, StartTime) as datetime)) as StartTime, dateadd(hh, 8, cast(CONVERT(date, StartTime) as datetime)) as EndTime
from #time t1
where not exists ( select dateadd(hh, 8, cast(CONVERT(date, T2.StartTime) as datetime)), t2.*
from #time T2
where 1 = 1
and t2.TimesheetId = t1.TimesheetId
and CONVERT(date, T2.StartTime) = CONVERT(date, T1.StartTime)
and t2.StartTime = dateadd(hh, 8, cast(CONVERT(date, t2.StartTime) as datetime))
)

mssql 30 minute time intervals beteen 2 datetime

I have below query and i want to get datetime in 30 min intervals between 2 datetime. Basicly I got it, but is limitited and wouln't return al results if the timediff is over 24 hrs.
For example:
#DateTime1 = 24/11/2016 18:00:00
#DateTime2 = 25/11/2016 06:00:00
Result: (in format "dd-HH:mm")
24-18:00
24-18:30
24-19:00
24-19:30
24-20:00
...
...
25-05:00
25-05:30
25-06:00
What I've tried.
SELECT number, DATEADD(MINUTE, number, #DateTime1) AS DateTimeLine, DATEPART(DAY, DATEADD(MINUTE, number, #DateTime1)) AS Days, DATEPART(MONTH,
DATEADD(MINUTE, number, #DateTime1)) AS Months, DATEPART(YEAR, DATEADD(MINUTE, number, #DateTime1)) AS Years, DATEPART(HOUR, DATEADD(MINUTE,
number, #DateTime1)) AS Hours, DATEPART(MINUTE, DATEADD(MINUTE, number, #DateTime1)) AS Minute, CAST(DATEADD(MINUTE, number, #DateTime1)
AS DATE) AS Date, CAST(DATEADD(MINUTE, number, #DateTime1) AS TIME) AS Time
FROM master.dbo.spt_values
WHERE (type = 'P') AND (DATEPART(MINUTE, DATEADD(MINUTE, number, #DateTime1)) = 30 OR DATEPART(MINUTE, DATEADD(MINUTE, number, #DateTime1)) = 0) AND (DATEADD(MINUTE, number, #DateTime1) <= #DateTime2)
ORDER BY number
A tally table is a great way to deal with this type of thing. I keep one in a view to avoid using spt_values.
create View [dbo].[cteTally] as
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select N from cteTally
Then your code becomes really simple too. A small amount of datemath and voila.
declare #DateTime1 datetime = '2016/11/24 18:00:00'
, #DateTime2 datetime = '2016/11/25 06:00:00'
select FORMAT(DATEADD(minute, (t.N - 1) * 30, #DateTime1), 'dd-HH:mm')
from cteTally t
where t.N <= (DATEDIFF(hour, #DateTime1, #DateTime2) * 2) + 1
I have a TVF which generates dynamic date/time ranges. It is faster than a recursive cte, and I think more flexible. You pass the date range, desired DatePart, and increment.
Declare #DateTime1 DateTime = '2016-11-24 18:00:00'
Declare #DateTime2 DateTime = '2016-11-25 06:00:00'
Select Format(RetVal,'dd-HH:mm') from [dbo].[udf-Range-Date](#DateTime1,#DateTime2,'MI',30)
Returns
24-18:00
24-18:30
24-19:00
24-19:30
24-20:00
24-20:30
24-21:00
24-21:30
24-22:00
24-22:30
24-23:00
24-23:30
25-00:00
....
25-04:30
25-05:00
25-05:30
25-06:00
The UDF if needed
CREATE FUNCTION [dbo].[udf-Range-Date] (#R1 datetime,#R2 datetime,#Part varchar(10),#Incr int)
Returns Table
Return (
with cte0(M) As (Select 1+Case #Part When 'YY' then DateDiff(YY,#R1,#R2)/#Incr When 'QQ' then DateDiff(QQ,#R1,#R2)/#Incr When 'MM' then DateDiff(MM,#R1,#R2)/#Incr When 'WK' then DateDiff(WK,#R1,#R2)/#Incr When 'DD' then DateDiff(DD,#R1,#R2)/#Incr When 'HH' then DateDiff(HH,#R1,#R2)/#Incr When 'MI' then DateDiff(MI,#R1,#R2)/#Incr When 'SS' then DateDiff(SS,#R1,#R2)/#Incr End),
cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (Select M from cte0) Row_Number() over (Order By (Select NULL)) From cte1 a, cte1 b, cte1 c, cte1 d, cte1 e, cte1 f, cte1 g, cte1 h ),
cte3(N,D) As (Select 0,#R1 Union All Select N,Case #Part When 'YY' then DateAdd(YY, N*#Incr, #R1) When 'QQ' then DateAdd(QQ, N*#Incr, #R1) When 'MM' then DateAdd(MM, N*#Incr, #R1) When 'WK' then DateAdd(WK, N*#Incr, #R1) When 'DD' then DateAdd(DD, N*#Incr, #R1) When 'HH' then DateAdd(HH, N*#Incr, #R1) When 'MI' then DateAdd(MI, N*#Incr, #R1) When 'SS' then DateAdd(SS, N*#Incr, #R1) End From cte2 )
Select RetSeq = N+1
,RetVal = D
From cte3,cte0
Where D<=#R2
)
/*
Max 100 million observations -- Date Parts YY QQ MM WK DD HH MI SS
Syntax:
Select * from [dbo].[udf-Range-Date]('2016-10-01','2020-10-01','YY',1)
Select * from [dbo].[udf-Range-Date]('2016-01-01','2017-01-01','MM',1)
*/
using a recursive Common Table Expression [CTE] is one pretty clean method. For the formatting I am showing FORMAT() from SQL-Server 2012+ you may consider using DATEPART etc to do it though as FORMAT() can have performance impact.
I do agree with #RossBush's comment if you do things like this a lot generating a calendar (dates) and a time dimensions is very helpful for these purposes.
DECLARE #DateTime1 DATETIME = '2016/11/24 18:00:00'
DECLARE #DateTime2 DATETIME = '2016/11/25 06:00:00'
;WITH cte30MinIncrements AS (
SELECT #DateTime1 as DT
UNION ALL
SELECT DATEADD(MINUTE,30,DT)
FROM
cte30MinIncrements
WHERE DATEADD(MINUTE,30,DT) <= #DateTime2
)
SELECT
*
,FORMAT(DT,'dd-HH:mm') as Formated
FROM
cte30MinIncrements
Please see if this works.
declare #DateTime1 DateTime = '2016-11-24 18:00:00'
declare #DateTime2 DateTime = '2016-11-25 18:00:00'
declare #Interval DateTime = #DateTime1
declare #vartmptable table(DT DateTime)
While (#Interval < #DateTime2)
begin
--select #Interval, FORMAT(#Interval,'dd-HH:mm')
insert into #vartmptable select #Interval
set #Interval = DATEADD(mi,30,#Interval)
end
select FORMAT(DT,'dd-HH:mm') from #vartmptable
What about this? You can use variables/ fixed values as necessary.
WITH CTE_Numbers
AS (
SELECT n = 1
UNION ALL
SELECT n + 1
FROM CTE_Numbers
WHERE n < 100
)
SELECT FORMAT(DATEADD(mi, n * 30, '2016/11/03'),'dd-HH:mm')
FROM CTE_Numbers

SQL incorrect syntax in tableau

I have this error message when I insert my SQL query in Tableau. How to solve this issue?
Is it SQL Server currently does not allow CTE's inside a subquery??
Below is the Tableau output when I insert query in
[Microsoft][SQL Server Native Client 11.0][SQL Server]Incorrect syntax near the keyword 'WITH'.
[Microsoft][SQL Server Native Client 11.0][SQL Server]Incorrect syntax near the keyword 'with'. If this statement is a common table
expression, an xmlnamespaces clause or a change tracking context
clause, the previous statement must be terminated with a semicolon.
[Microsoft][SQL Server Native Client 11.0][SQL Server]Incorrect syntax near ')'.
Below is my current CTE Query (recursive)
WITH shiftHours AS (
SELECT RowID,
y.EMPLOYEENAME AS EMPLOYEENAME,
-- flatten the first hour to remove the minutes and get the initial current hour
DATEADD(hour, DATEDIFF(hour, 0, ShiftA_Start), 0) AS currentHour,
ShiftA_Start,
ShiftA_End,
DATEPART(hour, ShiftA_Start) AS hourOrdinal,
-- determine how much of the first hour is applicable. if it is minute 0 then the whole hour counts
CAST(CASE
WHEN DATEADD(hour, DATEDIFF(hour, 0, ShiftA_Start), 0) = DATEADD(hour, DATEDIFF(hour, 0, ShiftA_End), 0) THEN DATEDIFF(minute, ShiftA_Start, ShiftA_End) / 60.0
WHEN DATEPART(minute, ShiftA_Start) = 0 THEN 1.0
ELSE (60 - DATEPART(minute, ShiftA_Start)) / 60.0
END AS DECIMAL(5,3)) AS hourValue
FROM (
-- use a ROW_NUMBER() to generate row IDs for the shifts to ensure each row is unique once it gets to the pivot
SELECT ROW_NUMBER() OVER(ORDER BY ShiftA_Start, ShiftA_End) AS RowID,
EMPLOYEENAME,
ShiftA_Start,
ShiftA_End
FROM (
-- this is where the data gets pulled from the source table and where the data types are converted from string to DATETIME
SELECT
EMPLOYEENAME,
CONVERT(DATETIME, LEFT(SHIFTA_start, 17), 103) AS ShiftA_Start,
CONVERT(DATETIME, LEFT(SHIFTA_end, 17), 103) AS ShiftA_End
from [TableName].[dbo].[TMS_People]
where
CONVERT(DATETIME, LEFT(SHIFTA_START, 17), 103) IS NOT NULL AND CONVERT(DATETIME, LEFT(SHIFTA_END, 17), 103) IS NOT NULL
AND CONVERT(DATETIME, LEFT(SHIFTA_START, 17), 103) != CONVERT(DATETIME, LEFT(SHIFTA_END, 17), 103)
-- this is also where you would add any filtering from the source table such as date ranges
) x
) AS y
UNION ALL
SELECT RowID,
EMPLOYEENAME,
-- add an hour to the currentHour each time the recursive CTE is called
DATEADD(hour, 1, currentHour) AS currentHour,
ShiftA_Start,
ShiftA_End,
DATEPART(hour, DATEADD(hour, 1, currentHour)) AS hourOrdinal,
CAST(CASE
-- when this is the last time period determine the amount of the hour that is applicable
WHEN DATEADD(hour, 2, currentHour) > ShiftA_End THEN DATEPART(minute, ShiftA_End) / 60.0
ELSE 1
END AS DECIMAL(5,3)) AS hourValue
from shiftHours
-- contine recursion until the next hour is after the ShiftEnd
WHERE DATEADD(hour, 1, currentHour) < ShiftA_End
)
SELECT *
FROM (
SELECT RowID,
EMPLOYEENAME,
ShiftA_Start,
ShiftA_End,
hourValue,
hourOrdinal
from shiftHours
) AS t
PIVOT (
SUM(hourValue)
FOR hourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt
OPTION (MAXRECURSION 0);
Tableau do not support SQL server CTE feature .
You should try to implement same logic using view or sub query or Stored proc/table valued function .
Please go throw below thread CTE (SQL Server) are not sported in Tableau.
https://community.tableau.com/thread/105965 .
View Sample code. for CTE
If we have user table (userId, userName, managerId)
CREATE VIEW vUSER AS
WITH UserCTE AS (
SELECT userId, userName, managerId,0 AS steps
FROM dbo.Users
WHERE userId = null
UNION ALL
SELECT mgr.userId, mgr.userName, mgr.managerId, usr.steps +1 AS steps
FROM UserCTE AS usr
INNER JOIN dbo.Users AS mgr
ON usr.managerId = mgr.userId
)
SELECT * FROM UserCTE ;
Create an sp ie MyProc and put all your query in it
ie
CREATE procedure dbo.MyProc AS
WITH UserCTE AS (
SELECT userId, userName, managerId,0 AS steps
FROM dbo.Users
WHERE userId = null
UNION ALL
SELECT mgr.userId, mgr.userName, mgr.managerId, usr.steps +1 AS steps
FROM UserCTE AS usr
INNER JOIN dbo.Users AS mgr
ON usr.managerId = mgr.userId
)
SELECT * FROM UserCTE ;
2 Create a link server using sp_addlinkedserver ie i am telling is Local
3 Call that sp in your view using openquery.
CREATE VIEW dbo.MyView
AS(
SELECT *
FROM openquery(Local,'exec mydb.dbo.MyProc'))
http://capnjosh.com/blog/how-to-call-a-stored-procedure-from-a-view-in-sql-server/

Resources