MVCC-3. Versions af rækker

Så vi har set på spørgsmål vedrørende isolering, og vi har gjort et afstikker om organisation af data på lavt niveau. Og endelig er vi nået til det mest interessante - rækkerne versioner.

Overskrift

Som vi allerede har nævnt, kan hver række samtidig eksistere i databasen i flere versioner. Man skal på en eller anden måde skelne en version fra en anden. Til dette formål har hver version to mærker, der bestemmer 'tiden' for denne versions gyldighed (xmin og xmax). I anførselstegn - fordi det ikke er tiden som sådan, der bruges, men en særlig stigende tæller. Og denne tæller - er transaktionsnummeret.

(Som sædvanligt er alt i virkeligheden mere kompliceret: transaktionsnumre kan ikke altid stige på grund af den begrænsede bitstørrelse af tælleren. Men disse detaljer vil vi gå i dybden med, når vi når til frysepunkter.)

Når en række oprettes, sættes værdien af xmin til nummeret på den transaktion, der udførte INSERT-kommandoen, mens xmax ikke udfyldes.

Når en række slettes, markerer værdien af xmax for den aktuelle version med transaktionsnummeret, der udførte DELETE.

Når en række ændres med UPDATE-kommandoen, udføres der faktisk to operationer: DELETE og INSERT. I den aktuelle version af rækken sættes xmax til nummeret på den transaktion, der udførte UPDATE. Derefter oprettes en ny version af den samme række; værdien af xmin matcher værdien af xmax i den forrige version.

Felterne xmin og xmax indgår i versionens header. Udover disse felter indeholder headeren også andre, eksempelvis:

  • infomask - en række bits, der bestemmer egenskaberne for denne version. Der er ret mange; hovedpunkterne vil vi gradvist se nærmere på.
  • ctid - en reference til den næste, nyere version af den samme række. For den nyeste, aktuelle version af rækken henviser ctid til selve denne version. Nummeret har formen (x,y), hvor x er sidenummeret, y er sekvensnummeret for pegeren i arrayet.
  • bitmask for usikre værdier - markerer de kolonner i denne version, som indeholder usikre værdier (NULL). NULL er ikke en af de almindelige værdier i datatyperne, derfor skal flaget opbevares separat.

Som resultat bliver overskriften ret lang — mindst 23 byte for hver version af strengen, og normalt mere på grund af NULL-bitkortet. Hvis tabellen er "snæver" (dvs. indeholder få kolonner), kan overheaden tage mere end den nyttige information.

Indsættelse

Lad os se nærmere på, hvordan operationer med strenge udføres på lavt niveau, og vi starter med indsættelse.

Til eksperimenter opretter vi en ny tabel med to kolonner og en indeks på en af dem:

=> CREATE TABLE t(
  id serial,
  s text
);
=> CREATE INDEX ON t(s);

Vi indsætter en række, efter vi har startet transaktionen.

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

Her er nummeret på vores nuværende transaktion:

=> SELECT txid_current();
 txid_current 
--------------
         3664
(1 række)

Lad os kigge på indholdet af siden. Funktionen heap_page_items fra extension pageinspect tillader os at få oplysninger om pegepinde og versioner af strenge:

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

Vi bemærker, at ordet heap (bunke) i PostgreSQL refererer til tabeller. Dette er endnu en underlig anvendelse af termen — en bunke er en kendt datastruktur, som ikke har noget med tabellen at gøre. Her benyttes ordet i betydningen "alt er samlet i en bunke", i modsætning til sorteredekser.

Funktionen viser data "som de er", i et format som er svært at opfatte. For at forstå det, lader vi kun en del af oplysningerne være og afkodere dem:

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

Dette er, hvad vi har gjort:

  • Vi har tilføjet en nul til pegerens nummer for at bringe det til samme format som t_ctid: (sidenummer, pegernummer).
  • Vi har afkodet tilstanden for pegeren lp_flags. Her er den "normal" — det betyder, at pegeren faktisk henviser til versionen af strengen. Andre værdier vil vi se på senere.
  • Af alle informationsbits har vi indtil videre kun identificeret to par. Bits xmin_committed og xmin_aborted viser, om transaktionen med nummer xmin er blevet bekræftet (eller annulleret). To tilsvarende bits henviser til transaktionen med nummer xmax.

Hvad ser vi? Ved indsættelse af en række i en tabelside vil en peger med nummer 1 blive vist, der refererer til den første og eneste version af rækken.

I rækkens version er feltet xmin udfyldt med nummeret på den aktuelle transaktion. Transaktionen er stadig aktiv, så begge bits xmin_committed og xmin_aborted er ikke sat.

