将具有许多连接的 SQL 查询拆分为较小的连接有帮助吗?

Ond*_*rka 19 join sql-server optimization

我们需要每晚在我们的 SQL Server 2008 R2 上做一些报告。计算报告需要几个小时。为了缩短时间,我们预先计算了一个表格。该表是基于 JOINining 12 个相当大(数千万行)的表创建的。

直到几天前,这个聚合表的计算才用了大约 4 个小时。我们的 DBA 将这个大连接拆分为 3 个较小的连接(每个连接 4 个表)。临时结果每次都保存到一个临时表中,供下次join使用。

DBA 增强的结果是,聚合表在 15 分钟内计算完成。我想知道这怎么可能。DBA 告诉我,这是因为服务器必须处理的数据数量较少。换句话说,在大的原始连接中,服务器必须处理比在总和较小的连接中更多的数据。但是,我认为优化器会使用原始大连接有效地执行此操作,自行拆分连接并仅发送下一个连接所需的列数。

他所做的另一件事是在其中一个临时表上创建了索引。但是,我再次认为优化器会在需要时创建适当的哈希表,并更好地优化计算。

我和我们的 DBA 讨论过这个问题,但他自己不确定是什么导致了处理时间的改善。他刚刚提到,他不会责怪服务器,因为计算如此大的数据可能会让人不知所措,而且优化器可能很难预测最佳执行计划...... 我明白这一点,但我想对确切原因有更多明确的答案。

所以,问题是:

  1. 什么可能导致大的改善?

  2. 将大连接拆分为小连接是标准程序吗?

  3. 在多个较小连接的情况下,服务器必须处理的数据量真的更小吗?

这是原始查询:

    Insert Into FinalResult_Base
SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TSK.CategoryId
    ,TT.[TestletId]
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) 
    ,TQ.[QuestionId]
    ,TS.StudentId
    ,TS.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] 
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,TS.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,TQ.[Position]  
    ,RA.SpecialNeeds        
    ,[Version] = 1 
    ,TestAdaptationId = TA.Id
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,AnswerType = TT.TestletAnswerTypeId
FROM 
    [TestQuestion] TQ WITH (NOLOCK)
    Join [TestTask] TT WITH (NOLOCK)            On TT.Guid = TQ.TestTaskId
    Join [Question] Q WITH (NOLOCK)         On TQ.QuestionId =  Q.QuestionId
    Join [Testlet] TL WITH (NOLOCK)         On TT.TestletId  = TL.Guid 
    Join [Test]     T WITH (NOLOCK)         On TL.TestId     =  T.Guid
    Join [TestSet] TS WITH (NOLOCK)         On T.TestSetId   = TS.Guid 
    Join [RoleAssignment] RA WITH (NOLOCK)  On TS.StudentId  = RA.PersonId And RA.RoleId = 1
    Join [Task] TSK WITH (NOLOCK)       On TSK.TaskId = TT.TaskId
    Join [Category] C WITH (NOLOCK)     On C.CategoryId = TSK.CategoryId
    Join [TimeWindow] TW WITH (NOLOCK)      On TW.Id = TS.TimeWindowId 
    Join [TestAdaptation] TA WITH (NOLOCK)  On TA.Id = TW.TestAdaptationId
    Join [TestCampaign] TC WITH (NOLOCK)        On TC.TestCampaignId = TA.TestCampaignId 
WHERE
    T.TestTypeId = 1    -- eliminuji ankety 
    And t.ProcessedOn is not null -- ne vsechny, jen dokoncene
    And TL.ShownOn is not null
    And TS.Redizo not in (999999999, 111111119)
END;
Run Code Online (Sandbox Code Playgroud)

DBA 出色工作后的新拆分连接:

    SELECT       
    TC.TestCampaignContainerId,
    TC.CategoryId As TestCampaignCategoryId,
    TC.Grade,
    TC.TestCampaignId,    
    T.TestSetId
    ,TL.TestId
    ,TL.SectionNo
    ,TL.Difficulty
    ,TestletName = Char(65+TL.SectionNo) + CONVERT(varchar(4),6 - TL.Difficulty) -- prevod na A5, B4, B5 ...
    ,TS.StudentId
    ,TS.ClassId
    ,TS.Redizo
    ,[Version] = 1 -- ? 
    ,TestAdaptationId = TA.Id
    ,TL.Guid AS TLGuid
    ,TS.TimeWindowId
