多くの結合を持つSQLクエリを小さな結合に分割すると役立ちますか?


18

SQL Server 2008 R2で毎晩レポートを作成する必要があります。レポートの計算には数時間かかります。時間を短縮するために、テーブルを事前計算します。このテーブルは、12の非常に大きな(数百万行)テーブルを結合して作成されます。

この集計テーブルの計算には、数日前までに約4時間かかりました。DBAは、この大きな結合を3つの小さな結合(それぞれ4つのテーブルに結合)に分割しました。一時的な結果は毎回一時テーブルに保存され、次の結合で使用されます。

DBA拡張の結果、集計テーブルは15分で計算されます。私はそれがどのように可能か疑問に思いました。DBAは、サーバーが処理しなければならないデータの数が少ないためだと言いました。言い換えれば、大きな元の結合では、サーバーは合計された小さな結合よりも多くのデータを処理する必要があります。ただし、元の大きな結合でオプティマイザが効率的に処理し、結合をそれ自体で分割し、次の結合に必要な数の列のみを送信すると仮定します。

彼が行ったもう1つのことは、一時テーブルの1つにインデックスを作成したことです。ただし、オプティマイザーは必要に応じて適切なハッシュテーブルを作成し、計算を全体的に最適化すると思います。

私はこれについて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)Is evil-悪いデータが戻ってくる可能性があります。WITH(ROWCOMMITTED)を試すことをお勧めします。
トムトム

1
@TomTomという意味READCOMMITTEDですか?ROWCOMMITTEDはこれまで見たことがありません。
ypercubeᵀᴹ

4
WITH(NOLOCK)は悪ではありません。人々がそれがそうであると考えるように見えるのは、魔法の弾丸ではありません。一般的なSQL Serverやソフトウェア開発のほとんどのものと同様に、その場所があります。
ゼーン

2
はい。ただし、NOLOCKがログに警告を生成し、さらに重要なこととして間違ったデータを返す可能性があることを考えると、私はそれを悪と考えます。クエリの実行中に主キーと選択されたキーを変更しないように保証されたテーブルでのみ使用できます。そして、はい、私はREADCOMMMITEDを意味しました、ごめんなさい。
トムトム

回答:


11

1 「検索スペース」の削減と、中間/後期結合の統計の改善。

クエリプロセッサがプランの作成さえ拒否した90テーブルの結合(ミッキーマウスのデザイン)を処理する必要がありました。このような結合をそれぞれ9つのテーブルの10個のサブ結合に分割すると、各結合の複雑さが劇的に低下し、テーブルが追加されるたびに指数関数的に増加します。さらに、クエリオプティマイザーはそれらを10のプランとして扱い、全体的に(潜在的に)より多くの時間を費やします(ポールホワイトはメトリックを持っているかもしれません!)。

中間結果テーブルには独自の新しい統計情報が含まれるようになりました。したがって、早い段階でゆがんですぐにサイエンスフィクションになってしまう深いツリーの統計情報と比較して、はるかに良好に結合します。

さらに、最初に最も選択的な結合を強制し、データボリュームを削減してツリーを上に移動できます。オプティマイザーよりもはるかに優れた述部の選択性を推定できる場合は、結合順序を強制しないでください。「Bushy Plans」を検索する価値があるかもしれません。

2効率とパフォーマンスが重要な場合、 それ私の見解で考慮されるべきです

3 必ずしもではありませんが、最も選択的な結合が早い段階で実行される場合があります


3
+1ありがとう。特にあなたの経験の説明のために。「述部の選択性をオプティマイザーよりもはるかによく推定できる場合は、結合順序を強制しないでください」と言って非常に真実です。
オンドレイピーターカ

2
それは実際に非常に有効な質問です。90テーブル結合は、「強制順序」オプションを使用するだけで強制的に計画を作成できます。順序がおそらくランダムで準最適であることは重要ではありませんでしたが、検索スペースを減らすだけで、Optimiserが数秒以内に計画を作成するのに十分でした(20秒後にタイムアウトするヒントはありません)。
ジョンアラン

