Nun, wir haben die Fragen zur und haben einen Exkurs über . Und schließlich sind wir bei dem Interessantesten angekommen – den Versionen von Zeilen.
Header
Wie bereits erwähnt, kann jede Zeile gleichzeitig in der Datenbank in mehreren Versionen vorhanden sein. Man muss eine Version von einer anderen unterscheiden. Zu diesem Zweck hat jede Version zwei Markierungen, die die „Zeit“ der Gültigkeit dieser Version bestimmen (xmin und xmax). In Anführungszeichen – weil nicht die Zeit an sich verwendet wird, sondern ein spezieller, fortlaufender Zähler. Und dieser Zähler ist die Transaktionsnummer.
(Wie üblich, ist in der Tat alles komplexer: Die Transaktionsnummer kann aufgrund der begrenzten Anzahl an Zählern nicht ständig ansteigen. Aber diese Details werden wir ausführlich behandeln, wenn wir zur Einfrierung kommen.)
Wenn eine Zeile erstellt wird, wird der Wert von xmin auf die Nummer der Transaktion gesetzt, die den INSERT-Befehl ausgeführt hat, während xmax nicht gefüllt wird.
Wenn eine Zeile gelöscht wird, wird der Wert von xmax der aktuellen Version mit der Nummer der Transaktion markiert, die das DELETE ausgeführt hat.
Wenn eine Zeile mit dem UPDATE-Befehl geändert wird, werden tatsächlich zwei Operationen ausgeführt: DELETE und INSERT. In der aktuellen Version der Zeile wird xmax auf die Nummer der Transaktion gesetzt, die das UPDATE ausgeführt hat. Dann wird eine neue Version der gleichen Zeile erstellt, deren Wert von xmin mit dem Wert von xmax der vorherigen Version übereinstimmt.
Die Felder xmin und xmax sind Teil des Headers der Zeilenversion. Neben diesen Feldern enthält der Header auch andere, wie zum Beispiel:
- infomask – eine Reihe von Bits, die die Eigenschaften dieser Version bestimmen. Davon gibt es ziemlich viele; die wichtigsten werden wir nach und nach betrachten.
- ctid – ein Verweis auf die nächste, neuere Version derselben Zeile. Bei der neuesten, aktuellen Version verweist ctid auf diese Version selbst. Die Nummer sieht wie (x,y) aus, wobei x die Seitenzahl und y die fortlaufende Nummer 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 eines Datentyps, daher muss das Zeichen separat gespeichert werden.
Infolgedessen ist der Header ziemlich groß – mindestens 23 Bytes für jede Version einer Zeile, oft mehr aufgrund der NULL-Bitmaske. Wenn die Tabelle „schmal“ ist (das heißt, wenig Spalten enthält), können die Overheadkosten höher sein als die nützlichen Informationen.
Einfügen
Betrachten wir genauer, wie Operationen mit Zeichenfolgen auf niedriger Ebene durchgeführt werden, und beginnen wir mit der Einfügung.
Für Experimente erstellen wir eine neue Tabelle mit zwei Spalten und einem Index auf eine davon:
=> CREATE TABLE t(
id serial,
s text
);
=> CREATE INDEX ON t(s);
Wir fügen eine Zeile ein, nachdem wir die Transaktion begonnen haben.
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
Hier ist die Nummer unserer aktuellen Transaktion:
=> SELECT txid_current();
txid_current
--------------
3664
(1 Zeile)
Werfen wir einen Blick auf den Inhalt der Seite. Die Funktion heap_page_items 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
-[ DATEN 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
Es ist zu beachten, dass der Begriff heap (Haufen) in PostgreSQL für Tabellen verwendet wird. Dies ist eine weitere seltsame Verwendung des Begriffs — ein Haufen ist eine bekannte , die mit einer Tabelle nichts zu tun hat. Hier wird das Wort im Sinne von "alles liegt durcheinander" verwendet, im Gegensatz zu sortierten Indizes.
Die Funktion zeigt die Daten "wie sie sind" in einem schwer verständlichen Format. Um Klarheit zu bekommen, belassen 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
-[ DATEN 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 gemacht:
- Wir haben der Zeigerzahl eine Null hinzugefügt, um sie in das gleiche Format wie t_ctid zu bringen: (Seitenzahl, Zeigerzahl).
- Wir haben den Zustand des Zeigers lp_flags entschlüsselt. Hier ist er "normal" — das bedeutet, dass der Zeiger tatsächlich auf eine Version der Zeile verweist. Andere Werte werden wir später betrachten.
- Von allen Informationsbits haben wir vorerst nur zwei Paare herausgegriffen. Die Bits xmin_committed und xmin_aborted zeigen an, ob die Transaktion mit der Nummer xmin festgeschrieben (abgebrochen) wurde. Zwei ähnliche Bits beziehen sich auf die Transaktion mit der Nummer xmax.
Was sehen wir also? Beim Einfügen einer Zeile in eine Tabelle erscheint ein Zeiger mit der Nummer 1, der auf die erste und einzige Version der Zeile verweist.
In der Zeilenversion ist das Feld xmin mit der Nummer der aktuellen Transaktion gefüllt. Die Transaktion ist noch aktiv, daher sind sowohl die Bits xmin_committed als auch xmin_aborted nicht gesetzt.
Das Feld ctid der Zeilenversion verweist auf dieselbe Zeile. Das bedeutet, dass keine neuere Version existiert.
Das Feld xmax ist mit einer fiktiven Nummer 0 gefüllt, da diese Version der Zeile nicht gelöscht wurde und aktuell ist. Transaktionen werden auf diese Nummer nicht achten, da das Bit xmax_aborted gesetzt ist.
Lassen Sie uns einen weiteren Schritt zur Verbesserung der Lesbarkeit machen, indem wir Informationsbits zu den Transaktionsnummern hinzufügen. Und wir erstellen eine Funktion, da wir die Abfrage noch mehrmals benötigen werden:
=> CREATE FUNCTION heap_page(relname text, pageno integer)
RETURNS TABLE(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 'unused'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to '||lp_off
WHEN 3 THEN 'dead'
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;
$$ LANGUAGE SQL;
In dieser Form ist deutlich verständlicher, 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 deutlich weniger detaillierte Informationen können auch aus der Tabelle selbst abgerufen werden, indem die Pseudospalten xmin und xmax verwendet werden:
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3664 | 0 | 1 | FOO
(1 Zeile)
Festschreibung
Nach erfolgreichem Abschluss der Transaktion muss ihr Status gespeichert werden – festgehalten werden, dass sie abgeschlossen ist. Dazu wird eine Struktur verwendet, die XACT genannt wird (bis zur Version 10 wurde sie CLOG (commit log) genannt, und dieser Name kann an verschiedenen Stellen noch vorkommen).
XACT ist keine Tabelle des Systemkatalogs; es handelt sich um Dateien im Verzeichnis PGDATA/pg_xact. In ihnen sind für jede Transaktion zwei Bits reserviert: committed und aborted – genau wie im Header der Zeilenversion. Diese Informationen sind auf mehrere Dateien verteilt, lediglich aus Gründen der Übersichtlichkeit; wir werden dieses Thema wieder aufgreifen, wenn wir das Thema des Einfrierens behandeln. Der Zugriff auf diese Dateien erfolgt seitenweise, wie bei allen anderen.
Beim Festlegen einer Transaktion wird im XACT das Bit committed für diese Transaktion gesetzt. Und das ist alles, was beim Festlegen passiert (wir sprechen bisher noch nicht über das Pre-Write-Log).
Wenn eine andere Transaktion auf die Tabellen-Seite zugreift, die wir gerade betrachtet haben, muss sie sich einigen Fragen stellen.
- Hat die Transaktion xmin bereits abgeschlossen? Wenn nicht, sollte die erstellte Zeilenversion nicht sichtbar sein.
Eine solche Überprüfung erfolgt durch den Blick auf eine weitere Struktur, die sich im gemeinsamen Speicher des Instanz befindet und ProcArray genannt wird. Dort befindet sich eine Liste aller aktiven Prozesse, und für jeden wird die Nummer seiner aktuellen (aktiven) Transaktion angegeben. - Falls abgeschlossen, wie — durch Festlegung oder Abbruch? Bei einem Abbruch sollte die Zeilenversion ebenfalls nicht sichtbar sein.
Genau dafür ist XACT zuständig. Obwohl die letzten Seiten von XACT im Speicher gepuffert werden, ist es dennoch aufwendig, jedes Mal XACT zu überprüfen. Daher wird der einmal ermittelte Status der Transaktion in den Bits xmin_committed und xmin_aborted der Zeilenversion gespeichert. Wenn eines dieser Bits gesetzt ist, wird der Status der Transaktion xmin als bekannt betrachtet, und die nächste Transaktion muss nicht mehr auf XACT zugreifen.
Warum werden diese Bits nicht von der Transaktion, die den Eintrag vornimmt, gesetzt? Bei einem Eintrag weiß die Transaktion noch nicht, ob sie erfolgreich abgeschlossen wird. Und zum Zeitpunkt der Festlegung ist nicht klar, welche Zeilen in welchen Seiten geändert wurden. Es kann viele dieser Seiten geben, und es wäre unpraktisch, sich diese zu merken. Zudem 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 Festlegung erheblich verlangsamen.
Die Kehrseite der Einsparung besteht darin, dass nach Änderungen jede Transaktion (selbst eine einfache Lesetransaktion — SELECT) beginnen kann, die Datenseiten im Pufferspeicher zu ändern.
Also, lassen Sie uns die Änderung festlegen.
=> COMMIT;
Es hat sich nichts auf der Seite geändert (aber wir wissen, dass der Status der Transaktion bereits in XACT gespeichert 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 zuerst auf die Seite zugegriffen hat, den Status der Transaktion xmin ermitteln und diesen 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)
Löschen
Beim Löschen einer Zeile wird im Feld xmax die Nummer der aktuellen löschenden Transaktion gespeichert, während das Bit xmax_aborted zurückgesetzt wird.
Es ist zu beachten, dass der feste Wert von xmax, der der aktiven Transaktion entspricht, als Sperre für die Zeile fungiert. Wenn eine andere Transaktion plant, diese Zeile zu aktualisieren oder zu löschen, muss sie warten, bis die Transaktion xmax abgeschlossen ist. Weitere Informationen zu Sperren werden wir später besprechen. Im Moment sei nur erwähnt, dass die Anzahl der Zeilensperren unbegrenzt ist. Sie benötigen keinen Platz im Arbeitsspeicher, und die Systemleistung leidet nicht unter ihrer Anzahl. Allerdings gibt es andere Nachteile „langer“ Transaktionen, aber dazu später mehr.
Löschen wir die Zeile.
=> BEGIN;
=> DELETE FROM t;
=> SELECT txid_current();
txid_current
--------------
3665
(1 Zeile)
Wir sehen, dass die Transaktionsnummer im Feld xmax gespeichert 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)
Abbruch
Das Rückgängigmachen von Änderungen funktioniert analog zur Bestätigung, jedoch wird im XACT für die Transaktion das Bit aborted gesetzt. Der Abbruch erfolgt genauso schnell wie die Bestätigung. Obwohl der Befehl ROLLBACK heißt, findet keine Rückführung der Änderungen statt: Alles, was die Transaktion an 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 im Versionsfeld der Zeile wird das Hinweisbit xmax_aborted gesetzt. Die Nummer xmax bleibt auf der Seite, aber niemand wird mehr darauf achten.
=> 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 wurde.
=> BEGIN;
=> UPDATE t SET s = 'BAR';
=> SELECT txid_current();
txid_current
--------------
3666
(1 Zeile)
Die Abfrage liefert eine Zeile (neue Version):
=> SELECT * FROM t;
id | s
----+-----
1 | BAR
(1 Zeile)
Aber auf der Seite sehen wir 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 Feld xmax gekennzeichnet. Dieses Wert wurde über das alte Wert geschrieben, da die vorherige Transaktion abgebrochen wurde. Das Bit xmax_aborted wurde zurückgesetzt, da der Status der laufenden Transaktion noch unbekannt ist.
Die erste Version der Zeile verweist jetzt auf die zweite (Feld t_ctid), als auf die neuere.
Auf der Indexseite erscheint ein zweiter Zeiger und eine zweite Zeile, die auf die zweite Version der Tabellenseite verweist.
Wie auch beim Löschen dient der Wert xmax in der ersten Version der Zeile als Indikator dafür, dass die Zeile gesperrt ist.
Nun beenden wir die Transaktion.
=> COMMIT;
Indizes
Bisher haben wir nur über Tabellenseiten gesprochen. Was passiert jedoch innerhalb von Indizes?
Die Informationen auf den Indexseiten hängen stark von der spezifischen Art des Index ab. Selbst innerhalb eines Indextyps können unterschiedliche Seitenarten existieren. Zum Beispiel hat ein B-Baum eine Seite mit Metadaten und „normale“ Seiten.
In der Regel enthält eine Seite jedoch ein Array von Zeiger auf Zeilen sowie die Zeilen selbst (ebenso wie auf der Tabellenseite). Zudem wird am Ende der Seite Platz für spezielle Daten reserviert.
Die Zeilen in Indizes können ebenfalls eine sehr unterschiedliche Struktur aufweisen, abhängig vom Index-Typ. Zum Beispiel enthalten für B-Bäume die zu Blattseiten gehörenden Zeilen den Wert des Indexierungsschlüssels und einen Verweis (ctid) auf die entsprechende Zeile in der Tabelle. Allgemein kann der Index jedoch ganz anders aufgebaut sein.
Der wichtigste Punkt ist, dass es in Indizes jeglicher Art keine Versionen von Zeilen gibt. Oder man kann davon ausgehen, dass jede Zeile genau durch eine Version dargestellt wird. Anders ausgedrückt, im Header der Indexzeile gibt es keine Felder xmin und xmax. Man kann davon ausgehen, dass die Verweise aus dem Index auf alle Tabellenversionen der Zeilen verweisen – weshalb man nur durch einen Blick in die Tabelle herausfinden kann, welche der Versionen die Transaktion sehen wird. (Wie üblich ist das nicht die ganze Wahrheit. In einigen Fällen ermöglicht die Sichtbarkeitstabelle eine Optimierung des Prozesses, aber darauf werden wir später näher eingehen.)
Auf der Indexseite finden wir jedoch 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 zu „sparen“.
Wenn eine Transaktion nur Daten liest, hat sie keinen Einfluss auf die Sichtbarkeit der Zeilenversionen. Daher gibt der überwachende Prozess zu Beginn der Transaktion eine virtuelle Nummer (virtual xid) aus. Diese Nummer besteht aus der Prozess-ID und einer fortlaufenden Zahl.
Die Ausgabe dieser Nummer erfordert keine Synchronisation zwischen allen Prozessen und erfolgt daher sehr schnell. Mit einem weiteren Grund für die Verwendung virtueller Nummern werden wir uns befassen, wenn wir über das Einfrieren sprechen.
Virtuelle Nummern werden in Daten snapshots nicht berücksichtigt.
Zu verschiedenen Zeitpunkten können im System durchaus virtuelle Transaktionen mit Nummern vorhanden sein, die bereits verwendet wurden, und das ist normal. Aber eine solche Nummer darf nicht in die Datenseiten geschrieben werden, denn bei der nächsten Anfrage an die Seite könnte sie jede Bedeutung verlieren.
=> BEGIN;
=> SELECT txid_current_if_assigned();
txid_current_if_assigned
--------------------------
(1 Zeile)
Wenn eine Transaktion beginnt, Daten zu ändern, erhält sie eine echte, einzigartige 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 sind definiert Speicherpunkte (savepoint), die es ermöglichen, Teile des Transaktionsvorgangs zurückzusetzen, ohne sie vollständig abzubrechen. Aber das passt nicht in das oben angegebene Schema, da der Status einer Transaktion für alle ihre Änderungen gleich ist und physisch keine Daten zurückgesetzt werden.
Um eine solche Funktionalität zu realisieren, wird eine Transaktion mit einem Speicherpunkt in mehrere separate verschachtelte Transaktionen (subtransaction) aufgeteilt, deren Status separat verwaltet werden kann.
Verschachtelte Transaktionen haben eine eigene Nummer (größer als die Nummer der Haupttransaktion). Der Status der verschachtelten Transaktionen wird auf die übliche Weise in XACT aufgezeichnet, doch der endgültige Status hängt vom Status der Haupttransaktion ab: Wenn sie abgebrochen wird, werden auch alle verschachtelten Transaktionen abgebrochen.
Die 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 ebenso organisiert sind wie die XACT-Puffer.
Verwechseln Sie nicht verschachtelte Transaktionen mit autonomen Transaktionen. Autonome Transaktionen hängen nicht voneinander ab, während dies bei verschachtelten der Fall ist. In einem normalen PostgreSQL gibt es keine autonomen Transaktionen, und das ist vielleicht auch besser: Sie sind sehr selten erforderlich, und deren Vorhandensein in anderen DBMS provoziert Missbrauch, unter dem dann alle leiden.
Wir bereinigen die Tabelle, starten die Transaktion und fügen eine Zeile ein:
=> TRUNCATE TABLE t;
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
=> SELECT txid_current();
txid_current
--------------
3669
(1 Zeile)
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
(1 Zeile)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3669 | 0 (a) | (0,1)
(1 Zeile)
Jetzt setzen wir einen Speicherpunkt und fügen eine weitere Zeile ein.
=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT txid_current();
txid_current
--------------
3669
(1 Zeile)
Bitte beachten Sie, dass die Funktion txid_current() die Nummer der Haupt- nicht der verschachtelten Transaktion zurückgibt.
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
3670 | 0 | 3 | XYZ
(2 Zeilen)
=> 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 Zeilen)
Wir rollen zu dem Speicherpunkt 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 Zeilen)
=> SELECT * FROM heap_page('t',0);
ctid | state | 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 von der abgebrochenen verschachtelten Transaktion hinzugefügt wurde.
Wir fixieren die Änderungen.
=> COMMIT;
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3669 | 0 | 2 | FOO
3671 | 0 | 4 | BAR
(2 Zeilen)
=> SELECT * FROM heap_page('t',0);
ctid | state | 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 gut zu erkennen, dass jede verschachtelte Transaktion ihren eigenen Status hat.
Es sei darauf hingewiesen, dass verschachtelte Transaktionen in SQL nicht explizit verwendet werden können, das heißt, man kann keine neue Transaktion beginnen, ohne die aktuelle zu beenden. Dieser Mechanismus wird implizit aktiviert, wenn Speicherpunkte verwendet werden, sowie bei der Verarbeitung von PL/pgSQL-Ausnahmen und in einigen anderen, exotischeren Fällen.
=> BEGIN;
BEGIN
=> BEGIN;
WARNUNG: Es läuft bereits eine Transaktion
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
WARNUNG: Es läuft keine 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. Jetzt wird die Transaktion als unterbrochen betrachtet, und keine Operation in ihr ist zulässig:
=> SELECT * FROM t;
FEHLER: Aktuelle Transaktion ist abgebrochen, Befehle werden bis zum Ende des Transaktionsblocks ignoriert
Und selbst wenn man versucht, die Änderungen zu fixieren, wird PostgreSQL eine Stornierung melden:
=> COMMIT;
ROLLBACK
Warum kann die Ausführung der Transaktion nach einem Fehler nicht fortgesetzt werden? Das liegt daran, dass der Fehler so aufgetreten sein könnte, dass wir Zugriff auf einen Teil der Änderungen hätten – die Atomarität wäre nicht nur der Transaktion, sondern auch des Operators verletzt. In unserem Beispiel, bei dem der Operator vor dem Fehler bereits 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 rows)
Es ist zu sagen, dass es in psql einen Modus gibt, der es tatsächlich ermöglicht, die Transaktion nach einem Fehler fortzusetzen, als ob die Aktionen des fehlerhaften Operators zurückgerollt würden.
=> 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 nicht schwer zu erraten, dass psql in diesem Modus tatsächlich vor jedem Befehl einen impliziten Speicherpunkt setzt und im Falle eines Fehlers zu diesem zurückrollt. Ein solcher Modus wird nicht standardmäßig verwendet, da das Setzen von Speicherpunkten (auch ohne zu diesen zurückzukehren) mit beträchtlichen Overheadkosten verbunden ist.
Quelle: habr.com
