为什么在这个 UPDATE 计划中有一个聚合?

Mic*_*een 5 sql-server execution-plan update

鉴于这种

declare @Data table (id int, fact char(1));
declare @Summary table (id int, collected varchar(99));

insert @Data(id, fact)
values
    (1, 'a'),
    (1, 'b'),
    (2, 'c'),
    (2, 'd'),
    (2, 'e');

-- Form a list of unique id values
insert @Summary(id, collected) select distinct id, '' from @Data;

-- Accumulate the fact values into collected
update s
set collected = collected + d.fact
from @Summary as s
inner join @Data as d
    on d.id = s.id;

select * from @Summary;
Run Code Online (Sandbox Code Playgroud)

我曾预料

id   collected
---  ---------
1    ab
2    cde
Run Code Online (Sandbox Code Playgroud)

但是我得到的是

id   collected
---  ---------
1    a
2    c
Run Code Online (Sandbox Code Playgroud)

首先fact每个id写入和其他人跳过。原因从计划中一目了然

在此处输入图片说明

其中 Stream Aggregate 通过 per 的第一行id;5 行从嵌套循环传递给它,并将 2 行传递给计算标量。

结果和一般计划形状是相同的,有和没有主键。更改为临时表或真实表没有区别。我可以在 SQL Server 2017 和 2019 上重现。

我的问题是在计划中插入聚合的理论依据是什么?我最好的猜测是它是万圣节保护的一种形式。我知道它的目的是防止在当前扫描位置之前跳过一行并因此被第二次更新。我可以看到,在没有聚合的情况下,将在 @Summary 中作为一行应用在这里会被多次触及。不过,这似乎是万圣节保护的一个非常广泛的应用。

我知道有很多方法可以实现这一点 - STRING_AGG 是最明显的。我真正的用例是围绕构建 JSON。这只是一个最小的可重现示例。我在这里的具体问题是关于理解优化器的语义和行为。

Jos*_*ell 7

这不是万圣节保护,这是正常的UPDATE语义。

UPDATE 文档中查看此警告:

如果 UPDATE 语句包含的 FROM 子句未指定为每个更新的列出现只有一个值,即 UPDATE 语句不是确定性的,则该语句的结果是未定义的。

SET言不累积像你期望它* -它只是在汇总表更新一次,每个限定行。

由于连接导致收集的潜在值重复,优化器引入了一个流聚合 - 本质上它按堆的“主键”(行定位器,Bmk1000在执行计划中称为)进行分组,以获得仅一个值collectedin每一行。

查看计划 XML,您可以看到内部唯一的“ANY”聚合用于在 collect 和 fact 的可能值中进行选择:

<ScalarOperator ScalarString="ANY(@Summary.[collected] as [s].[collected])">
  <Aggregate AggType="ANY" Distinct="false">
    <ScalarOperator>
      <Identifier>
        <ColumnReference Table="@Summary" Alias="[s]" Column="collected" />
      </Identifier>
    </ScalarOperator>
  </Aggregate>
</ScalarOperator>
Run Code Online (Sandbox Code Playgroud)

*当您将变量赋值添加到混合中时,这种累积确实有效,尽管它仍然不受“支持”。这被称为“古怪的更新”(参考