非常に類似したクエリ、大幅に異なるパフォーマンス


9

2つの非常によく似たクエリがあります

最初のクエリ:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,30,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

結果:267479

計画:https : //www.brentozar.com/pastetheplan/?id=BJWTtILyS


2番目のクエリ:

SELECT count(*)
FROM Audits a
    JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

結果:25650

計画:https : //www.brentozar.com/pastetheplan/?id=S1v79U8kS


最初のクエリは完了するまでに約1秒かかりますが、2番目のクエリは約20秒かかります。最初のクエリの数は2番目のクエリよりもはるかに多いため、これは完全に直観に反しています。これはSQL Server 2012上にあります

なぜそんなに違いがあるのですか?2番目のクエリを最初のクエリと同じくらい高速にするにはどうすればよいですか?


以下は、両方のテーブルのテーブル作成スクリプトです。

CREATE TABLE [dbo].[AuditRelatedIds](
    [AuditId] [bigint] NOT NULL,
    [RelatedId] [uniqueidentifier] NOT NULL,
    [AuditTargetTypeId] [smallint] NOT NULL,
 CONSTRAINT [PK_AuditRelatedIds] PRIMARY KEY CLUSTERED 
(
    [AuditId] ASC,
    [RelatedId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_AuditRelatedIdsRelatedId_INCLUDES] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC
)
INCLUDE (   [AuditId]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id] FOREIGN KEY([AuditId])
REFERENCES [dbo].[Audits] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditId_Audits_Id]

