MVCC-3. Versionen von Zeilen

So haben wir die Themen rund um Isolationbetrachtet und einen Einschub zur Datenorganisation auf niedriger Ebenegemacht. Nun kommen wir zu dem spannendsten Teil — den Versionsnummern.

Header

Wie bereits erwähnt, kann jede Zeile gleichzeitig in der Datenbank in mehreren Versionen existieren. Eine Version muss von der anderen durch etwas unterschieden werden. Zu diesem Zweck hat jede Version zwei Markierungen, die die „Zeit“ dieser Version festlegen (xmin und xmax). In Anführungszeichen — weil nicht wirklich die Zeit als solche verwendet wird, sondern ein spezieller fortlaufender Zähler. Und dieser Zähler ist die Transaktionsnummer.

(Wie gewohnt ist es in Wirklichkeit komplizierter: Die Transaktionsnummer kann nicht immer erhöht werden, aufgrund der begrenzten Bitanzahl des Zählers. Aber diese Details werden wir im Detail behandeln, wenn wir zur Einfrierung kommen.)

Wenn eine Zeile erstellt wird, wird der Wert von xmin auf die Transaktionsnummer gesetzt, die den INSERT-Befehl ausgeführt hat, und xmax bleibt leer.

Wenn eine Zeile gelöscht wird, wird der Wert von xmax der aktuellen Version mit der Transaktionsnummer markiert, die das DELETE ausgeführt hat.

Wenn eine Zeile mit dem Befehl UPDATE geändert wird, werden tatsächlich zwei Operationen ausgeführt: DELETE und INSERT. In der aktuellen Version der Zeile wird xmax auf die Transaktions-ID gesetzt, die das UPDATE ausgeführt hat. Dann wird eine neue Version derselben Zeile erstellt; der Wert von xmin stimmt mit dem Wert von xmax der vorherigen Version überein.

Die Felder xmin und xmax gehören zum Header der Zeilenversion. Neben diesen Feldern enthält der Header auch andere, zum Beispiel:

  • infomask — eine Reihe von Bits, die die Eigenschaften dieser Version definieren. Es gibt ziemlich viele; die wichtigsten werden wir schrittweise durchgehen.
  • ctid — ein Verweis auf die nächste, neuere Version derselben Zeile. Bei der neuesten, aktuellen Version der Zeile verweist ctid auf diese Version selbst. Die Nummer hat das Format (x,y), wobei x die Seitenummer und y die Ordinalnummer des Zeigers im Array ist.
  • Bitmaske für undefinierte Werte — kennzeichnet die Spalten dieser Version, die einen undefinierten Wert (NULL) enthalten. NULL ist kein gewöhnlicher Wert für Datentypen, weshalb dieses Merkmal separat gespeichert werden muss.

Dies führt dazu, dass der Header ziemlich groß wird – mindestens 23 Bytes für jede Zeilenversion und in der Regel mehr aufgrund der NULL-Bitkarte. Wenn die Tabelle "schmal" ist (d.h. wenige Spalten enthält), können die Overhead-Kosten größer sein als die nützlichen Informationen.

Einfügen

Lassen Sie uns näher betrachten, wie auf niedriger Ebene mit Strings gearbeitet wird, und anfangen mit der Einfügung.

Für Experimente erstellen wir eine neue Tabelle mit zwei Spalten und einem Index für eine davon:

=> CREATE TABLE t(
  id serial,
  s text
);
=> CREATE INDEX ON t(s);

Wir fügen eine Zeile ein, nachdem wir eine Transaktion gestartet haben.

=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');

Hier ist die Nummer unserer aktuellen Transaktion:

=> SELECT txid_current();
 txid_current 
--------------
         3664
(1 row)

Schauen wir uns den Inhalt der Seite an. Die Funktion heap_page_items aus der Erweiterung pageinspect ermöglicht es, Informationen über Zeiger und Versionen von Zeilen zu erhalten:

=> SELECT * FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-------------------
lp          | 1
lp_off      | 8160
lp_flags    | 1
lp_len      | 32
t_xmin      | 3664
t_xmax      | 0
t_field3    | 0
t_ctid      | (0,1)
t_infomask2 | 2
t_infomask  | 2050
t_hoff      | 24
t_bits      | 
t_oid       | 
t_data      | x0100000009464f4f

