SQL Server 2008 R2,按部门、员工和工作日期区分正常和加班时间

SQL Server 2008 R2, separate regular and overtime hours by Department, Employee and Workdate

我正在尝试为 HR 编写一份报告,按部门、员工和工作日期划分正常和加班时间,但我真的不确定如何开始。

下面创建了一个临时文件 table,其中包含一系列反映我将使用的实际数据的示例数据。

-- Create Temp Tables --
CREATE TABLE #Employee_Hours
(
        emp_num     nvarchar(20)
        ,dept       nvarchar(20)
        ,shift      nvarchar(3)
        ,work_date  Date
        ,start_time DateTime
        ,hrs        Decimal(19,8)
);

-- Populate #Employee_Hours with sample hours
INSERT #Employee_Hours(emp_num, dept, shift, work_date, start_time, hrs)
VALUES ('W-C0001', 'Admin', 'Day', '2015-02-22', '2015-02-22 06:30:00.000', 7.9)
      ,('W-C0001', 'Admin', 'Day', '2015-02-24', '2015-02-24 06:45:00.000', 8.6)
      ,('W-C0001', 'Admin', 'Day', '2015-02-26', '2015-02-26 08:00:00.000', 2.0)
      ,('W-C0001', 'Admin', 'Day', '2015-02-26', '2015-02-26 10:00:00.000', 0.5)
      ,('W-C0001', 'Asmb', 'Day', '2015-02-26', '2015-02-26 11:00:00.000', 1.33333)
      ,('W-C0001', 'Shop A', 'Day', '2015-02-26', '2015-02-26 11:30:00.000', 5.5)
      ,('G-C0000', 'Admin', 'Day', '2015-02-22', '2015-02-22 08:00:00.000', 4.5)
      ,('G-C0000', 'Admin', 'Day', '2015-02-22', '2015-02-22 12:30:00.000', 4.0)
      ,('G-C0000', 'Admin', 'Day', '2015-02-23', '2015-02-23 07:30:00.000', 8.25)
      ,('G-C0000', 'Admin', 'Day', '2015-02-24', '2015-02-24 08:30:00.000', 8.9)
      ,('W-R0000', 'Eng', 'Day', '2015-02-22', '2015-02-22 08:30:00.000', 8.02)
      ,('W-R0000', 'Eng', 'Day', '2015-02-24', '2015-02-24 09:30:00.000', 8.75)
GO

    SELECT * FROM #Employee_Hours

    DROP TABLE #Employee_Hours

数据table有五列:

  1. emp_num --员工编号
  2. dept --交易部
  3. shift --事务转移
  4. work_date --交易的工作日期
  5. start_time --交易开始时间
  6. hrs --每次交易的总小时数

现在,我们的一些员工整天都会换部门,当他们换部门时,他们会从原来的部门打卡,然后在另一个部门打卡。

请注意 emp_num 'W-C0001' 的交易,该员工在 work_date 2015 年 2 月 26 日有四笔交易。我们的加班是每个工作日8小时后的任何加班,这就是我的问题。

按部门汇总,work_date emp_num 'W-C0001' 示例数据如下所示:

 work_date  |  dept  | hrs
 02/26/2015   Admin    2.5
 02/26/2015   Asmb     1.3
 02/26/2015   Shop A   5.5

这在 work_date 2016 年 2 月 26 日加起来为 9.6 小时,即 8 小时常规时间和 1.3 小时加班时间。现在我需要把1.3小时的加班分配给work_date的第一个部门或者最后一个部门,这个加班分配是根据班次码决定的,如果是'Day'则分配给最后一个部门,否则到当天的第一个部门。

这当然很复杂,因为员工可以在部门 A 工作 8.5 小时,然后切换到部门 B 再工作一个小时。结果必须是 A 部门 8 小时注册,A 部门加班 0.5 小时,B 部门加班 1 小时。

我需要的使用上述示例的最终 select 语句的结果是:

  work_date   |  dept  |  hrs_reg  |  hrs_ot
 02/26/2015      Admin      2.5         0
 02/26/2015      Asmb       1.3         0
 02/26/2015      Shop A     4.2        1.3

更新 多亏了@TommCatt,我想我已经明白了。我正在使用 start_time 列来帮助确定哪个部门获得加班费。我将它添加到我的临时 table.

中的示例数据中

我稍微修改了他的答案,似乎给了我想要的。我包含了 start_time 列并在 CASE 表达式中使用它来将加班时间分配给 MAX(start_time).

