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


12

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クエリドキュメントには、複数の更新がサポートされていないと言いますが、INSERTとUPDATEを言及していません。

回答:


14

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

UPDATE基礎となるテーブルの同じ状態見るINSERTと行を意味し、val = 1そこではない、まだ。マニュアルはここを明確にします:

すべてのステートメントは同じスナップショット第13章を参照)で実行されるため、ターゲットテーブルへの相互の影響を「確認」することはできません。

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

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


14

これは実装の決定です。これはPostgresのドキュメント、WITHクエリ(共通テーブル式)で説明されています。この問題に関連する2つの段落があります。

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

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

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

INSERTが発生する前にスナップショットが取得されているため、UPDATEとDELETEはINSERTされた行を確認する方法がないため、挿入と更新および挿入と削除のケースは機能しません。これら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;

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

-- 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つは更新します。


ステートメントに同じ行にanがINSERTあり、次にa がある場合など、他にも同様の状況があり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番目の操作(更新または削除)には、新しく挿入された行を表示して影響を与える方法がないため、挿入のみが行われます。


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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.