创建具有多个连接的 SQL 视图时出现问题

Problem with creating a SQL view with multiple joins

我正在尝试创建一个视图,它将作为 Excel 在 API 中导出的基础。基本上,它包含的是有关特定项目的信息。可以向所述项目添加计算(这一切都发生在前端的表单上)。这些计算称为 EBIT、EBIT+ 和 OVI。用户可以添加其中一个、两个或全部,因此例如会有只有 EBIT 的项目,也有只有 EBIT 的项目,但也有只有 EBIT+ 和 OVI 的项目。 View 需要 return 项目信息和所有选定的计算在一行中,因此由于用户不会选择某些类型的计算,因此还需要类型安全。

我的观点代码:

CREATE VIEW [Signoff].[ExecelReport_uvw]
    AS SELECT 
    project.ProjectName,
    project.CreatedOn,
    
    project.ProjectId,
    subCategory.SubCategoryName,

    projectStatus.StatusName,
    overallCategory.CategoryName,
    projectUserResponsible.UserName,
    valueImprovementType.ValueImprovementTypeName,
    OVI.OverallImprovementTypeName,
    project.NameOfSuplier,
    improvementCalculation.Baseline,
    improvementCalculation.ImpactValue,
    project.ContractStartDate,
    project.ContractEndDate,
    userbusinessController.UserName as businessControllerName,
    businessControllerStatus.ApprovalStatusName businessControllerStatus,
    userbusinesOwner.UserName as businessOwnerName,
    businesOwnerStatus.ApprovalStatusName as businessOwnerStatus,
    userbusinessCFO.UserName as businessCFOName,
    businessCFOStatus.ApprovalStatusName as businessCFOStatus,
    project.IsEbitda,
    improvementCalculation.EBITDA

    FROM [Signoff].[Project] as project
    LEFT JOIN [Signoff].[OverallImprovementType] as OVI on project.OverallImprovementTypeId = OVI.OverallImprovementTypeId
    LEFT JOIN [Signoff].[SubCategory] as subCategory on project.GPSubCategory = subCategory.SubCategoryId
    LEFT JOIN [Signoff].[Category] as overallCategory on  project.GPCategory = overallCategory.CategoryId
    LEFT JOIN [Signoff].[ValueImprovementType] as valueImprovementType on project.ValueImprovementTypeId = valueImprovementType.ValueImprovementTypeId
    
    LEFT JOIN [Signoff].[Status] as projectStatus on project.ProjectStatus = projectStatus.StatusId
    LEFT JOIN [Signoff].[User] as projectUserResponsible on project.ProjectResponsible = projectUserResponsible.UserId

    LEFT JOIN [Signoff].[ProjectUser] as projectUserbusinessControler on project.ProjectId = projectUserbusinessControler.ProjectId AND projectUserbusinessControler.ProjectRoleId = 'A36FC6CD-9ED7-4AA8-B1BE-355E48BDE25A'
    LEFT JOIN [Signoff].[User] as userbusinessController on projectUserbusinessControler.ApproverId = userbusinessController.UserId
    LEFT JOIN [Signoff].[ApprovalStatus] as businessControllerStatus on projectUserbusinessControler.ApprovalStatusId = businessControllerStatus.ApprovalStatusId 

    LEFT JOIN [Signoff].[ProjectUser] as projectUserbusinessOwner on project.ProjectId = projectUserbusinessOwner.ProjectId AND projectUserbusinessOwner.ProjectRoleId = 'E1E23E4F-1CA4-4869-9387-43CEDAEBBBB0'
    LEFT JOIN [Signoff].[User] as userbusinesOwner on projectUserbusinessOwner.ApproverId = userbusinesOwner.UserId
    LEFT JOIN [Signoff].[ApprovalStatus] as businesOwnerStatus on projectUserbusinessOwner.ApprovalStatusId = businesOwnerStatus.ApprovalStatusId 

    LEFT JOIN [Signoff].[ProjectUser] as projectUserbusinessCFO on project.ProjectId = projectUserbusinessCFO.ProjectId AND projectUserbusinessCFO.ProjectRoleId = 'DA17CF66-1D61-460E-BF87-5D86744DF22A'
    LEFT JOIN [Signoff].[User] as userbusinessCFO on projectUserbusinessCFO.ApproverId = userbusinessCFO.UserId
    LEFT JOIN [Signoff].[ApprovalStatus] as businessCFOStatus on projectUserbusinessCFO.ApprovalStatusId = businessCFOStatus.ApprovalStatusId 

    LEFT JOIN [Signoff].[ProjectImprovementCalculation] as projectImprovementCalculation on project.ProjectId = projectImprovementCalculation.ProjectId
    LEFT JOIN [Signoff].[ImprovementCalculation] as improvementCalculation on projectImprovementCalculation.ImprovementCalculationId = improvementCalculation.ImprovementCalculationId