Beachten Sie, dass der Begriff Heap in PostgreSQL für Tabellen steht. Dies ist eine weitere seltsame Verwendung des Begriffs — ein Heap ist eine bekannte Datenstruktur, die mit einer Tabelle nichts gemein hat. Hier wird das Wort im Sinne von „alles in einem Haufen“ verwendet, im Gegensatz zu geordneten Indizes.

Die Funktion zeigt Daten „wie sie sind“ in einem für das menschliche Verständnis schwierigen Format. Um es nachvollziehbar zu machen, lassen wir nur einen Teil der Informationen und entschlüsseln sie:

=> SELECT '(0,'||lp||')' AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'unused'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'redirect to '||lp_off
         WHEN 3 THEN 'dead'
       END AS state,
       t_xmin as xmin,
       t_xmax as xmax,
       (t_infomask & 256) > 0  AS xmin_commited,
       (t_infomask & 512) > 0  AS xmin_aborted,
       (t_infomask & 1024) > 0 AS xmax_commited,
       (t_infomask & 2048) > 0 AS xmax_aborted,
       t_ctid
FROM heap_page_items(get_raw_page('t',0)) gx
-[ RECORD 1 ]-+-------
ctid          | (0,1)
state         | normal
xmin          | 3664
xmax          | 0
xmin_commited | f
xmin_aborted  | f
xmax_commited | f
xmax_aborted  | t
t_ctid        | (0,1)

Das haben wir getan:

  • Wir haben eine Null zur Zeiger-Nummer hinzugefügt, um sie in das gleiche Format zu bringen wie t_ctid: (Seitenzahl, Zeiger-Nummer).
  • Wir haben den Zustand des Zeigers lp_flags entschlüsselt. Hier ist er "normal" - das bedeutet, dass der Zeiger tatsächlich auf die Version der Zeile verweist. Andere Werte betrachten wir später.
  • Von allen Informationsbits haben wir bisher nur zwei Paare hervorgehoben. Die Bits xmin_committed und xmin_aborted zeigen an, ob die Transaktion mit der Nummer xmin bestätigt (oder abgebrochen) wurde. Zwei ähnliche Bits beziehen sich auf die Transaktion mit der Nummer xmax.

Was sehen wir also? Bei der Einfügung einer Zeile in die Tabellenseite wird ein Zeiger mit der Nummer 1 erstellt, der auf die erste und einzige Version der Zeile verweist.

In der Version der Zeile ist das Feld xmin mit der Nummer der aktuellen Transaktion gefüllt. Die Transaktion ist noch aktiv, daher sind beide Bits xmin_committed und xmin_aborted nicht gesetzt.

Das Feld ctid der Version der Zeile verweist auf dieselbe Zeile. Das bedeutet, dass es keine neuere Version gibt.

Das Feld xmax ist mit der fiktiven Nummer 0 gefüllt, da diese Version der Zeile nicht gelöscht wurde und aktuell ist. Die Transaktionen beachten diese Nummer nicht, da das Bit xmax_aborted gesetzt ist.

Machen wir einen weiteren Schritt zur Verbesserung der Lesbarkeit, indem wir die Informationsbits den Transaktionsnummern hinzufügen. Und wir erstellen eine Funktion, da die Anfrage uns noch mehrmals benötigt werden wird:

=> ERSTELLEN SIE EINE FUNKTION heap_page(relname text, pageno integer)
GIBT TABELLE zurück(ctid tid, state text, xmin text, xmax text, t_ctid tid)
AS $$
SELECT (pageno,lp)::text::tid AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'nicht benutzt'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'weiterleiten zu '||lp_off
         WHEN 3 THEN 'tod'
       END AS state,
       t_xmin || CASE
         WHEN (t_infomask & 256) > 0 THEN ' (c)'
         WHEN (t_infomask & 512) > 0 THEN ' (a)'
         ELSE ''
       END AS xmin,
       t_xmax || CASE
         WHEN (t_infomask & 1024) > 0 THEN ' (c)'
         WHEN (t_infomask & 2048) > 0 THEN ' (a)'
         ELSE ''
       END AS xmax,
       t_ctid
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ SPRACHE SQL;

In dieser Form ist deutlich klarer, was im Header der Zeilenversion passiert:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 Zeile)

