MVCC-3. String versiot

Olemme siis pohtineet aiheeseen liittyviä kysymyksiä eristys, ja vetäytyi noin tietojen järjestäminen alhaiselle tasolle. Ja lopuksi pääsimme mielenkiintoisimpaan osaan - merkkijonoversioihin.

Otsikko

Kuten olemme jo todenneet, jokainen rivi voi olla olemassa samanaikaisesti useissa versioissa tietokannassa. Yksi versio on jotenkin erotettava toisesta.Tätä tarkoitusta varten jokaisessa versiossa on kaksi merkkiä, jotka määrittävät tämän version "toimintaajan" (xmin ja xmax). Lainausmerkeissä - koska ei käytetä aikaa sinänsä, vaan erityistä kasvavaa laskuria. Ja tämä laskuri on tapahtuman numero.

(Kuten tavallista, todellisuus on monimutkaisempi: tapahtumanumero ei voi kasvaa koko ajan laskurin rajoitetun bittikapasiteetin vuoksi. Mutta katsotaan näitä yksityiskohtia tarkemmin, kun pääsemme jäätymään.)

Kun rivi luodaan, xmin asetetaan tapahtumanumeroksi, joka antoi INSERT-komennon, ja xmax jätetään tyhjäksi.

Kun rivi poistetaan, nykyisen version xmax-arvo merkitään DELETE-toiminnon suorittaneen tapahtuman numerolla.

Kun riviä muokataan UPDATE-komennolla, suoritetaan kaksi toimintoa: DELETE ja INSERT. Rivin nykyinen versio asettaa xmax:n yhtä suureksi kuin PÄIVITYKSEN suorittaneen tapahtuman numero. Sitten luodaan uusi versio samasta merkkijonosta; sen xmin-arvo on sama kuin edellisen version xmax-arvo.

xmin- ja xmax-kentät sisältyvät riviversion otsikkoon. Näiden kenttien lisäksi otsikko sisältää muita, esimerkiksi:

  • infomask on sarja bittejä, jotka määrittelevät tämän version ominaisuudet. Niitä on melko paljon; Tarkastellaan vähitellen tärkeimpiä.
  • ctid on linkki saman rivin seuraavaan, uudempaan versioon. Merkkijonon uusimmassa, uusimmassa versiossa ctid viittaa itse tähän versioon. Numeron muoto on (x,y), jossa x on sivunumero, y on taulukon indeksinumero.
  • null bitmap - Merkitsee ne tietyn version sarakkeet, jotka sisältävät nolla-arvon (NULL). NULL ei ole yksi tavallisista tietotyyppiarvoista, joten attribuutti on tallennettava erikseen.

Tämän seurauksena otsikko on melko suuri - vähintään 23 tavua jokaiselle rivin versiolle, ja yleensä enemmän NULL-bittikartan vuoksi. Jos taulukko on "kapea" (eli sisältää vähän sarakkeita), yleiskustannukset voivat viedä enemmän kuin hyödyllisiä tietoja.

lisätä

Katsotaanpa tarkemmin, kuinka matalan tason merkkijonotoiminnot suoritetaan, aloittaen lisäämisestä.

Luodaan kokeiluja varten uusi taulukko, jossa on kaksi saraketta ja indeksi toisessa:

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

Lisätään yksi rivi tapahtuman aloittamisen jälkeen.

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

Tässä on nykyinen tapahtumanumeromme:

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

Katsotaanpa sivun sisältöä. Pageinspect-laajennuksen heap_page_items-funktio antaa sinulle tietoa osoittimista ja riviversioista:

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

Huomaa, että PostgreSQL:n sana kasa viittaa taulukoihin. Tämä on toinen outo termin käyttö - kasa tunnetaan tietorakenne, jolla ei ole mitään yhteistä taulukon kanssa. Tässä sanaa käytetään merkityksessä "kaikki heitetään yhteen" toisin kuin järjestetyt indeksit.

Toiminto näyttää tiedot "sellaisenaan" vaikeasti ymmärrettävässä muodossa. Selvittääksemme sen, jätämme vain osan tiedoista ja tulkitsemme sen:

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

