优化使用多个CTE的复杂视图

时间:2018-09-17 11:09:22

标签: sql-server tsql optimization common-table-expression

我正在寻找有关此视图性能方面任何明显错误的指导,因为当前仅花费100万个46s来查询该视图,如果我在期间和年份中添加WHERE子句则需要300万个

该视图的目的是整理几个星期的财务信息,并按期间和年份对这些信息进行分组。然后在整个财务系统中引用此视图,以向其他财务系统报告和生成订单。

查询对于我来说太长了,无法在此处发布,因此我提供了查询的链接,我可以根据要求提供有关表的任何信息: https://pastebin.com/x8dV8KLJ

下面只是查询的常用表表达式,因为StackOverflow坚持我发布一些代码来附加链接:

ALTER VIEW [dbo].[UnitFinancialFigures] AS

WITH
ctePeriodWeekCount AS (
    SELECT Count(Week.WeekNo) AS WeekCount, PrdNo AS Period, PrdYear AS Year
    FROM Week
    INNER JOIN Period on Week.WeekEndDate BETWEEN Period.StartDate AND Period.EndDate
    GROUP BY PrdNo, PrdYear),

cteLabourFigures AS (
    SELECT Labour.LabUnitId, Labour.LabPeriod, Labour.LabYear,
    SUM(CASE WHEN LabourTypes.LTypeIsPayrollLabour = 1 THEN LabourBreakdown.LBrkAmount ELSE 0 END) AS PayRollLabour,
    SUM(CASE WHEN LabourTypes.LTypeIsPayrollLabour = 0 THEN LabourBreakdown.LBrkAmount ELSE 0 END) AS OtherLabour
    FROM Labour
    INNER JOIN LabourBreakdown on LabourBreakDown.LBrkLabId = Labour.LabId
    INNER JOIN LabourTypes on LabourTypes.LTypeId = LabourBreakdown.LBrkLTypeId
    GROUP BY Labour.LabUnitId, Labour.LabPeriod, Labour.LabYear),

cteMobileRelief AS (
    SELECT McDtUnitId, McHdPeriod, McHdYear, SUM(McDtAmount) AS MobileReliefTotal
    FROM MobileCostHeader
    INNER JOIN MobileCostDetail on MobileCostDetail.McDtHdId = MobileCostHeader.MCHdId
    GROUP BY McDtUnitId, McHdPeriod, McHdYear),

cteInvoices AS (
    SELECT Period.PrdNo, Period.PrdYear, Invoice.AccountID, SUM(Invoice.InvTotalInclVat) AS TotalInvoices,
    SUM(Invoice.InvVatAmount) AS TotalInvoicesVat
    FROM [SOP].Invoice
    INNER JOIN Period on Invoice.InvoiceDate BETWEEN Period.StartDate AND Period.EndDate
    GROUP By Period.PrdNo, Period.PrdYear, Invoice.AccountID),

cteSundryCosts AS (
    SELECT ScDtAccountId, ScDtPeriodNo, ScDtYearNo, SUM(ScdtTotal) AS SundryTotal,
    SUM(ScdtAmount) AS SundryNetTotal,
    SUM(ScdtVatAmount) AS SundryVatAmount
    FROM SundryCostDetail
    WHERE ScdtRepeat = 0
    GROUP BY ScDtAccountId, ScDtPeriodNo, ScDtYearNo),

cteAdditionalProfit AS (
    SELECT ScDtAccountId, ScDtPeriodNo, ScDtYearNo, SUM(ScdtTotal) AS AdditionalProfitTotal,
    SUM(ScdtAmount) AS AdditionalProfitNetTotal,
    SUM(ScdtVatAmount) AS AdditionalProfitVatAmount
    FROM SundryCostDetail
    WHERE ScdtRepeat = 0 AND SCdtExcludeFromGP = 0
    GROUP BY ScDtAccountId, ScDtPeriodNo, ScDtYearNo),

