在 Azure SQL 数据库(SQL2019 兼容)上,我有一个 ETL 进程,它以 DeltaTrack 模式填充 HISTORY 表。
在 Proc 中,有一个对 HISTORY 表的更新,查询引擎正在使用 SORT,但我有一个应该覆盖它的索引。
此 UPDATE 的用例是针对现有行,自从该行首次添加到 HISTORY 表以来,我们已向摄取添加了额外的列。
这种排序会导致我们更大/更宽的表上的更新速度极其缓慢。
如何调整索引或查询以删除查询 3中的排序?
这是根据京东要求更新的 执行计划
这是 DDL。
DROP TABLE IF EXISTS dbo.STAGE;
GO
CREATE TABLE dbo.STAGE
(
Id varchar(18) NULL,
CreatedDate varchar(4000) NULL,
LastModifiedDate varchar(4000) NULL,
LastReferencedDate varchar(4000) NULL,
[Name] varchar(4000) NULL,
OwnerId varchar(4000) NULL,
SystemTimestamp datetime2(7) NULL
)
GO
DROP TABLE IF EXISTS dbo.HISTORY;
GO
CREATE TABLE dbo.HISTORY
(
HistoryRecordId int IDENTITY(1,1) NOT NULL,
[Hash] binary(64) NOT NULL,
[IsActive] BIT NOT NULL ,
ActiveFromDateTime datetime2(7) NOT NULL,
ActiveToDateTime datetime2(7) NOT NULL,
Id varchar(18) NOT NULL,
CreatedDate datetime2(7) NULL,
LastModifiedDate datetime2(7) NULL,
LastReferencedDate datetime2(7) NULL,
[Name] varchar(80) NULL,
OwnerId varchar(18) NULL,
SystemTimestamp datetime2(7) NULL
)
GO
CREATE UNIQUE CLUSTERED INDEX [CL__HISTORY] ON dbo.HISTORY
(
Id ,
[ActiveToDateTime] ASC,
[IsActive] ASC
)
GO
CREATE NONCLUSTERED INDEX [IX__HISTORY_IsActive] ON dbo.HISTORY
(
[Id] ASC
)
INCLUDE([IsActive],[ActiveToDateTime])
GO
DROP TABLE IF EXISTS #updates;
GO
WITH src AS (
SELECT
CONVERT(VARCHAR(18), t.[Id]) AS [Id]
, CONVERT(DATETIME2, t.[CreatedDate]) AS [CreatedDate]
, CONVERT(DATETIME2, t.[LastModifiedDate]) AS [LastModifiedDate]
, CONVERT(DATETIME2, t.[LastReferencedDate]) AS [LastReferencedDate]
, CONVERT(VARCHAR(80), t.[Name]) AS [Name]
, CONVERT(VARCHAR(18), t.[OwnerId]) AS [OwnerId]
, CONVERT(DATETIME2, t.SystemTimestamp) AS SystemTimestamp
, dgst.[Hash]
, CONVERT(DATETIME2, SystemTimestamp) AS [ActiveFromDateTime]
, RN = ROW_NUMBER() OVER (
PARTITION BY
t.[Id]
ORDER BY CONVERT(DATETIME2, SystemTimestamp) DESC
)
FROM dbo.STAGE t
OUTER APPLY (
SELECT
CAST(HASHBYTES('SHA2_256',
COALESCE(CAST([CreatedDate] AS NVARCHAR(4000)), N'')
+ N'||' + COALESCE(CAST([LastModifiedDate] AS NVARCHAR(4000)), N'')
+ N'||' + COALESCE(CAST([LastReferencedDate] AS NVARCHAR(4000)), N'')
+ N'||' + COALESCE(CAST([Name] AS NVARCHAR(4000)), N'')
+ N'||' + COALESCE(CAST([OwnerId] AS NVARCHAR(4000)), N'')
+ N'||' + COALESCE(CAST(SystemTimestamp AS NVARCHAR(4000)), N'')
) AS BINARY(64)) AS [Hash]
) dgst
), tgt AS (
SELECT *
FROM dbo.HISTORY t
WHERE t.[ActiveToDateTime] > GETUTCDATE()
AND 1 = 1
)
SELECT
tgt.HistoryRecordId
, src.*
INTO #updates
FROM src
LEFT JOIN tgt
ON tgt.[Id] = src.[Id] WHERE src.RN = 1;
GO
--Create index on temp table (#updates)
CREATE NONCLUSTERED INDEX NCCI_#updates__Kimble_HISTORY_ForecastStatus
ON #updates ( [Id] , ActiveFromDateTime, [Hash] );
GO
UPDATE tgt
SET
tgt.[Hash] = src.[Hash]
, tgt.IsActive = 1
, tgt.[CreatedDate] = src.[CreatedDate]
, tgt.[LastModifiedDate] = src.[LastModifiedDate]
, tgt.[LastReferencedDate] = src.[LastReferencedDate]
, tgt.[Name] = src.[Name]
, tgt.[OwnerId] = src.[OwnerId]
, tgt.SystemTimestamp = src.SystemTimestamp
FROM dbo.HISTORY tgt
INNER JOIN #updates src
ON tgt.[Id] = src.[Id]
AND src.[ActiveFromDateTime] = tgt.[ActiveFromDateTime]
AND tgt.[Hash] <> src.[Hash] ;
GO
临时表中的列
Id
是唯一的,但您没有告诉优化器这一点。将临时表上现有的非聚集索引替换为:
注意索引是
UNIQUE
和CLUSTERED
。这将从计划中删除哈希匹配聚合(为每个未声明的键选择任意行值)。
现在为最终更新添加提示:
FORCESEEK
你应该得到一个没有排序或散列的计划,如下所示:
万圣节保护需要Eager Table Spool,因为您要更新集群键 ( IsActive )。
您可能会发现这种计划形状效果最好。您没有更新大量行。
引入原始排序是为了按键顺序将行呈现给聚集索引更新运算符。这有助于产生顺序访问模式,而不是为每次更新寻找聚集索引。上面的计划依赖于保留该键顺序,因此不需要排序。
我知道您说过您正在遵循某种模式,但脚本的许多方面似乎都是多余的、低效的或不安全的。
CONCAT_WS
.将其聚类
查询计划的主要问题是使用批处理模式排序。这让您如此沮丧的原因是因为除非它们是 Window Aggregate 的子运算符,否则所有行最终都会在单个线程上:
表上的索引无效的原因
#updates
是它没有被使用。SQL Server 不想执行 200 万次查找来获取所有不属于非聚集索引的请求列。您可能会更好地在
#updates
表上创建聚集索引,这将按键列对数据进行排序,并包含表中的所有其他列。然而!您可能仍然会获得批处理模式计划,并且它可能会使用哈希连接,因为这是批处理模式可以使用的唯一连接类型。由于散列连接不保留顺序(合并和某些类型的嵌套循环会保留顺序),因此您可能仍然会得到排序运算符。
您的选择是使用 OPTION(MERGE JOIN) 强制使用该连接类型,或使用 OPTION(USE HINT('DISALLOW_BATCH_MODE')) 禁用查询的批处理模式。
最后一个查询似乎主要是在等待BSORT(在 XML 视图中最容易看到):
等了将近4分钟。这指向批处理模式排序。事实上,这个查询中的排序是批处理模式的:
另外值得注意的是,CPU 时间仅为 10 秒,而实际(挂钟)时间为 46 秒。
所有这些都表明批处理模式存在问题。本文建议使用跟踪标志 9358 和其他解决方法。但它适用于 SQL Server 2016,因此不确定它在 2019 年如何工作。我个人会尝试强制查询以较低的兼容性级别运行。意思是
OPTION(USE HINT('QUERY_OPTIMIZER_COMPATIBILITY_LEVEL_140'))
在查询末尾添加。行存储上的批处理模式(我们在此处看到)从兼容性级别 150 开始可用,因此使用较低的级别应禁用它。作为旁注,索引 [IX__HISTORY_IsActive] ON dbo.HISTORY 似乎与聚集索引是多余的。它具有相同的第一个键列,并且包含其他键列。我会考虑放弃它。这将加快表上的任何更新并节省空间。