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


18

我们需要每晚在SQL Server 2008 R2上进行一些报告。计算报告需要几个小时。为了缩短时间,我们预先计算了一张桌子。该表是基于JOINining 12个很大的表(数十亿行)创建的。

直到几天前cca才花费了4个小时来计算此聚合表。我们的DBA将此大联接分成3个较小的联接(每个联接4个表)。每次都将临时结果保存到一个临时表中,该表将在下一个联接中使用。

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;

经过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;

3
警告词-WITH(NOLOCK)是邪恶的-可能会导致错误数据返回。我建议尝试使用(ROWCOMMITTED)。
TomTom

1
@TomTom是READCOMMITTED什么意思?我以前从未见过ROWCOMMITTED。
ypercubeᵀᴹ

4
WITH(NOLOCK)不是邪恶的。这似乎不是人们似乎认为的魔术子弹。像SQL Server和软件开发中的大多数事物一样,它也有自己的位置。
Zane

2
是的,但是鉴于NOLOCK可能会在日志中生成警告,并且-更重要的是-返回WRONG DATA,我认为这是邪恶的。它仅在保证查询运行时不更改主键和选定键的表上可用。是的,我的意思是READCOMMMITED,对不起。
TomTom

Answers:


11

1 减少了“搜索空间”,并为中级/后期联接提供了更好的统计信息。

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

中间结果表现在将拥有自己的新鲜统计信息,因此与之相比,深树的统计信息要早得多,而后者早就歪斜并在不久之后成为科幻小说,因此连接结果要好得多。

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

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

3 不一定,但它可能是,如果最有选择性的加入都早早就执行


3
+1谢谢。特别是为了描述您的经历。这样说是非常正确的:“如果可以比优化器更好地估计谓词的选择性,为什么不强加连接顺序。”
Ondrej Peterka

2
实际上,这是一个非常有效的问题。仅使用“强制订单”选项,就可以强制执行90个表的联接以生成计划。顺序可能是随机的而且不是最优的,这没关系,只是减少搜索空间就足以帮助Optimiser在几秒钟内创建计划(没有提示,它将在20秒后超时)。
John Alan

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

3
+1感谢您的确认和解释。你写的东西很有道理。
Ondrej Peterka

4

好吧,首先让我说您正在处理小数据-十亿分之几的数据并不大。我上次发布的DWH项目中,事实表增加了4亿行。每天。存放5年。

问题部分是硬件。由于大型联接可能会占用大量的临时空间,而且只有太多的RAM,因此,当您溢出到磁盘中时,事情就会变得很慢。因此,将工作分成较小的部分可能是有意义的,这仅仅是因为尽管SQL存在于集合的世界中,并且不关心大小,但是运行的服务器并不是无限的。在执行某些操作时,我已经很习惯摆脱64gb tempdb中的空间错误。

否则,只要保持状态稳定,查询优化器就不会不知所措。它实际上并不在乎表有多大,它实际上是根据不会增长的统计数据来工作的。说:如果您确实有一个大表(双十亿行),那么它们可能有点粗糙。

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

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


1
+1感谢您的回答。说这取决于硬件是有好处的。我们只有32 GB的RAM,可能还不够。
Ondrej Peterka

2
每当我读到这样的答案时,我都会感到沮丧-即使是几十万行,也会在数小时内在数据库服务器上造成CPU负载。也许尺寸的数量很高,但30个尺寸似乎不是太大。我认为您可以处理的行数很高,这来自一个简单的模型。更糟糕的是:整个数据都适合RAM。而且仍然需要几个小时。
flaschenpost 2013年

1
30个尺寸是很多-您确定模型已正确优化为星形吗?某些错误(例如,花费CPU的错误)在OP查询中使用GUID作为主键(uniqueidentifier)。我也很喜欢它们-作为唯一索引,主键是一个ID字段,可以使整个比较更快,并且索引更容易(4或8个字节,而不是18个字节)。这样的技巧可以节省大量CPU。
TomTom
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.