SQL Serverでは、次の場合にLOOP JOINを強制する必要がありますか?


15

通常、すべての標準的な理由から、結合ヒントを使用しないことをお勧めします。しかし、最近、パフォーマンスを向上させるために強制ループ結合をほぼ常に見つけるパターンを見つけました。実際、私はそれを使い始め、推奨しすぎているので、何かを見逃していないことを確認するためにセカンドオピニオンを得たいと思いました。代表的なシナリオを次に示します(例を生成するための非常に具体的なコードは最後にあります):

--Case 1: NO HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
JOIN SampleTable AS S ON S.ID = D.ID

--Case 2: LOOP JOIN HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID

SampleTableには100万行があり、そのPKはIDです。
一時テーブル#Driverには、1つの列、ID、インデックスなし、5万行しかありません。

私が一貫して見つけることは次のとおりです。

ケース1:NOヒント
SampleTableを上のインデックススキャン
ハッシュ結合
より高い期間を(平均333ms)
高いCPU(平均331ms)
論理低は(4714)を読み込み

ケース2:
SampleTable
ループ結合のLOOP JOIN HINT インデックスシーク
継続時間の短縮(平均204ミリ秒、39%減少)
CPUの減少(平均206、38 %減少)
はるかに高い論理読み取り(160015、34X増加)

最初は、2番目のケースの非常に高い読み取り値は、読み取り値を低くすることがパフォーマンスの適切な尺度と見なされることが多いため、少し怖いものでした。しかし、実際に何が起こっているのかを考えるほど、それは私には関係ありません。私の考えは次のとおりです。

SampleTableは4714ページに含まれ、約36MBを消費します。ケース1ですべてがスキャンされるため、4714の読み取りが行われます。さらに、100万回のハッシュを実行する必要がありますが、これはCPUを集中的に使用し、最終的に比例して時間を増加させます。ケース1でタイムアップするのは、このすべてのハッシュです。

ここで、ケース2について考えてみましょう。ハッシュを実行していませんが、代わりに50000の個別シークを実行しています。これが読み取りを促進しています。しかし、読み取りは比較してどれくらい高価ですか?それらが物理的な読み取りである場合、かなり高価になる可能性があると言うかもしれません。ただし、1)特定のページの最初の読み取りのみが物理的である可能性があり、2)たとえそうであっても、ケース1はすべてのページにヒットすることが保証されているため、同じまたはより悪い問題を抱えることに留意してください。

両方のケースが少なくとも1回は各ページにアクセスする必要があるという事実を考慮すると、どちらが高速であるか、100万回のハッシュ、またはメモリに対する約155000回の読み取りのどちらの問題なのでしょうか?私のテストでは後者のように見えますが、SQL Serverは一貫して前者を選択します。

質問

それでは私の質問に戻りましょう。テストでこの種の結果が表示されたときに、このLOOP JOINヒントを強制し続けるべきですか、それとも分析で何かが欠けていますか?私はSQL Serverのオプティマイザーに反対するのをためらっていますが、このような場合よりもはるかに早くハッシュ結合の使用に切り替えるように感じます。

更新2014-04-28

さらにテストを行い、上記の結果(2 CPUのVMで)が他の環境で複製できないことを発見しました(8と12 CPUの2つの異なる物理マシンで試しました)。オプティマイザーは、後者の場合、そのような顕著な問題がなくなるまでずっと良くなりました。振り返ってみれば明らかなように、学んだ教訓は、環境がオプティマイザーの動作に大きく影響する可能性があるということです。

実行計画

実行計画ケース1 計画1 実行計画ケース2 ここに画像の説明を入力してください

サンプルケースを生成するコード

------------------------------------------------------------
-- 1. Create SampleTable with 1,000,000 rows
------------------------------------------------------------    

CREATE TABLE SampleTable
    (  
       ID         INT NOT NULL PRIMARY KEY CLUSTERED
     , Number1    INT NOT NULL
     , Number2    INT NOT NULL
     , Number3    INT NOT NULL
     , Number4    INT NOT NULL
     , Number5    INT NOT NULL
    )

--Add 1 million rows
;WITH  
    Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows  
    Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows  
    Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows 
    Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows 
    Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows 
    Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows 
    FinalCte AS (SELECT  ROW_NUMBER() OVER (ORDER BY C) AS Number FROM   Cte5)
