web-dev-qa-db-ja.com

ヒントなしでマージ結合を強制する方法

少し前にクエリヒントを使用しましたが、ほとんどの場合SQL Serverは私よりも賢いことに気付きました。さらに、追加のインデックスを作成/データまたはクエリを再編成し、サーバーに計画を使用させるよりもはるかに良い結果を得る方が良いです。これは一般的に非効率ですが、一部のデータのサブセットには十分高速です。しかし今は、データをより適切に整理する方法がわからない状況にあります。

テーブルが2つあります。最初のテーブルT1は(Id、CustomerId)で、2番目のテーブルT2は同じ列を持っています。 CustomerIdでT1をT2に参加させたい。そして上位N行を取得します。この場合、オプティマイザはN topだけが必要であることを認識し、「特に、インデックスシークを使用する場合、ループを使用してN個の一致を非常にすばやく見つけます。」しかし、条件を満たすデータがないため、機能しません。したがって、それはloop join 25mのテーブルと100kのテーブルを結合するにはかなり時間がかかります。

enter image description here

SQL Serverにマージ結合の使用を強制すると、1秒で実行される次の計画が得られます。

enter image description here

次の2つの理由により、強制したくありません。

  • 第一に、先に述べたように、SQL Serverは十分スマートです。
  • 次に、ORMを使用しており、生成されたクエリにヒントを挿入するのは非常に難しいので、回避したいと思います。

この状況ではどうすればよいですか?

2
Alex Zhukovskiy

ネストされたループ結合からヒントなしのマージ結合に切り替えるには、マージ結合を使用した計画で、ネストされたループを使用した計画よりも推定コストを低くする必要があります。技術的には、プランの探索中に低コストのプランを見つけるためにクエリオプティマイザーも必要ですが、それについてできることは多くありません。

まず、マージ結合のコスト計算についてのメモ。私の経験では、SQL Serverはマージ結合コストについてかなり悲観的です。これは、少量のデータが変更されたときにIOマージ結合の要件がどの程度変化する可能性があるかを考えると理解できます。1つのテーブルに1〜10000の整数があり、もう1つは100001〜1100000の整数です。

DROP TABLE IF EXISTS X_SMALL_TABLE;

CREATE TABLE X_SMALL_TABLE (ID INT NOT NULL PRIMARY KEY (ID));

INSERT INTO X_SMALL_TABLE WITH (TABLOCK) (ID)
SELECT N
FROM dbo.GetNums(10000);

UPDATE STATISTICS X_SMALL_TABLE WITH FULLSCAN;

DROP TABLE IF EXISTS X_LARGE_TABLE;

CREATE TABLE X_LARGE_TABLE (ID INT NOT NULL PRIMARY KEY (ID));

INSERT INTO X_LARGE_TABLE WITH (TABLOCK) (ID)
SELECT N + 100000
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_LARGE_TABLE WITH FULLSCAN;

テーブルを結合すると、0行が返されます。 MERGE JOINを強制すると、SQL Serverは小さなテーブルからすべての行をスキャンし、大きなテーブルから1行だけをスキャンします。デフォルトでは、SQL Serverは結合キーに従って昇順でテーブルを走査します。ただし、クエリオプティマイザーは、1000000行すべてが大きなテーブルからスキャンされると推定し、その後、その演算子に比較的大きなコストを割り当てます。

plan 1

実際のプランからわかるように、スキャンされたのは1行だけです。小さなテーブルに新しい行を1つだけ挿入するとどうなりますか?

INSERT INTO X_SMALL_TABLE
SELECT 1200000;

UPDATE STATISTICS X_SMALL_TABLE WITH FULLSCAN;

小さなテーブルでID 10000に到達すると、マージ結合アルゴリズムはIDの順序で進むため、ID 1200000に到達するまで、大きなテーブルのすべての行をループする必要があります。 。

plan 2

