web-dev-qa-db-ja.com

再コンパイルによる埋め込みクエリのパラメータースニッフィングの修正

私のマルチステップクエリの1つは、オプションのフィルターを使用しています。動的sqlまたはifステートメントを使用するのではなく、option(recompile)を使用して、オプションのフィルターが提供されているかどうかに関係なく、フィルターされたステップに適切なインデックスを選択させようとしています。部分式の結果を保存しようとすると、これは機能しませんでした。

これをSSMSで次のように再現できました。

declare @UID bigint = 100

--Query A: Uses index on UID
select count(1) from Entries 
where (@UID is null or UID = @UID)
option(recompile);

--Query B: Does an index scan (much slower)
select @cnt = count(1) from Entries 
where (@UID is null or UID = @UID)
option(recompile);

--Query C: Uses index on UID
select @cnt = count(1) from Entries 
where (UID = @UID);

私の目標は、@UIDがnullでない場合、クエリBがUIDのインデックスを使用するようにすることです。条件付きでクエリCに置き換えることでこれを実現できますが、過度に冗長に感じられるため、回避することをお勧めします。ストアドプロシージャ内の次のクエリで使用するために中間結果を保存したいので、クエリAを使用できません。

一時テーブルを選択してnullifを使用してみましたが、これらのアプローチはシークではなくスキャンを実行しました。

2
Brian

@UIDNULLの場合、クエリはシークで実行できません。つまり、シークを取得するには、変数の値がNULLでないことと、クエリに パラメータ埋め込み最適化 を適用する必要があります。つまり、プランを作成する前に、オプティマイザは変数の値を確認する必要があります。通常、これはRECOMPILEヒントで実行できますが、常に実行できるとは限りません。リンクされたブログの最後の例は、変数に値を割り当てるときにパラメーター埋め込みの最適化を適用できないことを示しています。

サンプルクエリを実行して、その動作のヒントを確認することもできます。クエリAの場合、シーク述語として次のようになります。

Seek Keys[1]: Prefix: [SE_DB].[dbo].[Entries].UID = Scalar Operator((100))

ただし、クエリBの場合、次の述語が得られます。

[@UID] IS NULL OR [SE_DB].[dbo].[Entries].[UID]=[@UID]

クエリBの@UID参照は100に置き換えられなかったため、スキャンを取得する必要があります。

ドキュメント化されていないトレースフラグ8606 を使用して、最適化のために作成されたクエリツリーの違いも確認できます。

enter image description here

クエリAは左側にあり、クエリBは右側にあります。クエリAの場合、クエリオプティマイザーは変数のリテラル値を使用してクエリプランを作成します。クエリBの場合、クエリオプティマイザーは、変数が使用されたことだけを知って計画を作成します。

動的SQLや分岐コードを必要としない回避策はたくさんあります。 2つのサブクエリの結果を合計できます。

SELECT @cnt =
(
    select count(1) cnt
    from Entries 
    where @UID is null
) +
(
    select count(1) cnt
    from Entries 
    where @UID is not null and UID = @UID
);

UNION ALLを使用できます。

SELECT @cnt = SUM(cnt)
FROM
(
    select count(1) cnt
    from Entries 
    where @UID is null

    UNION ALL

    select count(1) cnt
    from Entries 
    where @UID is not null and UID = @UID
) t
option(recompile);

一時テーブルに挿入して、一時テーブルから値を取得することもできます。

select count(1) cnt INTO #t
from Entries
where (@UID is null or UID = @UID)
option(recompile);

SELECT @cnt = MAX(cnt) FROM #t;

DROP TABLE #t;
3
Joe Obbish