web-dev-qa-db-ja.com

このクエリの効率的なインデックスを作成するのに苦労しています

クエリのインデックスを作成するのに苦労していますが、ERPシステムの一部であるため、残念ながらクエリをまったく変更できません。問題は、このクエリの読み取りが100万回を超え、場合によっては10秒

述語、シーク述語、および両方の組み合わせを異なる順序で使用してみましたが、成功しませんでした。

変更できないクエリは次のとおりです。

SELECT  TOP (@0) "timestamp","Link ID","Record ID","URL1","URL2","URL3","URL4","Description","Type","Created","User ID","Company","Notify","To User ID" 
FROM "Database".dbo."Record Link" WITH(READUNCOMMITTED)  
WHERE ("Link ID">@1 AND (("Company"=@2 OR "Company"=@3) AND "Notify"=@4 AND ("To User ID" COLLATE Latin1_General_100_CI_AI LIKE @5 OR "To User ID" COLLATE Latin1_General_100_CI_AI LIKE @6))) 
ORDER BY "Link ID" ASC OPTION(OPTIMIZE for UNKNOWN, FAST 50)

役立つ場合のテーブルのスクリプトは次のとおりです。

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Record Link](
    [timestamp] [timestamp] NOT NULL,
    [Link ID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
    [Record ID] [varbinary](448) NOT NULL,
    [URL1] [nvarchar](250) NOT NULL,
    [URL2] [nvarchar](250) NOT NULL,
    [URL3] [nvarchar](250) NOT NULL,
    [URL4] [nvarchar](250) NOT NULL,
    [Description] [nvarchar](250) NOT NULL,
    [Type] [int] NOT NULL,
    [Note] [image] NULL,
    [Created] [datetime] NOT NULL,
    [User ID] [nvarchar](132) NOT NULL,
    [Company] [nvarchar](30) NOT NULL,
    [Notify] [tinyint] NOT NULL,
    [To User ID] [nvarchar](132) NOT NULL,
 CONSTRAINT [Record Link$0] PRIMARY KEY CLUSTERED 
(
    [Link ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

これは時間とIOの統計です

  SQL Server Execution Times:
   CPU time = 0 ms,  elapsed time = 0 ms.
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 4 ms.

(0 rows affected)
Table 'Record Link'. Scan count 1, logical reads 1018402, physical reads 3, read-ahead reads 1018391, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

(1 row affected)

 SQL Server Execution Times:
   CPU time = 8234 ms,  elapsed time = 12641 ms.

これが実行計画です: enter image description here

誰かがこのための効率的なインデックスを作成するのを手伝っていただければ幸いです。

次に実行計画へのリンクを示します。 https://www.brentozar.com/pastetheplan/?id=BkTxCbdN4

2
BainAnator

@DenisRubashkinが述べたように計画ガイドのルートをどこに移動するかについては、「空の」計画ガイドを作成して既存のヒントを削除できます:OPTION(OPTIMIZE for UNKNOWN, FAST 50)

使用できるプランガイドの例、一部のデータ型を変更する必要がある場合があります。

_EXEC sp_create_plan_guide   
    @name =  N'Guide1',  
    @stmt = N'SELECT  TOP (@0) "timestamp","Link ID","Record ID","URL1","URL2","URL3","URL4","Description","Type","Created","User ID","Company","Notify","To User ID" 
FROM test.dbo."Record Link" WITH(READUNCOMMITTED)  
WHERE ("Link ID">@1 AND (("Company"=@2 OR "Company"=@3) AND "Notify"=@4 AND ("To User ID" COLLATE Latin1_General_100_CI_AI LIKE @5 OR "To User ID" COLLATE Latin1_General_100_CI_AI LIKE @6))) 
ORDER BY "Link ID" ASC OPTION(OPTIMIZE for UNKNOWN, FAST 50)',  
    @type = N'SQL',  
    @params = '@0 int, @1 int, @2  [nvarchar](30), @3  [nvarchar](30), @4 tinyint, @5 nvarchar(264),@6 nvarchar(264)',  
    @hints = NULL;
_

@hintsは既存のヒントをオーバーライドします。何も指定しないことにより、OPTION(OPTIMIZE for UNKNOWN, FAST 50)は削除され、他のヒントは使用されません。

私のテストでは、私のデータは異なりますが、このインデックスが使用されました

_CREATE INDEX IX_Notify_Company_Link_ID
ON [dbo].[Record Link](Notify,Company,[Link ID])
INCLUDE([To User ID]);
_

これは、データセットに最適なインデックスにはなりません!計画ガイドが機能することを示す方法としてのみ使用されます。 YMMV

_NotifyCompanyの両方で使用されるシーク述語。それらは私のデータセットではあまり選択的ではないため、多くの行が読み取られます。

新しいプランの一部

enter image description here

PasteThePlan

@ PaulWhiteによるコメント

プランガイドのOPTION(RECOMPILE)を使用してパラメーターを埋め込むことで、追加の利点が見つかる場合があります。

プランガイドの@hintsパラメータを次のように変更する必要があります。

_    @hints = 'OPTION(RECOMPILE)';
_

クエリプランは毎回再コンパイルされますが、頻繁に実行されなければ問題にはなりません。

オプションrecompile付きのプラン

enter image description here

3つの主要な列すべてにシーク述語を使用した、より優れた計画が使用されました。

PasteThePlan

使用したテストクエリ

_SET STATISTICS IO, TIME ON;
EXEC SP_EXECUTESQL N'SELECT  TOP (@0) "timestamp","Link ID","Record ID","URL1","URL2","URL3","URL4","Description","Type","Created","User ID","Company","Notify","To User ID" 
FROM test.dbo."Record Link" WITH(READUNCOMMITTED)  
WHERE ("Link ID">@1 AND (("Company"=@2 OR "Company"=@3) AND "Notify"=@4 AND ("To User ID" COLLATE Latin1_General_100_CI_AI LIKE @5 OR "To User ID" COLLATE Latin1_General_100_CI_AI LIKE @6))) 
ORDER BY "Link ID" ASC OPTION(OPTIMIZE for UNKNOWN, FAST 50)',N'@0 int, @1 int, @2  [nvarchar](30), @3  [nvarchar](30), @4 tinyint, @5 nvarchar(264),@6 nvarchar(264)',@0=100,@1 = 99 ,@2  = 'NNNNNNNV',@3 = 'NNNNNNN',@4=1,@5='NNNNNNN1',@6='NNNNNNN2'
_

使用したテストデータ

_CREATE TABLE [dbo].[Record Link](
    [timestamp] [timestamp] NOT NULL,
    [Link ID] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,
    [Record ID] [varbinary](448) NOT NULL,
    [URL1] [nvarchar](250) NOT NULL,
    [URL2] [nvarchar](250) NOT NULL,
    [URL3] [nvarchar](250) NOT NULL,
    [URL4] [nvarchar](250) NOT NULL,
    [Description] [nvarchar](250) NOT NULL,
    [Type] [int] NOT NULL,
    [Note] [image] NULL,
    [Created] [datetime] NOT NULL,
    [User ID] [nvarchar](132) NOT NULL,
    [Company] [nvarchar](30) NOT NULL,
    [Notify] [tinyint] NOT NULL,
    [To User ID] [nvarchar](132) NOT NULL,
 CONSTRAINT [Record Link$0] PRIMARY KEY CLUSTERED 
(
    [Link ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 80) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
SET NOCOUNT ON;
DECLARE @I INT = 0;
WHILE @I <= 10000
BEGIN
INSERT INTO [dbo].[Record Link]
(       

    [Record ID]   ,
    [URL1]    ,
    [URL2]    ,
    [URL3]    ,
    [URL4]    ,
    [Description]    ,
    [Type]  ,
    [Note]  ,
    [Created]   ,
    [User ID]   ,
    [Company]   ,
    [Notify]   ,
    [To User ID]   )
    VALUES(convert(varbinary(448),'NNNNNNN'),'NNNNNNN','NNNNNNN','NNNNNNN','NNNNNNN','NNNNNNN',@I,'NNNNNNN',GETDATE(),'NNNNNNN'+CAST(@i as nvarchar(10)),'NNNNNNN',1,'NNNNNNN'+CAST(@i as nvarchar(10)))
    SET @I += 1
    END
    SET NOCOUNT OFF
INSERT INTO [dbo].[Record Link]
(       

    [Record ID]   ,
    [URL1]    ,
    [URL2]    ,
    [URL3]    ,
    [URL4]    ,
    [Description]    ,
    [Type]  ,
    [Note]  ,
    [Created]   ,
    [User ID]   ,
    [Company]   ,
    [Notify]   ,
    [To User ID]   )

    SELECT
     [Record ID]   ,
     [URL1] ,
     [URL2]    ,
     [URL3]    ,
     [URL4]    ,
     [Description] , 
     [Type]  ,
     [Note]  ,
     [Created]   ,
     [User ID]   ,
     [Company]   ,
     [Notify]   ,
     [To User ID]   
     FROM   [dbo].[Record Link]
     GO 7
_
3
Randi Vertongen

ちょうど2つの会社がある可能性があるため、_(Company, Notify, [Link ID]) INCLUDE ([To User ID])_のインデックスを使用します。キーに_Link ID_を含めることは、クラスター化インデックスキーのために冗長ですが、明示的な並べ替えのため、ここに残しておきます。

うまくいけば、2つのインデックスシークを持つプランが生成され、それらからデータをプルするMerge Join(Concat)演算子が含まれます。

ルックアップの転換点を回避するためにINCLUDE ([To User ID], "timestamp","Record ID","URL1","URL2","URL3","URL4","Description","Type","Created","User ID")が必要になる場合がありますが、その方法を確認してください。

編集:

私はこのテストデータを使用しました:

_INSERT INTO [dbo].[Record Link]
(       

    [Record ID]   ,
    [URL1]    ,
    [URL2]    ,
    [URL3]    ,
    [URL4]    ,
    [Description]    ,
    [Type]  ,
    [Note]  ,
    [Created]   ,
    [User ID]   ,
    [Company]   ,
    [Notify]   ,
    [To User ID]   )
    VALUES(newid(), newid(), newid(),newid(), newid(), newid(),abs(checksum(newid())) % 100, cast(newid() as varbinary(max)), getdate(), newid(), checksum(newid()), abs(checksum(newid())) % 255,newid())
go 10000


INSERT INTO [dbo].[Record Link]
(       

    [Record ID]   ,
    [URL1]    ,
    [URL2]    ,
    [URL3]    ,
    [URL4]    ,
    [Description]    ,
    [Type]  ,
    [Note]  ,
    [Created]   ,
    [User ID]   ,
    [Company]   ,
    [Notify]   ,
    [To User ID]   )
select     [Record ID]   ,
    [URL1]    ,
    [URL2]    ,
    [URL3]    ,
    [URL4]    ,
    [Description]    ,
    [Type]  ,
    [Note]  ,
    [Created]   ,
    [User ID]   ,
    [Company]   ,
    [Notify]   ,
    [To User ID]  
from [record link]
go 5
_

...そして私のインデックス:

_create index ixTest on [Record Link] (Company, Notify, [Link ID])  include ([To User ID], "timestamp","Record ID","URL1","URL2","URL3","URL4","Description","Type","Created","User ID")
_

次に、データを照会して、機能するいくつかの値を取得しました。

_declare @0 int = 50;
declare @1 int = 3000;
declare @2 nvarchar(30) = N'473788597'
declare @3 nvarchar(30) = N'508414347'
declare @4 tinyint = 100;
declare @5 nvarchar(132) = N'E3CB6DFC-8311-4DB4-9735-F2AF90747DCF'
declare @6 nvarchar(132) = N'Test2'

SELECT  TOP (@0) "timestamp","Link ID","Record ID","URL1","URL2","URL3","URL4","Description","Type","Created","User ID","Company","Notify","To User ID" 
FROM dbo."Record Link" WITH(READUNCOMMITTED)  
WHERE ("Link ID">@1 AND (("Company"=@2 OR "Company"=@3) AND "Notify"=@4 AND ("To User ID" COLLATE Latin1_General_100_CI_AI LIKE @5 OR "To User ID" COLLATE Latin1_General_100_CI_AI LIKE @6))) 
ORDER BY "Link ID" ASC OPTION(OPTIMIZE for UNKNOWN, FAST 50)
_

...そして私は次の計画を得ました:

enter image description here

基本的には、2つの会社でNested Loopを実行します。並べ替えがまだあるため、希望するほど良くはありませんが、インデックスを使用します。

2
Rob Farley

OPTIMIZE for UNKNOWNオプションを使用しているため、クラスター化インデックスの代わりにインデックスは使用されないと思います。このオプションを使用すると、クエリオプティマイザーはヒストグラムではなく統計の密度を使用するため、推定カーディナリティは常に十分大きくなるため、クラスター化インデックスのスキャンは、このような多数のキールックアップ操作よりも安価になります。

クライアント側でクエリを変更できない場合は、おそらく プランガイド を使用するのが最後の手段です。

1
Denis Rubashkin