cteCashReceived AS (
    SELECT CtUnitId, CtPeriod, CtYear,
    SUM(CtAmount - CtAgentVatAmount) AS GoodsAmount, SUM(CtVatAmount + CtAgentVatAmount) AS VatTotal, SUM(CtTotal) AS Total,
    SUM(VATFree.CanaAmount - VATFree.CanaAgentVatAmount) AS VATFreeGoodsAmount,
    SUM(Catering.CanaAmount - Catering.CanaAgentVatAmount) AS CateringGoodsAmount,
    SUM(Vending.CanaAmount - Vending.CanaAgentVatAmount) AS VendingGoodsAmount,
    SUM(CansConf.CanaAmount - CansConf.CanaAgentVatAmount) AS CansConfGoodsAmount
    FROM CashTran
    LEFT JOIN CashAnalysis AS VATFree ON CashTran.CtId = VATFree.CanaTranId AND VATFree.CanaCode = 'G201'
    LEFT JOIN CashAnalysis AS Catering ON CashTran.CtId = Catering.CanaTranId AND Catering.CanaCode = 'G205'
    LEFT JOIN CashAnalysis AS Vending ON CashTran.CtId = Vending.CanaTranId AND Vending.CanaCode = 'G202'
    LEFT JOIN CashAnalysis AS CansConf ON CashTran.CtId = CansConf.CanaTranId AND CansConf.CanaCode = 'G203'
    WHERE CtCategoryId = 3 AND CtApproved = 1 ANd CtTranType = 'R'
    GROUP BY CtUnitId, CtPeriod, CtYear),

cteFreeIssues AS (
    SELECT CtUnitId, CtPeriod, CtYear,
    SUM(CtAmount - CtAgentVatAmount) AS GoodsAmount, SUM(CtVatAmount + CtAgentVatAmount) AS VatTotal, SUM(CtTotal) AS Total,
    SUM(Catering.CanaAmount - Catering.CanaAgentVatAmount) AS CateringGoodsAmount,
    SUM(Vending.CanaAmount - Vending.CanaAgentVatAmount) AS VendingGoodsAmount,
    SUM(CansConf.CanaAmount - CansConf.CanaAgentVatAmount) AS CansConfGoodsAmount,
    SUM(Labour.CanaAmount - Labour.CanaAgentVatAmount) AS LabourGoodsAmount
    FROM CashTran
    INNER JOIN [SOP].Accounts ON Accounts.AcUniqueId = CtUnitId
    LEFT JOIN CashAnalysis AS Catering ON CashTran.CtId = Catering.CanaTranId AND Catering.CanaCode = 'G204'
    LEFT JOIN CashAnalysis AS Vending ON CashTran.CtId = Vending.CanaTranId AND Vending.CanaCode = 'G202'
    LEFT JOIN CashAnalysis AS CansConf ON CashTran.CtId = CansConf.CanaTranId AND CansConf.CanaCode = 'G203'
    LEFT JOIN CashAnalysis AS Labour ON CashTran.CtId = Labour.CanaTranId AND Labour.CanaCode = 'H995'
    WHERE CtCategoryId = 2
    GROUP BY CtUnitId, CtPeriod, CtYear),

cteExternalFreeIssues AS (
    SELECT CtUnitId, CtPeriod, CtYear, SUM(CtAmount - CtAgentVatAmount) AS GoodsAmount, SUM(CtVatAmount + CtAgentVatAmount) AS VatTotal, SUM(CtTotal) AS Total
    FROM CashTran
    INNER JOIN [SOP].Accounts on Accounts.AcUniqueId = CtUnitId
    WHERE CtCategoryId = 4
    GROUP BY CtUnitId, CtPeriod, CtYear),

cteAgencyLabour AS (
    SELECT PtUnitId, PtYear, PtPeriod, SUM(CASE WHEN PtTranType = 'I' THEN PanaGoodsAmount - PanaAgentVatAmount ELSE -(PanaGoodsAmount - PanaAgentVatAmount) END) Total
    FROM PurchaseTran
    INNER Join PurchaseAnalysis on PurchaseAnalysis.PanaTranId = PurchaseTran.PtId
    INNER JOIN [SOP].Accounts on Accounts.AcUniqueID = PtUnitId
    INNER JOIN UnitAnalysisConfig on UnitAnalysisConfig.UACfgAnalysisCode = PurchaseAnalysis.PanaPCode AND UnitAnalysisConfig.UACfgType = 1
    WHERE PurchaseAnalysis.PanaPCode = 'H995' AND (PtTranType = 'I' Or PtTranType = 'C') AND PtApproved =1
    GROUP BY PtUnitId, PtPeriod, PtYear),

