MVCC in PostgreSQL-3. Zeilenversionen

Nun, wir haben bereits über Isolation gesprochen und einen Exkurs in Bezug auf die Datenstruktur auf niedriger Ebene gemacht . Und wir haben endlich das Faszinierendste erreicht, nämlich die Zeilenversionen (Tupel).

Tupel-Header


Wie bereits erwähnt, können mehrere Versionen jeder Zeile gleichzeitig in der Datenbank vorhanden sein. Und wir müssen irgendwie eine Version von einer anderen unterscheiden. Zu diesem Zweck wird jede Version mit ihrer effektiven "Zeit" ( xmin ) und Ablauf "Zeit" ( xmax ) gekennzeichnet. Anführungszeichen geben an, dass ein spezieller Inkrementierungszähler anstelle der Zeit selbst verwendet wird. Und dieser Zähler ist die Transaktionskennung .

(In der Realität ist dies wie üblich komplizierter: Die Transaktions-ID kann aufgrund einer begrenzten Bittiefe des Zählers nicht immer inkrementiert werden. Weitere Einzelheiten hierzu werden wir jedoch untersuchen, wenn unsere Diskussion zum Einfrieren kommt.)

Beim xmin einer Zeile wird der Wert von xmin gleich der ID der Transaktion gesetzt, die den Befehl INSERT ausgeführt hat, während xmax nicht ausgefüllt ist.

Wenn eine Zeile gelöscht wird, wird der xmax Wert der aktuellen Version mit der ID der Transaktion gekennzeichnet, die DELETE ausgeführt hat.

Ein UPDATE-Befehl führt tatsächlich zwei nachfolgende Operationen aus: DELETE und INSERT. In der aktuellen Version der Zeile ist xmax gleich der ID der Transaktion, die UPDATE ausgeführt hat. Dann wird eine neue Version derselben Zeile erstellt, in der der Wert von xmin dem Wert von xmin der vorherigen Version entspricht.

xmin Felder xmin und xmax sind in der Kopfzeile einer Zeilenversion enthalten. Zusätzlich zu diesen Feldern enthält der Tupelheader weitere Felder, z.

  • infomask - mehrere Bits, die die Eigenschaften eines gegebenen Tupels bestimmen. Es gibt einige von ihnen, und wir werden sie im Laufe der Zeit besprechen.
  • ctid - ein Verweis auf die nächste, neuere Version derselben Zeile. ctid der neuesten, aktuellen Zeilenversion verweist auf ctid diese Version. Die Nummer hat die Form (x,y) , wobei x die Seitennummer und y die Ordnungsnummer des Zeigers im Array ist.
  • Die NULL-Bitmap, die die Spalten einer bestimmten Version markiert, die eine NULL enthalten. NULL ist kein regulärer Wert für Datentypen, daher müssen wir dieses Merkmal separat speichern.

Infolgedessen erscheint der Header ziemlich groß: Mindestens 23 Byte pro Tupel, jedoch aufgrund der NULL-Bitmap in der Regel größer. Wenn eine Tabelle "eng" ist (dh nur wenige Spalten enthält), können die Overhead-Bytes mehr Platz belegen als die nützlichen Informationen.

Einfügen


Schauen wir uns genauer an, wie die Operationen an Zeilen auf einer niedrigen Ebene ausgeführt werden, und beginnen wir mit einer Einfügung.

Zum Experimentieren 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 starten eine Transaktion, um eine Zeile einzufügen.

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

Dies ist die ID unserer aktuellen Transaktion:

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

Schauen wir uns den Inhalt der Seite an. Mit der Funktion heap_page_items aus der Erweiterung "pageinspect" können wir Informationen zu den Zeigern 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 das Wort "Heap" in PostgreSQL Tabellen bezeichnet. Dies ist eine weitere seltsame Verwendung eines Begriffs: Ein Heap ist eine bekannte Datenstruktur , die nichts mit einer Tabelle zu tun hat. Dieses Wort wird hier in dem Sinne verwendet, dass "alles aufgeschüttet ist", anders als in geordneten Indizes.