改进计算table:

CREATE TABLE [Signoff].[ImprovementCalculation]
(
    [ImprovementCalculationId] INT NOT NULL IDENTITY,
    [Baseline] INT NOT NULL,
    [TotalSpend] INT NOT NULL,
    [ImpactValue] INT NOT NULL,
    [ImpactPercentage] INT NOT NULL,
    [EBITDA] INT NOT NULL,
    [CalculationType] VARCHAR (255) NOT NULL
)
GO

ALTER TABLE [Signoff].[ImprovementCalculation]
ADD CONSTRAINT [PK_ImprovemntCalculation] PRIMARY KEY([ImprovementCalculationId]);
GO

项目改进计算table:

CREATE TABLE [Signoff].[ProjectImprovementCalculation]
(
    [ProjectImprovementCalculationId] INT NOT NULL IDENTITY,
    [ProjectId] UNIQUEIDENTIFIER NOT NULL,
    [ImprovementCalculationId] INT NOT NULL,
)
GO

ALTER TABLE [Signoff].[ProjectImprovementCalculation]
ADD CONSTRAINT [PK_ProjectImprovementCalculation] PRIMARY KEY([ProjectImprovementCalculationId]);
GO

ALTER TABLE [Signoff].[ProjectImprovementCalculation]
ADD CONSTRAINT FK_ProjectProjectImprovementCalculation
FOREIGN KEY (ProjectId) REFERENCES [Signoff].[Project](ProjectId);
GO

ALTER TABLE [Signoff].[ProjectImprovementCalculation]
ADD CONSTRAINT FK_ImprovementCalculationProjectImprovementCalculation
FOREIGN KEY (ImprovementCalculationId) REFERENCES [Signoff].[ImprovementCalculation](ImprovementCalculationId);
GO

以防万一,虽然我觉得没必要,项目table:

CREATE TABLE [Signoff].[Project]
(
    [ProjectId] UNIQUEIDENTIFIER NOT NULL DEFAULT (NEWID()), 
    [ProjectName] NVARCHAR(50) NOT NULL, 
    [LegalEntity] UNIQUEIDENTIFIER NOT NULL,
    [ValueImprovementTypeId] INT NOT NULL, 
    [OverallImprovementTypeId] INT NOT NULL, 
    [NameOfSuplier] NVARCHAR(50) NOT NULL, 
    [ContractStartDate] DATE NOT NULL, 
    [ContractEndDate] DATE NOT NULL,
    [GPCategory] UNIQUEIDENTIFIER NOT NULL,
    [GPSubCategory] UNIQUEIDENTIFIER NOT NULL,
    [ProjectResponsible] UNIQUEIDENTIFIER NOT NULL,
    [ProjectNumber] INT,
    [FullProjectNumber] VARCHAR(55),
    [ProjectStatus] UNIQUEIDENTIFIER NOT NULL DEFAULT '05c2f392-8b69-4915-a166-c4418889f9e8', 
    [IsCanceled] BIT NULL DEFAULT 0,
    [IsEbitda] BIT NOT NULL DEFAULT 0, 
    [CreatedOn] DATETIME NOT NULL DEFAULT SYSDATETIME()
)
GO

ALTER TABLE [Signoff].[Project]
ADD CONSTRAINT [PK_Project] PRIMARY KEY([ProjectId]);
GO

ALTER TABLE [Signoff].[Project]
ADD CONSTRAINT [FK_ProjectStatus] FOREIGN KEY ([ProjectStatus]) REFERENCES [Signoff].[Status]([StatusId]);
GO

我想出了这个解决方案,但它 return 在 table 中的不同行中的每个计算,我希望所有计算都在一个项目的一行中,所以不是我要找的:

LEFT JOIN [Signoff].[ProjectImprovementCalculation] as projectImprovementCalculation on project.ProjectId = projectImprovementCalculation.ProjectId
LEFT JOIN [Signoff].[ImprovementCalculation] as improvementCalculation on projectImprovementCalculation.ImprovementCalculationId = improvementCalculation.ImprovementCalculationId

有人知道怎么做吗?或者我正在以完全错误的方式解决问题?如果我写的信息有点乱,有看不懂的地方,我可以改一下。

我假设可用的 CalculationType 值是固定的,每个项目每种类型最多有一个改进计算,并且您希望为每种计算类型定义固定的专用列 BaseLine 和 ImpactValue。

