web-dev-qa-db-ja.com

シークを期待しながらスキャンを取得

SELECTステートメントを最適化する必要がありますが、SQL Serverはシークではなく常にインデックススキャンを実行します。これはもちろん、ストアドプロシージャ内にあるクエリです。

CREATE PROCEDURE dbo.something
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL    
AS

    SELECT [IdNumber], [Code], [Status], [Sex], 
           [FirstName], [LastName], [Profession], 
           [BirthDate], [HireDate], [ActiveDirectoryUser]
    FROM Employee
    WHERE (@Status IS NULL OR [Status] = @Status)
    AND 
    (
      @IsUserGotAnActiveDirectoryUser IS NULL 
      OR 
      (
        @IsUserGotAnActiveDirectoryUser IS NOT NULL AND       
        (
          @IsUserGotAnActiveDirectoryUser = 1 AND ActiveDirectoryUser <> ''
        )
        OR
        (
          @IsUserGotAnActiveDirectoryUser = 0 AND ActiveDirectoryUser = ''
        )
      )
    )

そしてこれがインデックスです:

CREATE INDEX not_relevent ON dbo.Employee
(
    [Status] DESC,
    [ActiveDirectoryUser] ASC
)
INCLUDE (...all the other columns in the table...); 

計画:

Plan picture

SQL Serverがスキャンを選択したのはなぜですか?どうすれば修正できますか?

列の定義:

[Status] int NOT NULL
[ActiveDirectoryUser] VARCHAR(50) NOT NULL

ステータスパラメータには次のものがあります。

NULL: all status,
1: Status= 1 (Active employees)
2: Status = 2 (Inactive employees)

IsUserGotAnActiveDirectoryUserは次のいずれかです。

NULL: All employees
0: ActiveDirectoryUser is empty for that employee
1: ActiveDirectoryUser  got a valid value (not null and not empty)
9
Bestter

空の文字列の検索が原因でスキャンが行われたとは思いません(その場合、フィルター処理されたインデックスを追加することはできますが、クエリの非常に特定のバリエーションのみに役立ちます)。パラメータの盗聴の被害を受ける可能性が高く、このクエリに提供するパラメータ(およびパラメータ値)のさまざまな組み合わせすべてに対して最適化されていない単一のプランになります。

1つのクエリでキッチンシンクを含むすべてのものを提供することを期待しているため、これを「キッチンシンク」プロシージャと呼びます

私はこれに対する私の解決策についてのビデオを持っています here が、基本的に、そのようなクエリについて私が持っている最高の経験は次のことです:

  • ステートメントを動的に作成します-これにより、パラメーターが指定されていない列に言及する句を省略できるようになり、次のような計画が確実に得られます値とともに渡された実際のパラメーターに対して最適化precisely
  • Use OPTION (RECOMPILE)-これは、特定のパラメーター値が間違ったタイプのプランを強制することを防ぎ、特にデータの偏り、悪い統計がある場合に役立ちます、またはステートメントの最初の実行で非定型の値が使用され、その後の実行とは異なり、より頻繁に実行される場合。
  • サーバーオプション_optimize for ad hoc workloads_を使用します-これにより、一度だけ使用されるクエリのバリエーションがプランキャッシュを汚染するのを防ぎます。

アドホックワークロードの最適化を有効にします。

_EXEC sys.sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'optimize for ad hoc workloads', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sys.sp_configure 'show advanced options', 0;
GO
RECONFIGURE WITH OVERRIDE;
_

手順を変更します。

_ALTER PROCEDURE dbo.Whatever
  @Status INT = NULL,
  @IsUserGotAnActiveDirectoryUser BIT = NULL
AS
BEGIN 
  SET NOCOUNT ON;
  DECLARE @sql NVARCHAR(MAX) = N'SELECT [IdNumber], [Code], [Status], 
     [Sex], [FirstName], [LastName], [Profession],
     [BirthDate], [HireDate], [ActiveDirectoryUser]
   FROM dbo.Employee -- please, ALWAYS schema prefix
   WHERE 1 = 1';

   IF @Status IS NOT NULL
     SET @sql += N' AND ([Status]=@Status)'

   IF @IsUserGotAnActiveDirectoryUser = 1
     SET @sql += N' AND ActiveDirectoryUser <> ''''';
   IF @IsUserGotAnActiveDirectoryUser = 0
     SET @sql += N' AND ActiveDirectoryUser = ''''';

   SET @sql += N' OPTION (RECOMPILE);';

   EXEC sys.sp_executesql @sql, N'@Status INT, @Status;
END
GO
_

監視できる一連のクエリに基づくワークロードが得られたら、実行を分析して、追加のインデックスまたは異なるインデックスから最も効果的なものを確認できます。これは、さまざまな角度から、単純な「どの組み合わせ」からでも実行できます。パラメータは最も頻繁に提供されますか?」 「どのクエリが最長のランタイムを持っていますか?」コードだけに基づいてこれらの質問に答えることはできません。anyインデックスは、試行している可能性のあるすべてのパラメーターの組み合わせのサブセットにのみ役立つことを提案することができますサポートする。たとえば、_@Status_がNULLの場合、その非クラスター化インデックスに対するシークは不可能です。したがって、ユーザーがステータスを気にしないこれらのケースでは、他の句に対応するインデックスがない限り、スキャンが行われます(ただし、現在のクエリロジックを考えると、そのようなインデックスも役に立ちません-空の文字列または空でない文字列のどちらかが正確に選択的ではありません).

