MVCC-3. Zeilenversionen

Nun, wir haben die Fragen zur Isolationund haben einen Exkurs ĂŒber die Organisation von Daten auf niedriger Ebene. 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 Datenstruktur, 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.

  1. 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.
  2. 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.

Fortsetzung.

Quelle: habr.com

60GB SSD 8Gb DDR4