65.9K
CodeProject 正在变化。 阅读更多。
Home

SQL 递归公用表表达式在项目调度实践中的应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.44/5 (4投票s)

2015年10月15日

CPOL

6分钟阅读

viewsIcon

11380

具有递归 SQL 公用表表达式的项目调度引擎。

引言

以下文章描述了如何在 Microsoft SQL Server 中使用专为高性能设计的递归公用表表达式来实现项目调度引擎。

该解决方案根据前置和后置依赖关系提供项目预测功能

背景

有时,项目调度的复杂性超出了 Primavera P6 等商业软件系统的能力范围。由于这些解决方案旨在满足大多数公司的需求,因此它们存在局限性。绕过这些局限性的成本超出了构建新软件的成本,而新软件可以以相当高的性能满足所有功能需求。

在我的案例中,项目的规模和复杂性导致不同计划、不同项目和超过 7000 个不同站点中超过 300 万个里程碑。这些里程碑在计划、项目、站点之间存在交叉依赖关系,并使用了不同的工作分解结构 (WBS)。

以下解决方案展示了如何构建单个存储过程,以在几秒钟内有效预测数百万个里程碑。

 

使用代码

我的示例项目是建造一所房子。为了简化流程,我将有一个项目开始日期,然后是 5 个里程碑,以及一个乔迁派对作为最后一个里程碑。

本次练习的目标是,仅通过了解项目开始日期和每项活动的持续时间,来预测乔迁派对的日期。

我们设计的引擎将在任何前置里程碑的预测日期或完成日期发生变化时,更新后续里程碑的预测日期。

为了存储所有与我的项目相关的里程碑,我正在创建一个名为 tblMilestone 的表。
该表存储里程碑的名称、预测日期、完成日期以及该活动的持续时间。

CREATE TABLE tblMilestone (
    MilestoneId INT IDENTITY(1, 1) ,
    Milestone NVARCHAR(128) NOT NULL ,
    ForecastDate DATETIME NULL ,
    CompletedDate DATETIME NULL ,
    DurationInDays INT NOT NULL
                       DEFAULT ( 0 ) ,
    PRIMARY KEY ( MilestoneId ) )
GO

在此练习中,我不会详细介绍如何使用索引和唯一键进行微调,我将尽可能简单地描述解决方案。

一开始,我唯一知道的是我将在某个日期开始工作,以及我为每项活动分配的完成时间。
我们的计划是建立一个通过计算更新预测日期的过程。

INSERT  INTO dbo.tblMilestone
        ( Milestone, ForecastDate, DurationInDays )<
VALUES  ( N'Project Start', '2016 Feb 1', 0 ),
        ( N'Buy Land', NULL, 30 ),
        ( N'Build Walls', NULL, 20 ),
        ( N'Build Roof', NULL, 15 ),
        ( N'Build Garden', NULL, 30 ),
        ( N'Key Handover', NULL, 2 )    ,
        ( N'Housewarming party', NULL, 5 )

如果项目里程碑是顺序的,我们会将持续天数之和加到开始日期以获得乔迁派对的日期。但有些活动可以与其他活动同时开始。

 

为了开始建造花园,我只需要购买土地,理论上,我可以同时建造花园和建造墙壁。尽管这不是最实际的方法,但假设只是为了举例说明。

在正常的父子关系中,我们可以将依赖项存储在 tblMilestone 中。但是,它将父里程碑的数量限制为一个。

假设,当我们买了地后,我们将同时开始建造墙壁和花园。完成墙壁后,我们就可以开始建造屋顶了。

因此,建造墙壁和屋顶所需的时间是 35 天,而花园将需要 30 天。因此,乔迁派对的最早日期是两项活动都完成后,即 35 天加上钥匙交接的 2 天。

因此,为了举办乔迁派对,我们需要完成所有活动。

在项目管理术语中,钥匙移交里程碑有两个前置任务
1. 完成屋顶建造(请注意:屋顶建造里程碑的前置任务是墙壁建造)
2. 完成花园建造

为了映射这种前置-后置关系,我们需要另一个表。

 

CREATE TABLE tblMilestoneDependency (
    MilestoneDependencyId INT IDENTITY(1, 1) ,
    PredecessorMilestoneId INT NULL --reference of the milestone to be finished before our activity can be started
    ,
    MilestoneId INT NOT NULL --current milestone in question
    ,
    PRIMARY KEY ( MilestoneDependencyId ) )
GO

ALTER TABLE dbo.tblMilestoneDependency
ADD CONSTRAINT FK_tblMilestoneDependency_PredecessorMilestoneId FOREIGN KEY (PredecessorMilestoneId) REFERENCES tblMilestone (MilestoneId)
GO

ALTER TABLE dbo.tblMilestoneDependency
ADD CONSTRAINT FK_tblMilestoneDependency_MilestoneId FOREIGN KEY (MilestoneId) REFERENCES tblMilestone (MilestoneId)