INSERT INTO SampleTable
SELECT Number, Number, Number, Number, Number, Number
FROM  FinalCte
WHERE Number <= 1000000

------------------------------------------------------------
-- Create 2 SPs that join from #Driver to SampleTable.
------------------------------------------------------------    
GO
IF OBJECT_ID('JoinTest_NoHint') IS NOT NULL DROP PROCEDURE JoinTest_NoHint
GO
CREATE PROC JoinTest_NoHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    JOIN SampleTable AS S ON S.ID = D.ID
GO
IF OBJECT_ID('JoinTest_LoopHint') IS NOT NULL DROP PROCEDURE JoinTest_LoopHint
GO
CREATE PROC JoinTest_LoopHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID
GO

------------------------------------------------------------
-- Create driver table with 50K rows
------------------------------------------------------------    
GO
IF OBJECT_ID('tempdb..#Driver') IS NOT NULL DROP TABLE #Driver
SELECT ID
INTO #Driver
FROM SampleTable
WHERE ID % 20 = 0

------------------------------------------------------------
-- Run each test and run Profiler
------------------------------------------------------------    

GO
/*Reg*/  EXEC JoinTest_NoHint
GO
/*Loop*/ EXEC JoinTest_LoopHint


------------------------------------------------------------
-- Results
------------------------------------------------------------    

/*

Duration CPU   Reads    TextData
315      313   4714     /*Reg*/  EXEC JoinTest_NoHint
309      296   4713     /*Reg*/  EXEC JoinTest_NoHint
327      329   4713     /*Reg*/  EXEC JoinTest_NoHint
398      406   4715     /*Reg*/  EXEC JoinTest_NoHint
316      312   4714     /*Reg*/  EXEC JoinTest_NoHint
217      219   160017   /*Loop*/ EXEC JoinTest_LoopHint
211      219   160014   /*Loop*/ EXEC JoinTest_LoopHint
217      219   160013   /*Loop*/ EXEC JoinTest_LoopHint
190      188   160013   /*Loop*/ EXEC JoinTest_LoopHint
187      187   160015   /*Loop*/ EXEC JoinTest_LoopHint

*/

回答:


13

SampleTableは4714ページに含まれ、約36MBを消費します。ケース1ですべてがスキャンされるため、4714の読み取りが行われます。さらに、100万回のハッシュを実行する必要がありますが、これはCPUを集中的に使用し、最終的に比例して時間を増加させます。ケース1でタイムアップするのは、このすべてのハッシュです。

ハッシュ結合(ブロッキング操作でもあるハッシュテーブルの構築)にはスタートアップコストがありますが、最終的にはハッシュ結合はSQL Serverでサポートされる3つの物理結合タイプの中で理論上の行ごとのコストが最も低くなります。 IOおよびCPUの用語。ハッシュ結合は、比較的小さなビルド入力と大きなプローブ入力を使用して実際に機能します。ただし、すべてのシナリオで「より良い」物理的な結合タイプはありません。

ここで、ケース2について考えてみましょう。ハッシュを実行していませんが、代わりに50000の個別シークを実行しています。これが読み取りを促進しています。しかし、読み取りは比較的高価ですか?それらが物理的な読み取りである場合、かなり高価になる可能性があると言うかもしれません。ただし、1)特定のページの最初の読み取りのみが物理的である可能性があり、2)たとえそうであっても、ケース1はすべてのページにヒットすることが保証されているため、同じまたはより悪い問題があることに留意してください。

各シークでは、bツリーをルートにナビゲートする必要があります。これは、単一のハッシュプローブと比較して計算コストが高くなります。さらに、ネストされたループ結合の内側の一般的なIOパターンは、ハッシュ結合へのプローブ側スキャン入力のシーケンシャルアクセスパターンと比較してランダムです。基礎となる物理IOサブシステムによっては、シーケンシャル読み取りがランダム読み取りよりも高速になる場合があります。また、SQL Serverの先読みメカニズムはシーケンシャルIOでより良く機能し、より大きな読み取りを発行します。

両方のケースが少なくとも1回は各ページにアクセスする必要があるという事実を考慮すると、どちらが高速であるか、100万回のハッシュ、またはメモリに対する約155000回の読み取りのどちらの問題なのでしょうか?私のテストでは後者のように見えますが、SQL Serverは一貫して前者を選択します。