INTO
    [#FinalResult_Base_1]
FROM 
    [TestSet] [TS] WITH (NOLOCK)
    JOIN [Test] [T] WITH (NOLOCK) 
        ON [T].[TestSetId] = [TS].[Guid] AND [TS].[Redizo] NOT IN (999999999, 111111119) AND [T].[TestTypeId] = 1 AND [T].[ProcessedOn] IS NOT NULL
    JOIN [Testlet] [TL] WITH (NOLOCK)
        ON [TL].[TestId] = [T].[Guid] AND [TL].[ShownOn] IS NOT NULL
    JOIN [TimeWindow] [TW] WITH (NOLOCK)
        ON [TW].[Id] = [TS].[TimeWindowId] AND [TW].[IsActive] = 1
    JOIN [TestAdaptation] [TA] WITH (NOLOCK)
        ON [TA].[Id] = [TW].[TestAdaptationId] AND [TA].[IsActive] = 1
    JOIN [TestCampaign] [TC] WITH (NOLOCK)
        ON [TC].[TestCampaignId] = [TA].[TestCampaignId] AND [TC].[IsActive] = 1
    JOIN [TestCampaignContainer] [TCC] WITH (NOLOCK)
        ON [TCC].[TestCampaignContainerId] = [TC].[TestCampaignContainerId] AND [TCC].[IsActive] = 1
    ;

 SELECT       
    FR1.TestCampaignContainerId,
    FR1.TestCampaignCategoryId,
    FR1.Grade,
    FR1.TestCampaignId,    
    FR1.TestSetId
    ,FR1.TestId
    ,TSK.CategoryId AS [TaskCategoryId]
    ,TT.[TestletId]
    ,FR1.SectionNo
    ,FR1.Difficulty
    ,TestletName = Char(65+FR1.SectionNo) + CONVERT(varchar(4),6 - FR1.Difficulty) -- prevod na A5, B4, B5 ...
    ,FR1.StudentId
    ,FR1.ClassId
    ,FR1.Redizo
    ,TT.ViewCount
    ,TT.SpentTime
    ,[Version] = 1 -- ? 
    ,FR1.TestAdaptationId
    ,TaskId = TSK.TaskId
    ,TaskPosition = TT.Position
    ,AnswerType = TT.TestletAnswerTypeId
    ,TT.Guid AS TTGuid

INTO
    [#FinalResult_Base_2]
FROM 
    #FinalResult_Base_1 FR1
    JOIN [TestTask] [TT] WITH (NOLOCK)
        ON [TT].[TestletId] = [FR1].[TLGuid] 
    JOIN [Task] [TSK] WITH (NOLOCK)
        ON [TSK].[TaskId] = [TT].[TaskId] AND [TSK].[IsActive] = 1
    JOIN [Category] [C] WITH (NOLOCK)
        ON [C].[CategoryId] = [TSK].[CategoryId]AND [C].[IsActive] = 1
    ;    

DROP TABLE [#FinalResult_Base_1]

CREATE NONCLUSTERED INDEX [#IX_FR_Student_Class]
ON [dbo].[#FinalResult_Base_2] ([StudentId],[ClassId])
INCLUDE ([TTGuid])

SELECT       
    FR2.TestCampaignContainerId,
    FR2.TestCampaignCategoryId,
    FR2.Grade,
    FR2.TestCampaignId,    
    FR2.TestSetId
    ,FR2.TestId
    ,FR2.[TaskCategoryId]
    ,FR2.[TestletId]
    ,FR2.SectionNo
    ,FR2.Difficulty
    ,FR2.TestletName
    ,TQ.[QuestionId]
    ,FR2.StudentId
    ,FR2.ClassId
    ,RA.SubjectId
    ,TQ.[QuestionPoints] -- 1+ good, 0 wrong, null no answer
    ,GoodAnswer  = Case When TQ.[QuestionPoints] Is null Then 0
                      When TQ.[QuestionPoints] > 0 Then 1 -- cookie
                      Else 0 End
    ,WrongAnswer = Case When TQ.[QuestionPoints] = 0 Then 1 
                      When TQ.[QuestionPoints] Is null Then 1
                     Else 0 End
    ,NoAnswer    = Case When TQ.[QuestionPoints] Is null Then 1 Else 0 End
    ,FR2.Redizo
    ,FR2.ViewCount
    ,FR2.SpentTime
    ,TQ.[Position] AS [QuestionPosition]  
    ,RA.SpecialNeeds -- identifikace SVP        
    ,[Version] = 1 -- ? 
    ,FR2.TestAdaptationId
    ,FR2.TaskId
    ,FR2.TaskPosition
    ,QuestionRate = Q.Rate
    ,TestQuestionId = TQ.Guid
    ,FR2.AnswerType
INTO
    [#FinalResult_Base]
FROM 
    [#FinalResult_Base_2] FR2
    JOIN [TestQuestion] [TQ] WITH (NOLOCK)
        ON [TQ].[TestTaskId] = [FR2].[TTGuid]
    JOIN [Question] [Q] WITH (NOLOCK)
        ON [Q].[QuestionId] = [TQ].[QuestionId] AND [Q].[IsActive] = 1

    JOIN [RoleAssignment] [RA] WITH (NOLOCK)
        ON [RA].[PersonId] = [FR2].[StudentId]
        AND [RA].[ClassId] = [FR2].[ClassId] AND [RA].[IsActive] = 1 AND [RA].[RoleId] = 1

    drop table #FinalResult_Base_2;

    truncate table [dbo].[FinalResult_Base];
    insert into [dbo].[FinalResult_Base] select * from #FinalResult_Base;

    drop table #FinalResult_Base;
Run Code Online (Sandbox Code Playgroud)

Joh*_*lan 12

1 减少“搜索空间”,并为中间/后期连接提供更好的统计数据。

我不得不处理查询处理器甚至拒绝创建计划的 90 表连接(米老鼠设计)。将这样的连接分解为 10 个子连接,每个子连接 9 个表,显着降低了每个连接的复杂性,随着每个附加表呈指数增长。此外,查询优化器现在将它们视为 10 个计划,总体上(可能)花费更多时间(Paul White 甚至可能有指标!)。

中间结果表现在将拥有自己的新统计数据,因此与一棵深树的统计数据相比,它们的结合要好得多,这些数据树的统计数据在早期变得倾斜并在不久之后成为科幻小说。

此外,您可以首先强制执行最具选择性的联接,从而减少沿树向上移动的数据量。如果您可以比优化器更好地估计谓词的选择性,为什么不强制连接顺序。可能值得搜索“Bushy Plans”。

2应该在我看来可以考虑,如果效率和性能是重要的

3 不一定,但如果最有选择性的连接在早期执行,则可能

  • +1 谢谢。特别是对于您的经历的描述。这句话非常正确:“如果您能比优化器更好地估计谓词的选择性,为什么不强制连接顺序。” (3认同)
  • 这实际上是一个非常有效的问题。只需使用“强制订单”选项,就可以强制 90 表连接生成计划。顺序可能是随机的和次优的并不重要,只要减少搜索空间就足以帮助优化器在几秒钟内创建一个计划(没有提示它会在 20 秒后超时)。 (2认同)

a1e*_*x07 8

  1. SQLServer 优化器通常做得很好。然而,它的目标不是生成最好的计划,而是快速找到足够好的计划。对于具有许多连接的特定查询,它可能会导致性能非常差。这种情况的良好迹象是实际执行计划中估计行数和实际行数之间的巨大差异。另外,我很确定初始查询的执行计划将显示许多比“合并连接”慢的“嵌套循环连接”。后者需要使用相同的键对两个输入进行排序,这是昂贵的,并且通常优化器会丢弃这样的选项。将结果存储在临时表中并添加适当的索引,因为您所做的结果 - 我的猜测 - 在为进一步连接选择更好的算法时(旁注 - 您遵循最佳实践,首先填充临时表,并在) 之后添加索引。此外,SQLServer 生成并保存临时表的统计信息,这也有助于选择合适的索引。
  2. 当连接数大于某个固定数时,我不能说有关于使用临时表的标准,但这绝对是一个可以提高性能的选项。这并不经常发生,但我有几次类似的问题(和类似的解决方案)。或者,您可以尝试自己找出最佳执行计划,存储并强制重用它,但这将花费大量时间(不能 100% 保证您会成功)。另一个注意事项 - 如果存储在临时表中的结果集相对较小(比如大约 10k 条记录),表变量的性能优于临时表。
  3. 我讨厌说“视情况而定”,但这可能是我对您的第三个问题的回答。优化器必须快速给出结果;您不希望它花费数小时试图找出最佳计划;每个连接都会增加额外的工作,有时优化器会“感到困惑”。

  • +1 感谢您的确认和解释。你写的东西很有道理。 (3认同)

Tom*_*Tom 5

好吧,让我首先说您处理的是小数据——数百万的 10ns 并不大。我在事实表中添加了 4 亿行的最后一个 DWH 项目。每天。储存 5 年。

部分是硬件问题。由于大型连接可能会使用大量临时空间并且只有这么多 RAM,因此当您溢出到磁盘时,事情会变慢很多。因此,将工作拆分为更小的部分可能是有意义的,因为尽管 SQL 存在于集合的世界中,并且不关心大小,但您运行的服务器并不是无限的。在某些操作期间,我非常习惯于在 64gb tempdb 中出现空间不足错误。

否则,只要 statitsics 是有序的,查询优化器就不会不堪重负。它并不真正关心表有多大——它通过真正不增长的统计数据工作。那说:如果你真的有一个大表(两位数的十亿行),那么它们可能有点粗糙。

还有一个锁定问题 - 除非您编程得很好,大连接可能会将表锁定数小时。我现在正在做 200gb 的复制操作,我通过一个业务密钥(有效循环)将它们拆分为 smllerparty,这使得锁更短。

最后,我们使用有限的硬件。

  • 每次我读到这样的答案时,我都会感到有点沮丧 - 即使是几千万行也会在我们的数据库服务器上造成几个小时的 CPU 负载。也许维数很高,但 30 维似乎不是一个太大的数字。我认为您可以处理的大量行来自一个简单的模型。更糟糕的是:整个数据都适合 RAM。而且还需要几个小时。 (2认同)