Feltet ctid i rækkens version henviser til den samme række. Det betyder, at der ikke eksisterer en nyere version.

Feltet xmax er udfyldt med en fiktiv nummer 0, da denne version af rækken ikke er slettet og er aktuel. Transaktioner vil ikke tage højder for dette nummer, da biten xmax_aborted er sat.

Lad os tage et skridt videre mod forbedring af læsbarheden ved at tilføje informationsbits til transaktionsnumrene. Og lad os oprette en funktion, da vi vil få brug for forespørgslen igen:

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

I denne form er det meget tydeligere, hvad der sker i overskriften på rækkens version:

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

En lignende, men betydeligt mindre detaljeret, information kan også opnås fra selve tabellen ved at bruge pseudokolonnerne xmin og xmax:

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

Bekræftelse

Ved en vellykket afslutning af transaktionen skal dens status huskes — at markere, at den er bekræftet. Dette sker ved hjælp af en struktur, kendt som XACT (og før version 10 blev den kaldt CLOG (commit log), et navn der stadig kan ses forskellige steder).

XACT — ikke en systemkatalogtabel; det er filer i kataloget PGDATA/pg_xact. For hver transaktion er der afsat to bits: committed og aborted — ligesom i versionshovedet for rækken. Denne information er opdelt på flere filer udelukkende for nemheds skyld; vi vil vende tilbage til dette emne, når vi ser på fryseprocessen. Arbejdet med disse filer foregår side for side, ligesom med alle andre.

Så når en transaktion i XACT bekræftes, sættes committed-biten for den pågældende transaktion. Og det er alt, hvad der sker ved bekræftelsen (selvfølgelig taler vi ikke endnu om forudskrivningsloggen).

Når en anden transaktion tilgår tabeloversigten, som vi netop har set på, er den nødt til at besvare flere spørgsmål.

  1. Er transaktionen xmin afsluttet? Hvis ikke, må den oprettede versions række ikke være synlig.
    Denne kontrol udføres ved at se på en anden struktur, der ligger i den delte hukommelse af instansen og kaldes ProcArray. Her findes listen over alle aktive processer, og for hver er det angivet, hvilket nummer der er på dens nuværende (aktive) transaktion.
  2. Hvis den er afsluttet, så hvordan — ved bekræftelse eller annullering? Hvis ved annullering, så må versionen af rækken heller ikke være synlig.
    Det er netop til dette formål, at XACT er nødvendigt. Men selvom de seneste sider af XACT opbevares i buffere i det operative hukommelse, er det stadig omkostningsfuldt at kontrollere XACT hver gang. Derfor bliver den engang konstaterede status for transaktionen gemt i bitene xmin_committed og xmin_aborted i versionsrækken. Hvis en af disse bits er indstillet, betragtes tilstanden af transaktionen xmin som kendt, og den næste transaktion behøver ikke længere at henvise til XACT.

Hvorfor bliver disse bits ikke sat af selve transaktionen, der udfører indsættelsen? Når indsættelsen finder sted, ved transaktionen endnu ikke, om den vil blive afsluttet med succes. Og på tidspunktet for bekræftelsen er det uklart, hvilke specifikke rækker i hvilke specifikke sider der er blevet ændret. Der kan være mange sådanne sider, og det er ikke rentabelt at huske dem. Desuden kan nogle sider være blevet udskudt fra buffer cachen til disken; at læse dem igen for at ændre bits ville væsentligt bremse bekræftelsen.

Den bagside, der følger med besparelsen, er, at efter ændringer kan enhver transaktion (selv en simpel læsning — SELECT) begynde at ændre datarodene i buffer cachen.

Lad os derfor bekræfte ændringen.

=> COMMIT;

Der er ikke sket nogen ændringer på siden (men vi ved, at transaktionsstatus allerede er gemt i XACT):

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

Nu skal transaktionen, der først henvendte sig til siden, bestemme transaktionsxmin-status og gemme den i informationsbitene:

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

Sletning

Ved sletning af en række gemmes nummeret på den nuværende slette-transaktion i feltet xmax for den aktuelle version, og biten xmax_aborted nulstilles.

Det skal bemærkes, at den indstillede værdi xmax, der svarer til den aktive transaktion, fungerer som en lås på rækken. Hvis en anden transaktion forsøger at opdatere eller slette denne række, skal den vente på at transaktionen xmax afsluttes. Mere om låse vil vi diskutere senere. Indtil da skal vi kun bemærke, at antallet af låse på rækker ikke er begrænset. De optager ikke plads i hukommelsen, og systemets ydeevne påvirkes ikke af deres antal. Dog har 'lange' transaktioner andre ulemper, men det vil vi også tale om senere.