ctePurchases AS (
    SELECT PurchaseTran.PtUnitId, PurchaseTran.PtPeriod, PurchaseTran.PtYear,
    SUM(CASE WHEN PtTranType = 'I' THEN PtTotal WHEN PtTranType = 'C' THEN - PtTotal ELSE 0 END) AS PurchaseTotal,
    SUM(CASE WHEN PtTranType = 'I' THEN PtAmount - PtAgentVatAmount WHEN PtTranType = 'C' THEN -(PtAmount - PtAgentVatAmount) ELSE 0 END) AS PurchaseNetTotal,
    SUM(CASE WHEN PtTranType = 'I' THEN PtVat + PtAgentVatAmount WHEN PtTranType = 'C' THEN -(PtVat + PtAgentVatAmount) ELSE 0 END) AS VatTotal,
    SUM(CASE WHEN PtSupplierId = CmpCashAccount THEN PtTotal ELSE 0 END) AS TotalCashPaid,
    SUM(CASE WHEN PtTranType = 'I' THEN Catering.PanaGoodsAmount - Catering.PanaAgentVatAmount WHEN PtTranType = 'C' THEN -(Catering.PanaGoodsAmount - Catering.PanaAgentVatAmount) ELSE 0 END) CateringTotal,
    SUM(CASE WHEN PtTranType = 'I' THEN Catering.PanaAgentVatAmount + Catering.PanaVatAmount WHEN PtTranType = 'C' THEN -(Catering.PanaAgentVatAmount + Catering.PanaVatAmount) ELSE 0 END) AS CateringVatAmount,
    SUM(CASE WHEN PtTranType = 'I' THEN Vending.PanaGoodsAmount - Vending.PanaAgentVatAmount WHEN PtTranType = 'C' THEN -(Vending.PanaGoodsAmount - Vending.PanaAgentVatAmount) ELSE 0 END) VendingTotal,
    SUM(CASE WHEN PtTranType = 'I' THEN Vending.PanaAgentVatAmount + Vending.PanaVatAmount WHEN PtTranType = 'C' THEN -(Vending.PanaAgentVatAmount + Vending.PanaVatAmount) ELSE 0 END) AS VendingVatAmount,
    SUM(CASE WHEN PtTranType = 'I' THEN CansConf.PanaGoodsAmount - CansConf.PanaAgentVatAmount WHEN PtTranType = 'C' THEN -(CansConf.PanaGoodsAmount - CansConf.PanaAgentVatAmount) ELSE 0 END) CansConfTotal,
    SUM(CASE WHEN PtTranType = 'I' THEN CansConf.PanaAgentVatAmount + CansConf.PanaVatAmount WHEN PtTranType = 'C' THEN -(CansConf.PanaAgentVatAmount + CansConf.PanaVatAmount) ELSE 0 END) AS CansConfVatAmount
    FROM PurchaseTran
    INNER JOIN CompParam on Compparam.CmpDefault = 1
    INNER JOIN [SOP].Accounts on Accounts.AcUniqueID = PurchaseTran.PtUnitId
    LEFT JOIN PurchaseAnalysis AS Catering on Catering.PanaTranId = PurchaseTran.PtId AND (Catering.PanaPCode = 'H201' OR Catering.PanaPCode = 'H401')
    LEFT JOIN PurchaseAnalysis AS Vending on Vending.PanaTranId = PurchaseTran.PtId AND Vending.PanaPCode = 'H202'
    LEFT JOIN PurchaseAnalysis AS CansConf on CansConf.PanaTranId = PurchaseTran.PtId AND CansConf.PanaPCode = 'H211'
    WHERE (PtTranType = 'I' OR PtTranType = 'C') AND PtApproved = 1
    GROUP BY PtUnitId, PtPeriod, PtYear),