Diese Funktion zeigt die Daten "wie sie sind" in einem schwer verständlichen Format an. Um die Dinge zu klären, belassen wir nur einen Teil der Informationen und interpretieren 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) 

Wir haben folgendes gemacht:

  • Der Zeigernummer wurde eine Null hinzugefügt, damit sie wie eine t_ctid : (Seitennummer, t_ctid ).
  • Interpretierte den Status des Zeigers lp_flags . Es ist hier "normal", was bedeutet, dass der Zeiger tatsächlich auf eine Zeilenversion verweist. Wir werden später über andere Werte sprechen.
  • Von allen Informationsbits haben wir bisher nur zwei Paare ausgewählt. xmin_committed Bits xmin_committed und xmin_aborted zeigen an, ob die Transaktion mit der ID xmin ist ( xmin ). Ein Paar ähnlicher Bits bezieht sich auf die Transaktion mit der ID xmax .

Was beobachten wir? Wenn eine Zeile eingefügt wird, wird auf der Tabellenseite ein Zeiger mit der Nummer 1 angezeigt, der auf die erste und die einzige Version der Zeile verweist.

Das Feld xmin im Tupel wird mit der ID der aktuellen Transaktion gefüllt. Da die Transaktion noch aktiv ist, sind die Bits xmin_committed und xmin_aborted nicht gesetzt.

Das Feld ctid der Zeilenversion verweist auf dieselbe Zeile. Dies bedeutet, dass keine neuere Version verfügbar ist.

Das xmax Feld wird mit der konventionellen Nummer 0 gefüllt, da das Tupel nicht gelöscht wird, xmax aktuell ist. Transaktionen ignorieren diese Nummer aufgrund des gesetzten xmax_aborted Bits.

Gehen wir noch einen Schritt weiter, um die Lesbarkeit zu verbessern, indem wir Informationsbits an Transaktions-IDs anhängen. Und lassen Sie uns die Funktion erstellen, da wir die Abfrage 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; 

Was in der Kopfzeile der Zeilenversion passiert, ist in dieser Form viel klarer:

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

Wir können ähnliche, aber weitaus weniger detaillierte Informationen aus der Tabelle selbst erhalten, indem xmin xmax xmin und xmax :

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

Commit


Wenn eine Transaktion erfolgreich ist, muss ihr Status gespeichert werden, dh, die Transaktion muss als festgeschrieben markiert werden. Zu diesem Zweck wird die XACT-Struktur verwendet. (Vor Version 10 hieß es CLOG (Festschreibungsprotokoll), und Sie werden wahrscheinlich immer noch auf diesen Namen stoßen.)

XACT ist keine Tabelle des Systemkatalogs, sondern Dateien im Verzeichnis PGDATA / pg_xact. In diesen Dateien werden für jede Transaktion zwei Bits zugewiesen - "festgeschrieben" und "abgebrochen" - genau so wie im Tupel-Header. Diese Informationen sind nur zur Vereinfachung auf mehrere Dateien verteilt. Wir werden darauf zurückkommen, wenn wir über das Einfrieren sprechen. PostgreSQL arbeitet mit diesen Dateien wie mit allen anderen auch seitenweise.

Wenn also eine Transaktion festgeschrieben wird, wird das Bit "festgeschrieben" für diese Transaktion in XACT gesetzt. Dies ist alles, was passiert, wenn die Transaktion festgeschrieben wird (obwohl wir das Write-Ahead-Protokoll noch nicht erwähnen).

