MVCC-3. String-Versionen

Daher haben wir uns mit Fragen im Zusammenhang mit befasst Isolierung, und zog sich zurück Organisation von Daten auf niedriger Ebene. Und schließlich kamen wir zum interessantesten Teil – den Streicherversionen.

Kopfzeile

Wie bereits erwähnt, kann jede Zeile gleichzeitig in mehreren Versionen in der Datenbank vorhanden sein. Eine Version muss irgendwie von einer anderen unterschieden werden. Zu diesem Zweck verfügt jede Version über zwei Markierungen, die den „Wirkungszeitpunkt“ dieser Version bestimmen (xmin und xmax). In Anführungszeichen - weil nicht die Zeit als solche verwendet wird, sondern ein spezieller zunehmender Zähler. Und dieser Zähler ist die Transaktionsnummer.

(Wie üblich ist die Realität komplizierter: Die Transaktionszahl kann aufgrund der begrenzten Bitkapazität des Zählers nicht ständig steigen. Wir werden uns diese Details jedoch im Detail ansehen, wenn wir zum Einfrieren kommen.)

Wenn eine Zeile erstellt wird, wird xmin auf die Transaktionsnummer gesetzt, die den INSERT-Befehl ausgegeben hat, und xmax bleibt leer.

Wenn eine Zeile gelöscht wird, wird der xmax-Wert der aktuellen Version mit der Nummer der Transaktion markiert, die das DELETE durchgeführt hat.

Wenn eine Zeile durch einen UPDATE-Befehl geändert wird, werden tatsächlich zwei Operationen ausgeführt: DELETE und INSERT. Die aktuelle Version der Zeile setzt xmax gleich der Nummer der Transaktion, die das UPDATE durchgeführt hat. Anschließend wird eine neue Version derselben Zeichenfolge erstellt. sein xmin-Wert stimmt mit dem xmax-Wert der vorherigen Version überein.

Die Felder xmin und xmax sind im Header der Zeilenversion enthalten. Zusätzlich zu diesen Feldern enthält der Header weitere, zum Beispiel:

  • Infomask ist eine Reihe von Bits, die die Eigenschaften dieser Version definieren. Davon gibt es ziemlich viele; Wir werden uns nach und nach mit den wichtigsten befassen.
  • ctid ist ein Link zur nächsten, neueren Version derselben Zeile. Für die neueste, aktuellste Version einer Zeichenfolge verweist die ctid auf diese Version selbst. Die Zahl hat die Form (x,y), wobei x die Seitenzahl und y die Indexnummer im Array ist.
  • Null-Bitmap – Markiert die Spalten einer bestimmten Version, die einen Nullwert (NULL) enthalten. NULL gehört nicht zu den normalen Datentypwerten, daher muss das Attribut separat gespeichert werden.

Infolgedessen ist der Header ziemlich groß – mindestens 23 Bytes für jede Version der Zeile und aufgrund der NULL-Bitmap normalerweise mehr. Wenn die Tabelle „schmal“ ist (d. h. nur wenige Spalten enthält), nimmt der Overhead möglicherweise mehr in Anspruch als die nützlichen Informationen.

Einfügen

Werfen wir einen genaueren Blick darauf, wie Zeichenfolgenoperationen auf niedriger Ebene ausgeführt werden, beginnend mit dem Einfügen.

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);

Fügen wir nach dem Starten einer Transaktion eine Zeile ein.

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

Hier ist unsere aktuelle Transaktionsnummer:

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

Schauen wir uns den Inhalt der Seite an. Mit der Funktion heap_page_items der pageinspect-Erweiterung können Sie Informationen über Zeiger und Zeilenversionen abrufen:

=> 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 sich das Wort Heap in PostgreSQL auf Tabellen bezieht. Dies ist eine weitere seltsame Verwendung des Begriffs – ein Haufen ist bekannt Datenstruktur, was mit der Tabelle nichts gemein hat. Hier wird das Wort im Sinne von „alles ist zusammengewürfelt“ im Gegensatz zu geordneten Indizes verwendet.

Die Funktion zeigt Daten „wie sie sind“ in einem schwer verständlichen Format. Um es herauszufinden, hinterlassen 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)