ALTER TABLE [dbo].[AuditRelatedIds]  WITH CHECK ADD  CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([AuditTargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[AuditRelatedIds] CHECK CONSTRAINT [FK_AuditRelatedIds_AuditTargetTypeId_AuditTargetTypes_Id]

CREATE TABLE [dbo].[Audits](
    [Id] [bigint] IDENTITY(1,1) NOT NULL,
    [TargetTypeId] [smallint] NOT NULL,
    [TargetId] [nvarchar](40) NOT NULL,
    [TargetName] [nvarchar](max) NOT NULL,
    [Action] [tinyint] NOT NULL,
    [ActionOverride] [tinyint] NULL,
    [Date] [datetime] NOT NULL,
    [UserDisplayName] [nvarchar](max) NOT NULL,
    [DescriptionData] [nvarchar](max) NULL,
    [IsNotification] [bit] NOT NULL,
 CONSTRAINT [PK_Audits] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetId] ON [dbo].[Audits]
(
    [TargetId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

SET ANSI_PADDING ON

CREATE NONCLUSTERED INDEX [IX_AuditsTargetTypeIdAction_INCLUDES] ON [dbo].[Audits]
(
    [TargetTypeId] ASC,
    [Action] ASC
)
INCLUDE (   [TargetId],
    [UserDisplayName]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100) ON [PRIMARY]

ALTER TABLE [dbo].[Audits]  WITH CHECK ADD  CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id] FOREIGN KEY([TargetTypeId])
REFERENCES [dbo].[AuditTargetTypes] ([Id])

ALTER TABLE [dbo].[Audits] CHECK CONSTRAINT [FK_Audits_TargetTypeId_AuditTargetTypes_Id]

3
テーブルスキーマとインデックスの詳細を取得できますか?私はあなたが計画が少し違うことに気づいたと確信しているように、明らかにそれは大きな違いを作っています。これらの詳細を取得できれば、オプションの種類を確認できるかもしれません。
カークサンダース

2
非常に簡単なヒントとして、INを使用する代わりに、必要な数の単一のTINYINT / INT列(クラスター化)を持つTempTableを作成してから、それにINNER JOINします。それ以外には、上記の@KirkSaundersのようなDDL情報が必要になる可能性があります
George.Palacios

2
何か特別なことはありTargetTypeId = 30ますか?この1つの値は実際に返される(予期される)データの量を歪めるため、計画は異なります。
アーロンバートランド

私はそれがひどく無茶苦茶であることを理解しますが、「最初のクエリは2番目よりもはるかに多くの行を返します」というステートメントです。不正解です。どちらも1行を返します;)
ypercubeᵀᴹ19年

1
私は両方のテーブルのテーブルのステートメントを作成して質問を更新
Chocoman

回答:


8

下部のTl; dr

なぜ悪い計画が選ばれたのか

プランを選択する主な理由はEstimated total subtreeコストです。

このコストは、より良いパフォーマンスのプランよりも悪いプランの方が低かった。

悪い計画の推定サブツリーコストの合計:

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

より良いパフォーマンスの計画のための推定合計サブツリーコスト

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


オペレーターの推定コスト

特定のオペレーターはこのコストの大部分を占める可能性があり、オプティマイザーが別のパス/プランを選択する理由になる可能性があります。

より良いパフォーマンスの計画では、の大部分は&結合で実行Subtreecostされます:index seeknested loops operator

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

私たちの悪いクエリプランでは、Clustered index seekオペレーターのコストは低くなります

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

これは、他の計画が選択された理由を説明する必要があります。

(そして30、それが871.510000推定コストを上回っているところに悪い計画のコストを増加させるパラメーターを追加することによって)。 推定推測™

より良いパフォーマンスの計画

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

悪い計画

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


これは私たちをどこに連れて行きますか?

この情報は、私たちの例に悪いクエリプランを強制する方法をもたらします (問題を複製するために使用されるデータについて、OPの問題をほぼ複製するDMLを参照してください)

INNER LOOP JOIN結合ヒントを追加する

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
    and a.TargetTypeId IN 
    (1,2,3,4,5,6,7,8,9,
    11,12,13,14,15,16,17,18,19,
    21,22,23,24,25,26,27,28,29,
    31,32,33,34,35,36,37,38,39,
    41,42,43,44,45,46,47,48,49,
    51,52,53,54,55,56,57,58,59,
    61,62,63,64,65,66,67,68,69,
    71,72,73,74,75,76,77,78,79)

近いですが、いくつかの結合順序の違いがあります:

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


書き直し

私の最初の書き換えの試みは、代わりにこれらすべての数値を一時テーブルに格納することでした。

CREATE TABLE #Numbers(Numbering INT)
INSERT INTO #Numbers(Numbering)
VALUES
(1),(2),(3),(4),(5),(6),(7),(8),(9),(11),(12),(13),(14),(15),(16),(17),(18),(19),
(21),(22),(23),(24),(25),(26),(27),(28),(29),(30),(31),(32),(33),(34),(35),
(36),(37),(38),(39),(41),(42),(43),(44),(45),(46),(47),(48),(49),(51),(52),
(53),(54),(55),(56),(57),(58),(59),(61),(62),(63),(64),(65),(66),(67),(68),
(69),(71),(72),(73),(74),(75),(76),(77),(78),(79);

そしてJOIN、大きな代わりにIN()

SELECT count(*)
FROM Audits a
   INNER LOOP JOIN AuditRelatedIds ari ON a.Id = ari.AuditId
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1';

クエリプランは異なりますが、まだ修正されていません。

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

AuditRelatedIdsテーブル上の巨大な推定オペレーターコストで

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


ここで私はそれに気づきました

プランを直接再作成できないのは、最適化されたビットマップフィルタリングのためです。

traceflags 7497&を使用して最適化されたビットマップフィルターを無効にすることで、プランを再作成できます。7498

SELECT count(*)
FROM Audits a 
   INNER JOIN AuditRelatedIds  ari ON a.Id = ari.AuditId 
   INNER JOIN #Numbers
   ON Numbering = a.TargetTypeId
WHERE 
    ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'
OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498);

最適化されたビットマップフィルターの詳細については、こちらをご覧ください

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

これは、ビットマップフィルターがない場合、オプティマイザは最初に#numberテーブルに結合してからテーブルに結合するほうが適切であると見なすことを意味しAuditRelatedIdsます。

注文OPTION (QUERYTRACEON 7497, QUERYTRACEON 7498, FORCE ORDER);を強制すると、その 理由がわかります。

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

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

良くない


maxdop 1と並行する機能の削除

追加時にMAXDOP 1高速クエリを実行し、単一のスレッド。

そして、このインデックスを追加します

CREATE NONCLUSTERED INDEX [IX_AuditRelatedIdsRelatedId_AuditId] ON [dbo].[AuditRelatedIds]
(
    [RelatedId] ASC,
    [AuditId] ASC
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];

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

マージ結合を使用しているとき。 ここに画像の説明を入力してください

強制注文クエリのヒントを削除する場合、または#Numbersテーブルを使用せIN()ずに代わりに使用する場合も同様です。

私のアドバイスは、追加を調べてMAXDOP(1)それがクエリに役立つかどうかを確認し、必要に応じて書き換えることです。

もちろん、最適化されたビットマップフィルタリングと実際に複数のスレッドを使用することにより、パフォーマンスが向上することにも注意してください。

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

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


TL; DR

推定コストは選択したプランを定義します。動作を再現でき、パフォーマンスの良い高速な方法でクエリを実行するためにoptimized bitmap filters+ parallellism演算子が追加されました。

'bad'を使用MAXDOP(1)して、毎回同じ制御された結果を得ることができる方法として、クエリに追加することを検討できます。merge joinparallellism

新しいバージョンにアップグレードして、CardinalityEstimationModelVersion="70"役立つ可能性のあるカーディナリティエスティメータバージョンを使用すると、役立つ場合 があります。

複数値フィルタリングを行うための数値一時テーブルも役立ちます。


OPの問題をほぼ再現するDML

私が認めたい以上の時間をこれに費やしました

set NOCOUNT ON;
DECLARE @I INT = 0
WHILE @I < 56
BEGIN
INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(500000) CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 10000 END as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;
SET @I +=1;
END

-- 'Bad Query matches'
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])
SELECT
TOP(25650)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') , 
CASE WHEN ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 = 30 then 29 ELSE ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) / 510 END as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2

