查询计划问题

sql*_*ana 5 sql-server query execution-plan update

我创建了一个 varbinary 哈希来检查 2 个表之间的更改。

这是执行计划,我对索引有点困惑,或者确实有更好的编写方式。 https://www.brentozar.com/pastetheplan/?id=HkHmqoczm

连接中的 2 列是目标中的 PK,并且在源中具有非聚集索引。困扰我的一点是排序导致的 tempdb 溢出。

Pau*_*ite 10

行存储

排序溢出本身可能可以通过启用跟踪标志 7470 来解决。请参阅修复:当估计的行数和行大小正确时,排序运算符溢出到 tempdb。该跟踪标志纠正了计算中的疏忽。它使用起来非常安全,在我看来应该默认开启。更改受跟踪标志保护,只是为了避免意外的计划更改。

也就是说,正如 Rob Farley 在他的回答中提到的那样,完全避免这种情况会更好。更改聚类键是实现这一目标的一种方法,但它可能不是最佳选择。

SQL Server 选择不使用非聚集索引来避免排序,因为该索引不提供更新可能需要的其他列。使用提示强制该索引将产生具有大量键查找的计划。高估计成本解释了为什么优化器更喜欢排序。

优化器目前无法自行考虑的另一种方法是找到将被更新的行的,然后为这些行获取额外的列(通过查找类型的操作)。提供的计划显示没有正在更新的行。如果这是常见情况,或者至少有一小部分行符合更新条件,则可能值得明确编码该逻辑。

执行计划中的另一个问题是每个目标更新行可能与多个源行相关联。这就是ANYStream Aggregate 运算符中存在聚合的原因。给定连接键上的多个匹配(以及散列上的不匹配),将用于更新的行是不确定的。

如果更新已写为MERGE,则在遇到多个源行时将引发错误。通常最好编写确定性更新,其中每个目标行最多与一个源行相关联。

例子

该问题没有提供 DDL 或太多背景知识,因此以下是一个简单的近似值,其中所有非键列都由单个大列表示,并从计划中推断出基数和索引:

DROP TABLE IF EXISTS 
    dbo.dwSource, 
    dbo.dwTarget;

CREATE TABLE dbo.dwSource
(
    loadkey bigint NOT NULL,
    mytableid integer NOT NULL,
    ppw_id integer NOT NULL,
    other_columns varchar(1000) NOT NULL,
    row_hash binary(20) NOT NULL,

    CONSTRAINT PK_dbo_dwSource
        PRIMARY KEY CLUSTERED (loadkey),
);

CREATE TABLE dbo.dwTarget
(
    mytableid integer NOT NULL,
    ppw_id integer NOT NULL,
    other_columns varchar(1000) NOT NULL,
    row_hash binary(20) NOT NULL,

    CONSTRAINT PK_dbo_dwTarget
        PRIMARY KEY CLUSTERED (ppw_id, mytableid)
);

UPDATE STATISTICS dbo.dwSource 
WITH ROWCOUNT = 1295450, PAGECOUNT = 100000;

UPDATE STATISTICS dbo.dwTarget 
WITH ROWCOUNT = 1296390, PAGECOUNT = 100000;
Run Code Online (Sandbox Code Playgroud)

鉴于近似模式(忽略源和目标上的非聚集索引),当前更新语句是:

UPDATE DT
SET DT.other_columns = DS.other_columns
FROM dbo.dwSource AS DS
JOIN dbo.dwTarget AS DT
    ON DT.ppw_id = DS.ppw_id
    AND DT.mytableid = DS.mytableid
WHERE DS.row_hash <> DT.row_hash;
Run Code Online (Sandbox Code Playgroud)

给予:

现有计划

如前所述,如果要更新的行数相对较少,则仅将定位键作为第一步可能是值得的。为了以最佳方式执行此操作,我们需要几个非聚集索引,它们可能与现有索引类似:

-- Narrower than the clustered primary key
CREATE UNIQUE INDEX [UQ dbo.dwTarget ppw_id, mytableid (row_hash)]
ON dbo.dwTarget (ppw_id, mytableid) 
INCLUDE (row_hash);