Folgendes haben wir getan:

  • Der Indexnummer wurde eine Null hinzugefügt, damit sie genauso aussieht wie t_ctid: (Seitennummer, Indexnummer).
  • Den Zustand des lp_flags-Zeigers entschlüsselt. Hier ist es „normal“ – das bedeutet, dass der Zeiger tatsächlich auf die Version des Strings verweist. Wir werden uns später andere Bedeutungen ansehen.
  • Von allen Informationsbits wurden bisher nur zwei Paare identifiziert. Die Bits xmin_committed und xmin_aborted geben an, ob die Transaktion Nummer xmin festgeschrieben (abgebrochen) wird. Zwei ähnliche Bits beziehen sich auf die Transaktionsnummer xmax.

Was sehen wir? Wenn Sie eine Zeile einfügen, wird auf der Tabellenseite eine Indexnummer 1 angezeigt, die auf die erste und einzige Version der Zeile verweist.

In der String-Version wird das xmin-Feld mit der aktuellen Transaktionsnummer gefüllt. Die Transaktion ist noch aktiv, daher sind die Bits xmin_committed und xmin_aborted nicht gesetzt.

Das Feld „Zeilenversion ctid“ bezieht sich auf dieselbe Zeile. Dies bedeutet, dass keine neuere Version existiert.

Das xmax-Feld wird mit der Dummy-Nummer 0 gefüllt, da diese Version der Zeile nicht gelöscht wurde und aktuell ist. Transaktionen berücksichtigen diese Zahl nicht, da das xmax_aborted-Bit gesetzt ist.

Machen wir einen weiteren Schritt zur Verbesserung der Lesbarkeit, indem wir den Transaktionsnummern Informationsbits hinzufügen. Und erstellen wir eine Funktion, da wir die Anfrage mehr als einmal 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 viel klarer, was in der Kopfzeile der Zeilenversion vor sich geht:

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

Ähnliche, aber deutlich weniger detaillierte Informationen können mithilfe der Pseudospalten xmin und xmax aus der Tabelle selbst gewonnen werden:

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

Befestigung

Wenn eine Transaktion erfolgreich abgeschlossen wurde, müssen Sie sich ihren Status merken – beachten Sie, dass sie festgeschrieben ist. Dazu wird eine Struktur namens XACT verwendet (und vor Version 10 hieß sie CLOG (Commit Log) und dieser Name ist noch an verschiedenen Stellen zu finden).

XACT ist keine Systemkatalogtabelle; Dies sind die Dateien im Verzeichnis PGDATA/pg_xact. Jeder Transaktion sind zwei Bits zugewiesen: Committed und Aborted – genau wie im Header der Zeilenversion. Diese Informationen sind lediglich der Einfachheit halber in mehrere Dateien aufgeteilt; wir werden auf dieses Problem zurückkommen, wenn wir über das Einfrieren nachdenken. Und die Arbeit mit diesen Dateien erfolgt wie bei allen anderen Seite für Seite.

Wenn also eine Transaktion in XACT festgeschrieben wird, wird das Festschreibungsbit für diese Transaktion gesetzt. Und das ist alles, was während des Commits passiert (obwohl wir noch nicht über das Voraufzeichnungsprotokoll sprechen).

Wenn eine andere Transaktion auf die gerade betrachtete Tabellenseite zugreift, muss sie mehrere Fragen beantworten.

  1. Ist die xmin-Transaktion abgeschlossen? Wenn nicht, sollte die erstellte Version der Zeichenfolge nicht sichtbar sein.
    Diese Prüfung wird durchgeführt, indem eine andere Struktur betrachtet wird, die sich im gemeinsamen Speicher der Instanz befindet und ProcArray heißt. Es enthält eine Liste aller aktiven Prozesse und für jeden wird die Nummer seiner aktuellen (aktiven) Transaktion angezeigt.
  2. Wenn abgeschlossen, wie dann – durch Festschreiben oder Abbrechen? Bei einem Abbruch sollte die Zeilenversion ebenfalls nicht sichtbar sein.
    Genau dafür ist XACT da. Aber obwohl die letzten Seiten von XACT in Puffern im RAM gespeichert werden, ist es immer noch teuer, XACT jedes Mal zu überprüfen. Sobald der Transaktionsstatus ermittelt ist, wird er daher in die Bits xmin_committed und xmin_aborted der String-Version geschrieben. Wenn eines dieser Bits gesetzt ist, gilt der Status der Transaktion xmin als bekannt und die nächste Transaktion muss nicht auf XACT zugreifen.

