非常相似的查询,性能差异很大


9

我有两个非常相似的查询

第一个查询:

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


第二个查询:

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


第一个查询大约需要一秒钟才能完成,而第二个查询大约需要20秒。这对我来说完全是违反直觉的,因为第一个查询的计数比第二个查询高得多。这是在SQL Server 2012

为什么会有如此大的差异?如何加快第二个查询的速度与第一个查询一样快?


这是两个表的创建表脚本:

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来创建一个TempTable,该TempTable带有单个TINYINT / INT列(群集),其中包含所需的数字,然后对其进行INNER JOIN。除此之外,我们可能还需要DDL信息,如上面提到的
@KirkSaunders

2
有什么特别的TargetTypeId = 30吗?似乎计划是不同的,因为这个值确实会使返回的数据量(预计将要歪曲)。
亚伦·伯特兰

我意识到这太令人讨厌了,但是语句“第一个查询返回的行比第二个查询多得多”。是不正确的。都返回1行;)
ypercubeᵀᴹ19年

1
我用两个表的create table语句更新了问题
Chocoman

Answers:


8

Tl; dr在底部

为什么选择了坏计划

选择一个计划而不是另一个计划的主要原因是Estimated total subtree成本。

不良计划的成本比绩效较好的计划的成本低。

坏计划的总估计子树成本:

在此处输入图片说明

效果更好的计划的估计子树总成本

在此处输入图片说明


运营商估计成本

某些运营商可以承担大部分费用,并且可能是优化器选择其他路径/计划的原因。

在我们执行效果更好的计划中,大部分Subtreecost是根据index seeknested 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);

然后添加一个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运算符,从而以高效,快速的方式执行查询。

您可以考虑将MAXDOP(1)查询添加到查询中,以希望每次都获得相同的受控结果,merge join并且没有“坏”的情况parallellism

升级到较新的版本并使用比CardinalityEstimationModelVersion="70"可能有所帮助的更高的基数估计器版本 。

数字临时表进行多值过滤也有帮助。


DML几乎可以复制OP的问题

我在此上花费的时间超过了我不愿承认的时间

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 **(typo)
Chocoman's

@Chocoman太好了!
乐于

1

从我可以看出,两个计划之间的主要区别是什么是“主过滤器”。

在第一个版本中,主要过滤器是派生的,该过滤器Audit.ID与之相关,ari.RelatedId = '1DD87CF1-286B-409A-8C60-3FFEC394FDB1'然后将该列表过滤为列表中的那些Audit.TargetTypeID

在第二个版本中,派生的主要过滤器Audit.ID与的列表有关Audit.TargetTypeID

由于添加Audit.TargetTypeID = 30似乎大大增加了记录数量(根据原始问题,分别为267,479和25,650)。这可能就是执行计划不同的原因。(据我了解),SQL将首先尝试执行最具选择性的功能,然后再应用其余规则。在第一个版本中,按by AuditRelatedID.RelatedID进行查询Audit.ID可能比尝试使用Audit.TargetTypeIDthen进行查找更具选择性Audit.ID

为了ypercube的功劳。您当然可以更新[AuditRelatedIds].[IX_AuditRelatedIdsRelatedId_INCLUDES]为在索引中同时包含RelatedIDAuditID作为索引的一部分,而不是AuditID作为索引的一部分INCLUDE。它不应该占用任何额外的索引空间,并且允许您在JOIN子句中使用这两列。这可能有助于查询优化器为两个查询创建相同的执行计划。

用类似的逻辑操作时,可能有一些好处的索引Audit,其包含TargetTypeID ASC, ID ASC实际的有序/过滤节点上(而不是作为部分INCLUDE)。这应该允许查询优化器进行过滤,Audit.TargetTypeID然后快速加入AuditReferenceIds.AuditID。现在,这可能会以两个查询都选择效率较低的计划而告终,因此,只有在尝试了ypercube的建议后才能尝试一下。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.