Tässä on mitä teimme:

  • Hakemistonumeroon lisättiin nolla, jotta se näyttäisi samalta kuin t_ctid: (sivunumero, hakemistonumero).
  • Selvitetty lp_flags-osoittimen tila. Tässä se on "normaali" - tämä tarkoittaa, että osoitin todella viittaa merkkijonon versioon. Tarkastellaan muita merkityksiä myöhemmin.
  • Kaikista informaatiobiteistä on toistaiseksi tunnistettu vain kaksi paria. Bitit xmin_committed ja xmin_aborted osoittavat, onko tapahtumanumero xmin sitoutunut (keskeytetty). Kaksi samanlaista bittiä viittaa tapahtumanumeroon xmax.

Mitä me näemme? Kun lisäät rivin, taulukkosivulle tulee indeksinumero 1, joka osoittaa rivin ensimmäistä ja ainoaa versiota.

Merkkijonoversiossa xmin-kenttä täytetään nykyisellä tapahtumanumerolla. Tapahtuma on edelleen aktiivinen, joten xmin_committed- ja xmin_aborted-bittejä ei ole asetettu.

Riviversio ctid -kenttä viittaa samaan riviin. Tämä tarkoittaa, että uudempaa versiota ei ole olemassa.

xmax-kenttä on täytetty valenumerolla 0, koska tätä rivin versiota ei ole poistettu ja se on ajan tasalla. Tapahtumat eivät kiinnitä huomiota tähän numeroon, koska xmax_aborted bitti on asetettu.

Otetaan vielä yksi askel luettavuuden parantamiseksi lisäämällä tapahtumanumeroihin tietobittejä. Ja luodaan funktio, koska tarvitsemme pyyntöä useammin kuin kerran:

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

Tässä muodossa on paljon selkeämpää, mitä riviversion otsikossa tapahtuu:

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

Samanlaisia, mutta huomattavasti vähemmän yksityiskohtaisia ​​tietoja voidaan saada itse taulukosta käyttämällä pseudo-sarakkeita xmin ja xmax:

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

Kiinnitys

Jos tapahtuma on suoritettu onnistuneesti, sinun on muistettava sen tila - huomaa, että se on sitoutunut. Tätä varten käytetään rakennetta nimeltä XACT (ja ennen versiota 10 sen nimi oli CLOG (commit log) ja tämä nimi löytyy edelleen eri paikoista).

XACT ei ole järjestelmäluettelotaulukko; nämä ovat tiedostot PGDATA/pg_xact-hakemistossa. Niissä on kaksi bittiä jokaiselle tapahtumalle: sitoutunut ja keskeytetty - aivan kuten riviversion otsikossa. Nämä tiedot on jaettu useisiin tiedostoihin vain mukavuuden vuoksi; palaamme tähän asiaan, kun harkitsemme jäädyttämistä. Ja työskentely näiden tiedostojen kanssa tapahtuu sivu sivulta, kuten kaikkien muidenkin kanssa.

Joten kun tapahtuma on sitoutunut XACT:ssä, sitoutunut bitti asetetaan tälle tapahtumalle. Ja tämä on kaikki, mitä tapahtuu sitoutumisen aikana (vaikka emme vielä puhu esitallennuslokista).

Kun jokin toinen tapahtuma avaa juuri tarkastelemamme taulukkosivun, sen on vastattava useisiin kysymyksiin.

  1. Onko xmin-tapahtuma tehty? Jos ei, luotu versio merkkijonosta ei saa olla näkyvissä.
    Tämä tarkistus suoritetaan tarkastelemalla toista rakennetta, joka sijaitsee ilmentymän jaetussa muistissa ja jota kutsutaan nimellä ProcArray. Se sisältää luettelon kaikista aktiivisista prosesseista, ja jokaiselle ilmoitetaan sen nykyisen (aktiivisen) tapahtuman numero.
  2. Jos se on valmis, miten - sitoutumalla vai peruuttamalla? Jos se peruutetaan, riviversion ei myöskään pitäisi olla näkyvissä.
    Juuri tätä varten XACT on tarkoitettu. Mutta vaikka XACT:n viimeiset sivut on tallennettu RAM:n puskureihin, on silti kallista tarkistaa XACT joka kerta. Siksi, kun tapahtuman tila on määritetty, se kirjoitetaan merkkijonoversion bitteihin xmin_committed ja xmin_aborted. Jos jokin näistä biteistä on asetettu, tapahtuman tila xmin katsotaan tunnetuksi, eikä seuraavan tapahtuman tarvitse käyttää XACT:tä.