Warum werden diese Bits nicht von der Transaktion selbst gesetzt, die die Einfügung durchführt? Wenn eine Einfügung erfolgt, weiß die Transaktion noch nicht, ob sie erfolgreich sein wird. Und im Moment des Commits ist nicht mehr klar, welche Zeilen auf welchen Seiten geändert wurden. Es kann viele solcher Seiten geben, und es ist unrentabel, sie auswendig zu lernen. Darüber hinaus können einige Seiten aus dem Puffercache auf die Festplatte entfernt werden; Sie erneut zu lesen, um die Bits zu ändern, würde den Commit erheblich verlangsamen.

Der Nachteil der Einsparungen besteht darin, dass nach Änderungen jede Transaktion (auch eine, die einen einfachen Lesevorgang durchführt – SELECT) beginnen kann, Datenseiten im Puffercache zu ändern.

Also, lasst uns die Änderung beheben.

=> COMMIT;

Auf der Seite hat sich nichts geändert (wir wissen jedoch, dass der Transaktionsstatus bereits in XACT aufgezeichnet ist):

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

Jetzt muss die Transaktion, die zuerst auf die Seite zugreift, den xmin-Transaktionsstatus ermitteln und in die Informationsbits schreiben:

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

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

Entfernung

Wenn eine Zeile gelöscht wird, wird die Nummer der aktuellen Löschtransaktion in das xmax-Feld der aktuellen Version geschrieben und das xmax_aborted-Bit gelöscht.

Beachten Sie, dass der festgelegte Wert von xmax, der der aktiven Transaktion entspricht, als Zeilensperre fungiert. Wenn eine andere Transaktion diese Zeile aktualisieren oder löschen möchte, muss sie warten, bis die Transaktion xmax abgeschlossen ist. Wir werden später mehr über das Blockieren sprechen. Im Moment beachten wir nur, dass die Anzahl der Zeilensperren unbegrenzt ist. Sie belegen keinen Platz im RAM und die Systemleistung wird durch ihre Anzahl nicht beeinträchtigt. Zwar haben „lange“ Transaktionen noch andere Nachteile, aber dazu später mehr.

Löschen wir die Zeile.

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

Wir sehen, dass die Transaktionsnummer in das xmax-Feld geschrieben wird, die Informationsbits jedoch nicht gesetzt sind:

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

Stornierung

Das Abbrechen von Änderungen funktioniert ähnlich wie das Festschreiben, nur dass in XACT das Aborted-Bit für die Transaktion gesetzt ist. Das Rückgängigmachen geht genauso schnell wie das Festschreiben. Obwohl der Befehl ROLLBACK heißt, werden Änderungen nicht rückgängig gemacht: Alles, was die Transaktion auf 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 row)

Beim Zugriff auf die Seite wird der Status überprüft und das Hinweisbit xmax_aborted auf die Zeilenversion gesetzt. Die xmax-Nummer selbst bleibt auf der Seite, aber niemand wird sie sehen.

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

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

Aktualisieren

Das Update funktioniert so, als würde zuerst die aktuelle Version der Zeile gelöscht und dann eine neue eingefügt.

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

Die Abfrage erzeugt eine Zeile (neue Version):

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

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 rows)

Die gelöschte Version wird mit der aktuellen Transaktionsnummer im xmax-Feld gekennzeichnet. Außerdem wird dieser Wert über den alten Wert geschrieben, da die vorherige Transaktion abgebrochen wurde. Und das Bit xmax_aborted wird gelöscht, da der Status der aktuellen Transaktion noch nicht bekannt ist.

Die erste Version der Zeile bezeichnet nun die zweite (t_ctid-Feld) als die neuere.

