我们正在尝试分析我们的员工每小时工作多长时间以进行趋势和预测。
我们有员工的时钟(SHIFTA_Start
)和时钟(SHIFTA_End
)。
然后我们将一个日期部分分为4个部分:
Start Time_Hour
Start Time_min
End Time_Hour
End Time_min
[我已经包含了我当前的输出,以及我希望在此图像中获得的预期结果] http://i.stack.imgur.com/1uhq0.png
给定开始时间和结束时间:
e.g。
它可以很好地填充在各自的小时位置,因为它很简单。
但是,如果员工在一夜之间工作,则给定开始时间和结束时间:
e.g。
无法填充小时槽。
为了简短起见,这是我从0小时到1小时的案例陈述的一部分
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
这是输出@Mike http://i.stack.imgur.com/laSKX.png
我当前的SQL语句是
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);
答案 0 :(得分:1)
这会给你你想要的东西。
;
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
答案 1 :(得分:0)
由于需要每小时获取一列,因此这是使用TSQL PIVOT
运算符的理想选择。 Pivot获取数据行并将其转换为列。
第一步是创建我们的所有行:每小时一行,并计算该小时的适用时间。我通过使用recursive CTE生成从0到23的所有数字来完成此操作。递归CTE是一种不断调用自身直到满足锚条件的查询。锚条件是WHERE
中第二个SQL语句的UNION
子句。
然后我使用ShiftStart_Hour
和ShiftStart_End
列加入,但您需要考虑其中一个是否在第二天(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
)
)
该连接每小时创建一行。现在我们需要使用CASE
子句中的SELECT
语句来计算该小时的适用程度:
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
之后我们终于可以使用PIVOT
运算符来获取我们正在寻找的最终格式的数据。完整的工作示例是:
/* 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;
此方法存在问题。因为我们实际上只关注时间而不是日期,如果一些超级努力的工作人员进行24小时轮班,那么它将无法得到正确的答案。但还有更好的方法。
我们可以使用它来迭代ShiftStart
和ShiftEnd
值之间的所有小时,而不是使用递归CTE来生成我们想要匹配的小时数。同时计算每小时的适用数量。在这种情况下,锚条件与表中的实际数据有关,而不仅仅是简单的算术。然后再次PIVOT
,以正确的格式获取结果集。
注意:此查询可能无法扩展超过几千行。日期范围过滤和数据集的其他限制应该有助于此。但是如果你需要处理更多的事情,你需要在某个地方坚持这些计算。
此示例适用于跨越多天的轮班:
/* 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);
这个答案已经超出了StackOverflow的最佳范围,但让我们解决最后的问题。您的表将日期存储为字符串。这是一个糟糕的设计决定,当你试图从这个数据库做报告时会给你带来很大的痛苦。
我无法看到你的字符串是如何格式化的,所以我只是假设你的转换函数是正确的:CONVERT(datetime, LEFT(b.SHIFTA_start,17),103)
如果它不起作用那么这些代码都不会起作用你会需要修复下面表格x
中的两个调用。我还假设您在上面更新的查询中给出的表名是正确的。如果不是,您将需要在同一个表表达式中修复它。应该明白需要改变什么。
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);
答案 2 :(得分:0)
你正在以这种方式努力。按原样使用DateTime,并使用计算列为您提供工作分钟(或小时)。
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)
)
所以MinutesWorked总是正确的。无需使用代码来解释夏令时,年度变化,夜间工作等等。
现在您拥有了正确的数据类型,您可以计算出您想要的数据。
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]
这会创建:
我使用的功能是:
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