PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Löschen Sie das übergeordnete Element, wenn es nicht von einem anderen untergeordneten Element referenziert wird

In PostgreSQL 9.1 oder höher Sie können dies mit einer einzigen Anweisung unter Verwendung eines datenmodifizierenden CTE tun . Dies ist im Allgemeinen weniger fehleranfällig. Es minimiert der Zeitrahmen zwischen den beiden DELETES, in denen ein Rennzustand vorliegt könnte bei gleichzeitigen Operationen zu überraschenden Ergebnissen führen:

WITH del_child AS (
    DELETE FROM child
    WHERE  child_id = 1
    RETURNING parent_id, child_id
    )
DELETE FROM parent p
USING  del_child x
WHERE  p.parent_id = x.parent_id
AND    NOT EXISTS (
   SELECT 1
   FROM   child c
   WHERE  c.parent_id = x.parent_id
   AND    c.child_id <> x.child_id   -- !
   );

SQL-Geige.

Das Kind wird in jedem Fall gelöscht. Ich zitiere das Handbuch:

Datenmodifizierende Anweisungen in WITH werden genau einmal und immer vollständig ausgeführt , unabhängig davon, ob die primäre Abfrage alle (oder tatsächlich alle) ihrer Ausgabe liest. Beachten Sie, dass dies von der Regel für SELECT abweicht in WITH :wie im vorherigen Abschnitt angegeben, Ausführung eines SELECT wird nur so weit getragen, wie die primäre Abfrage ihre Ausgabe erfordert.

Der Elternteil wird nur gelöscht, wenn er keinen anderen hat Kinder.
Beachten Sie die letzte Bedingung. Anders als man erwarten könnte, ist dies notwendig, denn:

Die Unteranweisungen in WITH werden gleichzeitig ausgeführt miteinander und mit der Hauptabfrage. Daher bei Verwendung von datenmodifizierenden Anweisungen in WITH , ist die Reihenfolge, in der die angegebenen Aktualisierungen tatsächlich erfolgen, unvorhersehbar. Alle Anweisungen werden mit demselben Snapshot ausgeführt (siehe Kapitel 13), sodass sie die Auswirkungen der anderen auf die Zieltabellen nicht „sehen“ können.

Fettdruck von mir.
Ich habe den Spaltennamen parent_id verwendet anstelle der nicht beschreibenden id .

Rennbedingung beseitigen

Um mögliche Race-Conditions zu eliminieren habe ich oben komplett erwähnt , sperren Sie zuerst die übergeordnete Zeile . Natürlich alle ähnliche Operationen müssen dem gleichen Verfahren folgen, damit es funktioniert.

WITH lock_parent AS (
   SELECT p.parent_id, c.child_id
   FROM   child  c
   JOIN   parent p ON p.parent_id = c.parent_id
   WHERE  c.child_id = 12              -- provide child_id here once
   FOR    NO KEY UPDATE                -- locks parent row.
   )
 , del_child AS (
   DELETE FROM child c
   USING  lock_parent l
   WHERE  c.child_id = l.child_id
   )
DELETE FROM parent p
USING  lock_parent l
WHERE  p.parent_id = l.parent_id
AND    NOT EXISTS (
   SELECT 1
   FROM   child c
   WHERE  c.parent_id = l.parent_id
   AND    c.child_id <> l.child_id   -- !
   );

Auf diese Weise nur eine Transaktion zu einem Zeitpunkt kann denselben Elternteil sperren. Es kann also nicht passieren, dass mehrere Transaktionen Kinder desselben Elternteils löschen, andere Kinder noch sehen und den Elternteil verschonen, während danach alle Kinder weg sind. (Aktualisierungen von Nicht-Schlüsselspalten sind weiterhin mit FOR NO KEY UPDATE erlaubt .)

Wenn solche Fälle nie vorkommen oder Sie damit leben können (fast nie), ist die erste Abfrage billiger. Andernfalls ist dies der sichere Pfad.

FOR NO KEY UPDATE wurde mit Postgres 9.4 eingeführt. Details im Handbuch. Verwenden Sie in älteren Versionen die stärkere Sperre FOR UPDATE stattdessen.