web-dev-qa-db-ja.com

CTEが失われた更新を受け入れるのはなぜですか?

クレイグリンガーがコメントしたときの意味がわかりません。

挿入トランザクションがロールバックすると、このソリューションは更新が失われる可能性があります。 UPDATEが行に影響を与えることを強制するチェックはありません。

オン https://stackoverflow.com/a/8702291/14731 。失われた更新がどのように発生するかを示すイベントのサンプルシーケンス(スレッド1がX、スレッド2がYなど)を提供してください。

8
Gili

おそらく、2つの別個のステートメントについて、前の回答にそのコメントを追加するつもりだったと思います。一年以上前だったので、もう完全にはわかりません。

WCTEベースのクエリは本来想定されている問題を実際に解決するわけではありませんが、1年後にもう一度確認すると、wCTEバージョンの更新が失われる可能性はありません。

(これらのソリューションはすべて、トランザクションごとに1つの行だけを変更しようとする場合にのみ機能します。1つのトランザクションで複数の変更を行おうとすると、ロールバックでの再試行ループが必要になるため、混乱が生じます。最低でも変更ごとにセーブポイントを使用する必要があります。)

失われた更新の対象となる2ステートメントバージョン。

2つの個別のステートメントを使用するバージョン は、アプリケーションがUPDATEステートメントとINSERTステートメントから影響を受けた行の数をチェックし、両方の場合に再試行しない限り、更新が失われる可能性がありますゼロです。

READ COMMITTED分離で2つのトランザクションがある場合に何が起こるか想像してみてください。

  • TX1はUPDATEを実行します(影響なし)
  • TX1はINSERTを実行します(行を挿入します)
  • TX2はUPDATEを実行します(影響なし、TX1によって挿入された行はまだ表示されていません)
  • TX1 COMMITs。
  • TX2はINSERTを実行します。これは、TX1によってコミットされた行を確認できる新しいスナップショットを取得します。 TX2がTX1によって挿入された行を見ることができるようになったため、EXISTS句はtrueを返します。

したがって、TX2は効果がありません。アプリが更新からの行数をチェックし、両方がゼロ行を報告した場合に挿入と再試行を行わない限り、トランザクションが影響を及ぼさなかったことをアプリは認識せず、楽に続行します。

影響を受ける行数を確認できる唯一の方法は、マルチステートメントではなく2つの個別のステートメントとして実行するか、プロシージャを使用することです。

SERIALIZABLE分離を使用できますが、シリアル化の失敗に対処するには再試行ループが必要です。

INSERTUPDATEが別のクエリではなく行に影響するかどうかに条件があるため、wCTEバージョンは失われた更新の問題から保護します。

WCTEは固有の違反を排除しません

書き込み可能なCTEバージョン はまだ信頼できるアップサートではありません。

これを同時に実行する2つのトランザクションを考えます。

  • どちらもVALUES句を実行します。

  • これで、どちらもUPDATE部分を実行します。 UPDATEs where句に一致する行がないため、どちらも更新から空の結果セットを返し、変更を加えません。

  • 今度は両方ともINSERT部分を実行します。 UPDATEは両方のクエリでゼロ行を返したため、どちらも行をINSERTしようとしました。

1つは成功します。 1つは固有の違反をスローして中止します。

アプリがクエリ(つまり、きちんと作成されたアプリ)からのエラー結果をチェックして再試行する限り、これはデータ損失の懸念の原因にはなりませんが、既存の2つのステートメントのバージョンよりも優れたソリューションになります。再試行ループの必要性がなくなるわけではありません。

WCTEが既存の2ステートメントバージョンよりも優れている点は、テーブルに対して個別のクエリを使用する代わりに、UPDATEの出力を使用してINSERTを決定することです。これは一部最適化ですが、更新が失われる2ステートメントバージョンの問題から一部を保護します。下記参照。

WCTEをSERIALIZABLE分離で実行できますが、一意の違反ではなく、シリアル化の失敗が発生します。再試行ループの必要性は変わりません。

WCTEは更新の喪失に対してしないようです

私のコメントは、この解決策は更新が失われる可能性があることを示唆していましたが、検討したところ、私は間違っていたのではないかと思います。

1年以上前であり、正確な状況を思い出すことはできませんが、ある挿入トランザクションが別の挿入トランザクションまたは挿入ロールを待機できるようにするために、一意のインデックスにトランザクション可視性ルールからの部分的な例外があるという事実を見逃したと思います続行する前に戻ってください。

または、wCTEのINSERTが、候補行がテーブルに存在するかどうかではなく、UPDATEが行に影響を与えるかどうかに依存しているという事実を見落としたかもしれません。

一意のインデックスで競合するINSERTsがコミット/ロールバックを待機しています

行を挿入して、クエリの1つのコピーを実行するとします。変更はまだコミットされていません。新しいタプルはヒープと一意のインデックスに存在しますが、分離レベルに関係なく、まだ他のトランザクションからは見えません。

これで、クエリの別のコピーが実行されます。最初のコピーがコミットされていないため、挿入された行はまだ表示されていないため、更新は何にも一致しません。クエリは引き続き挿入を試みます。これにより、別の進行中のトランザクションが同じキーを挿入していることがわかり、そのトランザクションがコミットまたはロールバックするのを待機してブロックされます

最初のトランザクションがコミットすると、上記のように、2番目のトランザクションは一意の違反で失敗します。最初のトランザクションがロールバックすると、2番目のトランザクションは代わりに挿入を続行します。

INSERTUPDATE行数に依存しているため、更新が失われるのを防ぎます

2つのステートメントの場合とは異なり、wCTEは更新の消失に対して脆弱ではないと思います。

UPDATEが機能しない場合、INSERTは常に実行されます。これは、UPDATEが外部テーブルの状態ではなく、何かをしたかどうかに厳密に依存するためです。そのため、固有の違反で失敗する可能性がありますが、影響を与えずに更新を完全に失うことはありません。

14
Craig Ringer