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]
Tl; dr下部
どちらか一方のプランを選択する主な理由は、_Estimated total subtree
_のコストです。
このコストは、より良いパフォーマンスのプランよりも悪いプランの方が低かった。
不良プランの推定サブツリーコストの合計:
より良いパフォーマンスの計画のための推定合計サブツリーコスト
オペレーターの推定コスト
特定のオペレーターはこのコストの大部分を占める可能性があり、オプティマイザーが別のパス/プランを選択する理由になる可能性があります。
より良いパフォーマンスの計画では、Subtreecost
の大部分は、結合を実行する_index seek
_&_nested loops operator
_で計算されます。
悪いクエリプランの場合、_Clustered index seek
_演算子のコストは低くなります
これは、他の計画が選択された理由を説明する必要があります。
(そして、パラメーター__30
_を追加することにより、不良プランのコストが_871.510000
_推定コストを上回っているところに増加します)。 推定推測™
より良いパフォーマンスの計画
悪い計画
これは私たちをどこに連れて行きますか?
この情報により、例で不適切なクエリプランを強制する方法が得られます(問題の複製に使用されるデータについては、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)
_
近いですが、いくつかの結合順序の違いがあります:
書き換え
私の最初の書き換えの試みは、代わりにこれらすべての数値を一時テーブルに格納することでした。
_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';
_
クエリプランは異なりますが、まだ修正されていません。
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
演算子が最後に追加されて、パフォーマンスの高い高速な方法でクエリを実行することがわかりました。
_merge join
_を使用し、「悪い」parallellism
を使用せずに、毎回同じ制御された結果を得ることができる方法として、クエリにMAXDOP(1)
を追加することを検討できます。
新しいバージョンにアップグレードし、_CardinalityEstimationModelVersion="70"
_よりも高いカーディナリティエスティメータバージョンを使用することも役立つ場合があります。
複数値のフィルタリングを行うための数値一時テーブルも役立ちます。
私が認めたい以上の時間をこれに費やしました
_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;
_
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.RelatedID
を使用してAudit.ID
を検索するクエリは、Audit.TargetTypeID
を使用してAudit.ID
を検索するよりも選択性が高いと考えられます。
Ypercubeの信用に。 RelatedID
をAuditID
の一部として使用するのではなく、AuditID
とINCLUDE
の両方をインデックスの一部として使用するように[AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]
を更新できます。追加のインデックススペースを占有することはなく、JOIN
句で両方の列を使用できます。これは、クエリオプティマイザーが両方のクエリに対して同じ実行プランを作成するのに役立ちます。
同様のロジックで操作すると、実際の順序付け/フィルタリングノード(Audit
の一部ではない)にTargetTypeID ASC, ID ASC
を含むINCLUDE
のインデックスにいくつかの利点がある場合があります。これにより、クエリオプティマイザはAudit.TargetTypeID
でフィルタリングし、すぐにAuditReferenceIds.AuditID
に結合できます。これで、両方のクエリが効率の悪いプランを選択するようになる可能性があるため、ypercubeの推奨を試してみて初めて試してみることにします。