実際の計画では、すべての行が両方のテーブルから読み取られたことを確認します。したがって、データの小さな変更により、マージ結合のパフォーマンスに大きな違いが生じる可能性があります。それに基づいて、SQL Serverがマージジョインのコスト計算について悲観的になるのは合理的であるように思われます。これは、クエリにマージジョインを表示させようとする場合に効果的ではありません。 2番目のクエリでは、CustomerDiscountCardsのすべての行をスキャンします。その操作だけでは、ネストされたループ結合計画全体よりも推定コストが高くなる可能性があります。

ヒントを使用してMERGE JOINを強制する場合、MERGE JOINjoin hintFORCE ORDERヒントをクエリに追加することに注意してください。質問の情報に基づいて、2番目のクエリは、MERGE JOINのため、厳密には迅速に実行されません。新しい結合順序のため、迅速に実行されます。オプティマイザは、両方のテーブルでフルスキャンを使用してCustomerDiscountCardsDiscountCardsを結合し、一致する行が0件あることを検出します。 0行の結果セットに対する内部結合は、クエリの実行がその時点で停止する可能性があるため(その前に発生するブロッキング演算子を無視して)、高速になります。したがって、おそらくヒントなしでは難しいマージ結合の強制に焦点を合わせるのではなく、結合順序を正しくすることに焦点を当てます。

結合順序を強制する簡単な方法は、必要な最初の結果セットを一時テーブルに具体化することです。最初のクエリでCustomerDiscountCardsDiscountCardsを結合して、一致する関連行を一時テーブルに配置できます。実際には一時テーブルに行を挿入しないため、テストケースでは1秒未満で実行する必要があります。 2番目のクエリが空のテーブルに結合する場合、ほぼ瞬時に終了するはずです。かなりの数の行と列をtempdbに入れる可能性があるため、実際に行を取得する他のパラメーター値について、クエリのパフォーマンスをテストする必要があります。

一時テーブルを使用せずにSQLを書き直して、結合順序を効果的に強制できる場合があります。もちろんFORCE ORDERヒントを使用するのが最も簡単な方法ですが、ヒントは使いたくないので、そのヒントは使いにくいかもしれません。これはクエリ全体に適用されるため、データが変更されると、パフォーマンスが低下する可能性があります。ヒントなしで結合順序を強制する方法についてのアイデアは、派生テーブルで最初に結合するテーブルをペアにして、余分なTOP演算子を追加するとSQL Serverが右側にプッシュされるように見えます方向:

INNER JOIN 
(
    SELECT TOP 9223372036854775807 ...
    FROM 
    CustomerDiscountCards
    INNER JOIN DiscountCards ON ...
) t ON ...

ただし、SQL Serverが実際にその結合を残りのクエリとは別に評価するように強制されている場合、結論を出すことはできません。前と同じように、テストする必要があります。

最初の問題は 行の目標 に関連しているように見えるため、考慮すべき最後のオプションを提供するために、行の目標をオプティマイザから隠すことができます。これを実現する1つの方法は、 OPTIMIZE FOR クエリヒントを使用し、TOPステートメントに変数を使用することです。以下のクエリスニペットを検討してください。

DECLARE @TOP BIGINT = 10;

SELECT TOP (@top)
...
OPTION (OPTIMIZE FOR (@TOP = 9223372036854775807))

そのクエリは、結果セットで見つかった最初の10行のみを返します。ただし、プランは最初の9223372036854775807行を返すかのように作成されます。これは、問題が発生したネストされたループ結合計画を不快にする可能性がありますが、これが一般的にどの程度うまく機能するかを言うのは困難です。

結局のところ、私たちが質問にある情報を使ってこれ以上話すことは困難です。おそらく、クエリが行を返す可能性があるため(常に行を返さないクエリを継続的に実行する理由はそれほど多くありません)、さまざまなケースすべてについてオプションをテストする必要があります。

6
Joe Obbish