Lad os slette rækken.

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

Vi ser, at transaktionsnummeret er blevet gemt i feltet xmax, men informationsbitene er ikke indstillet:

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

Rollback

Rollback af ændringer fungerer på samme måde som commit, blot der i XACT for transaktionen bliver biten aborted indstillet. Rul tilbage udføres lige så hurtigt som commit. Selvom kommandoen kaldes ROLLBACK, sker der ingen tilbageførsel af ændringer: alt, hvad transaktionen nåede at ændre i databladerne, forbliver uændret.

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

Ved henvendelse til siden vil status blive tjekket, og biten xmax_aborted vil blive indstillet i versionsrækken. Selve nummeret xmax forbliver på siden, men ingen vil se på det længere.

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

Opdatering

Opdatering fungerer som om den nuværende version af rækken blev slettet først, og derefter blev den nye indsat.

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

Forespørgslen returnerer én række (den nye version):

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

Men på siden ser vi begge versioner:

=> SELECT * FROM heap_page('t',0);
 ctid  | tilstand  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 3666  | (0,2)
 (0,2) | normal | 3666     | 0 (a) | (0,2)
(2 rækker)

Den fjernede version er markeret med nummeret på den aktuelle transaktion i feltet xmax. Dette nummer er skrevet over det gamle, fordi den forrige transaktion blev annulleret. Og biten xmax_aborted er nulstillet, da status for den aktuelle transaktion stadig er ukendt.

Den første version af rækken henviser nu til den anden (feltet t_ctid) som den nyere.

På indeksiden vises der en anden pointer og en anden række, der henviser til den anden version på tabelsiden.

Ligesom ved sletning fungerer værdien xmax i den første version af rækken som et tegn på, at rækken er låst.

Og lad os afslutte transaktionen.

=> COMMIT;

Indekser

Indtil nu har vi kun talt om tabelsider. Hvad sker der inden i indeksene?

Oplysningerne på indeksiderne afhænger stærkt af den specifikke type indeks. Selv inden for én type indeks kan der være forskellige slags sider. For eksempel har et B-træ en sidetype med metadata og "almindelige" sider.

Men typisk indeholder en side en række pointere til rækkerne og selve rækkerne (ligesom i tabelsiden). Derudover er der i slutningen af siden plads til specielle data.

Rækker i indeksene kan også have meget forskellig struktur afhængigt af den type indeks. For eksempel, for et B-træ indeholder rækker, der henviser til bladene, værdi af nøgleindeksering og en reference (ctid) til den tilsvarende tabelrække. Generelt kan indekset være indrettet på en helt anden måde.

Det vigtigste punkt er, at der ikke findes versionsrækker i indekser af nogen type. Eller man kan sige, at hver række er repræsenteret ved præcist én version. Med andre ord har overskriften på indeksrækken ikke felterne xmin og xmax. Man kan betragte referencerne fra indekset som henvisende til alle tabelversioner af rækkerne - så man kan kun finde ud af, hvilken version transaktionen vil se, ved at se i tabellen. (Som sædvanlig er dette ikke hele sandheden. I nogle tilfælde tillader synskortet at optimere processen, men det vil vi se nærmere på senere.)

På indeksiden finder vi pointere til begge versioner, både den aktuelle og den gamle:

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

Virtuelle transaktioner

I praksis anvender PostgreSQL en optimering, der muliggør at "sætte sparetegn" på transaktionsnumre.

Hvis en transaktion kun læser data, påvirker den ikke synligheden af rækkeversioner. Derfor tildeler tjenesteprocessen først transaktionen et virtuelt nummer (virtual xid). Nummeret består af proces-ID og et sekventielt nummer.

Tildelingen af dette nummer kræver ikke synkronisering mellem alle processer og udføres derfor meget hurtigt. En anden årsag til brugen af virtuelle numre vil blive præsenteret, når vi taler om frysefunktionaliteten.

Virtuelle numre tages ikke i betragtning i dataglæder.

På forskellige tidspunkter kan der helt sikkert være virtuelle transaktioner i systemet med numre, der allerede er blevet brugt, og det er normalt. Men et sådant nummer kan ikke skrives til datablade, da det kan miste al mening ved næste adgang til siden.

=> BEGIN;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                         
(1 række)

Hvis transaktionen begynder at ændre data, tildeles den et rigtigt, unikt transaktionsnummer.

=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                     3667
(1 række)

