web-dev-qa-db-ja.com

この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;

期待していた

id   collected
---  ---------
1    ab
2    cde

しかし、私が得るものは

id   collected
---  ---------
1    a
2    c

factごとの最初のidが書き込まれ、その他はスキップされます。その理由は計画から明らかです

enter image description here

stream Aggregateはidごとに最初の行を通過します。ネストされたループから5行が渡され、2行が計算スカラーに渡されます。

結果と一般的なプランの形状は、主キーがあってもなくても同じです。一時テーブル、または実際のテーブルに変更しても違いはありません。 SQL Server 2017および2019で再現できます。

私の質問は、集計を計画に挿入するための理論的根拠は何ですか?私の推測では、これはハロウィーンの保護の一種です。その目的は、行が現在のスキャン位置の前を飛び越えて、2回目に更新されるのを防ぐことであると理解しました。 @Summaryの行としてここに適用すると、集計がない場合は何度も影響を受けることがわかります。しかし、それはハロウィン保護の非常に広範なアプリケーションのようです。

私はこれを達成する方法があることを知っています-STRING_AGGが最も明白です。私の本当のユースケースは、JSONの構築です。これは、最小限の再現可能な例です。ここでの具体的な質問は、オプティマイザのセマンティクスと動作を理解することです。

5
Michael Green

これはハロウィーンの保護ではなく、通常のUPDATEセマンティクスです。

UPDATE のドキュメントからこの警告を確認してください:

ステートメントに、更新される列オカレンスごとに1つの値しか使用できないように指定されていないFROM句が含まれている場合、つまりUPDATEステートメントが確定的でない場合、UPDATEステートメントの結果は未定義です。

SETステートメントは、期待したように蓄積されません*-これは、サマリーテーブルの該当する各行を1回更新するだけです。

結合の結果、収集された値が重複する可能性があるため、オプティマイザはStream Aggregateを導入しました。本質的には、ヒープの「主キー」(行ロケータ、Bmk1000を実行プランに追加して、各行のcollectedの値を1つだけ取得します。

プランXMLを見ると、内部および 「ANY」集約 が、収集されたファクトの可能な値の中から選択するために使用されていることがわかります。

<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>

*変数の割り当てをミックスに追加すると、この種類の累積は機能しますが、「サポートされていません」。これは「風変わりな更新」と呼ばれます( reference

7
Josh Darnell

Joshの回答(Joshに感謝!)は、決定論について考えさせられました。

基本的な使用法がわかりますupdate table set column = value、重複するソース行がある場合、ソースから一致する値を宛先に書き込む質問に回答する必要があります。最も簡単な実装は、イテレータを最後まで実行させることです。ストレージエンジンが最後にクエリに渡す行は、ターゲットテーブルに書き込まれる値です。テーブルには固有の順序がないため、どの行が最後であるかは不確定です。

その不確定性を考えると、オプティマイザは好きな行を自由に選択できます。ステートメントのセマンティクスは、最初、最後、または任意の中間行が適用されるかどうかに関係なく満たされます。それを考えると、最初の行を選択し、後続の行を繰り返すという冗長な作業を回避することは理にかなっています。これはパフォーマンスの最適化ですが、更新のセマンティクスに対する制約ではなく、結果の正確さを保証するためにも存在しません。

私の例に移ると、オプティマイザが同じ形状を検出し(複数の一致するソース値で更新)、パフォーマンスの最適化を適用する方法を確認できます。これは、Microsoftがupdateステートメントの実装を指定した方法です。リレーショナルモデルには基本的なものはありません。つまり、そうである必要があります。

オプティマイザが複数の一致する行があることを認識し、また実行されている集計があることを認識する別の実装があると思います(set column = column + value)。次に、重複を排除するStream Aggregate(ANY)のパフォーマンス最適化を注入するのではなく、集約を実行します。もちろん、これはTSQLアップデートの仕様の変更です。他の構成要素を通じて動作が利用できるので、MSがそのような変更を実装する価値があるとは思いません。

2
Michael Green