【问题标题】:SQL : Calculating employee utilization hours for each daySQL:计算每天的员工使用时间
【发布时间】:2017-09-11 08:43:12
【问题描述】:

SQL:我在 StartTime 和 EndTime 列中有一个员工工作时间表。我想计算每个员工每天的工作时间,即使员工从一天开始轮班到第二天结束。

|Employee |StartTime         |EndTime         |
|A        | 01/01/2001 23:00 |02/01/2001 10:00|
|B        | 01/01/2001 21:00 |01/01/2001 22:00|

Output:
|Employee |Date         |HoursWorked              
|A        | 01/01/2001  | 1         |
|A        | 02/01/2001  | 10        |
|B        | 01/01/2001  | 1         |

【问题讨论】:

    标签: sql sql-server tsql


    【解决方案1】:

    这是一种使用递归 CTE 的方法:

    DECLARE @t TABLE(
    Employee NVARCHAR(10)
    ,StartTime DATETIME
    ,EndTime DATETIME
    )
    
    INSERT INTO @t VALUES ('A', '2001-01-01 23:00:00', '2001-01-03 10:00:00')
                         ,('A', '2001-01-05 21:00:00', '2001-01-06 22:00:00')
                         ,('A', '2001-01-07 21:00:00', '2001-01-08 22:00:00')
                         ,('B', '2001-01-01 21:00:00', '2001-01-01 22:00:00')
                         ,('B', '2001-01-02 21:00:00', '2001-01-03 02:00:00')
                         ,('C', '2001-01-03 02:00:00', '2001-01-04 00:00:00');
    
    WITH cte AS(
      SELECT 1 AS lvl, Employee, CONVERT(DATE, StartTime) StartTime_DATE, StartTime, EndTime
        FROM @t AS t
      UNION ALL
      SELECT lvl + 1 AS lvl, c.employee, DATEADD(d, 1, c.StartTime_DATE) StartTime_DATE, c.StartTime, c.EndTime
        FROM cte AS c
        WHERE DATEADD(d, 1, c.StartTime_DATE) < c.EndTime
    ),
    cteCalc AS(
      SELECT *
            ,CONVERT(DATE, StartTime) AS StartDate
            ,CASE WHEN lvl > 1 THEN CONVERT(DATETIME,CONVERT(DATE, DATEADD(d, DATEDIFF(d, StartTime, StartTime_DATE), StartTime))) ELSE DATEADD(d, DATEDIFF(d, StartTime, StartTime_DATE), StartTime) END AS StartTimeNew
            ,ISNULL(CONVERT(datetime, LEAD(StartTime_DATE) OVER (PARTITION BY Employee, CONVERT(DATE, StartTime) ORDER BY StartTime_DATE)), EndTime) AS EndTimeNew
        FROM cte
    
    )
    SELECT Employee, StartTime_DATE AS StartDate, DATEDIFF(MINUTE, StartTimeNew, EndTimeNew)/60.0 AS WorkHours
      FROM cteCalc
      ORDER BY Employee, StartTime_DATE
      OPTION (MAXRECURSION 0)
    

    【讨论】:

    • 嘿,这是一个不错的解决方案,但我想数据库中每个员工都有多条记录。想象一下以下测试数据:INSERT INTO @t VALUES ('A', '2001-01-01 23:00:00', '2001-01-03 10:00:00'), ('A', '2001-01-05 21:00:00', '2001-01-06 22:00:00'), ('A', '2001-01-07 21:00:00', '2001-01-08 22:00:00');。我认为使用 LEAD 会导致不同时期之间的意外输出,但我似乎无法理解为什么会这样? (startTimeNew = 一天中的合法开始时间,endTimeNew = 新时期开始时间的午夜)。
    • 我刚刚修改了查询并在 startdate 之前扩展了 LEAD 分区 - 乍一看现在看起来还可以:-)
    【解决方案2】:

    使用DATEDIFF 函数

    create table #employee (Employee varchar(10), StartTime datetime, EndTime datetime)
    insert into #employee values ('A','2001-01-01 23:00', '2001-01-02 10:00')
    
    select Employee, CAST(StartTime as DATE) [Date], DATEDIFF(HOUR,StartTime, EndTime)HoursWorked from #employee
    

    【讨论】:

    • 这没有提供所需的结果:员工 A 在 1.1 上工作了 1 小时。 2.1 10 小时。
    【解决方案3】:

    您可以尝试以下查询 -

    create table #Employee (
         Employee varchar(10), StartTime smalldatetime      , EndTime smalldatetime
    )
    go
    
    insert into #Employee   
    select 'A'     , ' 01/01/2001 23:00', '02/01/2001 10:00' 
    union all select 'B '     , '01/01/2001 21:00', '01/01/2001 22:00'   
    
    select Employee ,  cast(StartTime as date) [Date] ,  datediff(hour,StartTime, EndTime)HoursWorked              
    from #Employee 
    where cast(StartTime as date) = cast(EndTime as date )
    union all 
    select Employee ,  cast(StartTime as date) [Date] ,    datediff(hour,StartTime, cast( concat(cast(StartTime as date) ,' 23:59:59' )as smalldatetime))  HoursWorked                  
    from #Employee 
    where cast(StartTime as date) <> cast(EndTime as date ) 
    union all 
    select Employee ,  cast(EndTime as date) [Date] ,    datediff(hour, cast( concat(cast(EndTime as date) ,' 00:00:00' )as smalldatetime) ,EndTime) HoursWorked                  
    from #Employee 
    where cast(StartTime as date) <> cast(EndTime as date ) 
    

    【讨论】:

    • 似乎有效,但前提是您最多坚持两天......就像测试一样,我将员工 Bs EndTime 更改为 03.01。而不是 01.01。我只收到 2 行 - 1 行用于 01.01。一个用于 03.01。我的递归 CTE 的行为有点不同。
    • 是的,它只会工作 2 天。我不认为有人可以有争议地工作超过 2 天:),因为提供的数据与工作时间有关。
    【解决方案4】:

    这是拥有日历查找表的众多情形之一。有许多日历表脚本示例可以构建您想要的简单或健壮的东西,但我们假设您拥有的只是日历表中的日期列表。

    生成临时表并用示例数据填充它们...

    IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL 
    DROP TABLE #TestData;
    IF OBJECT_ID('tempdb..#CalendarLookupTable', 'U') IS NOT NULL 
    DROP TABLE #CalendarLookupTable;
    
    CREATE TABLE #TestData (
        EmployeeID INT NOT NULL,
        StartTime DATETIME NOT NULL,
        EndTime DATETIME NOT NULL 
        );
    INSERT #TestData (EmployeeID, StartTime, EndTime) VALUES
        (1, '01-01-2001 23:00', '01-02-2001 10:00')
        ,(2, '01-01-2001 21:00', '01-01-2001 22:00')
        ,(3, '01-02-2001 21:00', '01-04-2001 22:00');
    
    
    CREATE TABLE #CalendarLookupTable (
        consequtiveDate DATETIME NOT NULL
        );
    INSERT INTO #CalendarLookupTable(consequtiveDate) VALUES
        ('01-01-2001')
        ,('01-02-2001')
        ,('01-03-2001')
        ,('01-04-2001')
    

    查询表格(选择中的表格基本上将日期时间段拆分为同一天的时间段 - 然后我们计算开始时间段和结束时间段之间的 datediff 小时数) - 您可以重构为 CTE。

    SELECT
        rnd.EmployeeID
        ,CONVERT(date, rnd.StartTime)
        ,DATEDIFF(HOUR, StartTime, EndTime)
    FROM
        (
        SELECT td.EmployeeID AS [EmployeeID]
              ,CASE WHEN CAST(td.StartTime AS DATE) = clt.consequtiveDate THEN td.StartTime
                    WHEN CAST(td.EndTime AS DATE) = clt.consequtiveDate THEN CAST(CAST(td.EndTime AS DATE) AS DATETIME)
                    ELSE CAST(clt.consequtiveDate AS DATETIME)
               END AS [StartTime]
               ,CASE WHEN CAST(td.StartTime AS DATE) = clt.consequtiveDate THEN CAST(DATEADD(day,1,CAST(td.StartTime AS DATE))AS DATETIME)
                    WHEN CAST(td.EndTime AS DATE) = clt.consequtiveDate THEN td.EndTime
                    ELSE CAST(DATEADD(day,1,clt.consequtiveDate) AS DATETIME)
                END AS [EndTime]
        FROM #TestData AS td
        JOIN #CalendarLookupTable AS clt
          ON clt.consequtiveDate BETWEEN CAST(td.StartTime AS DATE) AND CAST(td.EndTime AS DATE)
         ) AS rnd
    

    如果需要,我来这里是为了进一步澄清。希望对你有效。我想它更健壮一些(适用于每个 SQL Server(不使用 concat))。

    编辑:您可以查看 this article on creating and using calendar tables in TSQL 。为什么我认为使用日历查找表会更好?想象一下您的业务逻辑发生了变化,您不仅需要返回每天的小时数,还需要返回员工必须获得的工资——也许在周末和/或国定假日工作意味着更高的小时工资?使用日历查找表使这变得非常容易。

    编辑2:Sample table-valued function that returns a pretty awesome calendar lookup table

    【讨论】:

      【解决方案5】:

      Edit2:我从 Tyron78 的解决方案中消除了 LEAD() 函数。通过这种方式,它可以与 2012 年之前的 SQL-Server 一起使用,并且性能更好,恕我直言:

      DECLARE @t TABLE(
          Employee NVARCHAR(10)
          ,StartTime DATETIME
          ,EndTime DATETIME
      )
      
      INSERT INTO @t
      VALUES   ('A', '2001-01-01 23:00:00', '2001-01-03 10:00:00')
              ,('A', '2001-01-05 21:00:00', '2001-01-06 22:00:00')
              ,('A', '2001-01-07 21:00:00', '2001-01-08 22:00:00')
              ,('B', '2001-01-01 21:00:00', '2001-01-01 22:00:00')
              ,('B', '2001-01-02 21:00:00', '2001-01-03 02:00:00')
              ,('C', '2001-01-03 02:00:00', '2001-01-04 00:00:00');
      
      WITH cte AS(
          SELECT      1 AS lvl,
                      Employee,
                      CONVERT(DATE, StartTime) StartTime_DATE,
                      StartTime,
                      EndTime
          FROM        @t AS t
      
          UNION ALL
      
          SELECT      lvl + 1 AS lvl,
                      c.employee,
                      DATEADD(d, 1, c.StartTime_DATE) StartTime_DATE,
                      c.StartTime,
                      c.EndTime
          FROM        cte AS c
          WHERE       DATEADD(d, 1, c.StartTime_DATE) < c.EndTime
      ),
      cteCalc AS(
        SELECT        *
                      ,CONVERT(DATE, StartTime) AS StartDate
                      ,case lvl
                          when 1 then StartTime
                          else CAST(StartTime_DATE as datetime)
                      end as StartTimeNew
                      ,case
                          when CAST(EndTime as date) = StartTime_DATE
                              then EndTime
                          else CAST(dateadd(day, 1, StartTime_DATE) as datetime)
                      end as EndTimeNew
          FROM        cte
      )
      SELECT      Employee,
                  StartTime_DATE AS StartDate,
                  DATEDIFF(MINUTE, StartTimeNew, EndTimeNew)/60.0 AS WorkHours
      FROM        cteCalc
      ORDER BY    Employee, StartTime_DATE
      OPTION      (MAXRECURSION 0)
      

      编辑:我推荐 Tyron78 的 CTE 解决方案,即使在较大的日期范围内,它在我的测试中表现也比我的要好 - 非常好!!! 一种解决方法:更改这一行:

      • WHERE DATEADD(d, 1, c.StartTime_DATE) = c.EndTime

      到这里

      • WHERE DATEADD(d, 1, c.StartTime_DATE)

      考虑到结束时间 = 午夜。

      真的很不错,学到东西了,谢谢!


      我的解决方案类似于日历查找,它使用函数 intTable(@minValue int, @maxValue int)。

      begin transaction
      go
      
      -- create table for sample data
      create table #Employee(
          Employee varchar(255) not null,
          StartTime datetime not null,
          EndTime datetime not null
      )
      go
      
      -- insert sample data, Employees C and D are for testing midnight and 'more then one day' situation
      insert into #Employee(Employee, StartTime, EndTime)
      values  ('A', convert(datetime, '01/01/2001 23:00', 103), convert(datetime, '02/01/2001 10:0', 103)),
              ('B', convert(datetime, '01/01/2001 21:00', 103), convert(datetime, '01/01/2001 22:00', 103)),
              ('C', convert(datetime, '01/01/2001 00:00', 103), convert(datetime, '02/01/2001 00:00', 103)),
              ('D', convert(datetime, '01/01/2001 23:59', 103), convert(datetime, '03/01/2001 10:07', 103))
      go
      
      -- we need a function to create a table of integers from a start to end point
      create function intTable(@minValue int, @maxValue int)
          returns @Integers table ( value int )
      AS
      begin
          declare @Index    int
          set @Index = @minValue
          while @Index <= @MaxValue
          begin
              insert into @Integers ( value ) VALUES ( @Index )
              set @Index = @Index + 1
          end
          return
      end
      go      
      
      /*
      variables for start and end of date range
      I don't recommend running this on large data sets with a long date-range !!!
      Best create a stored procedure or table-valued udf with 2 dates as input
      */
      declare @fromDate date, @toDate date
      -- set start/end of date-range
      select  @fromDate = min(StartTime),
              @toDate = max(EndTime)
      from    #Employee           
      
      ;
      /*
      create a table of ints for the date-range
      then create day start/end times for each day of the date-range
      */
      with dateRange(dayStart, dayEnd) as (
          select  convert(datetime, dateAdd(day, ints.value, @fromDate)) dayStart,
                  convert(datetime, dateAdd(day, ints.value + 1, @fromDate)) dayEnd
          from    intTable(0, datediff(day, @fromDate, @Todate)) ints
      )
      
      select      *,
                  datediff(hour, 0, dayEmplTimeWorked.TimeWorked) HoursWorked,
                  datepart(minute, dayEmplTimeWorked.TimeWorked) MinutesWorked
      from        (
                      select      dayEmployee.Employee,
                                  convert(date, dayEmployee.dayStart) Date,
                                  dayEmployee.dayEndTime - dayEmployee.dayStartTime TimeWorked
                      from        (
                          select      Employee.Employee,
                                      dateRange.dayStart,
                                      case when Employee.StartTime >= dateRange.dayStart then Employee.StartTime else dateRange.dayStart end dayStartTime,
                                      case when Employee.EndTime >= dateRange.dayEnd then dateRange.dayEnd else Employee.EndTime end dayEndTime
                              from    dateRange
                                      inner join
                                      #Employee Employee
                                      on  /*
                                          find overlaps between then 2 time ranges:
                                              dayStart - dayEnd vs. StartTime - EndTime   
                                          3 'between' comparisons, we don't need the fourth
                                          can't use BETWEEN since we dont want EndTime=midnight to account 0 minutes for next day
                                          */
                                          (
                                              dateRange.dayStart >= Employee.StartTime
                                              and
                                              dateRange.dayStart < Employee.EndTime
                                          )
                                          or
                                          (
                                              dateRange.dayEnd > Employee.StartTime
                                              and
                                              dateRange.dayEnd <= Employee.EndTime
                                          )
                                          or
                                          (
                                              Employee.StartTime >= dateRange.dayStart
                                              and
                                              Employee.StartTime < dateRange.dayEnd
                                          )
                                  ) dayEmployee
                  ) dayEmplTimeWorked
      order by    dayEmplTimeWorked.Employee,
                  dayEmplTimeWorked.Date
      
      -- cleanup
      drop function intTable
      go
      
      drop table #Employee
      go
      
      rollback transaction
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2022-06-21
        • 1970-01-01
        • 2015-08-14
        • 2023-01-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多