Wenn eine andere Transaktion auf die gerade betrachtete Tabellenseite zugreift, muss die erstere einige Fragen beantworten.

  1. Wurde die Transaktion xmin abgeschlossen? Wenn nicht, darf das erzeugte Tupel nicht sichtbar sein.
    Dies wird überprüft, indem eine andere Struktur durchsucht wird, die sich im gemeinsamen Speicher der Instanz befindet und ProcArray heißt. Diese Struktur enthält eine Liste aller aktiven Prozesse sowie die ID der jeweils aktuellen (aktiven) Transaktion.
  2. Wenn die Transaktion abgeschlossen wurde, wurde sie dann festgeschrieben oder rückgängig gemacht? Wenn es zurückgesetzt wurde, darf das Tupel auch nicht sichtbar sein.
    Genau dafür wird XACT benötigt. Es ist jedoch teuer, XACT jedes Mal zu überprüfen, obwohl die letzten Seiten von XACT in Puffern im gemeinsamen Speicher gespeichert sind. Sobald dies herausgefunden wurde, wird der Transaktionsstatus in die Bits xmin_committed und xmin_aborted des Tupels geschrieben. Wenn eines dieser Bits gesetzt ist, wird der Transaktionsstatus als bekannt behandelt und die nächste Transaktion muss XACT nicht überprüfen.

Warum setzt die Transaktion, die die Einfügung durchführt, diese Bits nicht? Wenn eine Einfügung ausgeführt wird, weiß die Transaktion noch nicht, ob sie erfolgreich abgeschlossen wird. Und zum Festschreibungszeitpunkt ist bereits unklar, welche Zeilen und auf welchen Seiten geändert wurden. Es kann eine Menge solcher Seiten geben, und es ist unpraktisch, sie im Auge zu behalten. Außerdem können einige der Seiten aus dem Puffer-Cache auf die Festplatte entfernt werden. Sie erneut zu lesen, um die Bits zu ändern, würde eine signifikante Verlangsamung des Commits bedeuten.

Die Kehrseite der Kostenersparnis ist, dass nach den Aktualisierungen jede Transaktion (auch die, die SELECT ausführt) beginnen kann, Datenseiten im Puffercache zu ändern.

Also verpflichten wir uns zur Veränderung.

 => COMMIT; 

Auf der Seite hat sich nichts geändert (aber wir wissen, dass der Transaktionsstatus bereits in XACT geschrieben wurde):

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

Nun muss eine Transaktion, die zuerst auf die Seite zugreift, den Status der Transaktion xmin 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) 

Löschen


Beim Löschen einer Zeile wird die ID der aktuellen xmax Feld xmax der aktuellen Version geschrieben und das Bit xmax_aborted zurückgesetzt.

Beachten Sie, dass der Wert von xmax , der der aktiven Transaktion entspricht, als Zeilensperre fungiert. Wenn eine andere Transaktion diese Zeile aktualisieren oder löschen wird, muss sie warten, bis die xmax Transaktion abgeschlossen ist. Wir werden später detaillierter auf Sperren eingehen. Beachten Sie an dieser Stelle nur, dass die Anzahl der Zeilensperren überhaupt nicht begrenzt ist. Sie belegen keinen Speicher und die Systemleistung wird von dieser Zahl nicht beeinflusst. Langfristige Transaktionen haben jedoch andere Nachteile, auf die später noch eingegangen wird.

Lassen Sie uns eine Zeile löschen.

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

Wir sehen, dass die Transaktions-ID in das xmax Feld geschrieben wird, 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 row) 

Abbrechen


Der Abbruch einer Transaktion funktioniert ähnlich wie das Festschreiben, außer dass das Bit "Abgebrochen" in XACT gesetzt ist. Ein Abbruch erfolgt so schnell wie ein Commit. Obwohl der Befehl ROLLBACK heißt, werden die Änderungen nicht zurückgesetzt: Alles, was die Transaktion bereits geändert hat, bleibt unberührt.

 => 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 gesetzt. Obwohl die Zahl xmax selbst noch auf der Seite vorhanden ist, wird sie nicht xmax .

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

Update


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

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

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

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

