Различные операторы select
в конце кода могут использоваться, чтобы просмотреть промежуточные результаты из CTE и разобраться в том, что делается шаг за шагом.(Это также обычно полезный способ разбить проблему на более простые части и отладить их по одной за раз.)
Стоит отметить, что DateDiff
возвращает число пересечений границ .Пожалуйста, обратитесь к документации для получения дополнительной информации.
Оставленное в качестве упражнения для читателя разрывает CTE для использования курсоров и циклов, как того требует OP.
declare @Rates as Table ( RateId Int Identity, Shift Int, StartTime Time, EndTime Time, Rate Decimal(6,2) );
-- A rate is applicable from the StartTime up to, but not including, the EndTime .
-- StartTime < EndTime unless the EndTime is 00:00 indicating a rate that applies until midnight.
-- A rate cannot span across midnight, but two entries for a single shift may be used to continue a rate past midnight.
insert into @Rates ( Shift, StartTime, EndTime, Rate ) values
( 5, '00:00', '07:00', 800.00 ),
( 0, '07:00', '09:00', 0.00 ), -- No rate supplied in homework assignment.
( 1, '09:00', '12:00', 350.00 ),
( 2, '12:00', '17:00', 400.00 ),
( 3, '17:00', '21:00', 500.00 ),
( 4, '21:00', '00:00', 600.00 );
select * from @Rates order by Shift;
declare @Work as Table ( WorkId Int Identity, WorkerId Int, Started DateTime, Ended DateTime );
insert into @Work ( WorkerId, Started, Ended ) values
( 1, '2000-01-01T11:00:00', '2000-01-01T11:15:00' ), -- Single rate.
( 1, '2000-01-01T09:00:00', '2000-01-01T12:00:00' ), -- Single rate.
( 1, '2000-01-01T10:00:00', '2000-01-01T16:00:00' ), -- Multiple rates.
( 5, '2000-01-01T00:00:00', '2000-01-01T04:00:00' ), -- Single rate starting at midnight.
( 6, '2000-01-01T10:00:00', '2000-01-02T00:00:00' ), -- Multiple rates ending at midnight.
( 7, '2000-01-01T10:00:00', '2000-01-02T16:00:00' ), -- Multiple dates and rates.
( 8, '2000-01-01T10:00:00', '2000-01-03T16:00:00' ), -- Multiple dates and rates.
( 9, '2000-01-01T10:00:00', '2000-01-04T00:00:00' ); -- Multiple dates and rates.
select * from @Work order by Started, WorkerId;
declare @Midnight as Time = '00:00'; -- For easier reading.
with
Ten ( Number ) as ( select * from ( values (0), (1), (2), (3), (4), (5), (6), (7), (8), (9) ) as Digits( Number ) ),
TenUp2 ( Number ) as ( select 42 from Ten as L cross join Ten as R ),
Numbers ( Number ) as ( select Row_Number() over ( order by ( select NULL ) ) from TenUp2 ),
Work as ( -- Split out the date/times into separate date and time columns.
select WorkId, WorkerId,
Cast( Started as Date ) as StartedDate, Cast( Started as Time ) as StartedTime,
Cast( Ended as Date ) as EndedDate, Cast( Ended as Time ) as EndedTime
from @Work ),
WorkOverDates as ( -- Split work across dates into separate rows for each date.
-- Work completed in a single day.
select WorkId, WorkerId, StartedDate, StartedTime, EndedDate, EndedTime
from Work
where StartedDate = EndedDate
union
-- First day of work that spans dates.
select WorkId, WorkerId, StartedDate, StartedTime, StartedDate, @Midnight
from Work
where StartedDate <> EndedDate
union
-- Last day of work that spans dates.
select WorkId, WorkerId, EndedDate, @Midnight, EndedDate, EndedTime
from Work
where StartedDate <> EndedDate and EndedTime <> @Midnight
union
-- Add any intermediate days, just in case someone worked a really long time.
select WorkId, WorkerId, DateAdd( day, N.Number, StartedDate ), @Midnight, DateAdd( day, N.Number, StartedDate ), @Midnight
from Work as W inner join
Numbers as N on N.Number < DateDiff( day, StartedDate, EndedDate )
where DateDiff( day, StartedDate, EndedDate ) > 1 ),
WorkOverRates as ( -- For each work row generate rows for all of the applicable rates (for each date).
select WOD.WorkId, WOD.WorkerId, WOD.StartedDate, WOD.StartedTime, WOD.EndedDate, WOD.EndedTime,
R.RateId, R.Shift, R.StartTime, R.EndTime, R.Rate
from WorkOverDates as WOD inner join
-- The general test for overlapping ranges is: Start1 <= End2 and Start2 <= End1.
@Rates as R on ( WOD.StartedTime < R.EndTime or R.EndTime = @Midnight ) and
( R.StartTime < WOD.EndedTime or WOD.EndedTime = @Midnight ) ),
PaidIntervals as ( -- Determine the hours worked from at each rate (for each date).
select WorkId, WorkerId, StartedDate, StartedTime, EndedDate, EndedTime,
RateId, Shift, StartTime, EndTime, Rate,
DateDiff( millisecond,
-- From the later of the work or rate start time to ...
case when StartedTime < StartTime then StartTime else StartedTime end,
-- ... the earlier of the work or rate end time allowing for midnight.
case
when EndedTime = @Midnight and EndTime = @Midnight then DateAdd( day, 1, 0 )
when EndedTime = @Midnight then EndTime
when EndTime = @Midnight then EndedTime
when EndedTime < EndTime then EndedTime
else EndTime end ) / 3600000.0 as HoursWorked
from WorkOverRates ),
PaySummary as ( -- Summarize all of the rate periods for each WorkId .
select WorkId, Sum( HoursWorked ) as TotalHours, Count( 42 ) as RatePeriods,
Sum( Rate * HoursWorked ) as TotalPay
from PaidIntervals
group by WorkId )
-- To see the intermediate results in the CTE use one of the following select statements instead of the final select :
--select * from Numbers;
--select * from Work order by WorkId;
--select * from WorkOverDates order by WorkId, StartedDate;
--select * from WorkOverRates order by WorkId, StartedDate, StartedTime;
--select * from PaidIntervals order by WorkId, StartedDate, StartedTime;
--select * from PaySummary order by WorkId;
-- Put the summary together with the original work data.
select W.WorkId, W.WorkerId, W.Started, W.Ended, PS.TotalHours, PS.RatePeriods, PS.TotalPay
from @Work as W inner join
PaySummary as PS on PS.WorkId = W.WorkId
order by PS.WorkId;