-- Not guaranteed to be unique    
CREATE INDEX [IX dbo.dwSource ppw_id, mytableid (loadkey, row_hash)]
ON dbo.dwSource (ppw_id, mytableid) 
INCLUDE (loadkey, row_hash);
Run Code Online (Sandbox Code Playgroud)

然后我们可以编写一个查询来定位更新键确保只有一个源行映射到每个目标行(任意选择具有最高 的行loadkey):

-- Find keys for updated rows
SELECT
    DS.ppw_id,
    DS.mytableid,
    loadkey = MAX(DS.loadkey)
INTO #Delta
FROM dbo.dwSource AS DS
WHERE EXISTS
(
    SELECT 1 
    FROM dbo.dwTarget AS DT
    WHERE
        DT.ppw_id = DS.ppw_id
        AND DT.mytableid = DS.mytableid
        AND DT.row_hash <> DS.row_hash
)
GROUP BY
    DS.ppw_id,
    DS.mytableid;
Run Code Online (Sandbox Code Playgroud)

关键位置计划

如果测试表明此查询将受益于并行性,则可以添加OPTION (USE HINT ('ENABLE_PARALLEL_PLAN_PREFERENCE'))提示以提供:

平行钥匙定位计划

现在我们有了键,我们可以使用以下命令告诉优化器有关唯一性的信息:

ALTER TABLE #Delta
ADD PRIMARY KEY CLUSTERED (ppw_id, mytableid);
Run Code Online (Sandbox Code Playgroud)

那么最后的更新是:

UPDATE DT
SET DT.other_columns = DS.other_columns
FROM #Delta AS DEL
JOIN dbo.dwTarget AS DT WITH (INDEX(1))
    ON DT.ppw_id = DEL.ppw_id
    AND DT.mytableid = DEL.mytableid
JOIN dbo.dwSource AS DS
    ON DS.loadkey = DEL.loadkey;
Run Code Online (Sandbox Code Playgroud)

更新计划

这确保非键列只查找实际更新的行。该WITH (INDEX(1))提示可以确保行集共享,可以使用(因此指数直接寻求提供更新的位置)。如果测试表明优化器自然选择的替代计划在实践中更好,则可以省略这一点。请注意,在此处选择嵌套循环很重要。您可能需要使用类似OPTION (FAST 1). 如果更新的行数确实总是很小的一部分,优化器应该自然地选择嵌套循环计划。

列存储

键位置计划(使用 Right Semi Merge Join)仍然相当昂贵,因为读取和测试两个表中的所有行。

如果您对索引有完全的自由(并且没有其他重大缺点),则可以通过静态(未更新)列上的几个辅助列存储索引获得潜在的最佳计划:

CREATE NONCLUSTERED COLUMNSTORE INDEX nccsi 
ON dbo.dwSource (ppw_id, mytableid, loadkey, row_hash);

CREATE NONCLUSTERED COLUMNSTORE INDEX nccsi 
ON dbo.dwTarget (ppw_id, mytableid, row_hash);
Run Code Online (Sandbox Code Playgroud)

这使得关键位置计划:

列存储键位置计划

以散列的内存授予为代价,该计划为所有运算符(并行插入除外)提供纯批处理模式操作,以及早期位图半连接减少。这可能会非常快地执行。

使用批处理模式执行时,列存储性能最令人印象深刻,并且数据以最佳方式排列在高度压缩的段中。与行存储相比,数据更改可能更慢,并且由于删除的行位图和行保存在行存储增量段中,可能会导致性能降低。选择和维护最佳列存储配置不一定是微不足道的,请参阅从Columnstore 索引 - 概述开始的文档。


这些是您可以根据自己的情况进行调查和应用(或不应用)的主要替代方案。

您还可以考虑将哈希码列设为固定长度binary而不是varbinary,假设您使用的任何哈希实现都会产生固定长度的结果。我还假设您很高兴接受散列未检测到更改的小机会。