web-dev-qa-db-ja.com

SQL Serverは、ディメンションテーブルとの入れ子ループ結合を選択し、各行をシークします

SQL Serverが最適ではない実行プランを生成するという問題に直面します。ネストされたループ結合と次元テーブルへのシークと2M読み取りの実行です。

ソート操作の見積もりは、450 K行ではなく100行であり、プランの選択に影響する可能性があります。

NestedLoop: https://www.brentozar.com/pastetheplan/?id=B110MZ2Pm または NestedLoop plan

これはテストDB内にあります。同じスキーマとほぼ同じデータを持つ追加のDBがあります。

まったく同じクエリを(両方ともSSMSから)実行すると、ハッシュ結合と次元テーブルスキャン(32K読み取り)を使用して異なるプランが生成されます。

HashJoin: https://www.brentozar.com/pastetheplan/?id=r1Jm7b2D7 または Hash plan

問題を理解して解決するための助けが必要です。

ヒントHash Jointで回避できますが、同じインスタンス上の2つの類似したDBが異なるプランを生成することは意味がありません。

更新#1:推定コストが異なることがわかったので、SQL Serverが並列実行するとハッシュ結合が選択されます。

シングルスレッドではネストされたループになります。

アップデート#2:同じテーブルからのSELECT中に同じ問題が発生しました。列の数に依存します(推定コスト)。列の数を減らすと、実行プランはネストされたループに落ち、ディメンションテーブルを探します。

6
Yorik

一方の環境でシリアルネストループ結合プランを取得し、もう一方の環境でハッシュ結合を取得する理由は3つあるようです。あなたが提供した情報に基づいて、最良の修正はクエリヒントまたはクエリを2つの部分に分割することを含みます。

  1. 環境の違い

    1つの環境にはCCIに480662行があり、もう1つの環境には686053行があります。私はそれをほとんど同じものとは呼びません。また、環境によってはハードウェアや構成に違いがあるように見えます。少なくとも、非常に不運になっているようです。 251 MBの推定データのシリアルソートにはIO 0.0037538ユニットのコストがあります。351MBの推定データのパラレルソートにはIOコスト23.1377があります。並列処理によって割り引かれている場合でも、ユニット数。エンジンは、並列計画の比較的大量のデータをスピルすることを想定しています。このような違いにより、環境間で計画が異なる可能性があります。

  2. オプティマイザが 行の目標 コスト削減を誤って適用するため、ネストされたループ結合プランを優先できます

    ネストされたループプランは、ソートから100行のみを出力する必要があるかのようにコストがかかります。

    bad row goal

    ただし、クエリのSELECT句には次のものが含まれています。COUNT(*) OVER ()

    エンジンは、集計の正しい結果を生成するために、すべての行を読み取る必要があります。これは実際に実際の計画で発生することであり、インデックスシークは100回ではなく450k回実行されます。このコスト削減は、さまざまなバージョン(2016 SP1ベースまでテストしました)、両方のCE、多くの異なるウィンドウ関数、およびバッチモードと行モードの両方で発生するようです。これは、製品の制限であり、クエリプランが最適ではなくなります。

  3. ネストされたループ結合プランは バッチモードのソートでの制限 のためシリアルです

    シリアルの入れ子になったループ結合が並列処理に適している(CTFPによって異なる)可能性があり、オプティマイザが低コストの並列プランを見つけられなかった理由を疑問に思うかもしれません。オプティマイザには、並列バッチモードのソートがネストされたループ結合(行モードで実行する必要がある)の最初の子になるのを防ぐヒューリスティックがあります。問題は、並列バッチモードの並べ替えでは、すべての行が単一のスレッドに配置され、並列ネストされたループ結合ではうまく機能しないことです。ソートをループ結合の親になるように移動しても、(オプティマイザの問題により)インデックスシークの推定実行数は減少しません。その結果、CTFPがデフォルトの5に設定されていても、最終的にはシリアルプランになる可能性が高くなります。


これは問題の再現ですが、SQL Serverのバージョンをサポートしていないため、PasteThePlanにアップロードできません。

drop table if exists cci_216665;

create table cci_216665 (
    SORT_ID BIGINT,
    JOIN_ID BIGINT,
    COL1 BIGINT,
    COL2 BIGINT,
    COL3 BIGINT,
    INDEX CCI CLUSTERED COLUMNSTORE
);

INSERT INTO cci_216665 WITH (TABLOCK)
SELECT TOP (500000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 50
, 0, 0, 0
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

drop table if exists YEAH_NAH;

CREATE TABLE dbo.YEAH_NAH (ID INT, FILLER VARCHAR(20), PRIMARY KEY (ID));

INSERT INTO dbo.YEAH_NAH WITH (TABLOCK)
SELECT TOP (50) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, 'CHILLY BIN'
FROM master..spt_values t1;

GO

-- takes 780 ms of CPU with nested loops
SELECT TOP (100)
*, COUNT(*) OVER ()
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID;

-- takes 111 ms of CPU with hash join
SELECT TOP (100)
*, COUNT(*) OVER ()
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID
OPTION (HASH JOIN);

問題を解決する最も簡単な方法は、クエリを2つに分割することです。これを行う1つの方法を次に示します。

SELECT COUNT(*)
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID;

SELECT TOP (100) *
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID;

私のマシンでは、これは実際にはハッシュ結合計画よりも高速ですが、同じ結果が表示されない場合があります。一般的に、あなたのようなクエリに対する最初の試みは、最初の100行のみが必要な場合に、OVER句のないウィンドウ集計を回避することです。

合理的な代替策は、DISABLE_OPTIMIZER_ROWGOAL SQL Server 2016 SP1で導入されたヒントを使用します。このタイプのクエリの場合、行の目標に問題があるため、このヒントは統計などに依存せずに問題に直接対処します。私はそれを採用するのに比較的安全なヒントだと思います。

SELECT TOP (100)
*, COUNT(*) OVER ()
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID
OPTION (USE HINT('DISABLE_OPTIMIZER_ROWGOAL'));

これにより、私のマシンでハッシュ結合計画が作成されます。

6
Joe Obbish