Aber wir können beide Versionen auf der Seite sehen:

 => 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 ID der aktuellen Transaktion im Feld xmax . Darüber hinaus hat dieser Wert den alten Wert überschrieben, seit die vorherige Transaktion zurückgesetzt wurde. Das xmax_aborted Bit wird zurückgesetzt, da der Status der aktuellen Transaktion noch nicht bekannt ist.

Die erste Version der Zeile verweist jetzt auf die zweite, und zwar auf eine neuere.

Die Indexseite enthält jetzt den zweiten Zeiger und die zweite Zeile, die auf die zweite Version auf der Tabellenseite verweisen.

Wie beim Löschen zeigt der Wert von xmax in der ersten Version an, dass die Zeile gesperrt ist.

Zuletzt verpflichten wir uns zur Transaktion.

 => COMMIT; 

Indizes


Bisher haben wir nur über Tabellenseiten gesprochen. Aber was passiert in Indizes?

Informationen auf Indexseiten hängen stark vom jeweiligen Indextyp ab. Darüber hinaus kann auch ein Indextyp unterschiedliche Seitentypen haben. Beispiel: Ein B-Baum enthält die Metadatenseite und "normale" Seiten.

Dennoch enthält eine Indexseite normalerweise ein Array von Zeigern auf die Zeilen und Zeilen selbst (genau wie Tabellenseiten). Außerdem wird am Ende einer Seite etwas Platz für spezielle Daten reserviert.

Zeilen in Indizes können je nach Indextyp auch unterschiedliche Strukturen haben. Beispiel: In einem B-Baum enthalten die für Blattseiten relevanten Zeilen den Wert des ctid und einen Verweis ( ctid ) auf die entsprechende Tabellenzeile. Generell kann ein Index ganz anders aufgebaut sein.

Der wichtigste Punkt ist, dass es in Indizes jeglichen Typs keine Zeilenversionen gibt. Oder wir können davon ausgehen, dass jede Zeile nur durch eine Version dargestellt wird. Mit anderen Worten enthält der Header der Indexzeile nicht die Felder xmin und xmax . Im Moment können wir davon ausgehen, dass Referenzen aus dem Index auf alle Versionen von Tabellenzeilen verweisen. Um herauszufinden, welche der Zeilenversionen für eine Transaktion sichtbar sind, muss PostgreSQL die Tabelle untersuchen. (Wie üblich ist dies nicht die ganze Geschichte. Manchmal ermöglicht die Sichtbarkeitskarte die Optimierung des Prozesses, aber wir werden dies später diskutieren.)

Hier auf der Indexseite finden wir Verweise auf beide Versionen: die aktuelle und die vorherige:

 => 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 nutzt PostgreSQL eine Optimierung, mit der Transaktions-IDs "sparsam" verbraucht werden können.

Wenn eine Transaktion nur Daten liest, hat dies keine Auswirkungen auf die Sichtbarkeit von Tupeln. Daher weist der Backend-Prozess der Transaktion zunächst eine virtuelle ID (virtuelle xid) zu. Diese ID besteht aus der Prozesskennung und einer fortlaufenden Nummer.

Die Zuweisung dieser virtuellen ID erfordert keine Synchronisation zwischen allen Prozessen und erfolgt daher sehr schnell. Wir werden einen weiteren Grund für die Verwendung virtueller IDs kennenlernen, wenn wir über das Einfrieren sprechen.

Datenschnappschüsse berücksichtigen die virtuelle ID überhaupt nicht.

Zu verschiedenen Zeitpunkten kann das System über virtuelle Transaktionen mit bereits verwendeten IDs verfügen. Dies ist in Ordnung. Diese ID kann jedoch nicht auf Datenseiten geschrieben werden, da die ID beim nächsten Zugriff auf die Seite bedeutungslos werden kann.

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

Wenn eine Transaktion beginnt, Daten zu ändern, erhält sie eine echte, eindeutige Transaktions-ID.

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

 => COMMIT; 

Subtransaktionen


Sicherungspunkte