この場合、可能なStatus値のセットとそれらの値の分散方法によっては、OPTION (RECOMPILE)が不要になる場合があります。しかし、100行を生成するいくつかの値と数十万を生成するいくつかの値がある場合、そこに(このクエリの複雑さを考えるとわずかなCPUコストであっても)必要な場合があります。できるだけ多くのケースでシークを取得します。値の範囲が十分に有限である場合、動的SQLを使用してトリッキーなことを行うこともできます。「私は_@Status_にこの非常に選択的な値があるため、その特定の値が渡されると、このわずかな変更をこれは別のクエリと見なされ、そのパラメータ値に対して最適化されるようにクエリテキスト。」

11
Aaron Bertrand

免責事項:この回答の内容の一部は、DBAを弱体化させる可能性があります。純粋なパフォーマンスの観点からアプローチしています-常にインデックススキャンを取得するときにインデックスシークを取得する方法。

これで終わりです。

クエリは、「キッチンシンククエリ」と呼ばれるものです。単一のクエリで、さまざまな検索条件に対応できます。ユーザーが@statusに値を設定した場合、そのステータスでフィルタリングする必要があります。 @statusNULLの場合、すべてのステータスなどを返します。

これにより、インデックス作成に問題が生じますが、検索条件はすべて「等しい」基準であるため、検索可能性とは関係ありません。

これはsargableです:

WHERE [status]=@status

これはnotで検索可能です。SQLServerは、インデックスで単一の値を検索する代わりに、すべての行についてISNULL([status], 0)を評価する必要があるためです。

WHERE ISNULL([status], 0)=@status

私は台所の流しの問題をより簡単な形で再現しました:

CREATE TABLE #work (
    A    int NOT NULL,
    B    int NOT NULL
);

CREATE UNIQUE INDEX #work_ix1 ON #work (A, B);

INSERT INTO #work (A, B)
VALUES (1,  1), (2,  1),
       (3,  1), (4,  1),
       (5,  2), (6,  2),
       (7,  2), (8,  3),
       (9,  3), (10, 3);

次のようにすると、Aがインデックスの最初の列であっても、インデックススキャンが実行されます。

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE (@a IS NULL OR @a=A) AND
      (@b IS NULL OR @b=B);

ただし、これによりインデックスシークが生成されます。

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL;

管理可能な量のパラメーター(この場合は2つ)を使用している限り、おそらくUNION一連のシーククエリ-基本的にすべての検索条件の順列を使用できます。 3つの基準がある場合、これは乱雑に見え、4つでは完全に管理できなくなります。あなたは警告されました。

DECLARE @a int=4, @b int=NULL;

SELECT *
FROM #work
WHERE @a=A AND
      @b IS NULL
UNION ALL
SELECT *
FROM #work
WHERE @a=A AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b=B
UNION ALL
SELECT *
FROM #work
WHERE @a IS NULL AND
      @b IS NULL;

ただし、これら4つのうちの3つ目でインデックスシークを使用するには、(B, A)に2つ目のインデックスが必要になります。これらの変更でクエリがどのように見えるかを以下に示します(読みやすくするためのクエリのリファクタリングを含む)。

DECLARE @Status int = NULL,
        @IsUserGotAnActiveDirectoryUser bit = NULL;

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE [Status]=@Status AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='')

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser IS NULL

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=1 AND ActiveDirectoryUser<>''

UNION ALL

SELECT [IdNumber], [Code], [Status], [Sex], [FirstName], [LastName],
       [Profession], [BirthDate], [HireDate], [ActiveDirectoryUser]
FROM Employee
WHERE @Status IS NULL AND
      @IsUserGotAnActiveDirectoryUser=0 AND (ActiveDirectoryUser IS NULL OR ActiveDirectoryUser='');

...さらに、2つのインデックス列を逆にしたEmployeeに追加のインデックスが必要です。

完全を期すために、x=@xは、xNULLと等しくなることはないため、NULLNULLにすることはできないことを暗黙に意味することを述べておきます。これにより、クエリが少し簡略化されます。

そして、はい、Aaron Bertrandの動的SQL回答は、ほとんどの場合(つまり、再コンパイルを実行できる場合は常に)より良い選択です。

3

あなたの基本的な質問は「なぜ」であるように思われ、55分程度の答えが見つかるかもしれません TechEdでのAdam Machanicによる素晴らしいプレゼンテーション 数年前。

5分55分の5分について触れますが、プレゼンテーション全体に時間をかける価値はあります。クエリのクエリプランを見ると、検索に残余述語があることがわかります。基本的に、SQLはインデックスのすべての部分を「見る」ことはできません。それらの一部は、不等式やその他の条件によって隠されているためです。結果は、述語に基づくスーパーセットのインデックススキャンです。その結果はスプールされ、残りの述語を使用して再スキャンされます。

Scan Operator(F4)のプロパティを確認し、プロパティリストに「Seek Predicate」と「Predicate」の両方があるかどうかを確認します。

他の人が示したように、クエリをそのままインデックス化することは困難です。私は最近多くの同様のものに取り組んでおり、それぞれが異なるソリューションを必要としています。 :(

3
Ray

インデックスシークがインデックススキャンよりも優先されるかどうかを質問する前に、経験則の1つは、返される行数と基になるテーブルの合計行数を比較することです。たとえば、クエリが100万行のうち10行を返すことが予想される場合、おそらくインデックスシークはインデックススキャンよりも非常に優先されます。ただし、クエリから数千行(またはそれ以上)が返される場合、インデックスシークは必ずしも優先されるとは限りません。

クエリは複雑ではないため、実行計画を投稿していただければ、より良いアイデアが得られる可能性があります。

0
jyao