Warum können in einen CTE eingefügte Zeilen nicht in derselben Anweisung aktualisiert werden?

11

In PostgreSQL 9.5 wird anhand einer einfachen Tabelle Folgendes erstellt:

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

Ich führe SQL aus, um einen Wert einzufügen, und aktualisiere ihn dann in derselben Anweisung:

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

Das Ergebnis ist, dass das UPDATE ignoriert wird:

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

Warum ist das? Ist diese Einschränkung Teil des SQL-Standards (dh in anderen Datenbanken vorhanden) oder etwas Spezifisches für PostgreSQL, das möglicherweise in Zukunft behoben wird? In der WITH- Abfragedokumentation heißt es, dass mehrere UPDATEs nicht unterstützt werden, INSERTs und UPDATEs jedoch nicht erwähnt werden.

Jeff Turner
quelle

Antworten:

13

Alle Aussagen in einem CTE erfolgen praktisch gleichzeitig. Das heißt, sie basieren auf demselben Snapshot der Datenbank.

Der UPDATEsieht den gleichen Status der zugrunde liegenden Tabelle wie der INSERT, was bedeutet, dass die Zeile mit val = 1noch nicht vorhanden ist. Das Handbuch verdeutlicht hier:

Alle Anweisungen werden mit demselben Snapshot ausgeführt (siehe Kapitel 13 ), sodass sie die Auswirkungen des anderen auf die Zieltabellen nicht "sehen" können.

Jede Anweisung kann in der RETURNINGKlausel sehen, was von einem anderen CTE zurückgegeben wird . Die zugrunde liegenden Tabellen sehen für sie jedoch gleich aus.

Sie benötigen zwei Anweisungen (in einer einzigen Transaktion) für das, was Sie versuchen. Das gegebene Beispiel sollte eigentlich nur ein einzelnes sein INSERT, aber das kann an dem vereinfachten Beispiel liegen.

Erwin Brandstetter
quelle
13

Dies ist eine Umsetzungsentscheidung. Es wird in der Postgres-Dokumentation unter WITHAbfragen (allgemeine Tabellenausdrücke) beschrieben . Es gibt zwei Absätze, die sich auf das Problem beziehen.

Erstens der Grund für das beobachtete Verhalten:

Die Sub-Anweisungen in WITHgleichzeitig ausgeführt werden miteinander und mit der Hauptabfrage . Wenn Sie datenmodifizierende Anweisungen in verwenden WITH, ist die Reihenfolge, in der die angegebenen Aktualisierungen tatsächlich stattfinden, daher nicht vorhersehbar. Alle Anweisungen werden mit demselben Snapshot ausgeführt (siehe Kapitel 13), sodass sie die Auswirkungen des anderen auf die Zieltabellen nicht "sehen" können. Dies verringert die Auswirkungen der Unvorhersehbarkeit der tatsächlichen Reihenfolge der Zeilenaktualisierungen und bedeutet, dass RETURNINGDaten die einzige Möglichkeit sind, Änderungen zwischen verschiedenen WITHUnteranweisungen und der Hauptabfrage zu kommunizieren . Ein Beispiel dafür ist, dass in ...

Nachdem ich einen Vorschlag zusammen gepostet habe pgsql-docs gepostet hatte, erklärte Marko Tiikkaja (was mit Erwins Antwort übereinstimmt):

Die Fälle Insert-Update und Insert-Delete funktionieren nicht, da die UPDATEs und DELETEs die INSERTed-Zeilen nicht sehen können, da ihr Snapshot vor dem INSERT erstellt wurde. An diesen beiden Fällen ist nichts Unvorhersehbares.