Mikseivät nämä bitit asetu tapahtuman itse tekemällä lisäyksellä? Kun lisäys tapahtuu, tapahtuma ei vielä tiedä onnistuuko se. Ja sitoutumishetkellä ei ole enää selvää, millä riveillä mitkä sivut muuttuivat. Tällaisia ​​sivuja voi olla paljon, ja niiden ulkoa ottaminen ei ole kannattavaa. Lisäksi jotkin sivut voidaan häätää puskurivälimuistista levylle; niiden lukeminen uudelleen bittien vaihtamiseksi hidastaisi sitoutumista merkittävästi.

Säästöjen huono puoli on se, että muutosten jälkeen mikä tahansa tapahtuma (myös yksinkertaisen lukemisen suorittava - SELECT) voi alkaa muuttaa datasivuja puskurin välimuistissa.

Korjataan siis muutos.

=> COMMIT;

Mikään ei ole muuttunut sivulla (mutta tiedämme, että tapahtuman tila on jo tallennettu XACT:hen):

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

Nyt sivulle ensimmäisenä siirtyvän tapahtuman on määritettävä xmin-tapahtuman tila ja kirjoitettava se tietobitteihin:

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

Poistaminen

Kun rivi poistetaan, nykyisen poistotapahtuman numero kirjoitetaan nykyisen version xmax-kenttään ja bitti xmax_aborted tyhjennetään.

Huomaa, että aktiivista tapahtumaa vastaava xmax-arvo toimii rivilukkona. Jos toinen tapahtuma haluaa päivittää tai poistaa tämän rivin, sen on odotettava tapahtuman xmax valmistumista. Puhumme estämisestä lisää myöhemmin. Toistaiseksi huomioimme vain, että rivilukkojen määrä on rajoittamaton. Ne eivät vie tilaa RAM-muistista ja järjestelmän suorituskyky ei kärsi niiden määrästä. Totta, "pitkillä" liiketoimilla on muita haittoja, mutta niistä lisää myöhemmin.

Poistetaan rivi.

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

Näemme, että tapahtumanumero on kirjoitettu xmax-kenttään, mutta tietobittejä ei ole asetettu:

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

Peruutus

Muutosten keskeyttäminen toimii samalla tavalla kuin sitoutuminen, vain XACT:ssa keskeytetty bitti asetetaan tapahtumalle. Kumoaminen on yhtä nopeaa kuin sitoutuminen. Vaikka komennon nimi on ROLLBACK, muutoksia ei peruuteta: kaikki, mitä tapahtuma onnistui muuttamaan tietosivuilla, pysyy ennallaan.

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

Kun sivua avataan, tila tarkistetaan ja xmax_aborted-vihjebitti asetetaan riviversioon. Itse xmax-numero jää sivulle, mutta kukaan ei katso sitä.

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

Päivittää

Päivitys toimii ikään kuin se ensin poistaisi rivin nykyisen version ja lisäisi sitten uuden.

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

Kysely tuottaa yhden rivin (uusi versio):

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

Mutta sivulla näemme molemmat versiot:

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

Poistettu versio on merkitty nykyisellä tapahtumanumerolla xmax-kenttään. Lisäksi tämä arvo kirjoitetaan vanhan päälle, koska edellinen tapahtuma peruutettiin. Ja xmax_aborted bitti tyhjennetään, koska nykyisen tapahtuman tilaa ei vielä tiedetä.

Rivin ensimmäinen versio viittaa nyt toiseen (t_ctid-kenttä) uudempaan versioon.

Toinen hakemisto ilmestyy hakemistosivulle ja toinen rivi viittaa taulukkosivun toiseen versioon.

