当我有索引时获取 SORT 运算符

Gee*_*zer 3 sql-server execution-plan azure-sql-database sort-operator query-performance

在 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
Run Code Online (Sandbox Code Playgroud)

Pau*_*ite 7

临时表中的列Id是唯一的,但您没有告诉优化器这一点。

将临时表上现有的非聚集索引替换为:

CREATE UNIQUE CLUSTERED INDEX CCI_#updates__Id
ON #updates ([Id]);
Run Code Online (Sandbox Code Playgroud)

注意索引是UNIQUECLUSTERED

这将从计划中删除哈希匹配聚合(为每个未声明的键选择任意行值)。这个聚合很慢,因为它由于内存不足而溢出到磁盘,但要点是,仅需要聚合是因为 SQL Server 无法确定表中的一行HISTORY最多与临时表中的一行匹配。向临时表添加唯一性保证可以解决该问题并删除聚合。

现在为最终更新添加提示:FORCESEEK

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   
    WITH (FORCESEEK) -- NEW!
    ON tgt.[Id] = src.[Id]
    AND src.[ActiveFromDateTime] = tgt.[ActiveFromDateTime] 
    AND tgt.[Hash]  <> src.[Hash]; 
Run Code Online (Sandbox Code Playgroud)

你应该得到一个没有排序或散列的计划,如下所示:

预期计划

万圣节保护需要Eager Table Spool,因为您要更新集群键 ( IsActive )。

您可能会发现这种计划形状效果最好。您没有更新大量行。

引入原始排序是为了按键顺序将行呈现给聚集索引更新运算符。这有助于产生顺序访问模式,而不是为每次更新寻找聚集索引。上面的计划依赖于保留该键顺序,因此不需要排序。


我知道您说过您正在遵循某种模式,但脚本的许多方面似乎都是多余的、低效的或不安全的。

  1. 历史表对标识列没有唯一约束。
  2. 哈希计算可以使用CONCAT_WS.
  3. 哈希计算不使用日期转换的样式格式。
  4. 保存到临时表的HistoryRecordId列永远不会被使用。
  5. 目前尚不清楚您是否使用散列来保存任何内容而不是直接比较列。
  6. 您的最终更新无条件更改集群键列IsActive,需要万圣节保护。您可以考虑不这样做,或者仅在绝对必要时才这样做,也许在单独的更新中。这完全取决于该列的含义以及您的流程所保证的内容。


Eri*_*ing 5

将其聚类

查询计划的主要问题是使用批处理模式排序。这让您如此沮丧的原因是因为除非它们是 Window Aggregate 的子运算符,否则所有行最终都会在单个线程上:

坚果

表上的索引无效的原因#updates是它没有被使用。SQL Server 不想执行 200 万次查找来获取所有不属于非聚集索引的请求列。

坚果

您可能会更好地在#updates表上创建聚集索引,这将按键列对数据进行排序,并包含表中的所有其他列。

CREATE CLUSTERED INDEX 
    NCCI_#updates__Kimble_HISTORY_ForecastStatus 
ON 
    #updates 
    ([Id], ActiveFromDateTime, [Hash])
WITH
    (SORT_IN_TEMPDB = ON, DATA_COMPRESSION = PAGE);
Run Code Online (Sandbox Code Playgroud)

然而!您可能仍然会获得批处理模式计划,并且它可能会使用哈希连接,因为这是批处理模式可以使用的唯一连接类型。由于散列连接不保留顺序(合并和某些类型的嵌套循环会保留顺序),因此您可能仍然会得到排序运算符。

您的选择是使用 OPTION(MERGE JOIN) 强制使用该连接类型,或使用 OPTION(USE HINT('DISALLOW_BATCH_MODE')) 禁用查询的批处理模式。