조인이 많은 SQL 쿼리를 더 작은 조인으로 나누는 것이 도움이됩니까?


18

SQL Server 2008 R2에서 매일 밤보고해야합니다. 보고서를 계산하는 데 몇 시간이 걸립니다. 시간을 단축하기 위해 테이블을 미리 계산합니다. 이 테이블은 12 개의 아주 큰 (수십억 개의 행) 테이블을 JOINining을 기반으로 작성됩니다.

이 집계 테이블의 계산은 며칠 전까지 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) 이블-나쁜 데이터가 다시 나타날 수 있습니다. WITH (ROWCOMMITTED) 시도해보십시오.
TomTom

1
@TomTom 무슨 뜻 READCOMMITTED인가요? 나는 전에 ROWCOMMITTED를 본 적이 없다.
ypercubeᵀᴹ

4
WITH (NOLOCK)은 악이 아닙니다. 사람들이 생각하는 것은 마법의 총알이 아닙니다. SQL Server 및 소프트웨어 개발의 대부분과 마찬가지로 일반적으로 그 자리에 있습니다.
Zane

2
예, 그러나 NOLOCK이 로그에 경고를 생성하고 더 중요한 것은 잘못된 데이터를 반환 할 수 있다고 가정하면 악의적이라고 생각합니다. 쿼리가 실행되는 동안 기본 키와 선택한 키가 변경되지 않도록 보장 된 테이블에서만 사용할 수 있습니다. 그리고 네, READCOMMMITED를 의미했습니다. 죄송합니다.
TomTom

답변:


11

1 '검색 공간'감소 및 중간 / 후기 조인에 대한 더 나은 통계와 결합.

쿼리 프로세서가 계획을 세우지 않은 90 테이블 조인 (미키 마우스 디자인)을 처리해야했습니다. 이러한 조인을 각각 9 개의 테이블로 구성된 10 개의 서브 조인으로 나누면 각 조인의 복잡성이 급격히 줄어들어 각 추가 테이블과 함께 기하 급수적으로 증가합니다. 또한 Query Optimiser는 이제이를 10 개의 계획으로 취급하여 전체적으로 더 많은 시간을 소비 할 수 있습니다 (Paul White에는 메트릭이있을 수도 있습니다).

중간 결과표는 이제 자체적으로 새로운 통계를 가지므로, 초기에 비뚤어지고 곧 공상 과학 소설로 끝나는 깊은 나무의 통계와 비교할 때 훨씬 더 잘 결합됩니다.

또한 가장 선택적 조인을 먼저 강제 실행하여 트리 위로 이동하는 데이터 볼륨을 줄입니다. 옵티 마이저보다 술어의 선택성을 훨씬 더 잘 평가할 수 있다면 조인 순서를 강제하지 마십시오. "Bushy Plans"를 검색 할 가치가 있습니다.

2 그것은 해야 효율과 성능이 중요한 경우, 내 관점에서 고려되어야

3 필요하지 않게, 그러나 가장 선택이 초기에 실행되는 조인 경우 수


3
+1 감사합니다. 특히 당신의 경험에 대한 설명. "최적화 기보다 술어의 선택성을 훨씬 더 잘 평가할 수 있다면 조인 순서를 강제하지 마십시오."
Ondrej Peterka 2016 년

2
실제로 매우 유효한 질문입니다. 'Force Order'옵션을 사용하여 계획을 작성하기 위해 90 테이블 조인을 강제 할 수 있습니다. 순서가 임의적이고 차선책 일 수는 없었지만 검색 공간을 줄이는 것만으로도 Optimiser가 몇 초 내에 계획을 작성하는 데 충분했습니다 (20 초 후에 시간이 초과되지 않는 힌트 없음).
John Alan