In SQL werden Sicherungspunkte definiert, mit denen einige Vorgänge der Transaktion rückgängig gemacht werden können, ohne dass sie vollständig abgebrochen werden. Dies ist jedoch mit dem obigen Modell nicht kompatibel, da der Transaktionsstatus für alle Änderungen gleich ist und kein physisches Rollback für Daten ausgeführt wird.

Um diese Funktionalität zu implementieren, wird eine Transaktion mit einem Sicherungspunkt in mehrere separate Subtransaktionen unterteilt, deren Status separat verwaltet werden kann.

Subtransaktionen haben ihre eigenen IDs (größer als die ID der Haupttransaktion). Der Status von Subtransaktionen wird auf übliche Weise in XACT geschrieben, der endgültige Status hängt jedoch vom Status der Haupttransaktion ab: Wenn ein Rollback durchgeführt wird, werden auch alle Subtransaktionen zurückgesetzt.

Informationen zur Verschachtelung von Subtransaktionen werden in Dateien des Verzeichnisses PGDATA / pg_subtrans gespeichert. Auf diese Dateien wird über Puffer im gemeinsamen Speicher der Instanz zugegriffen, die genauso aufgebaut sind wie XACT-Puffer.

Verwechseln Sie Subtransaktionen nicht mit autonomen Transaktionen. Autonome Transaktionen hängen in keiner Weise voneinander ab, während Subtransaktionen davon abhängen. Es gibt keine autonomen Transaktionen im regulären PostgreSQL, was vielleicht zum Besseren ist: Sie werden tatsächlich äußerst selten benötigt, und ihre Verfügbarkeit in anderen DBMS führt zu Missbrauch, unter dem jeder leidet.

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 legen wir einen Sicherungspunkt fest 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 ID der Haupttransaktion und nicht der Subtransaktion zurückgibt.

 => 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 Sicherungspunkt zurückkehren und die dritte Zeile einfügen.

 => ROLLBACK TO sp; => INSERT INTO t 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 rückgängig gemachte Subtransaktion hinzugefügt wurde.

Änderungen übernehmen.

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

Es ist jetzt deutlich zu sehen, dass jede Subtransaktion ihren eigenen Status hat.

Beachten Sie, dass SQL die explizite Verwendung von Subtransaktionen nicht zulässt. Sie können also keine neue Transaktion starten, bevor Sie die aktuelle abgeschlossen haben. Diese Technik wird implizit angewendet, wenn Sicherungspunkte verwendet werden und wenn PL / pgSQL-Ausnahmen behandelt werden, sowie in einigen anderen, exotischeren Situationen.

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


Was passiert, wenn während des Vorgangs 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 wird die Transaktion als abgebrochen behandelt und es sind keine Operationen darin erlaubt:

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

Und selbst wenn wir versuchen, die Änderungen festzuschreiben, meldet PostgreSQL das Rollback:

 => COMMIT; 
 ROLLBACK 

Warum kann die Transaktion nach einem Fehler nicht mehr ausgeführt werden? Die Sache ist, dass der Fehler auftreten könnte, so dass wir Zugriff auf einen Teil der Änderungen erhalten würden, das heißt, die Atomizität würde nicht nur für die Transaktion, sondern auch für einen einzelnen Operator unterbrochen. In unserem Beispiel hätte der Operator beispielsweise eine Zeile aktualisieren können, bevor der Fehler auftrat:

 => 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 erwähnenswert, dass psql über einen Modus verfügt, mit dem die Transaktion nach einem Fehler fortgesetzt werden kann, als ob die Auswirkungen 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 einfach herauszufinden, dass in diesem Modus psql vor jedem Befehl einen impliziten Sicherungspunkt erstellt und bei einem Fehler ein Rollback darauf initiiert. Dieser Modus wird standardmäßig nicht verwendet, da das Einrichten von Sicherungspunkten (auch ohne Rollback) einen erheblichen Mehraufwand mit sich bringt.

Lesen Sie weiter .

Source: https://habr.com/ru/post/de477648/


All Articles