WITH EmpOvertime
(
    emp_num
    ,dept
    ,shift
    ,work_date
    ,start_time
    ,hrs
    ,DailyTotal
    ,OTHours
)
AS
(
    SELECT h.emp_num
        ,h.dept
        ,h.shift
        ,h.work_date
        ,h.start_time
        ,h.hrs
        ,SUM(h.hrs) OVER(PARTITION BY h.emp_num, h.work_date)
        ,CASE 
            WHEN Sum(h.hrs) OVER(PARTITION BY h.emp_num, h.work_date) - 8 > 0
            THEN Sum(h.hrs) OVER(PARTITION BY h.emp_num, h.work_date) - 8
            ELSE 0
         END
    FROM #Employee_Hours AS h
)
SELECT emp_num
    ,dept
    ,shift
    ,work_date
    ,hrs
    ,DailyTotal
    ,CASE -- Only subract overtime from the last entry for the work day
        WHEN start_time = (SELECT MAX(h2.start_time)
                                FROM #Employee_Hours AS h2
                                WHERE h2.emp_num = ot.emp_num
                                    AND h2.work_date = ot.work_date)
        THEN hrs - OTHours
        ELSE hrs
    END AS RegHours
    ,CASE -- Only allocate overtime to the last entry for the work day
        WHEN start_time = (SELECT MAX(h2.start_time)
                                FROM #Employee_Hours AS h2
                                WHERE h2.emp_num = ot.emp_num
                                    AND h2.work_date = ot.work_date)
        THEN OTHours
        ELSE 0
    END AS OTHours
FROM EmpOvertime AS ot
ORDER BY ot.emp_num, ot.work_date;

结果如下:

emp_num dept    shift work_date     hrs     DailyTotal  RegHours    OTHours
------- -----   ----  ----------    ---     ---         ---         ---
G-C0000 Admin   Day   2015-02-22    4.5     8.5         4.5         0.0
G-C0000 Admin   Day   2015-02-22    4.0     8.5         3.5         0.5
W-C0001 Admin   Day   2015-02-22    7.9     7.9         7.9         0.0
W-C0001 Admin   Day   2015-02-24    8.6     8.6         8.0         0.6
W-C0001 Admin   Day   2015-02-26    2.0     9.3         2.0         0.0
W-C0001 Admin   Day   2015-02-26    0.5     9.3         0.5         0.0
W-C0001 Asmb    Day   2015-02-26    1.3     9.3         1.3         0.0
W-C0001 Shop A  Day   2015-02-26    5.5     9.3         4.1         1.3

请注意员工 'W-C0001'RegHours 列如何用于 work_date '2015-02-26' 现在显示 work_date 中所有行的完整小时数,但减去 OT 的最后一个条目除外。与 OTHours 列相同,work_date 中的行为零,最后一行除外。

我现在可以使用班次代码来确定是将加班分配给工作日的第一笔交易还是最后一笔交易。

直到现在我还没有使用过 OVER(PARTITION BY ),但能够从 TommCatt 的回答中弄清楚它们是如何工作的。

是否有一种更简洁的方法可以根据 start_time 然后是我正在使用的 CASE 表达式将 OT 分配给最后或第一笔交易?

丑陋,并不总是有效,但类似的东西:

SELECT work_date,
dept,
CASE WHEN ot.tot IS NOT NULL  THEN total - ot.tot ELSE total END AS s,
ot.tot

FROM  (
SELECT eh.work_date, 
eh.dept, 
eo.d,
SUM(eh.hrs) total,
eo.t
FROM #Employee_Hours eh
JOIN (
SELECT work_date,  SUM(hrs) t, MAX(dept) d
FROM #Employee_Hours
GROUP BY work_date
) eo
ON eo.work_date = eh.work_date
WHERE eh.work_date = '2015-02-26'
GROUP BY eh.work_date, eh.dept, eo.d, eo.t
) a
CROSS APPLY (SELECT  CASE WHEN t <= 8 THEN 0 ELSE CASE WHEN d = dept THEN t-8 END END tot) AS ot

如果您不了解分析,我不确定我能否向您解释它们。我对数据库概念非常精通,但在它们最终开始吸收之前,我不得不反复使用分析。我仍然不喜欢使用它们,因为虽然它们通常很方便,但它们需要 sooo 大量测试以确保您得到正确的答案。

这个想法是,您必须让每位员工在每一天的总工作时间 运行。然后有一个列忽略 运行 每日总计,直到它超过 8 小时。将其输入到另一个查询中,该查询将小时数分为正常时间和加班时间。

SQLFiddle

with
EmpOvertime( emp_num, dept, shift, work_date, hrs, RunningHrs, OTHours )as(
    select  h.emp_num, h.dept, h.shift, h.work_date, h.hrs,
            Sum( h.hrs )over( partition by h.emp_num, h.work_date order by h.hrs ),
            case when Sum( h.hrs )over( partition by h.emp_num, h.work_date order by h.hrs ) - 8 > 0
                then Sum( h.hrs )over( partition by h.emp_num, h.work_date order by h.hrs ) - 8
                else 0
            end
    from    Employee_Hours  h
)
select  emp_num, dept, shift, work_date, hrs ClockedHrs, RunningHrs, hrs - OTHours RegHours, OTHours
from    EmpOvertime ot
order by ot.emp_num, ot.work_date;

我已经将 CTE 中 Sum 的原始总计显示为 RunningHrs,因此您可以直接看到生成的值。一旦您对查询感到满意,就可以将其删除,因为它不会直接用于最终查询。

这不完全是您要求的,但您没有显示任何方式来指定班次的第一个或最后一个时段。没有办法将其用于查询。所以加班时间刚好出现在每日总加班时间开始超过8小时的时期。我希望这足以让您入门。