web-dev-qa-db-ja.com

CTEに挿入された行を同じステートメントで更新できないのはなぜですか?

PostgreSQL 9.5では、次のように作成された単純なテーブルがあるとします。

create table tbl (
    id serial primary key,
    val integer
);

SQLを実行して値を挿入し、次に同じステートメントでUPDATEします。

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

その結果、UPDATEは無視されます。

testdb=> select * from tbl;
┌────┬─────┐
│ id │ val │
├────┼─────┤
│  1 │   1 │
└────┴─────┘

どうしてこれなの?この制限はSQL標準(つまり、他のデータベースに存在)の一部ですか、それとも将来修正される可能性があるPostgreSQLに固有の何かですか? WITHクエリ のドキュメントには、複数のUPDATEはサポートされていないと記載されていますが、INSERTとUPDATEについては触れられていません。

13
Jeff Turner

CTEのすべてのステートメントは、実質的に同時に発生します。つまり、データベースの同じスナップショットに基づいています。

UPDATEは、基礎となるテーブルの状態をINSERTと同じ状態で表示します。つまり、val = 1はまだありません。 マニュアルはここを明確にします:

すべてのステートメントは同じsnapshotChapter 1 を参照)で実行されるため、相互の影響を「見る」ことができませんターゲットテーブル。

各ステートメントcanは、RETURNING句で別のCTEによって返されたものを確認できます。ただし、基になるテーブルはすべて同じように見えます。

2つのステートメントが(単一のトランザクションで)実行しようとしていることのために必要になります。与えられた例は、最初は本当に単一のINSERTである必要がありますが、これは単純化された例が原因である可能性があります。

15

これは実装の決定です。 Postgresのドキュメント WITH Queries(Common Table Expressions) で説明されています。この問題に関連する2つの段落があります。

まず、観察された動作の理由:

WITHのサブステートメントは同時に実行されます互いにそしてメインクエリを使用して。したがって、WITHでデータ変更ステートメントを使用する場合、指定された更新が実際に行われる順序は予測できません。 すべてのステートメントは同じスナップショット(第13章を参照)で実行されるため、ターゲットテーブルへの相互の影響を「見る」ことができません。これにより、行の更新の実際の順序が予測できないことによる影響が緩和されます、とは、RETURNINGデータが、異なるWITHサブステートメントとメインクエリの間で変更を伝達する唯一の方法であることを意味します。これの例は、.. 。

pgsql-docs に沿って提案を投稿した後、Marko Tiikkajaが説明しました(これはErwinの回答に同意します)。

INSERTが発生する前にスナップショットが取得されているため、UPDATEとDELETEにはINSERTされた行を表示する方法がないため、insert-updateとinsert-deleteのケースは機能しません。これらの2つのケースについて予測できないことは何もありません。

したがって、ステートメントが更新されない理由は、上記の最初の段落(「スナップショット」について)で説明できます。 CTEを変更すると何が起こるかというと、それらすべてとメインクエリが実行され、ステートメント実行の直前と同じように、データ(テーブル)の同じスナップショットが「参照」されます。 CTEはRETURNING句を使用して、相互に挿入したり、更新したり、削除したりしたものに関する情報をメインクエリに渡すことができますが、テーブルの変更を直接確認することはできません。だからあなたのステートメントで何が起こるか見てみましょう:

WITH newval AS (
    INSERT INTO tbl(val) VALUES (1) RETURNING id
) UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id;

CTE(newval)の2つの部分があります。

-- newval
     INSERT INTO tbl(val) VALUES (1) RETURNING id

そしてメインクエリ:

-- main 
UPDATE tbl SET val=2 FROM newval WHERE tbl.id=newval.id

実行の流れは次のようなものです。

           initial data: tbl
                id │ val 
                 (empty)
               /         \
              /           \
             /             \
    newval:                 \
       tbl (after newval)    \
           id │ val           \
            1 │   1           |
                              |
    newval: returns           |
           id                 |
            1                 |
               \              |
                \             |
                 \            |
                    main query

その結果、メインクエリがtbl(スナップショットに表示)をnewvalテーブルと結合すると、空のテーブルと1行のテーブルが結合されます。明らかにそれは0行を更新します。したがって、ステートメントが新しく挿入された行を変更することはありませんでした。

あなたの場合の解決策は、最初に正しい値を挿入するようにステートメントを書き直すか、2つのステートメントを使用することです。 1つは挿入し、もう1つは更新します。


ステートメントにINSERTがあり、次に同じ行にDELETEがあるような、他の同様の状況があります。削除もまったく同じ理由で失敗します。

Update-updateとupdate-deleteを使用する他のいくつかのケースとその動作は、同じドキュメントページの次の段落で説明されています。

1つのステートメントで同じ行を2回更新することはサポートされていません。変更は1つしか行われませんが、どれを変更するかを確実に予測することは簡単ではありません(場合によっては不可能です)。これは、同じステートメントですでに更新された行の削除にも適用されます。更新のみが実行されます。したがって、通常、1つのステートメントで1つの行を2回変更することは避けてください。 特に、メインステートメントまたは兄弟サブステートメントによって変更された同じ行に影響を与える可能性のあるWITHサブステートメントの記述は避けてください。このようなステートメントの影響は予測できません。

そして、Marko Tiikkajaからの返信で:

Update-updateとupdate-deleteのケースは、(insert-updateとinsert-deleteのケースと同じ)基礎となる実装の詳細が原因で、明示的にではありません
update-updateケースは内部的にはハロウィーンの問題のように見えるため機能しません。Postgresは、どのタプルが2回更新しても問題がないか、どのタプルがハロウィーンの問題を再帰するかを知る方法がありません。

したがって、理由は同じです(CTEの変更方法と各CTEが同じスナップショットを表示する方法)。ただし、これら2つのケースでは詳細が異なります。複雑なため、更新と更新のケースでは結果が予測できないためです。

Insert-update(あなたの場合)と同様のinsert-deleteの結果は予測可能です。 2番目の操作(更新または削除)には、新しく挿入された行を表示して影響を与える方法がないため、挿入のみが行われます。


ただし、推奨される解決策は、同じ行を複数回変更しようとするすべてのケースで同じです:しないでください。各行を一度変更するステートメントを記述するか、個別の(2つ以上の)ステートメントを使用します。

15
ypercubeᵀᴹ