一种方法是使用嵌套联接,这将有效地对每种计算类型左联接 ProjectImprovementCalculation 和 ImprovementCalculation 的内部联接组合一次。然后可以在最终 select 列表中单独引用每个结果。

类似于:

SELECT ...
    IC_AAA.BaseLine, IC_AAA.ImpactValue,
    IC_BBB.BaseLine, IC_BBB.ImpactValue,
    ...
FROM ...
LEFT JOIN Signoff.ProjectImprovementCalculation as PIC_AAA
    JOIN Signoff.ImprovementCalculation as IC_AAA
        ON IC_AAA.ImprovementCalculationId = PIC_AAA.ImprovementCalculationId
        AND IC_AAA.CalculationType = 'AAA'
    ON PIC_AAA.ProjectId = project.ProjectId
LEFT JOIN Signoff.ProjectImprovementCalculation as PIC_BBB
    JOIN Signoff.ImprovementCalculation as IC_BBB
        ON IC_BBB.ImprovementCalculationId = PIC_BBB.ImprovementCalculationId
        AND IC_BBB.CalculationType = 'BBB'
    ON PIC_BBB.ProjectId = project.ProjectId
...

语法有点奇怪,两个 JOINs 后跟两个 ON 子句。如果允许使用括号会更清楚,但(据我所知)这不是语法的一部分。

有多种替代方法可以实现同样的效果。以下使用 OUTER APPLY:

SELECT ...
    AAA.BaseLine, AAA.ImpactValue,
    BBB.BaseLine, BBB.ImpactValue,
    ...
FROM ...
OUTER APPLY (
    SELECT IC.*
    FROM Signoff.ProjectImprovementCalculation as PIC
    JOIN Signoff.ImprovementCalculation as IC
        ON IC.ImprovementCalculationId = PIC.ImprovementCalculationId
        AND IC.CalculationType = 'AAA'
    WHERE PIC.ProjectId = project.ProjectId
) AAA
OUTER APPLY (
    SELECT IC.*
    FROM Signoff.ProjectImprovementCalculation as PIC
    JOIN Signoff.ImprovementCalculation as IC
        ON IC.ImprovementCalculationId = PIC.ImprovementCalculationId
        AND IC.CalculationType = 'BBB'
    WHERE PIC.ProjectId = project.ProjectId
) BBB
...

使用通用 Table 表达式 (CTE) 可以减少一些重复并使查询更具可读性。

;WITH ImprovementCTE AS (
    SELECT PIC.ProjectId, IC.*
    FROM Signoff.ProjectImprovementCalculation as PIC
    JOIN Signoff.ImprovementCalculation as IC
        ON IC.ImprovementCalculationId = PIC.ImprovementCalculationId
)
SELECT ...
    AAA.BaseLine, AAA.ImpactValue,
    BBB.BaseLine, BBB.ImpactValue,
    ...
FROM ...
LEFT JOIN ImprovementCTE AAA
    ON AAA.ProjectId = project.ProjectId
    AND AAA.CalculationType = 'AAA'
LEFT JOIN ImprovementCTE BBB
    ON BBB.ProjectId = project.ProjectId
    AND BBB.CalculationType = 'BBB'
...

您也可以尝试在单个 CROSS APPLY:

中使用 条件聚合
SELECT ...
    IC.BaseLineAAA, IC.ImpactValueAAA,
    IC.BaseLineBBB, IC.ImpactValueBBB,
    ...
FROM ...
CROSS APPLY (
    SELECT
        BaseLineAAA = SUM(CASE WHEN IC.CalculationType = 'AAA' THEN IC.BaseLine),
        ImpactValueAAA = SUM(CASE WHEN IC.CalculationType = 'AAA' THEN IC.ImpactValue),
        BaseLineBBB = SUM(CASE WHEN IC.CalculationType = 'BBB' THEN IC.BaseLine),
        ImpactValueBBB = SUM(CASE WHEN IC.CalculationType = 'BBB' THEN IC.ImpactValue),
        ...
    FROM Signoff.ProjectImprovementCalculation as PIC
    JOIN Signoff.ImprovementCalculation as IC
        ON IC.ImprovementCalculationId = PIC.ImprovementCalculationId
    WHERE PIC.ProjectId = project.ProjectId
) IC

我希望还有其他方法,例如使用 PIVOT。

如果以上似乎满足您的需要,您仍应 运行 测试并检查执行计划以查看哪个执行计划最佳。有些人可能倾向于检索所有 ImprovementCalculation 行,即使项目的子集已 selected。

要处理缺失的计算类型,您可以使用 ISNULL() 函数提供默认值。如果您需要在其他数字结果中强制使用空白值,您可能需要使用类似 ISNULL(CONVERT(VARCHAR(50), result), '').

的内容