Ähnliche, aber wesentlich weniger detaillierte Informationen können auch aus der Tabelle selbst gewonnen werden, indem die Pseudospalten xmin und xmax verwendet werden:

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3664 |    0 |  1 | FOO
(1 Zeile)

Commit

Nach erfolgreichem Abschluss der Transaktion muss ihr Status gespeichert werden — zu kennzeichnen, dass sie abgeschlossen ist. Dazu wird eine Struktur verwendet, die XACT genannt wird (vor Version 10 wurde sie CLOG (Commit-Protokoll) genannt, und dieser Begriff kann an verschiedenen Stellen noch vorkommen).

XACT — es handelt sich nicht um eine Systemkatalogtabelle; es sind Dateien im Verzeichnis PGDATA/pg_xact. In diesen Dateien stehen für jede Transaktion zwei Bits zur Verfügung: committed und aborted — genau wie im Header der Zeile. Diese Informationen sind auf mehrere Dateien verteilt, um die Bequemlichkeit zu erhöhen. Wir werden in einem späteren Abschnitt noch einmal auf dieses Thema zurückkommen, wenn wir die Freeze-Option besprechen. Der Zugriff auf diese Dateien erfolgt seitenweise, wie bei allen anderen auch.

Wenn eine Transaktion in XACT festgeschrieben wird, wird für diese Transaktion das Bit committed gesetzt. Das ist alles, was bei der Festschreibung passiert (wir sprechen jedoch noch nicht über das Write-Ahead-Log).

Wenn eine andere Transaktion auf die Tabellenzeile zugreift, die wir gerade angesehen haben, muss sie sich einigen Fragen stellen.

  1. Ist die Transaktion xmin abgeschlossen? Wenn nicht, sollte die erstellte Zeile nicht sichtbar sein.
    Diese Überprüfung erfolgt durch den Zugriff auf eine weitere Struktur, die sich im gemeinsamen Speicher der Instanz befindet und ProcArray genannt wird. Darin befindet sich eine Liste aller aktiven Prozesse, und für jeden ist die Nummer seiner aktuellen (aktiven) Transaktion angegeben.
  2. Wenn sie abgeschlossen ist, dann wie – durch Fixierung oder Stornierung? Bei einer Stornierung sollten die Versionen der Zeilen ebenfalls nicht sichtbar sein.
    Dafür ist XACT genau richtig. Obwohl die letzten Seiten von XACT im Arbeitsspeicher gepuffert werden, ist es dennoch mühsam, jedes Mal XACT zu überprüfen. Daher wird der einmal ermittelte Status der Transaktion in den Bits xmin_committed und xmin_aborted der Zeilenversion festgehalten. Wenn eines dieser Bits gesetzt ist, wird der Status der Transaktion xmin als bekannt betrachtet und die nachfolgende Transaktion muss nicht mehr auf XACT zugreifen.

Warum werden diese Bits nicht von der Transaktion gesetzt, die den Einfügevorgang durchführt? Bei einer Einfügung weiß die Transaktion noch nicht, ob sie erfolgreich abgeschlossen wird. Zum Zeitpunkt der Fixierung ist bereits unklar, welche spezifischen Zeilen in welchen spezifischen Seiten geändert wurden. Es könnten viele Seiten betroffen sein, und es wäre nachteilig, sie zu merken. Darüber hinaus könnten einige Seiten aus dem Pufferspeicher auf die Festplatte verdrängt worden sein; sie erneut zu lesen, um die Bits zu ändern, würde die Fixierung erheblich verlangsamen.

Die Kehrseite der Einsparungen besteht darin, dass nach Änderungen jede Transaktion (selbst eine einfache Abfrage – SELECT) beginnen kann, Datenseiten im Zwischenspeicher zu verändern.

Lassen Sie uns die Änderung festhalten.

=> COMMIT;

Auf der Seite hat sich nichts verändert (aber wir wissen, dass der Status der Transaktion bereits in XACT aufgezeichnet wurde):

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 Zeile)

Jetzt muss die Transaktion, die als erste auf die Seite zugegriffen hat, den Status der Transaktion xmin bestimmen und ihn in die Informationsbits schreiben:

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 Zeile)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 0 (a) | (0,1)
(1 Zeile)

Entfernen