6
  1. 通常、SQLServerオプティマイザーは適切に機能します。ただし、その目標は、可能な限り最良の計画を生成することではなく、十分に迅速な計画を見つけることです。多くの結合を持つ特定のクエリでは、パフォーマンスが非常に低下する可能性があります。そのような場合の良い兆候は、実際の実行計画の推定行数と実際の行数の大きな違いです。また、最初のクエリの実行プランには、「マージ結合」よりも遅い「ネストされたループ結合」が多く表示されると確信しています。後者では、両方の入力を同じキーを使用してソートする必要がありますが、これは高価であり、通常オプティマイザーはそのようなオプションを破棄します。結果を一時テーブルに保存し、結果と同じように適切なインデックスを追加します-私の推測では-さらなる結合のためのより良いアルゴリズムを選択する際に(サイドノート-最初に一時テーブルにデータを入れることでベストプラクティスに従います、およびインデックスの追加)。さらに、SQLServerは一時テーブルの統計を生成および保持し、適切なインデックスの選択にも役立ちます。
  2. 結合数が特定の固定数よりも大きい場合、一時テーブルの使用に関する標準があるとは言えませんが、パフォーマンスを改善できるオプションであることは間違いありません。これは頻繁には起こりませんが、似たような問題(および同様の解決策)が何度かありました。または、最良の実行計画を自分で考えて、それを保存して強制的に再利用することもできますが、膨大な時間がかかります(成功することを100%保証するものではありません)。別の注意事項-一時テーブルに格納されている結果セットが比較的小さい場合(約1万件のレコードなど)、テーブル変数は一時テーブルよりも優れている場合。
  3. 「それは依存する」と言うのは嫌いですが、おそらくあなたの3番目の質問に対する私の答えです。オプティマイザーは結果を迅速に提供する必要があります。最良の計画を見つけようとして何時間も費やしたくないでしょう。各結合は余分な作業を追加し、オプティマイザーが「混乱する」ことがあります。

3
+1確認と説明をありがとう。あなたが書いたことは理にかなっています。
オンドレイピーターカ

4

さて、あなたは小さなデータで作業していると言ってみましょう-数千万は大きくありません。最後のDWHプロジェクトでは、ファクトテーブルに4億行追加されました。1日あたり。5年間の保管。

問題は部分的にハードウェアです。大規模な結合では一時スペースが大量に使用され、RAMが非常に多いため、ディスクにオーバーフローする瞬間はかなり遅くなります。そのため、SQLがセットの世界に存在し、サイズを気にせず、実行するサーバーが無限ではないため、作業を小さな部分に分割することは理にかなっています。いくつかの操作中に、64GBのtempdbでスペース不足エラーが発生するのに非常に慣れています。

それ以外の場合、統計が正常である限り、クエリオプティマイザーは圧倒されません。テーブルの大きさはあまり気にしません-実際には成長しない統計によって機能します。言った:本当に大きなテーブル(2桁の10億行)がある場合、それらは少し粗いかもしれません。

また、ロックの問題もあります。うまくプログラムしない限り、大規模な結合によってテーブルが数時間ロックされる可能性があります。私は現在200gbのコピー操作を行っていますが、ビジネスキー(効果的にループ)によってそれらをsmllerpartyに分割し、ロックを大幅に短くしています。

最後に、限られたハードウェアで作業します。


1
+1ご回答ありがとうございます。ハードウェアに依存していると言っても良い点があります。RAMはわずか32 GBであり、おそらく十分ではありません。
オンドレイピーターカ

2
そのような答えを読むたびに少しイライラしています。数十億行でもデータベースサーバーに何時間もCPU負荷をかけています。次元の数は多いかもしれませんが、30次元はそれほど大きな数ではないようです。処理できる行の数が非常に多いのは、単純なモデルによるものだと思います。さらに悪いことに、データ全体がRAMに収まります。そしてまだ数時間かかります。
フラッシェンポスト

1
30次元は大いに-モデルが星に適切に最適化されていると確信していますか?たとえば、CPUに負荷がかかるなどのいくつかの間違い-OPクエリでは、GUIDを主キー(一意識別子)として使用しています。私もそれらが大好きです-一意のインデックスとして、主キーはIDフィールドであり、比較全体を高速化し、インデックスをより複雑にします(18ではなく4または8バイト)。そのようなトリックはCPUのトンを節約します。
トムトム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.