SQL Serverクエリオプティマイザーは、多くの仮定を行います。1つは、クエリによって行われたページへの最初のアクセスにより、物理IOが発生することです(「コールドキャッシュの仮定」)。同じクエリによって既にメモリに読み込まれたページから後の読み取りが行われる可能性がモデル化されますが、これは経験に基づいた推測にすぎません。

オプティマイザーのモデルがこのように機能する理由は、一般に最悪の場合に最適化する方がよいためです(物理IOが必要です)。多くの欠点は、並列処理とメモリ内の実行によって隠されます。すべてのデータがメモリ内にあると仮定した場合にオプティマイザが生成するクエリプランは、その仮定が無効であることが判明した場合、パフォーマンスが非常に低下する可能性があります。

コールドキャッシュの仮定を使用して作成された計画は、代わりにウォームキャッシュが仮定された場合と同様に十分に機能しない場合がありますが、通常、最悪の場合のパフォーマンスは優れています。

これらの種類の結果がテストで示される場合、このLOOP JOINヒントを強制し続ける必要がありますか、それとも分析で何かが欠けていますか?私はSQL Serverのオプティマイザーに反対するのをためらっていますが、このような場合よりもはるかに早くハッシュ結合の使用に切り替えるように感じます。

次の2つの理由から、これを行うには非常に注意する必要があります。まず、ヒントも黙って物理的にはあなたにも指定された場合と同様に、クエリの記述された順に(一致させるために参加強制参加OPTION (FORCE ORDER)。これは深刻なオプティマイザが利用可能な選択肢を制限し、常にあなたが望むものではないかもしれない。OPTION (LOOP JOIN)力はネストされたループをクエリのために結合しますが、書かれた結合順序を強制しません。

第二に、データセットのサイズは小さいままであり、ほとんどの論理読み取りはキャッシュから行われると仮定しています。これらの仮定が(おそらく時間とともに)無効になると、パフォーマンスが低下します。組み込みのクエリオプティマイザーは、変化する状況への対応に非常に優れています。その自由を取り除くことは、あなたが真剣に考えるべきものです。

全体として、ループ結合を強制する説得力のある理由がない限り、私はそれを避けます。通常、デフォルトの計画は最適にかなり近いものであり、状況の変化に直面しても回復力が高い傾向があります。


ポールありがとう。優れた詳細分析。私が行ったいくつかのさらなるテストに基づいて、一時テーブルのサイズが5Kから100Kの間である場合、この特定の例ではオプティマイザーの経験に基づいた推測が一貫してオフになっていると思います。私たちの要件が一時テーブルが50K未満であることを保証しているという事実を考えると、それは私にとって安全なようです。私は好奇心が強いです、あなたはまだこれを知っているどんな種類の結合ヒントも避けますか?
ジョニー14

1
@JohnnyMヒントには理由があります。正当な理由がある場合は、それらを使用しても構いません。とはいえ、私はめったに使用しません、暗黙的なので結合ヒントをFORCE ORDER。奇妙な機会に私は結合ヒントを使用しますが、多くの場合、OPTION (FORCE ORDER)理由を説明するコメントを追加します。
ポールホワイトモニカを復活

0

100万行のテーブルに対して結合された50,000行は、インデックスのないすべてのテーブルにとって大量のようです。

あなたが実際に解決しようとしている問題から非常に隔離されているため、この場合に何をすべきかを正確に伝えることは困難です。かなりの量の行を持つ多くのインデックスのない一時テーブルに対して結合するコード内の一般的なパターンではないことを確かに願っています。

それが言っていることだけを例に取って、#Driverにインデックスを付けてみませんか?D.IDは本当にユニークですか?もしそうなら、それはEXISTSステートメントと意味的に同等であり、少なくともSQL Serverに、Dの重複値のSの検索を続けたくないことを知らせます。

SELECT S.*
INTO #Results
FROM SampleTable S
WHERE EXISTS (SELECT * #Driver D WHERE S.ID = D.ID);

つまり、このパターンでは、LOOPヒントを使用しません。私は単純にこのパターンを使用しません。可能であれば、優先度の高い順に次のいずれかを実行します。

  • 可能であれば、#Driverに一時テーブルではなくCTEを使用します
  • IDの#Driverに一意の非クラスター化インデックスを使用します(#Driverを使用するのはこれだけで、テーブル自体のデータが必要ない場合は、そのテーブルのデータが実際に必要な場合は、クラスタ化インデックスにすることをお勧めします)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.