Beim Löschen einer Zeile wird die Nummer der aktuellen löschenden Transaktion im xmax-Feld der aktuellen Version gespeichert, und das Bit xmax_aborted wird zurückgesetzt.

Beachten Sie, dass der festgelegte xmax-Wert, der mit der aktiven Transaktion verbunden ist, als Zeilenblockierung fungiert. Wenn eine andere Transaktion plant, diese Zeile zu aktualisieren oder zu löschen, muss sie warten, bis die Transaktion xmax abgeschlossen ist. Mehr über Sperren werden wir später sprechen. Derzeit sei nur erwähnt, dass die Anzahl der Zeilenblockierungen unbegrenzt ist. Sie belegen keinen Platz im Arbeitsspeicher, und die Systemleistung leidet nicht unter ihrer Anzahl. Allerdings haben "lange" Transaktionen andere Nachteile, aber dazu später mehr.

Zeile löschen.

=> BEGIN;
=> DELETE FROM t;
=> SELECT txid_current();
 txid_current 
--------------
         3665
(1 Zeile)

Wir sehen, dass die Transaktionsnummer im Feld xmax eingetragen wurde, aber die Informationsbits nicht gesetzt sind:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 Zeile)

Abbrechen

Das Rückgängigmachen von Änderungen funktioniert ähnlich wie das Festschreiben, nur dass im XACT für die Transaktion das Abbruchbit gesetzt wird. Das Rückgängigmachen erfolgt genauso schnell wie das Festschreiben. Obwohl der Befehl ROLLBACK genannt wird, findet kein Rückschritt der Änderungen statt: Alles, was die Transaktion in den Datenseiten geändert hat, bleibt unverändert.

=> ROLLBACK;
=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 Zeile)

Beim Zugriff auf die Seite wird der Status überprüft und das Bit xmax_aborted in die Version der Zeile gesetzt. Die tatsächliche xmax-Nummer bleibt auf der Seite, wird aber nicht mehr betrachtet.

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 Zeile)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   |   xmax   | t_ctid 
-------+--------+----------+----------+--------
 (0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
(1 Zeile)

Aktualisierung

Das Update funktioniert so, als ob zunächst die aktuelle Version der Zeile gelöscht und dann eine neue eingefügt wird.

=> BEGIN;
=> UPDATE t SET s = 'BAR';
=> SELECT txid_current();
 txid_current 
--------------
         3666
(1 Zeile)

Die Abfrage gibt eine Zeile (neue Version) zurück:

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | BAR
(1 Zeile)

Auf der Seite sehen wir jedoch beide Versionen:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 3666  | (0,2)
 (0,2) | normal | 3666     | 0 (a) | (0,2)
(2 Zeilen)

Die gelöschte Version ist mit der Nummer der aktuellen Transaktion im xmax-Feld gekennzeichnet. Dieser Wert wird dabei über den alten geschrieben, da die vorherige Transaktion abgebrochen wurde. Das Bit xmax_aborted wurde zurückgesetzt, da der Status der aktuellen Transaktion noch unbekannt ist.

Die erste Version der Zeile verweist jetzt auf die zweite (t_ctid-Feld) als die neuere.

Auf der Indexseite erscheint ein zweiter Zeiger und eine zweite Zeile, die auf die zweite Version in der Tabellen-Seite verweist.

Wie auch beim Löschen dient der xmax-Wert in der ersten Zeile als Indikator dafür, dass die Zeile gesperrt ist.

Nun beenden wir die Transaktion.

=> COMMIT;

Indizes

Bisher haben wir nur über Tabellen-Seiten gesprochen. Was passiert aber innerhalb der Indizes?

Die Informationen in den Indexseiten hängen stark vom spezifischen Index-Typ ab. Sogar innerhalb eines Typs können unterschiedliche Seitenarten vorkommen. Beispielsweise hat ein B-Baum eine Metadatenseite und 'normale' Seiten.

Dennoch enthält eine Seite normalerweise ein Array von Zeigern auf die Zeilen sowie die Zeilen selbst (wie auch bei einer Tabellen-Seite). Außerdem wird am Ende der Seite Platz für spezielle Daten reserviert.

Zeilen in Indizes können ebenfalls sehr unterschiedliche Strukturen je nach Index-Typ aufweisen. Bei einem B-Baum enthalten die zu den Blattseiten gehörenden Zeilen den Wert des Indexschlüssels und einen Verweis (ctid) auf die entsprechende Tabelle. Im Allgemeinen kann ein Index jedoch ganz anders aufgebaut sein.

Der wichtigste Punkt ist, dass es in Indizes jeglicher Art keine Versionen von Zeilen gibt. Man kann auch sagen, dass jede Zeile genau einer Version entspricht. Mit anderen Worten, im Header einer Indexzeile gibt es keine Felder xmin und xmax. Es kann gesagt werden, dass die Links im Index auf alle tabellarischen Versionen von Zeilen verweisen – deshalb kann man nur durch einen Blick in die Tabelle verstehen, welche Version die Transaktion sehen wird. (Wie üblich ist das nicht die ganze Wahrheit. In einigen Fällen erlaubt die Sichtbarkeitskarte, den Prozess zu optimieren, aber darauf kommen wir später zurück.)

Auf der Indexseite finden wir Zeiger auf beide Versionen, sowohl auf die aktuelle als auch auf die alte:

= > SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1);
 itemoffset | ctid  
