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