Aivan kuten poistamisen yhteydessä, xmax-arvo rivin ensimmäisessä versiossa on osoitus siitä, että rivi on lukittu.

No, viedään kauppa loppuun.

=> COMMIT;

Indeksit

Toistaiseksi olemme puhuneet vain taulukkosivuista. Mitä indeksien sisällä tapahtuu?

Hakemistosivujen tiedot vaihtelevat suuresti hakemistotyypin mukaan. Ja jopa yhden tyyppisessä hakemistossa on erityyppisiä sivuja. Esimerkiksi B-puussa on metatietosivu ja "tavalliset" sivut.

Sivulla on kuitenkin yleensä joukko osoittimia riveille ja itse riveille (kuten taulukkosivulla). Lisäksi sivun lopussa on tilaa erikoistiedoille.

Indeksien riveillä voi myös olla hyvin erilaisia ​​rakenteita indeksin tyypistä riippuen. Esimerkiksi B-puussa lehtisivuihin liittyvät rivit sisältävät indeksointiavaimen arvon ja viitteen (ctid) vastaavaan taulukon riviin. Yleensä indeksi voidaan rakentaa täysin eri tavalla.

Tärkeintä on, että minkään tyyppisissä hakemistoissa ei ole riviversioita. No, tai voimme olettaa, että jokaista riviä edustaa täsmälleen yksi versio. Toisin sanoen indeksirivin otsikossa ei ole xmin- ja xmax-kenttiä. Voimme olettaa, että linkit indeksistä johtavat kaikkiin rivien taulukkoversioihin - joten voit selvittää, minkä version tapahtuma näkee vain katsomalla taulukkoa. (Kuten aina, tämä ei ole koko totuus. Joissakin tapauksissa näkyvyyskartta voi optimoida prosessin, mutta tarkastelemme tätä tarkemmin myöhemmin.)

Samaan aikaan hakemistosivulta löydämme osoittimet molempiin versioihin, sekä nykyiseen että vanhaan:

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

Virtuaaliset liiketoimet

Käytännössä PostgreSQL käyttää optimointeja, joiden avulla se voi "tallentaa" tapahtumanumeroita.

Jos tapahtuma vain lukee dataa, sillä ei ole vaikutusta riviversioiden näkyvyyteen. Siksi palveluprosessi antaa ensin tapahtumalle virtuaalisen xid:n. Numero koostuu prosessitunnuksesta ja järjestysnumerosta.

Tämän numeron antaminen ei vaadi synkronointia kaikkien prosessien välillä ja on siksi erittäin nopea. Tutustumme toiseen virtuaalinumeroiden käytön syihin, kun puhumme jäädytyksestä.

Virtuaalilukuja ei oteta millään tavalla huomioon datan tilannekuvissa.

Eri ajankohtina järjestelmässä voi hyvinkin olla virtuaalisia tapahtumia jo käytettyjen numeroiden kanssa, ja tämä on normaalia. Mutta tällaista numeroa ei voi kirjoittaa tietosivuille, koska sivulle seuraavan kerran avattaessa se voi menettää merkityksensä.

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

Jos tapahtuma alkaa muuttaa tietoja, sille annetaan todellinen, yksilöllinen tapahtumanumero.

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

=> COMMIT;

Sisäkkäiset tapahtumat

Tallenna pisteitä

SQL:ssä määritelty säästää pisteitä (savepoint), joiden avulla voit peruuttaa osan tapahtumasta keskeyttämättä sitä kokonaan. Mutta tämä ei sovi yllä olevaan kaavioon, koska tapahtumalla on sama tila kaikille sen muutoksille ja fyysisesti mitään tietoja ei palauteta.

Tämän toiminnon toteuttamiseksi tapahtuma, jossa on tallennuspiste, jaetaan useisiin erillisiin sisäkkäisiä tapahtumia (alitapahtuma), jonka tilaa voidaan hallita erikseen.