------------+-------
          1 | (0,2)
          2 | (0,1)
(2 Zeilen)

Virtuelle Transaktionen

In der Praxis nutzt PostgreSQL eine Optimierung, die es ermöglicht, Transaktionsnummern „einzusparen“.

Wenn eine Transaktion nur Daten liest, hat sie keinen Einfluss auf die Sichtbarkeit von Zeilenversionen. Daher weist der betreuende Prozess der Transaktion zunächst eine virtuelle Nummer (virtual xid) zu. Diese Nummer besteht aus einer Prozess-ID und einer fortlaufenden Zahl.

Die Vergabe dieser Nummer erfordert keine Synchronisation zwischen allen Prozessen und erfolgt daher sehr schnell. Eine weitere Verwendung virtueller Nummern werden wir kennenlernen, wenn wir über das Einfrieren sprechen.

Virtuelle Nummern werden in den Datensicherungen nicht erfasst.

Zu verschiedenen Zeitpunkten können in der Systemwelt virtuelle Transaktionen mit bereits verwendeten Nummern existieren, und das ist normal. Eine solche Nummer darf jedoch nicht in den Datenseiten gespeichert werden, da sie bei einem erneuten Zugriff auf die Seite ihren gesamten Kontext verlieren könnte.

=> BEGIN;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                         
(1 Zeile)

Wenn jedoch die Transaktion beginnt, Daten zu ändern, erhält sie eine echte, eindeutige Transaktionsnummer.

=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                     3667
(1 Zeile)

=> COMMIT;

Verschachtelte Transaktionen

Speicherpunkte

In SQL definiert Speicherpunkte (savepoint), die es ermöglichen, einen Teil der Transaktionsoperation rückgängig zu machen, ohne sie vollständig zu unterbrechen. Dies passt jedoch nicht in das oben angegebene Schema, da der Status für die gesamte Transaktion einheitlich ist und physisch keine Daten zurückgesetzt werden.

Um eine solche Funktionalität zu implementieren, wird die Transaktion mit einem Savepoint in mehrere separate verschachtelte Transaktionen (Subtransaktionen) aufgeteilt, deren Status separat verwaltet werden kann.

Verschachtelte Transaktionen haben ihre eigene Nummer (größer als die Nummer der Haupttransaktion). Der Status der verschachtelten Transaktionen wird gewöhnlich im XACT aufgezeichnet, doch der endgültige Status hängt vom Status der Haupttransaktion ab: wenn diese abgebrochen wird, werden auch alle verschachtelten Transaktionen abgebrochen.

Informationen über die Verschachtelung von Transaktionen werden in Dateien im Verzeichnis PGDATA/pg_subtrans gespeichert. Der Zugriff auf die Dateien erfolgt über Puffer im gemeinsamen Speicher der Instanz, die ebenfalls wie die XACT-Puffer organisiert sind.

Verwechseln Sie nicht geschachtelte Transaktionen mit autonomen Transaktionen. Autonome Transaktionen sind unabhängig voneinander, während geschachtelte voneinander abhängen. In einem normalen PostgreSQL gibt es keine autonomen Transaktionen, und das ist auch gut so: Sie sind nur sehr selten sinnvoll, und ihre Existenz in anderen DBMS führt zu Missbrauch, von dem alle betroffen sind.