起始里程碑没有前置任务,插入 NULL 将被过程解释为起始活动。

INSERT  INTO dbo.tblMilestoneDependency
        ( PredecessorMilestoneId, MilestoneId )
VALUES  ( NULL, 1 ),
        ( 1, 2 ), -- buy the land when the project started    
        ( 2, 3 ), -- start building the wall when the land has bought
        ( 2, 5 ), -- start building the garden when the land has bought
        ( 3, 4 ), -- start building the roof when the walls are standing
        ( 4, 6 ), -- start organising the key handover when the garden and the roof finished
        ( 5, 6 ),
        ( 6, 7 ) -- start organising the housewawrming party

 

总而言之,我们的项目具有“分支”依赖关系。它包含一个示例,即一个里程碑有多个后置任务,换句话说,当一个活动完成后,可以开始另外两个活动。
并且还包括在开始另一项活动之前需要完成多项活动的示例。所以我们可以说我们的示例涵盖了项目管理中的大多数情况。

 


所以让我们写一个查询来获取前置任务的预测日期,添加活动的持续时间并显示后置任务的新预测日期。

一种简单的方法是循环遍历后续里程碑并用新的预测日期更新它们。但是,该查询的性能会非常低。

但是,当公司运行 50 个计划,每个计划有 5-15 个项目,每个项目有大约 120 个里程碑,并且这些里程碑需要在 7000 个不同的地点实现时,会发生什么?里程碑有时在计划之间存在交叉依赖。例如:为了在项目 B 中开始一个里程碑,项目 A 中需要完成另一个里程碑。

由于里程碑的数量很容易超过数百万,低效的调度程序可能会导致触发下一个活动的延迟。

下面的查询显示了如何根据第一个里程碑更新第二个里程碑的 ForecastDate。但是,此脚本不会遍历链,而是始终更新下一个级联里程碑。

SELECT  predm.Milestone PredecessorMilestone ,
        m.Milestone ,
        predm.ForecastDate PredecessorForecastDate ,
        DATEADD(DAY, m.DurationInDays,
                CASE WHEN md.PredecessorMilestoneId IS NULL
                     THEN m.ForecastDate
                     ELSE predm.ForecastDate
                END) ,
        m.DurationInDays
FROM    dbo.tblMilestoneDependency md
INNER JOIN dbo.tblMilestone m
        ON m.MilestoneId = md.MilestoneId -- show all the dependencies of the milestones
LEFT JOIN dbo.tblMilestone predm
        ON predm.MilestoneId = md.PredecessorMilestoneId -- show the name of predecessors if there is any
ORDER BY md.MilestoneId

 

在下面的结果集中,我们可以看到“购买土地”里程碑的新预测日期,比“项目开始”预测日期晚 30 天。

Calculated forecast for the next milestone only

 

 

引入递归公共表表达式

幸运的是,SQL 中引入了递归公用表表达式,因此我们能够用一个查询填充里程碑链。

让我们修改前面的查询,并将其用作我们递归公用表表达式中的源表。

 

;WITH    cteCurrentSchedule
          AS ( SELECT   predm.Milestone PredecessorMilestone ,
                        m.Milestone ,
                        predm.MilestoneId PredecessorMilestoneId ,
                        m.MilestoneId ,
                        predm.ForecastDate PredecessorForecastDate ,
                        m.ForecastDate ,
                        m.CompletedDate,
                        m.DurationInDays
               FROM     dbo.tblMilestoneDependency md
               INNER JOIN dbo.tblMilestone m
                        ON m.MilestoneId = md.MilestoneId -- show all the dependencies of the milestones
               LEFT JOIN dbo.tblMilestone predm
                        ON predm.MilestoneId = md.PredecessorMilestoneId -- show the name of predecessors if there is any
             ),
        cteNewSchedule
          AS (
--select the start milestones
               SELECT   sch.Milestone ,
                        sch.MilestoneId ,
                        COALESCE(sch.CompletedDate, sch.ForecastDate) ForecastDate ,
                        sch.DurationInDays
               FROM     cteCurrentSchedule sch
               WHERE    sch.PredecessorMilestoneId IS NULL
               UNION ALL
               SELECT   cs.Milestone ,
                        cs.MilestoneId ,
                        COALESCE(cs.CompletedDate,
                                 DATEADD(DAY, cs.DurationInDays,
                                         ns.ForecastDate)) ,
                        cs.DurationInDays
               FROM     cteNewSchedule ns
               INNER JOIN cteCurrentSchedule cs
                        ON ns.MilestoneId = cs.PredecessorMilestoneId
             )
    SELECT  u.Milestone ,
            u.MilestoneId ,
            m.ForecastDate ,
            MAX(u.ForecastDate) NewForecastDate ,
            m.CompletedDate ,
            u.DurationInDays  --take the longest parth if  multiple available
    FROM    cteNewSchedule AS u
    INNER JOIN dbo.tblMilestone m
            ON m.MilestoneId = u.MilestoneId
    GROUP BY u.Milestone ,
            u.MilestoneId ,
            m.ForecastDate ,
            m.CompletedDate ,
            u.DurationInDays
    ORDER BY MAX(u.ForecastDate)