-- Extra matches with 30
SELECT MAX([Id]) FROM [dbo].[Audits];
--28000001 Upper value

INSERT INTO  [dbo].[Audits] WITH(TABLOCK) 
([TargetTypeId],
    [TargetId],
    [TargetName],
    [Action],
    [ActionOverride] ,
    [Date] ,
    [UserDisplayName],
    [DescriptionData],
    [IsNotification]) 
SELECT top(241829) 30 as rownum2 -- TILL 50 and no 30
,'bla','bla2',1,1,getdate(),'bla3','Bla4',1
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2;



;WITH CTE AS
(SELECT
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) as rownum1, 
('1DD87CF1-286B-409A-8C60-3FFEC394FDB1') as gu , 
30 as rownum2 -- TILL 50 and no 30
FROM master.dbo.spt_values spt1
CROSS APPLY master.dbo.spt_values spt2
CROSS APPLY master.dbo.spt_values spt3
)
--267479 - 25650 = 241829
INSERT INTO  [dbo].[AuditRelatedIds] WITH(TABLOCK)
    ([AuditId] ,
    [RelatedId]  ,
    [AuditTargetTypeId])

SELECT TOP(241829) rownum1,gu,rownum2 FROM CTE
WHERE rownum1 > 28000001
ORDER BY rownum1 ASC;

とてもいい説明!追加するMAXDOP 0と修正されたようです。どうもありがとうございました!
Chocoman

1
MAXDOP 1 **(タイプミス)
Chocoman

@チョコマンすごい!
喜んで

1

2つのプランの主な違いは、「プライマリフィルター」の違いです。

最初のバージョンでは、メインフィルターが派生してAudit.IDおり、これはari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'、リストに含まれている人までそのリストをフィルターすることに関連Audit.TargetTypeIDしています。

2番目のバージョンでは、メインフィルターはAudit.IDのリストに関連する派生ですAudit.TargetTypeID

の追加によりAudit.TargetTypeID = 30、レコード数が大幅に増加したようです(元の質問によると、それぞれ267,479および25,650)。そのため、実行計画が異なります。(私が理解しているように)SQLは最初に最も選択的な機能を実行し、その後残りのルールを適用しようとします。最初のバージョンでは、AuditRelatedID.RelatedIDto then find によるクエリは、then find Audit.IDを使用Audit.TargetTypeIDするよりもおそらく選択的Audit.IDでした。

ypercubeの信用に。あなたは確かに更新することができます[AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]持っている両方RelatedIDAuditIDインデックスの一部の代わりに持つなどAuditIDの一部としてINCLUDE。追加のインデックススペースを占有することはなく、JOIN句で両方の列を使用できます。これは、クエリオプティマイザーが両方のクエリに対して同じ実行プランを作成するのに役立ちます。

同様のロジックで操作すると、実際の順序付け/フィルタリングノード(の一部としてではない)Auditを含むインデックスに利点がある場合があります。これにより、クエリオプティマイザがフィルタリングして、すぐにに参加できるようになります。これで、両方のクエリで効率の悪いプランが選択される可能性があります。そのため、ypercubeの推奨を試した後でのみ、試してみます。TargetTypeID ASC, ID ASCINCLUDEAudit.TargetTypeIDAuditReferenceIds.AuditID

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