Lassen Sie uns die Tabelle leeren, eine Transaktion starten und eine Zeile einfügen:

=> TRUNCATE TABLE t;
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 row)

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
(1 row)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
(1 row)

Jetzt setzen wir einen Savepoint und fügen eine weitere Zeile ein.

=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 row)

Beachten Sie, dass die Funktion txid_current() die Nummer der Haupttransaktion und nicht der geschachtelten Transaktion ausgibt.

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3670 |    0 |  3 | XYZ
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
 (0,2) | normal | 3670 | 0 (a) | (0,2)
(2 rows)

Wir rollen zum Savepoint zurück und fügen die dritte Zeile ein.

=> ROLLBACK TO sp;
=> INSERT INTO t(s) VALUES ('BAR');
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | status  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669     | 0 (a) | (0,1)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671     | 0 (a) | (0,3)
(3 Zeilen)

Auf der Seite sehen wir weiterhin die Zeile, die durch eine abgebrochene verschachtelte Transaktion hinzugefügt wurde.

Änderungen festhalten.

=> COMMIT;
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | status  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669 (c) | 0 (a) | (0,1)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
(3 Zeilen)

Jetzt ist klar zu erkennen, dass jede verschachtelte Transaktion ihren eigenen Status hat.

Es ist zu beachten, dass verschachtelte Transaktionen in SQL nicht explizit verwendet werden können, d. h. es kann keine neue Transaktion begonnen werden, ohne die aktuelle abzuschließen. Dieser Mechanismus wird implizit bei der Verwendung von Speicherpunkten sowie bei der Verarbeitung von PL/pgSQL-Ausnahmen und in einigen anderen, eher exotischen Fällen aktiviert.

=> BEGIN;
BEGIN
=> BEGIN;
WARNUNG:  Es gibt bereits eine laufende Transaktion
BEGIN
=> COMMIT;
BESTÄTIGEN
=> COMMIT;
WARNUNG:  Es gibt keine laufende Transaktion
COMMIT

Fehler und Atomarität von Operationen

Was passiert, wenn bei der Ausführung einer Operation ein Fehler auftritt? Zum Beispiel so:

=> BEGIN;
=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 Zeilen)

=> UPDATE t SET s = repeat('X', 1/(id-4));
FEHLER:  Division durch Null

Ein Fehler ist aufgetreten. Die Transaktion wird nun als abgebrochen betrachtet und keine Operationen sind in ihr zulässig:

=> SELECT * FROM t;
ERROR: Die aktuelle Transaktion ist abgebrochen, Befehle werden bis zum Ende des Transaktionsblocks ignoriert.

Und selbst wenn man versucht, die Änderungen zu bestätigen, wird PostgreSQL einen Rückgängig machen:

=> COMMIT;
ROLLBACK

Warum kann die Transaktion nach einem Fehler nicht fortgesetzt werden? Das liegt daran, dass der Fehler so aufgetreten sein könnte, dass wir Zugang zu einem Teil der Änderungen erhalten hätten — die Atomarität wäre selbst des Operators verletzt worden. Wie in unserem Beispiel, wo der Operator vor dem Fehler eine Zeile aktualisiert hat:

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669 (c) | 3672  | (0,4)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671 (c) | 0 (a) | (0,3)
 (0,4) | normal | 3672     | 0 (a) | (0,4)
(4 Zeilen)

Es sei gesagt, dass es im psql einen Modus gibt, der es dennoch ermöglicht, die Transaktion nach einem Fehler fortzusetzen, als ob die Aktionen des fehlerhaften Operators zurückgesetzt werden.

=> set ON_ERROR_ROLLBACK on
=> BEGIN;
=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 Zeilen)

=> UPDATE t SET s = repeat('X', 1/(id-4));
FEHLER:  Division durch Null

=> SELECT * FROM t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 Zeilen)

=> COMMIT;

Es ist leicht zu erraten, dass psql in diesem Modus vor jeder Anweisung einen impliziten Speicherpunkt setzt und im Falle eines Fehlers zu diesem zurückkehrt. Ein solches Modus wird standardmäßig nicht verwendet, da das Setzen von Speicherpunkten (auch ohne Rückkehr zu diesen) erhebliche Overhead-Kosten verursacht.

Fortsetzung.

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster