SQL:计算每天的员工利用小时数

时间:2017-09-11 08:43:12

标签: sql sql-server tsql

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         |

5 个答案:

答案 0 :(得分:2)

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

compress

答案 1 :(得分:0)

使用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

答案 2 :(得分:0)

您可以尝试以下查询 -

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 ) 

答案 3 :(得分:0)

这是拥有日历查找表的很多情况之一。有许多日历表脚本的例子可以根据您的需要构建简单或健壮的东西,但是让我们假装您拥有的是日历表中的日期列表。

生成临时表并用样本数据填充......

dlib::array2d<rgb_pixel> convert(ByteArray img);

查询表格(select中的表格基本上在同一天内的时间段内分割日期时间段 - 然后我们计算开始和结束时间段之间的小时数) - 你可以重构为CTE。

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')

如果需要,我可以在这里进一步澄清。希望对你有效。我猜它有点健壮(适用于每个SQL Server(不使用concat))。

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

Edit2:Sample table-valued function that returns a pretty awesome calendar lookup table

答案 4 :(得分:0)

Edit2:我从Tyron78的解决方案中删除了LEAD()函数。这种方式适用于2012年之前的SQL-Server并执行更好的imho:

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解决方案,它在我的测试中表现更好,即使是大日期范围 - 非常好! 一个解决方法:改变这一行:

  • WHATE DATEADD(d,1,c.StartTime_DATE)&lt; = c.EndTime

到这个

  • WHATE DATEADD(d,1,c.StartTime_DATE)&lt; c.EndTime

考虑到结束时间=午夜。

非常好,学到了一些东西,谢谢!

我的解决方案类似于日历查找,它使用函数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