cteSundryPurchases AS (
    SELECT PtUnitId, PtYear, PtPeriod,
    SUM(CASE WHEN PtTranType = 'I' THEN PanaGoodsAmount - PanaAgentVatAmount ELSE -(PanaGoodsAmount - PanaAgentVatAmount) END) Total,
    SUM(CASE WHEN PtTranType = 'I' THEN PanaAgentVatAmount + PanaVatAmount WHEN PtTranType = 'C' THEN -(PanaAgentVatAmount + PanaVatAmount) ELSE 0 END) AS VatAmount
    FROM PurchaseTran
    INNER Join PurchaseAnalysis on PurchaseAnalysis.PanaTranId = PurchaseTran.PtId
    INNER JOIN [SOP].Accounts on Accounts.AcUniqueID = PurchaseTran.PtUnitId
    INNER JOIN UnitAnalysisConfig on UnitAnalysisConfig.UACfgAnalysisCode = PurchaseAnalysis.PanaPCode AND (UnitAnalysisConfig.UACfgType = 1 OR UnitAnalysisConfig.UACfgType = 3)
    WHere UnitAnalysisConfig.UACfgTandOLabel = 'Sundry' AND (PtTranType = 'I' Or PtTranType = 'C') AND PtApproved = 1
    GROUP BY PtUnitId, PtPeriod, PtYear),

cteDiscount AS (
    SELECT PurchaseTran.PtUnitId, SUM(CASE WHEN PtTRanType = 'I' THEN PtDiscountAmount ELSE -PtDiscountAmount END) AS DiscountTotal, PtPeriod, PtYear
    FROM PurchaseTran
    WHERE PurchaseTran.PtApproved = 1 And (PurchaseTran.PtTranType = 'I' OR PurchaseTran.PtTranType = 'C')
    GROUP BY PurchaseTran.PtUnitId, PtPeriod, PtYear),

cteUnitPeriodOpeningStock AS (
    SELECT UStkId, UStkOpening, UnitStock.UStkUnitId, UnitStock.UStkPeriod, UnitStock.UStkYearNo, SUM(OpeningCatering.USAnaAmount) AS OpeningCateringStock, SUM(OpeningBeverage.USAnaAmount) AS OpeningBeverageStock, SUM(OpeningSundry.USAnaAmount) AS OpeningSundryStock, SUM(OpeningCansConf.USAnaAmount) AS OpeningCansConfStock
    FROM UnitStock
    LEFT JOIN UnitStockAnalysis AS OpeningCatering ON UnitStock.UStkId = OpeningCatering.USAnaTranId AND OpeningCatering.USanaType = 'O' AND OpeningCatering.USAnaCode = 'CAT001'
    LEFT JOIN UnitStockAnalysis AS OpeningBeverage ON UnitStock.UStkId = OpeningBeverage.USAnaTranId AND OpeningBeverage.USanaType = 'O' AND OpeningBeverage.USAnaCode = 'BEV001'
    LEFT JOIN UnitStockAnalysis AS OpeningSundry ON UnitStock.UStkId = OpeningSundry.USAnaTranId AND OpeningSundry.USanaType = 'O' AND OpeningSundry.USAnaCode = 'SUN001'
    LEFT JOIN UnitStockAnalysis AS OpeningCansConf ON UnitStock.UStkId = OpeningCansConf.USAnaTranId AND OpeningCansConf.USanaType = 'O' AND OpeningCansConf.USAnaCode = 'CAN001'
    INNER JOIN Week on Week.WeekNo = UnitStock.UStkWeeKNo AND Week.WeekYear = UnitStock.UStkYearNo
    INNER JOIN (
        SELECT UStkUnitId, UStkPeriod, MIN(Week.WeekEndDate) OpeningDate
        FROM UnitStock
        INNER JOIN Week on Week.WeekNo = UnitStock.UStkWeeKNo AND Week.WeekYear = UnitStock.UStkYearNo
        GROUP BY UStkUnitId, UStkPeriod) OpeningData on OpeningData.OpeningDate = Week.WeekEndDate AND OpeningData.UStkUnitId = UnitStock.UStkUnitId
    GROUP BY UStkId, UStkOpening, UnitStock.UStkUnitId, UnitStock.UStkPeriod, UnitStock.UStkYearNo),

