我如何编写查询以将剩余的小时数提前用于负载均衡计划?

How can I write a query to carry a remaining balance of hours forward for load leveling a schedule?

我有一个查询结果,其中每周安排的总小时数按时间顺序排列,没有间隙,并且每周可以处理固定的小时数。任何未处理的时间都应结转到接下来的一个或多个星期。以下信息可用。

Week | Hours | Capacity
1       2000     160
2        100     160
3          0     140
4        150     160
5        500     160
6       1500     160

每周它都应该减少新的小时数加上容量结转的小时数,但绝不会低于零。正值应带入下周。

Week | Hours | Capacity | LeftOver = (Hours + LAG(LeftOver) - Capacity)
1        400     160          240 (400 +   0 - 160) 
2        100     160          180 (100 + 240 - 160)
3          0     140           40 (  0 + 180 - 140) 
4         20     160            0 ( 20 +  40 - 160) (no negative, change to zero)
5        500     160          340 (500 +   0 - 160)
6          0     160          180 (  0 + 340 - 160)

我假设这可以通过 cte 递归和一个不低于零的 运行 值来完成,但我找不到任何具体的例子来说明如何编写。

好吧,你没有看错,递归通用table表达式确实是构建解决方案的一个选项。

递归查询的构造一般可以分步完成。 运行 您在每一步之后的查询并验证结果。

  1. 定义递归的“锚点”:递归从哪里开始
    这里的开始由Week = 1定义。
  2. 定义递归迭代:迭代之间的关系是什么
    这里是递增的周数d.Week = r.Week + 1

可以使用 case 表达式解决避免负数的问题。

示例数据

create table data
(
  Week int,
  Hours int,
  Capacity int
);

insert into data (Week, Hours, Capacity) values
(1, 400, 160),
(2, 100, 160),
(3,   0, 140),
(4,  20, 160),
(5, 500, 160),
(6,   0, 160);

解决方案

with rcte as
(
  select d.Week,
         d.Hours,
         d.Capacity,
         case
           when d.Hours - d.Capacity > 0
           then d.Hours - d.Capacity
           else 0
         end as LeftOver
  from data d
  where d.Week = 1
union all
  select d.Week,
         d.Hours,
         d.Capacity,
         case
           when d.Hours + r.LeftOver - d.Capacity > 0
           then d.Hours + r.LeftOver - d.Capacity
           else 0
         end
  from rcte r
  join data d
    on d.Week = r.Week + 1
)
select r.Week,
       r.Hours,
       r.Capacity,
       r.LeftOver
from rcte r
order by r.Week;

结果

Week  Hours  Capacity  LeftOver
----  -----  --------  --------
1     400    160       240
2     100    160       180
3     0      140       40
4     20     160       0
5     500    160       340
6     0      160       180

Fiddle 查看实际情况。

我最终编写了一些 CTE,然后编写了一个递归 CTE,并得到了我需要的东西。这里的容量是一个静态数字,但稍后将替换为考虑假期和休假的数字。还需要考虑第一周的初始 'LeftOver' 值,但可以将此查询与较早的日期期间一起使用,以查找 LeftOver 值为零的最近日期,然后将其用作新的开始日期,然后过滤掉最终查询中的前几周。

DECLARE @StartDate date = (SELECT MAX(FirstDayOfWorkWeek) FROM dbo._Calendar WHERE Date <= GETDATE());
DECLARE @EndDate date = DATEADD(week, 12, @StartDate);
DECLARE @EmployeeQty int = (SELECT ISNULL(COUNT(*), 0) FROM Employee WHERE DefaultDepartment IN (4) AND Hidden = 0 AND DateTerminated IS NULL);

WITH hours AS ( 
/* GRAB ALL NEW HOURS SCHEDULED FOR EACH WEEK IN THE SELECTED PERIOD */
SELECT c.FirstDayOfWorkWeek as [Date]
  , SUM(budget.Hours) as hours
FROM dbo.Project_Phase phase
  JOIN dbo.Project_Budget_Labor budget on phase.ID = budget.Phase
  JOIN dbo._Calendar c on CONVERT(date, phase.Date1) = c.[Date]
WHERE phase.CompletedOn IS NULL AND phase.Project <> 4266
  AND phase.Date1 BETWEEN @StartDate AND @EndDate
  AND budget.Department IN (4)
GROUP BY c.FirstDayOfWorkWeek
)
, weeks AS (
/* CREATE BLANK ROWS FOR EACH WEEK AND JOIN TO ACTUAL HOURS TO ELIMINATE GAPS */
/* ADD A ROW NUMBER FOR RECURSION IN NEXT CTE */
SELECT cal.[Date]
  , ROW_NUMBER() OVER(ORDER BY cal.[Date]) as [rownum]
  , ISNULL(SUM(hours.Hours), 0) as Hours
FROM (SELECT FirstDayOfWorkWeek as [Date] FROM dbo._Calendar WHERE [Date] BETWEEN @StartDate AND @EndDate GROUP BY FirstDayOfWorkWeek) as cal
  LEFT JOIN hours on cal.[Date] = hours.[Date]
GROUP BY cal.[Date]
)
, spread AS (
/* GRAB FIRST WEEK AND USE RECURSION TO CREATE RUNNING TOTAL THAT DOES NOT DROP BELOW ZERO*/
SELECT TOP 1 [Date]
  , rownum
  , Hours
  , @EmployeeQty * 40 as Capacity
  , CONVERT(numeric(9,2), 0.00) as LeftOver
  , Hours as running
FROM weeks
ORDER BY rownum
UNION ALL
SELECT curr.[Date]
  , curr.rownum 
  , curr.Hours
  , @EmployeeQty * 40 as Capacity
  , CONVERT(numeric(9,2), CASE WHEN curr.Hours + prev.LeftOver - (@EmployeeQty * 40) < 0 THEN 0 ELSE curr.Hours + prev.LeftOver - (@EmployeeQty * 40) END) as LeftOver
  , curr.Hours + prev.LeftOver as running
FROM weeks curr
  JOIN spread prev on curr.rownum = (prev.rownum + 1)
)

SELECT spread.Hours as NewHours
  , spread.LeftOver as PrevHours
  , spread.Capacity
  , spread.running as RunningTotal
  , CASE WHEN running < Capacity THEN running ELSE Capacity END as HoursThisWeek
FROM spread