在SQL Server中创建时隙

时间:2016-04-26 02:15:00

标签: sql-server tableau

我们正在尝试分析我们的员工每小时工作多长时间以进行趋势和预测。

我们有员工的时钟(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。

  • 开始时间 - 上午9点40分(0940)
  • 结束时间 - 下午6时15分(1815年)

它可以很好地填充在各自的小时位置,因为它很简单。

但是,如果员工在一夜之间工作,则给定开始时间和结束时间:

e.g。

  • 开始时间 - 晚上9:30(2130)
  • 结束时间 - 早上7点(0700)

无法填充小时槽。

为了简短起见,这是我从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);

3 个答案:

答案 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_HourShiftStart_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小时轮班,那么它将无法得到正确的答案。但还有更好的方法。

我们可以使用它来迭代ShiftStartShiftEnd值之间的所有小时,而不是使用递归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] 

这会创建:

enter image description here

我使用的功能是:

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