Ein zweiter Index erscheint auf der Indexseite und eine zweite Zeile verweist auf die zweite Version auf der Tabellenseite.

Genau wie beim Löschen ist der xmax-Wert in der ersten Version der Zeile ein Hinweis darauf, dass die Zeile gesperrt ist.

Nun, lasst uns die Transaktion abschließen.

=> COMMIT;

Indizes

Bisher haben wir nur über Tabellenseiten gesprochen. Was passiert innerhalb der Indizes?

Die Informationen auf Indexseiten variieren stark je nach Indextyp. Und selbst ein Indextyp hat unterschiedliche Seitentypen. Ein B-Baum verfügt beispielsweise über eine Metadatenseite und „normale“ Seiten.

Allerdings verfügt die Seite normalerweise über ein Array von Zeigern auf die Zeilen und die Zeilen selbst (genau wie eine Tabellenseite). Darüber hinaus ist am Ende der Seite Platz für spezielle Daten.

Auch Zeilen in Indizes können je nach Indextyp sehr unterschiedliche Strukturen haben. Beispielsweise enthalten für einen B-Baum die Zeilen, die sich auf Blattseiten beziehen, den Indexierungsschlüsselwert und einen Verweis (ctid) auf die entsprechende Tabellenzeile. Generell kann der Index auch ganz unterschiedlich aufgebaut sein.

Der wichtigste Punkt ist, dass es in Indizes jeglicher Art keine Zeilenversionen gibt. Nun, oder wir können davon ausgehen, dass jede Zeile durch genau eine Version repräsentiert wird. Mit anderen Worten, es gibt keine xmin- und xmax-Felder im Indexzeilenheader. Wir können davon ausgehen, dass Links vom Index zu allen Tabellenversionen der Zeilen führen – Sie können also nur durch einen Blick auf die Tabelle herausfinden, welche Version die Transaktion sehen wird. (Wie immer ist dies nicht die ganze Wahrheit. In manchen Fällen kann die Sichtbarkeitskarte den Prozess optimieren, aber wir werden später genauer darauf eingehen.)

Gleichzeitig finden wir auf der Indexseite Hinweise auf beide Versionen, sowohl die aktuelle als auch die alte:

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

Virtuelle Transaktionen

In der Praxis verwendet PostgreSQL Optimierungen, die es ermöglichen, Transaktionszahlen zu „speichern“.

Wenn eine Transaktion nur Daten liest, hat dies keinen Einfluss auf die Sichtbarkeit von Zeilenversionen. Daher gibt der Dienstprozess zunächst eine virtuelle XID für die Transaktion aus. Die Nummer besteht aus einer Prozess-ID und einer Sequenznummer.

Die Vergabe dieser Nummer erfordert keine Synchronisierung aller Prozesse und ist daher sehr schnell. Einen weiteren Grund für die Verwendung virtueller Nummern werden wir kennenlernen, wenn wir über das Einfrieren sprechen.

Virtuelle Nummern werden in Datenschnappschüssen in keiner Weise berücksichtigt.

Zu unterschiedlichen Zeitpunkten kann es durchaus zu virtuellen Transaktionen im System mit bereits genutzten Nummern kommen, was normal ist. Eine solche Zahl kann jedoch nicht in Datenseiten geschrieben werden, da sie beim nächsten Zugriff auf die Seite möglicherweise ihre Bedeutung verliert.

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

Beginnt eine Transaktion, 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 row)

=> COMMIT;

Verschachtelte Transaktionen

Punkte sparen

In SQL definiert Punkte speichern (Savepoint), mit denen Sie einen Teil einer Transaktion abbrechen können, ohne sie vollständig zu unterbrechen. Dies passt jedoch nicht in das obige Diagramm, da die Transaktion bei allen Änderungen den gleichen Status hat und physisch keine Daten zurückgesetzt werden.

Um diese Funktionalität zu implementieren, wird eine Transaktion mit einem Sicherungspunkt in mehrere separate Transaktionen aufgeteilt verschachtelte Transaktionen (Teilvorgang), dessen Status separat verwaltet werden kann.

