web-dev-qa-db-ja.com

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

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

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

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

ここでの実行計画

またはそれを見る:

enter image description here

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

enter image description here

クエリ:

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

実行計画:

enter image description here

16
Serdia

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

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

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;

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

enter image description here

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

enter image description here

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

クラスタ化インデックスと非クラスタ化インデックスを持つテーブルは、興味深い例になります。以下の表には3つの列があります。クラスタリングキーであるID、Dat、非クラスタ化インデックスIX_Table、および3番目の列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列にWord Testが含まれているテーブルからすべての列を返すように要求しています。ここにはいくつかの選択肢があります。テーブル(つまり、クラスター化インデックス)を見ることができます。ただし、テーブルはID列で並べられているため、Test列にDatが含まれている行については何も示されていないため、全体をスキャンする必要があります。もう1つのオプション(およびSQL Serverによって選択されるオプション)は、IX_Table1非クラスター化インデックスをシークしてDat = 'Test'の行を見つけることですが、Oth列も必要なので、SQL Serverはクラスター化インデックスへのルックアップを実行する必要があります「キー検索」操作。これはそのための計画です:

enter image description here

非クラスター化インデックスを変更してincludesOth列を含める場合:

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_Table1の行を検索する必要があるため、単一の非クラスター化インデックスシークが表示されます。これには、Othの値とID列(主キー)の値が含まれ、自動的に表示されます。すべての非クラスター化インデックス。計画:

enter image description here

23
Max Vernon

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

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

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

6
Daniel Björk

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

このクエリでは、tblQuotesに27の非クラスター化インデックスは必要ありません。 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が原因で発生することもあります。不完全な結合により、必要以上の行数を取得しています。これも修正できます。

4
KumarHarsh