パフォーマンスを低下させるKey Lookup(クラスター化)オペレーターを排除


16

実行計画でキー検索(クラスター化)演算子を削除するにはどうすればよいですか?

テーブルにはtblQuotes既にクラスター化インデックス(QuoteID)と27個の非クラスター化インデックスがあるため、これ以上作成しないようにしています。

QuoteIDはクエリにクラスター化インデックス列を入れて、それが役立つことを願っていますが、残念ながらまだ同じです。

実行計画はこちら

またはそれを見る:

ここに画像の説明を入力してください

これは、キールックアップオペレーターによるものです。

ここに画像の説明を入力してください

クエリ:

declare
        @EffDateFrom datetime ='2017-02-01',
        @EffDateTo   datetime ='2017-08-28'

SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

IF OBJECT_ID('tempdb..#Data') IS NOT NULL
    DROP TABLE #Data 
CREATE TABLE #Data
(
    QuoteID int NOT NULL,   --clustered index

    [EffectiveDate] [datetime] NULL, --not indexed
    [Submitted] [int] NULL,
    [Quoted] [int] NULL,
    [Bound] [int] NULL,
    [Exonerated] [int] NULL,
    [ProducerLocationId] [int] NULL,
    [ProducerName] [varchar](300) NULL,
    [BusinessType] [varchar](50) NULL,
    [DisplayStatus] [varchar](50) NULL,
    [Agent] [varchar] (50) NULL,
    [ProducerContactGuid] uniqueidentifier NULL
)
INSERT INTO #Data
    SELECT 
        tblQuotes.QuoteID,

          tblQuotes.EffectiveDate,
          CASE WHEN lstQuoteStatus.QuoteStatusID >= 1   THEN 1 ELSE 0 END AS Submitted,
          CASE WHEN lstQuoteStatus.QuoteStatusID = 2 or lstQuoteStatus.QuoteStatusID = 3 or lstQuoteStatus.QuoteStatusID = 202 THEN 1 ELSE 0 END AS Quoted,
          CASE WHEN lstQuoteStatus.Bound = 1 THEN 1 ELSE 0 END AS Bound,
          CASE WHEN lstQuoteStatus.QuoteStatusID = 3 THEN 1 ELSE 0 END AS Exonareted,
          tblQuotes.ProducerLocationID,
          P.Name + ' / '+ P.City as [ProducerName], 
        CASE WHEN tblQuotes.PolicyTypeID = 1 THEN 'New Business' 
             WHEN tblQuotes.PolicyTypeID = 3 THEN 'Rewrite'
             END AS BusinessType,
        tblQuotes.DisplayStatus,
        tblProducerContacts.FName +' '+ tblProducerContacts.LName as Agent,
        tblProducerContacts.ProducerContactGUID
FROM    tblQuotes 
            INNER JOIN lstQuoteStatus 
                on tblQuotes.QuoteStatusID=lstQuoteStatus.QuoteStatusID
            INNER JOIN tblProducerLocations P 
                On P.ProducerLocationID=tblQuotes.ProducerLocationID
            INNER JOIN tblProducerContacts 
                ON dbo.tblQuotes.ProducerContactGuid = tblProducerContacts.ProducerContactGUID

WHERE   DATEDIFF(D,@EffDateFrom,tblQuotes.EffectiveDate)>=0 AND DATEDIFF(D, @EffDateTo, tblQuotes.EffectiveDate) <=0
        AND dbo.tblQuotes.LineGUID = '6E00868B-FFC3-4CA0-876F-CC258F1ED22D'--Surety
        AND tblQuotes.OriginalQuoteGUID is null

select * from #Data

実行計画:

ここに画像の説明を入力してください


Estimated vs Actual行は、顕著な違いを示しています。おそらく、SQLは適切な見積もりを行うためのデータがないため、不適切な計画を選択しています。どのくらいの頻度で統計を更新しますか?
RDFozz 2017

回答:


23

クエリが結果を返すために必要な行を見つけるために使用されるインデックスに格納されていない列からクエリプロセッサが値を取得する必要がある場合、さまざまなフレーバーのキールックアップが発生します。

次のコードを例にとります。ここでは、単一のインデックスを持つテーブルを作成しています。

USE tempdb;

IF OBJECT_ID(N'dbo.Table1', N'U') IS NOT NULL
DROP TABLE dbo.Table1
GO

CREATE TABLE dbo.Table1
(
    Table1ID int NOT NULL IDENTITY(1,1)
    , Table1Data nvarchar(30) NOT NULL
);

CREATE INDEX IX_Table1
ON dbo.Table1 (Table1ID);
GO

テーブルに1,000,000行を挿入して、処理するデータをいくつか用意します。

INSERT INTO dbo.Table1 (Table1Data)
SELECT TOP(1000000) LEFT(c.name, 30)
FROM sys.columns c
    CROSS JOIN sys.columns c1
    CROSS JOIN sys.columns c2;
GO

ここで、「実際の」実行プランを表示するオプションを使用してデータをクエリします。

SELECT *
FROM dbo.Table1
WHERE Table1ID = 500000;

クエリプランは以下を示します:

ここに画像の説明を入力してください

クエリは、テーブル全体をスキャンしてその値を探すよりもはるかに高速なので、IX_Table1インデックスを調べて行を見つけTable1ID = 5000000ます。ただし、クエリの結果を満たすために、クエリプロセッサはテーブル内の他の列の値も見つける必要があります。これが「RIDルックアップ」の出番です。テーブルで、Table1ID値500000 を含む行に関連付けられた行ID(RIDルックアップのRID)を探し、Table1Data列から値を取得します。プランの「RID Lookup」ノードの上にマウスを置くと、次のように表示されます。

ここに画像の説明を入力してください