Sisäkkäisillä tapahtumilla on oma numero (suurempi kuin päätapahtuman numero). Sisäkkäisten tapahtumien tila tallennetaan tavalliseen tapaan XACT:ssa, mutta lopullinen tila riippuu päätapahtuman tilasta: jos se peruutetaan, myös kaikki sisäkkäiset tapahtumat peruutetaan.

Tiedot tapahtuman sisäkkäisyydestä tallennetaan tiedostoihin PGDATA/pg_subtrans-hakemistossa. Tiedostoja käytetään ilmentymän jaetussa muistissa olevien puskureiden kautta, jotka on järjestetty samalla tavalla kuin XACT-puskurit.

Älä sekoita sisäkkäisiä tapahtumia itsenäisiin tapahtumiin. Autonomiset tapahtumat eivät ole millään tavalla riippuvaisia ​​toisistaan, mutta sisäkkäiset tapahtumat riippuvat toisistaan. Tavallisessa PostgreSQL:ssä ei ole itsenäisiä tapahtumia, ja ehkä parasta: niitä tarvitaan hyvin, hyvin harvoin, ja niiden läsnäolo muissa DBMS-järjestelmissä aiheuttaa väärinkäyttöä, josta kaikki sitten kärsivät.

Tyhjennetään taulukko, aloitetaan tapahtuma ja lisätään rivi:

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

Laitetaan nyt tallennuspiste ja lisätään toinen rivi.

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

Huomaa, että txid_current()-funktio palauttaa päätapahtumanumeron, ei sisäkkäistä tapahtumanumeroa.

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

Palataan takaisin tallennuspisteeseen ja lisätään kolmas rivi.

=> ROLLBACK TO sp;
=> INSERT INTO t(s) VALUES ('BAR');
=> SELECT xmin, xmax, * FROM t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3671 |    0 |  4 | BAR
(2 rows)

=> SELECT * FROM heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3669     | 0 (a) | (0,1)
 (0,2) | normal | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normal | 3671     | 0 (a) | (0,3)
(3 rows)

Näemme sivulla edelleen peruutetun sisäkkäisen tapahtuman lisäämän rivin.

Korjaamme muutokset.

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

Nyt näet selvästi, että jokaisella sisäkkäisellä tapahtumalla on oma tilansa.

Huomaa, että sisäkkäisiä tapahtumia ei voida käyttää eksplisiittisesti SQL:ssä, eli et voi aloittaa uutta tapahtumaa suorittamatta nykyistä tapahtumaa loppuun. Tämä mekanismi aktivoituu implisiittisesti käytettäessä tallennuspisteitä, samoin kuin käsiteltäessä PL/pgSQL-poikkeuksia ja useissa muissa, eksoottisemmissa tapauksissa.

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

Virheet ja toimintojen atomiteetti

Mitä tapahtuu, jos toimintoa suoritettaessa tapahtuu virhe? Esimerkiksi näin:

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

Tapahtui virhe. Nyt tapahtuma katsotaan keskeytetyksi, eikä siinä sallita mitään toimintoja:

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

Ja vaikka yrität tehdä muutoksia, PostgreSQL raportoi keskeytyksestä:

=> COMMIT;
ROLLBACK

Miksi kauppa ei voi jatkua epäonnistumisen jälkeen? Tosiasia on, että virhe voi syntyä siten, että saisimme pääsyn osaan muutoksista - ei edes tapahtuman, vaan operaattorin atomiteetti rikottaisiin. Kuten esimerkissämme, jossa operaattori onnistui päivittämään yhden rivin ennen virhettä:

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

On sanottava, että psql:ssä on tila, joka silti sallii tapahtuman jatkumisen epäonnistumisen jälkeen ikään kuin virheellisen operaattorin toimet peruutettaisiin.

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

Ei ole vaikea arvata, että tässä tilassa psql todella asettaa implisiittisen tallennuspisteen ennen jokaista komentoa, ja epäonnistuessaan käynnistää sen palautuksen. Tätä tilaa ei käytetä oletusarvoisesti, koska tallennuspisteiden asettaminen (jopa ilman niihin palaamista) aiheuttaa merkittäviä lisäkustannuksia.

Jatketaan.

Lähde: will.com

Lisää kommentti