6
  1. SQLServer 최적화 프로그램은 일반적으로 잘 작동합니다. 그러나 목표는 가능한 최상의 계획을 세우는 것이 아니라 신속하게 충분히 좋은 계획을 찾는 것입니다. 조인이 많은 특정 쿼리의 경우 성능이 매우 저하 될 수 있습니다. 이러한 경우를 잘 나타내는 것은 실제 실행 계획에서 예상 행 수와 실제 행 수 사이의 큰 차이입니다. 또한 초기 쿼리의 실행 계획에 '병합 조인'보다 느린 많은 '중첩 루프 조인'이 표시됩니다. 후자는 동일한 키를 사용하여 두 입력을 정렬해야하므로 비용이 많이 들고 일반적으로 옵티마이 저는 이러한 옵션을 버립니다. 추가 조인을 위해 더 나은 알고리즘을 선택할 때 결과를 임시 테이블에 저장하고 적절한 인덱스를 추가하십시오. 및 이후에 색인 추가). 또한 SQLServer는 임시 테이블에 대한 통계를 생성하고 유지하여 적절한 인덱스를 선택하는 데 도움이됩니다.
  2. 조인 수가 고정 수보다 클 때 임시 테이블 사용에 대한 표준이 있다고 말할 수는 없지만 성능을 향상시킬 수있는 옵션입니다. 그것은 자주 발생하지 않지만 비슷한 문제 (및 비슷한 해결책)가 몇 번이나 발생했습니다. 또는 최상의 실행 계획을 직접 파악하여 저장하고 재사용 할 수는 있지만 시간이 많이 걸립니다 (100 % 성공하지는 못함). 또 다른 참고 사항-임시 테이블에 저장된 결과 집합이 비교적 작은 경우 (예 : 약 10k 레코드) 테이블 변수가 임시 테이블보다 성능이 우수합니다.
  3. 나는 '그것에 달려있다'고 말하는 것을 싫어하지만 아마도 세 번째 질문에 대한 나의 대답 일 것입니다. 최적화 프로그램은 빠른 결과를 제공해야합니다. 최상의 계획을 세우는 데 몇 시간을 소비하고 싶지는 않습니다. 각 조인은 추가 작업을 추가하고 때로는 옵티마이 저가 혼란스러워합니다.

3
확인 및 설명에 +1 감사합니다. 당신이 쓴 것은 말이됩니다.
Ondrej Peterka 2016 년

4

음, 작은 데이터를 다루는 것으로 시작하겠습니다. 천만 대가 크지 않습니다. 내가 마지막 DWH 제트기에는 팩트 테이블에 4 억 개의 행이 추가되었습니다. 하루에. 5 년간 보관.

문제는 부분적으로 하드웨어입니다. 큰 조인은 많은 임시 공간을 사용하고 RAM이 너무 많기 때문에 디스크에 넘친 순간이 훨씬 느려집니다. 따라서 SQL이 세트의 세계에 살고 크기에 신경 쓰지 않고 실행하는 서버가 무한하지 않기 때문에 작업을 더 작은 부분으로 나누는 것이 좋습니다. 일부 작업 중에 64GB tempdb에서 공간 부족 오류를 얻는 데 꽤 익숙합니다.

그렇지 않으면 통계가 순서대로있는 한 쿼리 최적화 프로그램이 압도되지 않습니다. 실제로 테이블이 얼마나 큰지 신경 쓰지 않습니다. 실제로 자라지 않는 통계로 작동합니다. 주의 사항 : 실제로 큰 테이블 (두 자리 수십억 행 수)이있는 경우 약간 거칠 수 있습니다.

큰 조인이 테이블을 몇 시간 동안 잠글 수 있도록 프로그래밍하지 않는 한 잠금 문제도 있습니다. 현재 200GB 복사 작업을 수행하고 있으며 비즈니스 키 (효과적으로 반복)를 통해 잠금을 훨씬 짧게 유지하여 smllerparty으로 분할하고 있습니다.

결국, 우리는 제한된 하드웨어로 작업합니다.


1
답변에 +1 감사합니다. 그것이 HW에 달려 있다는 좋은 지적이 있습니다. 32GB의 RAM 만 있으면 충분하지 않습니다.
Ondrej Peterka 2016 년

2
그런 답변을 읽을 때마다 약간 실망합니다. 심지어 수십만 행조차도 데이터베이스 서버에서 몇 시간 동안 CPU 부하를 발생시킵니다. 어쩌면 차원 수가 많을 수도 있지만 30 차원은 그리 크지 않은 것 같습니다. 처리 할 수있는 매우 많은 수의 행이 간단한 모델에서 나온 것 같습니다. 더 나쁜 것은 : 전체 데이터가 RAM에 적합하다는 것입니다. 그리고 여전히 몇 시간이 걸립니다.
flaschenpost

1
30 치수는 LOT입니다. 모델이 별표에 올바르게 최적화되어 있습니까? 예를 들어 OP 쿼리에서 CPU 비용이 발생하는 일부 실수는 GUID를 기본 키 (고유 식별자)로 사용하는 것입니다. 나는 그들도 너무 좋아합니다-고유 인덱스, 기본 키는 ID 필드이며 전체 비교를 더 빠르게하고 인덱스는 nawwox (18이 아닌 4 또는 8 바이트)입니다. 이와 같은 트릭은 TON의 CPU를 절약합니다.
TomTom
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.