LEFT OUTER JOIN 中嵌套“OR”條件的 SQL Server 性能不佳
我有一些棘手的 SQL,這給我帶來了一些麻煩。我想向一個鍵表引入幾個新列,根據使用者配置(維度 1-10),可能會或可能不會使用其組件。出於架構原因,這些儲存為“UNIQUEIDENTIFIER”。
但是,在我的數據庫中引入這些新維度列後,我現在在其他幾個操作此鍵表中的數據的儲存過程中面臨性能問題,我將在下面介紹其中一個。似乎我為支持這些新列而實施的方法效率低下,我不確定為什麼會出現這種情況以及解決它的最佳方法。
(包括 SQL)
--In real usage this would be set by user configuration DECLARE @Dimension1Enabled BIT = 1 DECLARE @Dimension2Enabled BIT = 1 DECLARE @Dimension3Enabled BIT = 1 DECLARE @Dimension4Enabled BIT = 1 DECLARE @Dimension5Enabled BIT = 1 DECLARE @Dimension6Enabled BIT = 1 DECLARE @Dimension7Enabled BIT = 1 DECLARE @Dimension8Enabled BIT = 1 DECLARE @Dimension9Enabled BIT = 1 DECLARE @Dimension10Enabled BIT = 1 /* --Here's the table definition for reference CREATE TABLE [dbo].[Table1]( [Column1] [int] NOT NULL, [Column2] [uniqueidentifier] NOT NULL, [Column3] [uniqueidentifier] NOT NULL, [Column4] [char](3) NOT NULL, [Dimension1] [uniqueidentifier] NULL, [Dimension2] [uniqueidentifier] NULL, [Dimension3] [uniqueidentifier] NULL, [Dimension4] [uniqueidentifier] NULL, [Dimension5] [uniqueidentifier] NULL, [Dimension6] [uniqueidentifier] NULL, [Dimension7] [uniqueidentifier] NULL, [Dimension8] [uniqueidentifier] NULL, [Dimension9] [uniqueidentifier] NULL, [Dimension10] [uniqueidentifier] NULL, [Period] [int] NOT NULL, [Amt] [numeric](28, 12) NOT NULL, [EndingBal] [numeric](28, 12) NOT NULL, [PlanningSourceID] [int] NOT NULL, [Column5] [uniqueidentifier] NULL ) ON [PRIMARY] --Simply contains 0-12 CREATE TABLE [dbo].[Table2] ( [FiscalPeriod] [int] NOT NULL ,[FiscalPeriodDescription] [varchar](50) NOT NULL ,[FiscalPeriodDescriptionMonthName] [varchar](20) NULL ,[FiscalPeriodName] [varchar](255) NULL ) */ SELECT pfd.* FROM ( SELECT PFDE1.Column1 ,PFDE1.Column2 ,PFDE1.Column3 ,PFDE1.Column4 ,PFDE1.Dimension1 ,PFDE1.Dimension2 ,PFDE1.[Dimension3] ,PFDE1.[Dimension4] ,PFDE1.[Dimension5] ,PFDE1.[Dimension6] ,PFDE1.[Dimension7] ,PFDE1.[Dimension8] ,PFDE1.[Dimension9] ,PFDE1.[Dimension10] ,p.FiscalPeriod AS Period ,0 AS Amt ,0 AS EndingBal ,PFDE1.Column5 FROM ( SELECT DISTINCT PFDE1.Column1 ,PFDE1.Column2 ,PFDE1.Column3 ,PFDE1.Column4 ,PFDE1.Dimension1 ,PFDE1.Dimension2 ,PFDE1.[Dimension3] ,PFDE1.[Dimension4] ,PFDE1.[Dimension5] ,PFDE1.[Dimension6] ,PFDE1.[Dimension7] ,PFDE1.[Dimension8] ,PFDE1.[Dimension9] ,PFDE1.[Dimension10] ,PFDE1.Column5 FROM [dbo].[Table1] PFDE1 ) PFDE1 CROSS JOIN [dbo].[Table2] p ) pfd LEFT JOIN [dbo].[Table1] pfde ON PFDE.Column1 = pfd.Column1 AND PFDE.Column2 = pfd.Column2 AND PFDE.Column3 = pfd.Column3 AND PFDE.Column4 = pfd.Column4 --This section causes great slowness AND (@Dimension1Enabled = 0 OR PFDE.Dimension1 = pfd.Dimension1) AND (@Dimension2Enabled = 0 OR PFDE.Dimension2 = pfd.Dimension2) AND (@Dimension3Enabled = 0 OR PFDE.Dimension3 = pfd.Dimension3) AND (@Dimension4Enabled = 0 OR PFDE.Dimension4 = pfd.Dimension4) AND (@Dimension5Enabled = 0 OR PFDE.Dimension5 = pfd.Dimension5) AND (@Dimension6Enabled = 0 OR PFDE.Dimension6 = pfd.Dimension6) AND (@Dimension7Enabled = 0 OR PFDE.Dimension7 = pfd.Dimension7) AND (@Dimension8Enabled = 0 OR PFDE.Dimension8 = pfd.Dimension8) AND (@Dimension9Enabled = 0 OR PFDE.Dimension9 = pfd.Dimension9) AND (@Dimension10Enabled = 0 OR PFDE.Dimension10 = pfd.Dimension10) AND PFDE.Period = pfd.Period AND PFDE.Column5 = pfd.Column5 WHERE pfde.Column1 IS NULL
Table1 中有 50,000 條記錄,此查詢的性能很差,至少幾分鐘。刪除“@DimensionXEnabled = 0 OR”條件,查詢將在幾秒鐘內執行。
我覺得我有幾個選項可以優化它,我可以執行以下操作之一:
- 將“維度”列更改為 NOT NULL 並填充 0 Guid 值 - 我相信這會對性能產生影響,因為現在我要處理每次插入的大量數據。我已經測試了這個選項,它確實工作得更好,但有一個明確的性能影響,如果可能的話我想避免它。
- 將查詢轉換為動態 SQL 並動態生成連接條件 - 需要在 100 多個其他地方執行此操作,並且有一個充滿動態 SQL 的數據庫,也不期待這個選項。
- 在 ISNULL 語句中包裝“Dimension1 = Dimension1”,而不是基於變數的連接條件 - 我相信這會導致連接語句效率低下,因為查詢將不再能夠使用索引。
我覺得 SQL 優化器對這個查詢做出了錯誤的決定,如果我編寫沒有 OR 條件的查詢,它執行得很快,即使結果完全相同。除了長時間執行的連接之外,查詢計劃看起來幾乎相同,大部分處理時間用於 LEFT OUTER JOIN 語句合併:
(帶有“或”條件)
(沒有“或”條件)
我不確定如何繼續,我的問題是,有更好的選擇嗎?
更新 1
我一直在對此進行更多研究,嘗試各種方法來解決性能不佳的問題,到目前為止,我還沒有找到任何好的解決方案,只是提供了更多資訊:
- 嘗試列“IS NULL”但沒有成功(
PFDE.Dimension1 IS NULL OR PFDE.Dimension1 = pfd.Dimension1
),執行速度更慢,這似乎沒有做到。- 累加入“Hints”(HASH、MERGE、LOOP),這裡沒有性能影響,實際上在某些情況下它們只是執行速度較慢。
- 嘗試了選項 3,實際上這似乎執行得非常快,我認為因為 Table2 沒有任何索引,這種方法在未來可能會成為一個問題……
- 我嘗試過動態 SQL(@AaronBertrand 推薦),實際上結果有點令人印象深刻(30 秒+),不知道為什麼它表現如此糟糕,但這可能是我可以通過進一步優化來改進的東西,我正在研究這更..
- 我嘗試了“選項(重新編譯);” 在這篇文章中提到。看到了明顯的改進(約 15 秒),仍然沒有尺寸那麼快,但也許我可以迭代其他一些東西。
聽起來像廚房水槽查詢。對於大多數使用動態 SQL 的情況,您可以使其很好地工作,因為您可以為每個參數組合生成不同的執行計劃。
DECLARE @sql nvarchar(max) = N' SELECT DISTINCT PFDE1.Column1 ,PFDE1.Column2 ,PFDE1.Column3 ,PFDE1.Column4 ,PFDE1.Dimension1 ,PFDE1.Dimension2 ... ,PFDE1.[Dimension10] ,PFDE1.Column5 FROM [dbo].[Table1] PFDE1 ) PFDE1 CROSS JOIN [dbo].[Table2] p ) pfd LEFT JOIN [dbo].[Table1] pfde ON PFDE.Column1 = pfd.Column1 AND PFDE.Column2 = pfd.Column2 AND PFDE.Column3 = pfd.Column3 AND PFDE.Column4 = pfd.Column4' + CASE WHEN @Dimension1Enabled = 1 THEN N' AND PFDE.Dimension1 = pfd.Dimension1' ELSE N'' END + CASE WHEN @Dimension2Enabled = 1 THEN N' AND PFDE.Dimension2 = pfd.Dimension2' ELSE N'' END + ... + CASE WHEN @Dimension10Enabled = 1 THEN N' AND PFDE.Dimension10 = pfd.Dimension10' ELSE '' END + N' AND PFDE.Period = pfd.Period AND PFDE.Column5 = pfd.Column5 WHERE pfde.Column1 IS NULL;'; EXEC sys.sp_executesql @sql;
我也會放棄
DISTINCT
- 如果由於重複而確實有必要,那麼還有一個潛在的問題需要解決。
IMO,最大的錯誤是您使用的地方
CROSS JOIN
。首先自我加入
[dbo].[Table1]
並過濾結果集,然後使用此結果集CROSS JOIN
。它將帶來巨大的推動作用。SELECT pfd.* FROM ( SELECT PFDE1.Column1 ,PFDE1.Column2 ,PFDE1.Column3 ,PFDE1.Column4 ,PFDE1.Dimension1 ,PFDE1.Dimension2 ,PFDE1.[Dimension3] ,PFDE1.[Dimension4] ,PFDE1.[Dimension5] ,PFDE1.[Dimension6] ,PFDE1.[Dimension7] ,PFDE1.[Dimension8] ,PFDE1.[Dimension9] ,PFDE1.[Dimension10] ,p.FiscalPeriod AS Period ,0 AS Amt ,0 AS EndingBal ,PFDE1.Column5 FROM ( SELECT DISTINCT PFDE1.Column1 ,PFDE1.Column2 ,PFDE1.Column3 ,PFDE1.Column4 ,PFDE1.Dimension1 ,PFDE1.Dimension2 ,PFDE1.[Dimension3] ,PFDE1.[Dimension4] ,PFDE1.[Dimension5] ,PFDE1.[Dimension6] ,PFDE1.[Dimension7] ,PFDE1.[Dimension8] ,PFDE1.[Dimension9] ,PFDE1.[Dimension10] ,PFDE1.Column5 FROM [dbo].[Table1] PFDE1 LEFT JOIN [dbo].[Table1] pfde ON PFDE.Column1 = PFDE1.Column1 AND PFDE.Column2 = PFDE1.Column2 AND PFDE.Column3 = PFDE1.Column3 AND PFDE.Column4 = PFDE1.Column4 AND PFDE.Period = PFDE1.Period AND PFDE.Column5 = PFDE1.Column5 AND (@Dimension1Enabled = 0 OR PFDE.Dimension1 = PFDE1.Dimension1) AND (@Dimension2Enabled = 0 OR PFDE.Dimension2 = PFDE1.Dimension2) AND (@Dimension3Enabled = 0 OR PFDE.Dimension3 = PFDE1.Dimension3) AND (@Dimension4Enabled = 0 OR PFDE.Dimension4 = PFDE1.Dimension4) AND (@Dimension5Enabled = 0 OR PFDE.Dimension5 = PFDE1.Dimension5) AND (@Dimension6Enabled = 0 OR PFDE.Dimension6 = PFDE1.Dimension6) AND (@Dimension7Enabled = 0 OR PFDE.Dimension7 = PFDE1.Dimension7) AND (@Dimension8Enabled = 0 OR PFDE.Dimension8 = PFDE1.Dimension8) AND (@Dimension9Enabled = 0 OR PFDE.Dimension9 = PFDE1.Dimension9) AND (@Dimension10Enabled = 0 OR PFDE.Dimension10 = PFDE1.Dimension10) WHERE pfde.Column1 IS NULL ) PFDE1 CROSS JOIN [dbo].[Table2] p ) pfd
希望我的觀點很清楚,它未經測試。
毫無疑問 Dynamic Sql 會進一步改進它,
EXEC sys.sp_executesql @sql
我也會放棄 DISTINCT - 如果由於重複而確實有必要,那麼還有一個潛在的問題需要解決。