=> COMMIT;

Nedstillede transaktioner

Gemepunkter

I SQL er der defineret gemepunkter (savepoint), der gør det muligt at annullere en del af transaktionens operationer uden at afbryde hele transaktionen. Men dette passer ikke ind i den tidligere nævnte skema, da status for transaktionen er ens for alle dens ændringer, og ingen data fysiske rulles tilbage.

For at implementere en sådan funktionalitet opdeles transaktionen med et gemepunkt i flere separate nedstillede transaktioner (subtransaction), hvis status kan styres individuelt.

Nedstillede transaktioner har deres eget nummer (større end hovedtransaktionen). Status for nedstillede transaktioner registreres på normal vis i XACT, men den endelige status afhænger af status for hovedtransaktionen: hvis den annulleres, annulleres også alle nedstillede transaktioner.

Information om indlejring af transaktioner gemmes i filer i mappen PGDATA/pg_subtrans. Tilgang til filerne sker gennem buffere i delt hukommelse af forekomsten, organiseret som XACT-bufre.

Forveksl ikke indlejrede transaktioner og autonome transaktioner. Autonome transaktioner afhænger ikke af hinanden, mens indlejrede gør. Der er ingen autonome transaktioner i almindelig PostgreSQL, og det er måske til det bedre: de er meget sjældent nødvendige, og deres tilstedeværelse i andre DBMS'er kan føre til misbrug, som alle derefter lider under.

Lad os rydde tabellen, starte en transaktion og indsætte en række:

=> TRUNCATE TABLE t;
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 række)

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
(1 række)

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

Nu sætter vi et gemmepunkt og indsætter endnu en række.

=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT txid_current();
 txid_current 
--------------
         3669
(1 række)

Bemærk, at funktionen txid_current() giver nummeret på den primære, ikke den indlejrede, transaktion.

=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3670 |    0 |  3 | XYZ
(2 rækker)

=> 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 rækker)

Lad os rulle tilbage til gemmepunktet og indsætte den tredje række.

=> 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 rækker)

=> 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 rækker)

På siden ser vi stadig rækken tilføjet af den annullerede indlejrede transaktion.

Vi fastslår ændringerne.

=> COMMIT;
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rækker)

=> 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 rækker)

Nu er det tydeligt, at hver indlejret transaktion har sin egen status.

Bemærk, at indlejrede transaktioner ikke kan bruges i SQL eksplicit, dvs. man kan ikke starte en ny transaktion uden at afslutte den nuværende. Denne mekanisme aktiveres implicit ved brug af gemmepunkter, samt ved behandling af undtagelser i PL/pgSQL og i en række andre, mere eksotiske, tilfælde.

=> BEGIN;
BEGIN
=> BEGIN;
ADVARSEL: der er allerede en transaktion i gang
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
ADVARSEL: der er ingen transaktion i gang
COMMIT

Fejl og atomaritet af operationer

Hvad sker der, hvis der opstår en fejl under udførelsen af en operation? For eksempel sådan:

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

=> UPDATE t SET s = repeat('X', 1/(id-4));
FEJL: division med nul

Der opstod en fejl. Transaktionen betragtes nu som afbrudt, og ingen operationer er tilladt i den:

=> SELECT * FROM t;
FEJL: den nuværende transaktion er afbrudt, kommandoer ignoreres indtil slutningen af transaktionsblokken

Og selv hvis man forsøger at bekræfte ændringerne, vil PostgreSQL rapportere om annullering:

=> COMMIT;
ROLLBACK

Hvorfor kan man ikke fortsætte med at udføre transaktionen efter en fejl? Det er fordi, fejlen kunne være opstået på en måde, så vi ville have fået adgang til dele af ændringerne — atomariteten for både transaktionen og operatoren ville være blevet krænket. Som i vores eksempel, hvor operatoren nåede at opdatere en række før fejlen:

=> 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 rækker)

Det skal siges, at psql har en tilstand, der alligevel tillader at fortsætte arbejdet i transaktionen efter en fejl, som om handlingerne fra den fejlslagne operator blev rullet tilbage.

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

=> UPDATE t SET s = repeat('X', 1/(id-4));
FEJL: division med nul

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

=> COMMIT;

Det er ikke svært at gætte, at psql i denne tilstand faktisk sætter en implicit gemmepunkt før hver kommando, og i tilfælde af en fejl initierer det et rollback til dette punkt. Denne tilstand bruges ikke som standard, da opsætning af gemmepunkter (selv uden rollback til dem) er forbundet med betydelige omkostninger.

Fortsættelse.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster