数据固有地排序,就好像它是聚集索引一样


8

我有下表,其中包含750万条记录:

CREATE TABLE [dbo].[TestTable](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [TestCol] [nvarchar](50) NOT NULL,
    [TestCol2] [nvarchar](50) NOT NULL,
    [TestCol3] [nvarchar](50) NOT NULL,
    [Anonymised] [tinyint] NOT NULL,
    [Date] [datetime] NOT NULL,
CONSTRAINT [PK_TestTable] 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]

我注意到,当日期字段上存在非聚集索引时:

CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date])

-并且我运行以下查询:

UPDATE TestTable 
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Date] <= '25 August 2016'

-索引访问操作返回的数据经过排序,以匹配PK / CX的键顺序,从而降低了性能。

查询计划

我很惊讶地发现从日期字段中删除索引实际上将查询性能提高了约30%,因为它不再执行排序:

查询计划

我的理论(这对您中比较有经验的人来说可能是显而易见的)是,它发现date列的隐式排序与主键/聚簇索引完全相同。

所以我的问题是:是否可以利用这一事实来提高查询的性能?


1
我没有查看计划,但是我怀疑性能(好吧,持续时间,这些无用的估计成本百分比数字都没有提高)是因为它不必更新您删除的索引,而不是因为排序操作。
亚伦·伯特兰

@AaronBertrand我可能没有正确阅读这些内容,因此如果我写错了,请更正我,但是两个查询计划中似乎都存在索引更新操作。你是在说别的吗?
AproposArmadillo

1
我再说一次,我没有看计划。您说过“从日期字段中删除索引可以提高查询的性能” ...如果删除了索引,则该索引不应该出现在计划中,因此,也许您收集了错误的计划或实际上并未删除该计划认为您做了索引。再一次,某个计划的某些估计百分比是一个指标,但实际上并没有以任何方式反映出真实的绩效衡量。它是在查询甚至运行之前就计算出的估计值。
亚伦·伯特兰

@Aaron Bertrand,它无论如何都不必更新索引,因为[Date]不在更新的字段中。
Denis Rubashkin

1
@Shaffanhoon您是否尝试过[Date]DESC顺序重新创建索引?只是好奇,因为谓词是<=。此外,如果索引Date(默认情况下ACS为order)对其他查询有所帮助,那么您可以尝试向UPDATE添加表提示以强制其使用PK?或者,可以将其分为两部分:创建一个临时表,使用[Id]基于填充[Date] <= '25 August 2016',然后WHERE从UPDATE中删除并添加FROM dbo.TestTable tt INNER JOIN #tmp ids ON ids.[Id] = tt.[Id]。毕竟,它是一个UPDATE,它需要查找实际的行,索引或否。
所罗门·鲁茨基

Answers:


7

我模拟了大部分重现您问题的测试数据:

INSERT INTO [dbo].[TestTable] WITH (TABLOCK)
SELECT TOP (7000000) N'*NOT GDPR*', N'*NOT GDPR*', N'*NOT GDPR*', 0, DATEADD(DAY, q.RN  / 16965, '20160801')
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) q
ORDER BY q.RN
OPTION (MAXDOP 1);


DROP INDEX IF EXISTS [dbo].[TestTable].IX_TestTable_Date;
CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date]);

使用非聚集索引的查询的统计信息:

表“ TestTable”。扫描计数1,逻辑读1299838,物理读0,预读读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:CPU时间= 984毫秒,经过的时间= 988毫秒。

使用聚集索引的查询的统计信息:

表“ TestTable”。扫描计数1,逻辑读72609,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:CPU时间= 781毫秒,经过的时间= 772毫秒。

提出您的问题:

是否可以利用这一事实来改善查询的性能?

是。您可以使用已经必须的非聚集索引来有效地找到id需要更新的最大值。如果将其保存到变量中并对其进行过滤,则将获得针对该更新的查询计划,该更新将进行聚集索引扫描(不进行排序),该更新将尽早停止并因此减少IO。这是一个实现:

DECLARE @Id INT;

SELECT TOP (1) @Id = Id
FROM dbo.TestTable 
WHERE [Date] <= '25 August 2016'
ORDER BY [Date] DESC, Id DESC;

UPDATE TestTable 
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Id] < @Id AND [Date] <= '25 August 2016'
AND [Anonymised] <> 1 -- optional
OPTION (MAXDOP 1);

为新查询运行统计信息:

表“ TestTable”。扫描计数1,逻辑读3,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

表“ TestTable”。扫描计数1,逻辑读4776,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server执行时间:CPU时间= 515毫秒,经过的时间= 510毫秒。

以及查询计划:

好的查询计划

综上所述,您希望加快查询速度的想法向我暗示了您计划多次运行查询。现在,您的查询在该date列上有一个开放式过滤器。是否确实需要多次对行进行匿名处理?您是否可以避免更新或扫描已经匿名的行?当然,更新带有两边日期的日期范围应该更快。您也可以将Anonymised列添加到索引中,但是在UPDATE查询过程中需要更新该索引。总之,如果可以的话,避免一遍又一遍地处理相同的数据。

由于Clustered Index Update操作员已完成工作,因此排序时的原始查询速度较慢。在索引查找和排序上花费的时间仅407毫秒。您可以在实际计划中看到这一点。该计划以行模式执行,因此花在排序上的时间就是该运算符和每个子运算符的时间:

在此处输入图片说明

剩下的排序操作大约需要1600毫秒的时间。SQL Server需要从聚簇索引中读取页面才能执行更新。您可以看到该Clustered Index Update操作员执行1205921逻辑读取。你可以阅读更多有关此分拣DML优化,优化预读博客文章保罗·怀特

您拥有的另一个查询计划(不进行排序)花费683毫秒用于聚集索引扫描,而Clustered Index Update操作员则花费550毫秒。更新运算符对此查询不执行任何IO。

关于排序计划为何较慢的一个简单答案是,与群集索引扫描计划相比,SQL Server在该计划的群集索引上执行了更多的逻辑读取。即使所有需要的数据都在内存中,进行这些逻辑读取仍然会产生开销和成本。就我所知,计划不会为您提供更多详细信息,因此很难获得更好的答案。可以使用PerfView或其他基于ETW跟踪的工具来比较查询之间的调用堆栈:

在此处输入图片说明

左边是进行聚集索引扫描的查询,右边是进行排序的查询。我将呼叫堆栈标记为蓝色或红色,仅出现在一个查询中。毫不奇怪,用于排序查询的具有大量采样CPU周期的不同调用堆栈似乎与对聚簇索引执行更新所需的逻辑读取有关。此外,同一操作的查询之间的采样周期数也有所不同。对于样本,具有排序的查询花费31个周期来获取锁存器,而具有扫描的查询仅花费9个周期来获取锁存器。

我怀疑由于查询计划运营商成本限制,SQL Server选择较慢的计划。运行时间差异的部分原因可能在于硬件或您所使用的SQL Server版本。无论如何,SQL Server都无法确定date列的隐式排序与聚簇索引完全相同。数据是从聚簇索引扫描中以聚簇键顺序返回的,因此在进行聚簇索引更新时,无需执行排序来优化IO。

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.