cteUnitPeriodClosingStock AS (
    SELECT UStkId, UStkClosing, UnitStock.UStkUnitId, UnitStock.UStkPeriod, UnitStock.UStkYearNo, SUM(ClosingCatering.USAnaAmount) AS ClosingCateringStock, SUM(ClosingBeverage.USAnaAmount) AS ClosingBeverageStock, SUM(ClosingSundry.USAnaAmount) AS ClosingSundryStock, SUM(ClosingCansConf.USAnaAmount) AS ClosingCansConfStock
    FROM UnitStock
    LEFT JOIN UnitStockAnalysis AS ClosingCatering ON UnitStock.UStkId = ClosingCatering.USAnaTranId AND ClosingCatering.USanaType = 'C' AND ClosingCatering.USAnaCode = 'CAT001'
    LEFT JOIN UnitStockAnalysis AS ClosingBeverage ON UnitStock.UStkId = ClosingBeverage.USAnaTranId AND ClosingBeverage.USanaType = 'C' AND ClosingBeverage.USAnaCode = 'BEV001'
    LEFT JOIN UnitStockAnalysis AS ClosingSundry ON UnitStock.UStkId = ClosingSundry.USAnaTranId AND ClosingSundry.USanaType = 'C' AND ClosingSundry.USAnaCode = 'SUN001'
    LEFT JOIN UnitStockAnalysis AS ClosingCansConf ON UnitStock.UStkId = ClosingCansConf.USAnaTranId AND ClosingCansConf.USanaType = 'C' AND ClosingCansConf.USAnaCode = 'CAN001'
    INNER JOIN Week on Week.WeekNo = UnitStock.UStkWeeKNo AND Week.WeekYear = UnitStock.UStkYearNo
    INNER JOIN (
        SELECT UStkUnitId, UStkPeriod, MAX(Week.WeekEndDate) ClosingDate
        FROM UnitStock
        INNER JOIN Week on Week.WeekNo = UnitStock.UStkWeeKNo AND Week.WeekYear = UnitStock.UStkYearNo
        GROUP BY UStkUnitId, UStkPeriod) ClosingData on ClosingData.ClosingDate = Week.WeekEndDate AND ClosingData.UStkUnitId = UnitStock.UStkUnitId
    GROUP BY UStkId, UStkClosing, UnitStock.UStkUnitId, UnitStock.UStkPeriod, UnitStock.UStkYearNo)

编辑回复后 感谢您的反馈。我遵循了建议,并分别优化了每个CTE查询。我的错误是期望执行计划告诉我在运行视图时是否缺少建议的索引。直到我分解查询后,我才真正得到任何建议。知道我没有做任何明显有损于性能的事情也很有帮助。添加各种索引后,我的视图达到了30秒左右。

2 个答案:

答案 0 :(得分:1)

根据Tim的评论,(未实现的)视图的性能与基础查询一样好。因此,首先分析查询以进行优化,例如在频繁使用的列上(在联接中或作为外键)添加非聚簇索引。

除此之外,您还可以查看 Indexed Views 以进一步提高性能。但是索引视图也有一些问题,因为它们并不是提高视图性能的可靠方法,例如:

  1. 在由大量索引视图或较少但非常复杂的索引视图引用的表上执行DML时,也必须更新那些引用的索引视图。结果,DML查询性能可能会大大降低,或者在某些情况下甚至无法生成查询计划。 在这种情况下,请在生产使用前测试您的DML查询,分析查询计划并调整/简化DML语句,例如UPDATE,DELETE或INSERT操作。
  2. 建立索引视图的成本取决于维护聚集索引(以及您可以选择添加的任何非聚集索引)。必须权衡维护索引的成本与索引提供的查询优化所带来的好处。

有关更多信息,请参见:

https://docs.microsoft.com/en-us/sql/relational-databases/views/create-indexed-views?view=sql-server-2017

https://www.red-gate.com/simple-talk/sql/learn-sql-server/sql-server-indexed-views-the-basics/

https://www.codeproject.com/Articles/199058/SQL-Server-Indexed-Views-Speed-Up-Your-Select-Quer

答案 1 :(得分:0)

如果您所有的CTE查询都在可接受的水平上执行,但是整个语句却不(常见),那么您应该将CTE转换为这样的临时文件:

--WITH
--ctePeriodWeekCount AS (...

--becomes

SELECT Count(Week.WeekNo) AS WeekCount, PrdNo AS Period, PrdYear AS Year
INTO #ctePeriodWeekCount FROM Week
INNER JOIN Period on Week.WeekEndDate BETWEEN Period.StartDate AND Period.EndDate
GROUP BY PrdNo, PrdYear

然后通过创建并使用适当的临时表来遵循CTE顺序(只需在名称前添加#)。不要忘记将临时存储放在最后一次使用的状态,以尽快释放资源。最后,如果最后使用过滤器,请确保在临时创建SELECT语句中使用它。

即使最终的CTE表现接近单个CTE,使用临时模板对性能也是有利的-当将来发生更改时,处理性能问题的可能性也较小。