结果集显示了所有相关里程碑的预测日期。

Projected Forecast dates

通过该结果,您可以使用最新的预测日期更新您的原始表 (tblMilestone),以在用户控件上表示数据(本文未涵盖)。

总而言之,我们可以将逻辑放入存储过程中。当存储过程执行时,如果任何里程碑的前置任务发生更改,它将更新预测日期。

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:        <Author, Csaba Hegyi>
-- Create date: <Create Date, 2015 Sep 21>
-- Description:    <Description, This stored procedure updates milestones forecast dates if any of the predecessors' forecast date od completition date changed>
-- =============================================
ALTER PROCEDURE sp_Milestone_Reschedule
AS
BEGIN
    SET NOCOUNT ON;

;WITH    cteCurrentSchedule
      AS ( SELECT   predm.Milestone PredecessorMilestone ,
                    m.Milestone ,
                    predm.MilestoneId PredecessorMilestoneId ,
                    m.MilestoneId ,
                    predm.ForecastDate PredecessorForecastDate ,
                    m.ForecastDate ,
                    m.CompletedDate ,
                    m.DurationInDays
           FROM     dbo.tblMilestoneDependency md
           INNER JOIN dbo.tblMilestone m
                    ON m.MilestoneId = md.MilestoneId -- show all the dependencies of the milestones
           LEFT JOIN dbo.tblMilestone predm
                    ON predm.MilestoneId = md.PredecessorMilestoneId -- show the name of predecessors if there is any
         ),
    cteNewSchedule
      AS (
--select the start milestones
           SELECT   sch.Milestone ,
                    sch.MilestoneId ,
                    COALESCE(sch.CompletedDate, sch.ForecastDate) ForecastDate ,
                    sch.DurationInDays
           FROM     cteCurrentSchedule sch
           WHERE    sch.PredecessorMilestoneId IS NULL
           UNION ALL
           SELECT   cs.Milestone ,
                    cs.MilestoneId ,
                    COALESCE(cs.CompletedDate,
                             DATEADD(DAY, cs.DurationInDays,
                                     ns.ForecastDate)) ,
                    cs.DurationInDays
           FROM     cteNewSchedule ns
           INNER JOIN cteCurrentSchedule cs
                    ON ns.MilestoneId = cs.PredecessorMilestoneId
         ),
         cteLongestPathPerMilestone
         as
         (
         SELECT ns.MilestoneId, MAX(ns.ForecastDate)  ForecastDate FROM cteNewSchedule ns 
         GROUP BY ns.MilestoneId
         )

UPDATE m
SET  ForecastDate = u.ForecastDate
FROM    cteLongestPathPerMilestone AS u
INNER JOIN dbo.tblMilestone m
        ON m.MilestoneId = u.MilestoneId
WHERE m.CompletedDate IS NULL OR m.ForecastDate IS NULL

END
GO


存储过程仅重新调度尚未输入完成日期的里程碑。作为示例,我将更新其中两个里程碑的完成日期,以查看该过程如何计算后续里程碑的预测日期。

它将使用前置里程碑的完成日期作为事实来源,而不是其预测日期。


UPDATE  dbo.tblMilestone
SET     CompletedDate = DATEADD(DAY, 3, ForecastDate)
WHERE   MilestoneId = 1
GO

UPDATE  dbo.tblMilestone
SET     CompletedDate = DATEADD(DAY, 15, ( SELECT    ForecastDate
                                              FROM      dbo.tblMilestone
                                              WHERE     MilestoneId = 1
                                            ))
WHERE   MilestoneId = 2

查看结果

EXEC sp_Milestone_Reschedule

SELECT * FROM dbo.tblMilestone m

结果集是

Milestone result

希望您喜欢这篇文章,如果您有任何问题,请随时与我联系。

 

关注点

最具有挑战性的一部分是在设计解决方案时,当一个里程碑有多个前置任务,这意味着其中一个前置任务已经完成,但该里程碑的活动仍然必须等待最长的路径完成。

我遇到了一个解决方案,即计算所有可能路径的预测日期,并取最大日期(最长路径)来更新里程碑。这在最终的 select 语句中表示为 NewForecastDate。

实际应用比这个示例项目更复杂。该示例仅涵盖一种里程碑依赖类型,即“完成到开始”。在实施过程中,我提出了针对“开始到开始”、“开始到完成”和“完成到完成”场景的解决方案。

历史

暂无更改。

© . All rights reserved.