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


9

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...); 

計画:

計画写真

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)

実際の実行プランをどこかに投稿できますか(写真ではなく、XML形式の.sqlplanファイル)?私の推測では、プロシージャを変更しましたが、実際にはステートメントレベルで新しいコンパイルを取得しませんでした。クエリのテキストを変更して(テーブル名にスキーマプレフィックスを追加するなど)、有効な値を渡すことはできます@Statusか?
アーロンバートランド

1
また、インデックス定義は疑問を投げかけます-なぜキーが重要なのStatus DESCですか?にはいくつの値があり、Statusそれらは(数が小さい場合)何であり、各値はほぼ同じように表されますか?SELECT TOP (20) [Status], c = COUNT(*) FROM dbo.Employee GROUP BY [Status] ORDER BY c DESC;
Aaron Bertrandの

回答:


11

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

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

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

  • ステートメントを動的に作成します。これにより、パラメーターが指定されていない列に言及する句を省略できるようになり、値とともに渡された実際のパラメーターに対して正確に最適化された計画が確実に得られます。
  • 使用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

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

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


3
私はこのアプローチを何度も使用してきましたが、オプティマイザーがあなたがとにかくやるべきだと思う方法で物事を実行させる素晴らしい方法です。Kim Trippがここで同様のソリューションについて話します: sqlskills.com/blogs/kimberly/high-performance-proceduresさらに 、数年前にPASSで彼女が行ったセッションのビデオがあり、なぜそれが機能するのかについて非常に詳細に説明しています。つまり、それはベルトランド氏がここで言ったことに実際にトンを追加するものではありません。これは、誰もがツールベルトに入れておくべきツールの1つです。キャッチオールクエリの手間を大幅に節約できます。
mskinner 2016年

3

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

これで終わりです。

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

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

これはsargableです:

WHERE [status]=@status

SQL Server はインデックス内の単一の値を検索するのではなく、すべての行を評価する必要があるため、これは検索可能ではありません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;

(B, A)ただし、これら4つのうちの3つ目でインデックスシークを使用するには、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='');

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

完全をx=@x期すために、私は暗黙的には、xがと等しくなるNULLことNULLはないので、それが不可能であることを意味することを言及する必要がありますNULL。これにより、クエリが少し簡略化されます。

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


3

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

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

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

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


0

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

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


100万個のテーブルから数千行をフィルタリングしますが、それでもシークが必要です。それでも、テーブル全体をスキャンするよりも大幅にパフォーマンスが向上します。
Daniel Hutmacher、2016年

-6

これはオリジナルのフォーマット済みです

DECLARE @Status INT = NULL,
        @IsUserGotAnActiveDirectoryUser BIT = NULL    

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 =  '')
         )
    )

これはリビジョンです-100%確実ではありませんが、(たぶん)
1つでも試してみるか、おそらく問題になります。
これにより、ActiveDirectoryUser nullが壊れます。

  WHERE isnull(@Status, [Status]) = [Status]
    AND (      (     isnull(@IsUserGotAnActiveDirectoryUser, 1) = 1 
                 AND ActiveDirectoryUser <> '' ) 
           OR  (     isnull(@IsUserGotAnActiveDirectoryUser, 0) = 0 
                 AND ActiveDirectoryUser =  '' )
        )

3
この答えがOPの質問をどのように解決するかは私には不明です。
Erik

@エリックOPを試してみませんか?ORが2つ消えました。これがクエリのパフォーマンスに役立たないことが確かにわかりますか?
パパラッツォ2016年

@ypercubeᵀᴹIsUserGotAnActiveDirectoryUser IS NOT NULLは削除されました。これら二つの不要な削除AN ORとIsUserGotAnActiveDirectoryUser IS NULL削除します。このクエリはOPよりも速く実行されませんか?
パパラッツォ2016年

@ypercubeᵀᴹ多くのことができたでしょう。私はもっ​​と簡単なものを探していません。2つまたはなくなっています。または通常、クエリプランには適していません。ここにはクラブのようなものがありますが、私はそのクラブの一員ではありません。しかし、私は生計を立てるためにこれを行い、私が機能していることがわかっているものを投稿します。私の回答は反対票の影響を受けません。
パパラッツォ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.