web-dev-qa-db-ja.com

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

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]
9
Chocoman

Tl; dr下部

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

どちらか一方のプランを選択する主な理由は、_Estimated total subtree_のコストです。

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

不良プランの推定サブツリーコストの合計:

enter image description here

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

enter image description here


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

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

より良いパフォーマンスの計画では、Subtreecostの大部分は、結合を実行する_index seek_&_nested loops operator_で計算されます。

enter image description here

悪いクエリプランの場合、_Clustered index seek_演算子のコストは低くなります

enter image description here

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

(そして、パラメーター__30_を追加することにより、不良プランのコストが_871.510000_推定コストを上回っているところに増加します)。 推定推測™

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

enter image description here

悪い計画

enter image description here


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

この情報により、例で不適切なクエリプランを強制する方法が得られます(問題の複製に使用されるデータについては、DMLでOPの問題をほぼ複製します)

_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)
_

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

enter image description here


書き換え

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

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

そして、大きなIN()の代わりにJOINを追加します

_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';
_

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

enter image description here

AuditRelatedIdsテーブルの巨大なオペレーターコストの見積もり

enter image description here


気づいたのはここです

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

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

最適化されたビットマップフィルターの詳細 ここ

enter image description here

つまり、ビットマップフィルターがない場合、オプティマイザーは最初に_#number_テーブルに結合し、次にAuditRelatedIdsテーブルに結合することをお勧めします。

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

enter image description here

enter image description here

良くない


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];
_

enter image description here

マージ結合の使用中。 enter image description here

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

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

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

enter image description here

enter image description here


TL; DR

推定コストは選択されたプランを定義します。動作を再現することができ、_optimized bitmap filters_ + parallellism演算子が最後に追加されて、パフォーマンスの高い高速な方法でクエリを実行することがわかりました。

_merge join_を使用し、「悪い」parallellismを使用せずに、毎回同じ制御された結果を得ることができる方法として、クエリにMAXDOP(1)を追加することを検討できます。

新しいバージョンにアップグレードし、_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;
_
8
Randi Vertongen

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

最初のバージョンでは、メインフィルターはAudit.IDari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'に関連しているものを導き出し、次にそのリストをAudit.TargetTypeIDがリストに含まれていた人までフィルターしました。

2番目のバージョンでは、メインフィルターはAudit.IDAudit.TargetTypeIDのリストに関連していることを導き出していました。

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

Ypercubeの信用に。 RelatedIDAuditIDの一部として使用するのではなく、AuditIDINCLUDEの両方をインデックスの一部として使用するように[AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]を更新できます。追加のインデックススペースを占有することはなく、JOIN句で両方の列を使用できます。これは、クエリオプティマイザーが両方のクエリに対して同じ実行プランを作成するのに役立ちます。

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

1
Kirk Saunders