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;
I have the following two tables
CREATE TABLE Ep
([E] varchar(9), [M] varchar(9), [DTE] DATETIME)
;
INSERT INTO Ep
([E], [M], [DTE])
VALUES
('1595861-1', '1595861-1', CONVERT(datetime, '2002-11-26 14:18:00', 20)),
('1595904-1', '1595904-1', CONVERT(datetime, '2002-11-24 15:15:00', 20)),
('1596298-1', '1596298-1', CONVERT(datetime, '2002-12-17 11:12:00', 20)),
('1596357-1', '1596357-1', CONVERT(datetime, '2002-12-09 19:57:00', 20)),
('1596369-1', '1596369-1', CONVERT(datetime, '2002-12-11 06:00:00', 20)),
('1596370-1', '1596370-1', CONVERT(datetime, '2002-12-19 12:31:00', 20)),
('1596473-2', '1596473-1', CONVERT(datetime, '2002-12-15 08:39:00', 20)),
('1596473-3', '1596473-1', CONVERT(datetime, '2002-12-20 08:39:00', 20)),
('1596473-4', '1596473-1', CONVERT(datetime, '2002-12-13 08:39:00', 20)),
('1596473-5', '1596473-1', CONVERT(datetime, '2002-12-16 08:39:00', 20)),
('1596473-1', '1596473-1', CONVERT(datetime, '2002-12-14 08:39:00', 20))
;
CREATE TABLE Mp
([E] varchar(9), [M] varchar(9), [DTE] DATETIME)
;
INSERT INTO Mp
([E], [M], [DTE])
VALUES
('', '1595861-1', CONVERT(datetime, '2002-11-26 14:18:00', 20)),
('', '1595904-1', CONVERT(datetime, '2002-11-24 15:15:00', 20)),
('', '1596298-1', CONVERT(datetime, '2002-12-17 11:12:00', 20)),
('', '1596357-1', CONVERT(datetime, '2002-12-09 19:57:00', 20)),
('', '1596369-1', CONVERT(datetime, '2002-12-11 06:00:00', 20)),
('', '1596370-1', CONVERT(datetime, '2002-12-19 12:31:00', 20)),
('', '1596473-1', CONVERT(datetime, '2002-12-17 08:39:00', 20))
;
Currently I am updating the [E] field in the Mp table via a match on [M] where the DTE field (in Mp) is within a certian range (say +-3 days). The query to do this is currently
UPDATE [Mp]
SET [E] = [Ep].[E]
FROM [Mp] INNER JOIN [Ep]
ON [Mp].[M] = [Ep].[M]
WHERE [Mp].[DTE] BETWEEN [Ep].[DTE] - 3 AND [Ep].[DTE] + 3;
This updates [Mp].[E] for [Mp].[M] = N'1596473-1' to 1596473-2. Essentailly the first entry SQL Server finds that is valid. However, I want to update this query so that SQL Server matches on the [M] field in the required date range (as it does now), but for the [Ep].[DTE] values that is closest to that in the [Mp].[DTE] value of 2002-12-17 08:39:00.
I have looked at adding a DATEDIFF clause, in the following way
UPDATE [Mp]
SET [E] = [Ep].[E]
FROM [Mp] INNER JOIN [Ep]
ON [Mp].[M] = [Ep].[M]
WHERE [Mp].[DTE] BETWEEN [Ep].[DTE] - 3 AND [Ep].[DTE] + 3
ORDER BY DATEDIFF(minutes, [Mp].[DTE], [Ep].[DTE]);
Clearly I can't do this, but I am unsure how to ammend this so that it works. The final data for [Mp] after the update should be
1595861-1 1595861-1 2002-11-26 14:18:00.000
1595904-1 1595904-1 2002-11-24 15:15:00.000
1596298-1 1596298-1 2002-12-17 11:12:00.000
1596357-1 1596357-1 2002-12-09 19:57:00.000
1596369-1 1596369-1 2002-12-11 06:00:00.000
1596370-1 1596370-1 2002-12-19 12:31:00.000
**1596473-5** 1596473-1 2002-12-17 08:39:00.000
Thanks for your time.
Please first check which data following CTE produces as output
For nearest data, I used SQL ROW_NUMBER function with Partition By clause according to the time difference calculation fetched by DATEDIFF() function. To eliminate previous and following records, I used ABS() mathematical function.
select
*,
ROW_NUMBER() over (partition by [Mp].[M] order by abs(datediff(mi, [Mp].[DTE], [Ep].[DTE]))) as rn,
abs(datediff(mi, [Mp].[DTE], [Ep].[DTE])) diff
from Mp
left join Ep
on [Mp].[M] = [Ep].[M]
WHERE [Mp].[DTE] BETWEEN dateadd(dd,-3,[Ep].[DTE]) AND dateadd(dd,3,[Ep].[DTE])
Then using the same CTE expression in an UPDATE command as following, you can populate the desired data into your target database table
;with cte as (
select
[Mp].[M] as M,
[Ep].E as E,
ROW_NUMBER() over (partition by [Mp].[M] order by abs(datediff(mi, [Mp].[DTE], [Ep].[DTE]))) as rn,
abs(datediff(mi, [Mp].[DTE], [Ep].[DTE])) diff
from Mp
left join Ep
on [Mp].[M] = [Ep].[M]
WHERE [Mp].[DTE] BETWEEN dateadd(dd,-3,[Ep].[DTE]) AND dateadd(dd,3,[Ep].[DTE])
)
update [Mp]
set [E] = cte.E
from [Mp]
inner join cte on [Mp].M = cte.M and cte.rn = 1
where
cte.E is not null
After execution of the UPDATE statement, the target table data is as follows
I hope this is what you require
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