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