「出力リスト」には、RIDルックアップによって返される列が含まれています。

クラスタ化インデックスと非クラスタ化インデックスを持つテーブルは、興味深い例になります。以下の表には3つの列があります。Dat非クラスター化インデックスによってインデックスが作成されたクラスター化キーであるIDとIX_Table3番目の列Oth

USE tempdb;

IF OBJECT_ID(N'dbo.Table1', N'U') IS NOT NULL
DROP TABLE dbo.Table1
GO

CREATE TABLE dbo.Table1
(
    ID int NOT NULL IDENTITY(1,1) 
        PRIMARY KEY CLUSTERED
    , Dat nvarchar(30) NOT NULL
    , Oth nvarchar(3) NOT NULL
);

CREATE INDEX IX_Table1
ON dbo.Table1 (Dat);
GO

INSERT INTO dbo.Table1 (Dat, Oth)
SELECT TOP(1000000) CRYPT_GEN_RANDOM(30), CRYPT_GEN_RANDOM(3)
FROM sys.columns c
    CROSS JOIN sys.columns c1
    CROSS JOIN sys.columns c2;
GO

次のクエリ例を見てください。

SELECT *
FROM dbo.Table1
WHERE Dat = 'Test';

SQL Serverに、Dat列に単語が含まれているテーブルからすべての列を返すように要求していますTest。ここにはいくつかの選択肢があります。テーブル(つまり、クラスター化インデックス)を見ることができます。ただし、テーブルはID列順に並べられているため、列全体をスキャンする必要があります。これにより、列に含まTestれる行については何もわかりませんDat。もう1つのオプション(およびSQL Serverによって選択されるオプション)は、IX_Table1非クラスター化インデックスをシークして行を見つけるDat = 'Test'ことですが、Oth列も必要なので、SQL Serverは "Keyルックアップ」操作。これはそのための計画です:

ここに画像の説明を入力してください

列が含まれるように非クラスター化インデックスを変更すると、次のようなりOthます。

DROP INDEX IX_Table1
ON dbo.Table1;
GO

CREATE INDEX IX_Table1
ON dbo.Table1 (Dat)
INCLUDE (Oth);        <---- This is the only change
GO

次に、クエリを再実行します。

SELECT *
FROM dbo.Table1
WHERE Dat = 'Test';

SQL Server は、の値を含むインデックスDat = 'Test'内の行と、すべての非クラスター化インデックス。計画:IX_Table1OthID

ここに画像の説明を入力してください


5

エンジンが、フェッチしようとしているすべての列を含まないインデックスを使用することを選択したため、キールックアップが発生します。したがって、インデックスはselectおよびwhereステートメントの列をカバーしていません。

キールックアップを排除するには、欠落している列(キールックアップの出力リストの列)= ProducerContactGuid、QuoteStatusID、PolicyTypeID、およびProducerLocationIDを含める必要があります。または、代わりにクエリでクラスター化インデックスを強制的に使用する方法もあります。

テーブルの27の非クラスター化インデックスは、パフォーマンスが低下する可能性があることに注意してください。更新、挿入、または削除を実行する場合、SQL Serverはすべてのインデックスを更新する必要があります。この余分な作業は、パフォーマンスに悪影響を及ぼす可能性があります。


また、インデックスが多すぎると、実行プランのコンパイルが混乱し、選択が最適化されない可能性があることにも注意してください。
偏った

4

このクエリに含まれるデータ量について言及するのを忘れていました。また、なぜ一時テーブルに挿入するのですか?表示する必要があるだけの場合は、挿入ステートメントを実行しないでください。

このクエリの目的では、tblQuotes27の非クラスター化インデックスは必要ありません。1つのクラスター化インデックスと5つの非クラスター化インデックス、またはおそらく6つの非クラスター化インデックスが必要です。

このクエリでは、次の列にインデックスが必要です。

QuoteStatusID
ProducerLocationID
ProducerContactGuid
EffectiveDate
LineGUID
OriginalQuoteGUID

次のコードにも気付きました。

DATEDIFF(D, @EffDateFrom, tblQuotes.EffectiveDate) >= 0 AND 
DATEDIFF(D, @EffDateTo, tblQuotes.EffectiveDate) <= 0

NON Sargable、それはインデックスを利用することはできませんすなわち。

そのコードを次のようにSARgable変更します。

tblQuotes.EffectiveDate >= @EffDateFrom 
AND  tblQuotes.EffectiveDate <= @EffDateFrom

あなたの主な質問に答えるには、「なぜキールックアップを取得しているのですか」:

KEY Look upクエリで言及されている列の一部がカバリングインデックスに存在しないため、取得しています。

グーグルして、Covering Indexまたはについて勉強することができInclude indexます。

私の例では、tblQuotes.QuoteStatusIDが非クラスター化インデックスであるとすると、DisplayStatusもカバーできます。結果セットにDisplayStatusが必要なため。インデックスに存在せず、結果セットに存在する列は、回避するためにカバーできますKEY Look Up or Bookmark lookup。これはインデックスをカバーする例です:

create nonclustered index tblQuotes_QuoteStatusID 
on tblQuotes(QuoteStatusID)
include(DisplayStatus);

**免責事項:**上記は私の例にすぎないことを思い出してください。DisplayStatusは分析後に他の非CIでカバーされる場合があります。

同様に、クエリに関連する他のテーブルにインデックスとカバーインデックスを作成する必要があります。

あなたはIndex SCANあなたの計画にも入っています。

これは、テーブルにインデックスがないため、または大量のデータがある場合に、オプティマイザがインデックスシークを実行するのではなくスキャンすることを決定するために発生する可能性があります。

これは、が原因で発生することもありますHigh cardinality。不完全な結合により、必要以上の行数を取得しています。これも修正できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.