Der Grund, warum Ihre Aussage nicht aktualisiert wird, kann im ersten Absatz (über "Schnappschüsse") erläutert werden. Wenn Sie CTEs ändern, werden alle und die Hauptabfrage ausgeführt und "sehen" denselben Snapshot der Daten (Tabellen) wie unmittelbar vor der Ausführung der Anweisung. CTEs können mithilfe der RETURNINGKlausel Informationen darüber, was sie eingefügt / aktualisiert / gelöscht haben, aneinander und an die Hauptabfrage übergeben , aber sie können die Änderungen in den Tabellen nicht direkt sehen. Mal sehen, was in Ihrer Aussage passiert:

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

Wir haben 2 Teile, den CTE ( newval):

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

und die Hauptabfrage:

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

Der Ablauf der Ausführung ist ungefähr so:

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

Als Ergebnis, wenn die Hauptabfrage dem beitritt tbl (wie im Snapshot dargestellt) mit der newvalTabelle verknüpft, wird daher eine leere Tabelle mit einer einzeiligen Tabelle verknüpft. Offensichtlich werden 0 Zeilen aktualisiert. Die Anweisung kam also nie wirklich dazu, die neu eingefügte Zeile zu ändern, und das sehen Sie.

Die Lösung in Ihrem Fall besteht darin, entweder die Anweisung neu zu schreiben, um die richtigen Werte einzufügen, oder 2 Anweisungen zu verwenden. Eine, die eingefügt und eine zweite aktualisiert werden muss.


Es gibt andere, ähnliche Situationen, als ob die Anweisung ein INSERTund dann ein DELETEin denselben Zeilen hätte. Das Löschen würde aus genau den gleichen Gründen fehlschlagen.

Einige andere Fälle mit Update-Update und Update-Delete sowie deren Verhalten werden in einem folgenden Absatz auf derselben Dokumentenseite erläutert.

Der Versuch, dieselbe Zeile zweimal in einer einzelnen Anweisung zu aktualisieren, wird nicht unterstützt. Es findet nur eine der Änderungen statt, aber es ist nicht einfach (und manchmal nicht möglich), zuverlässig vorherzusagen, welche. Dies gilt auch für das Löschen einer Zeile, die bereits in derselben Anweisung aktualisiert wurde: Es wird nur die Aktualisierung durchgeführt. Daher sollten Sie generell vermeiden, eine einzelne Zeile zweimal in einer einzelnen Anweisung zu ändern. Vermeiden Sie insbesondere das Schreiben von WITH-Unteranweisungen, die sich auf dieselben Zeilen auswirken können, die durch die Hauptanweisung oder eine Unteranweisung eines Geschwisters geändert wurden. Die Auswirkungen einer solchen Aussage sind nicht vorhersehbar.

Und in der Antwort von Marko Tiikkaja:

Die Fälle Update-Update und Update-Delete werden explizit nicht durch dieselben zugrunde liegenden Implementierungsdetails verursacht (wie die Fälle Insert-Update und Insert-Delete).
Der Update-Update-Fall funktioniert nicht, da er intern wie das Halloween-Problem aussieht und Postgres nicht wissen kann, welche Tupel zweimal aktualisiert werden könnten und welche das Halloween-Problem wieder einführen könnten.

Der Grund ist also der gleiche (wie modifizierende CTEs implementiert werden und wie jeder CTE denselben Snapshot sieht), aber die Details unterscheiden sich in diesen beiden Fällen, da sie komplexer sind und die Ergebnisse im Fall von Update-Update unvorhersehbar sein können.

Im Insert-Update (wie in Ihrem Fall) und einem ähnlichen Insert-Delete sind die Ergebnisse vorhersehbar. Nur das Einfügen erfolgt, da der zweite Vorgang (Aktualisieren oder Löschen) die neu eingefügten Zeilen nicht sehen und beeinflussen kann.


Die vorgeschlagene Lösung ist jedoch für alle Fälle gleich, in denen versucht wird, dieselben Zeilen mehrmals zu ändern: Tun Sie es nicht. Schreiben Sie entweder Anweisungen, die jede Zeile einmal ändern, oder verwenden Sie separate (2 oder mehr) Anweisungen.

ypercubeᵀᴹ
quelle