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

Apr*_*llo 8 sql-server sql-server-2017

我有下表,其中有 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]
Run Code Online (Sandbox Code Playgroud)

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

CREATE NONCLUSTERED INDEX IX_TestTable_Date ON [dbo].[TestTable] ([Date])
Run Code Online (Sandbox Code Playgroud)

- 我运行以下查询:

UPDATE TestTable 
SET TestCol='*GDPR*', TestCol2='*GDPR*', TestCol3='*GDPR*', Anonymised=1
WHERE [Date] <= '25 August 2016'
Run Code Online (Sandbox Code Playgroud)

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

查询计划

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

查询计划

我的理论,这对于你们中更有经验的人来说可能是显而易见的,它已经发现日期列的隐式排序与主键/聚集索引完全相同。

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

Joe*_*ish 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]);
Run Code Online (Sandbox Code Playgroud)

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

表'测试表'。扫描计数1,逻辑读1299838,物理读0,预读0,lob逻辑读0,lob物理读0,lob预读0。

SQL Server 执行时间:CPU 时间 = 984 毫秒,已用时间 = 988 毫秒。

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

表'测试表'。扫描计数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);
Run Code Online (Sandbox Code Playgroud)

运行新查询的统计信息:

表'测试表'。扫描计数 1,逻辑读取 3,物理读取 0,预读读取 0,lob 逻辑读取 0,lob 物理读取 0,lob 预读读取 0。

表'测试表'。扫描计数 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 毫秒的聚集索引扫描时间和大约 550 毫秒的Clustered Index Update操作时间。更新操作符不会为此查询做任何 IO。

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

在此处输入图片说明

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

我怀疑 SQL Server 由于查询计划操作员的成本限制而选择了较慢的计划。运行时间差异的部分原因可能是硬件或您的 SQL Server 版本。在任何情况下,SQL Server 都无法确定日期列的隐式排序与聚集索引完全相同。数据是按聚集键顺序从聚集索引扫描返回的,因此在进行聚集索引更新时不需要执行排序以尝试优化IO。