Verschachtelte Transaktionen haben eine eigene Nummer (höher als die Nummer der Haupttransaktion). Der Status verschachtelter Transaktionen wird in XACT wie gewohnt erfasst, der endgültige Status hängt jedoch vom Status der Haupttransaktion ab: Wird diese abgebrochen, werden auch alle verschachtelten Transaktionen abgebrochen.

Informationen zur Transaktionsverschachtelung werden in Dateien im Verzeichnis PGDATA/pg_subtrans gespeichert. Der Zugriff auf Dateien erfolgt über Puffer im gemeinsam genutzten Speicher der Instanz, die auf die gleiche Weise wie XACT-Puffer organisiert sind.

Verwechseln Sie verschachtelte Transaktionen nicht mit autonomen Transaktionen. Autonome Transaktionen sind in keiner Weise voneinander abhängig, verschachtelte Transaktionen hingegen schon. Im regulären PostgreSQL gibt es keine autonomen Transaktionen, und das ist vielleicht das Beste: Sie werden sehr, sehr selten benötigt und ihre Präsenz in anderen DBMS provoziert Missbrauch, unter dem dann alle leiden.

Löschen wir die Tabelle, starten eine Transaktion und fügen die Zeile ein:

=> 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 Speicherpunkt 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 Haupttransaktionsnummer zurückgibt, nicht die verschachtelte Transaktionsnummer.

=> 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)

Lassen Sie uns zum Speicherpunkt zurückkehren und die dritte Zeile einfügen.

=> 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  | 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 rows)

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

Wir beheben die Änderungen.

=> 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  | 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 rows)

Jetzt können Sie deutlich erkennen, dass jede verschachtelte Transaktion ihren eigenen Status hat.

Beachten Sie, dass verschachtelte Transaktionen in SQL nicht explizit verwendet werden können, d. h. Sie können keine neue Transaktion starten, ohne die aktuelle abzuschließen. Dieser Mechanismus wird implizit bei der Verwendung von Sicherungspunkten sowie bei der Behandlung von PL/pgSQL-Ausnahmen und in einer Reihe anderer, exotischerer Fälle aktiviert.

=> BEGIN;
BEGIN
=> BEGIN;
WARNING:  there is already a transaction in progress
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
WARNING:  there is no transaction in progress
COMMIT

Fehler und Atomizität von Operationen

Was passiert, wenn beim Ausführen einer Operation ein Fehler auftritt? Zum Beispiel so:

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

=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero

Ein Fehler ist aufgetreten. Jetzt gilt die Transaktion als abgebrochen und es sind keine Operationen darin zulässig:

=> SELECT * FROM t;
ERROR:  current transaction is aborted, commands ignored until end of transaction block

Und selbst wenn Sie versuchen, die Änderungen zu übernehmen, meldet PostgreSQL einen Abbruch:

=> COMMIT;
ROLLBACK

Warum kann eine Transaktion nach einem Fehler nicht fortgesetzt werden? Tatsache ist, dass ein Fehler in der Weise entstehen könnte, dass wir Zugriff auf einen Teil der Änderungen erhalten würden – die Atomizität nicht einmal der Transaktion, sondern des Operators wäre verletzt. Wie in unserem Beispiel, wo es dem Operator gelang, eine Zeile vor dem Fehler zu aktualisieren:

=> 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 muss gesagt werden, dass psql über einen Modus verfügt, der es immer noch ermöglicht, die Transaktion nach einem Fehler fortzusetzen, als ob die Aktionen des fehlerhaften Operators rückgängig gemacht würden.

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

=> UPDATE t SET s = repeat('X', 1/(id-4));
ERROR:  division by zero

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

=> COMMIT;

Es ist nicht schwer zu erraten, dass psql in diesem Modus tatsächlich vor jedem Befehl einen impliziten Speicherpunkt setzt und im Fehlerfall ein Rollback darauf einleitet. Dieser Modus wird standardmäßig nicht verwendet, da das Festlegen von Sicherungspunkten (auch ohne Rollback darauf) einen erheblichen Mehraufwand mit sich bringt.

Fortgesetzt.

Source: habr.com

Kommentar hinzufügen