MVCC-3. String versiot

Olemme siis käsitelleet asiaan liittyviä kysymyksiä eristysja teki sivupolun aiheesta matalan tason dataorganisaatioJa lopuksi pääsimme mielenkiintoisimpaan osaan – jousiversioihin.

Otsikko

Kuten olemme jo käsitelleet, jokainen tietokannan rivi voi esiintyä samanaikaisesti useissa versioissa. Yksi versio on erotettava toisesta jotenkin. Tätä varten jokaisella versiolla on kaksi markkeria, jotka määrittelevät sen voimassaoloajan (xmin ja xmax). Lainausmerkkien käyttö johtuu siitä, että käytössä ei ole itse aika, vaan erityinen kasvava laskuri. Tämä laskuri on tapahtumanumero.

(Kuten tavallista, asiat ovat itse asiassa monimutkaisempia: tapahtumanumero ei voi kasvaa jatkuvasti laskurin rajallisen bittisyvyyden vuoksi. Mutta käsittelemme näitä yksityiskohtia tarkemmin, kun pääsemme jäädyttämiseen.)

Kun rivi luodaan, xmin-arvoksi asetetaan INSERT-komennon suorittaneen tapahtuman tapahtumatunnus ja xmax-arvo jätetään tyhjäksi.

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

Kun riviä muokataan UPDATE-komennolla, suoritetaan itse asiassa kaksi toimintoa: DELETE ja INSERT. Rivin nykyinen versio asetetaan xmax-arvoon, joka on yhtä suuri kuin UPDATE-komennon suorittaneen tapahtuman tapahtumatunnus. Tämän jälkeen samasta rivistä luodaan uusi versio; sen xmin-arvo vastaa edellisen version xmax-arvoa.

Riviversion otsikkoon sisältyvät xmin- ja xmax-kentät. Näiden kenttien lisäksi otsikko sisältää myös muita, kuten:

  • infomask on joukko bittejä, jotka määrittelevät tietyn version ominaisuudet. Niitä on melko monta; käsittelemme tärkeimpiä vähitellen.
  • ctid on viittaus saman merkkijonon seuraavaan, uudempaan versioon. Merkkijonon uusimman, nykyisen version ctid viittaa itse kyseiseen versioon. Numero on muotoa (x,y), jossa x on sivunumero ja y on osoittimen indeksi taulukossa.
  • NULL-bittikartta – merkitsee tietyn version sarakkeet, jotka sisältävät NULL-arvon. NULL ei ole yleinen tietotyypin arvo, joten lippu on tallennettava erikseen.

Tämän seurauksena otsikko on melko suuri – vähintään 23 tavua riviä kohden versiossa, ja yleensä enemmän NULL-bittikartan vuoksi. Jos taulukko on "kapea" (eli siinä on vähän sarakkeita), lisätyö voi olla hyödyllisen tiedon suurempaa.

lisätä

Tarkastellaanpa lähemmin, miten merkkijonooperaatiot suoritetaan matalalla tasolla, alkaen lisäyksestä.

Kokeita varten luodaan uusi taulukko, jossa on kaksi saraketta ja indeksi toiselle niistä:

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

Lisätään yksi rivi, kun ensin on aloitettu tapahtuma.

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

Tässä on nykyinen tapahtumanumeromme:

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

Tarkastellaanpa 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ä sana heap (keap) PostgreSQL:ssä viittaa taulukoihin. Tämä on toinen outo käyttötapa terminä – heap on tunnettu tietorakenne, jolla ei ole mitään tekemistä taulukon kanssa. Tässä sanaa käytetään merkityksessä "kaikki heitetään yhteen", toisin kuin järjestettyjen indeksien merkityksessä.

Funktio näyttää tiedot "sellaisenaan" vaikeasti ymmärrettävässä muodossa. Selkeyttääksemme sitä, jätämme jäljelle vain osan tiedoista ja dekoodaamme ne:

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

  • Lisäsin indeksinumeroon nollan, jotta se näyttäisi samalta kuin t_ctid: (sivunumero, indeksinumero).
  • Olemme selvittäneet lp_flags-osoittimen tilan. Tässä se on "normaali", mikä tarkoittaa, että osoitin osoittaa merkkijonoversioon. Tarkastelemme muita arvoja myöhemmin.
  • Kaikista informaatiobiteistä on toistaiseksi tunnistettu vain kaksi paria. xmin_committed- ja xmin_aborted-bitit osoittavat, onko tapahtumanumero xmin vahvistettu (tai keskeytetty). Kaksi samanlaista bittiä viittaa tapahtumanumeroon xmax.

Mitä näemme? Kun lisäämme rivin, taulukkosivulle ilmestyy osoitin numero 1, joka osoittaa rivin ensimmäiseen ja ainoaan versioon.

Riviversiossa xmin-kenttään lisätään nykyinen tapahtumanumero. Tapahtuma on edelleen aktiivinen, joten sekä xmin_committed- että xmin_aborted-bitit ovat asettamattomia.

Rivin version ctid-kenttä viittaa samaan riviin, mikä tarkoittaa, että uudempaa versiota ei ole olemassa.

xmax-kenttään lisätään näennäisnumero 0, koska tätä riviversiota ei ole poistettu ja se on edelleen ajan tasalla. Transaktiot jättävät tämän numeron huomiotta, koska xmax_aborted-bitti on asetettu.

Otetaanpa askel kohti luettavuuden parantamista lisäämällä databittejä tapahtumanumeroihin. Ja luodaan funktio, koska tarvitsemme tätä kyselyä uudelleen:

=> 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ä versiomerkkijonon otsikossa tapahtuu:

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

Samankaltaista, mutta huomattavasti vähemmän yksityiskohtaista tietoa voidaan saada taulukosta itsestään käyttämällä xmin- ja xmax-pseudosarakkeita:

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

Kiinnitys

Kun tapahtuma on suoritettu onnistuneesti, sen tila on muistettava – se on merkittävä committediksi. Tämä tehdään käyttämällä XACT-nimistä rakennetta (ennen versiota 10 sitä kutsuttiin nimellä CLOG (commit log), ja tätä nimeä käytetään edelleen useissa eri paikoissa).

XACT ei ole järjestelmäluettelotaulukko; se on tiedostoja PGDATA/pg_xact-hakemistossa. Ne sisältävät kaksi bittiä kutakin tapahtumaa kohden: commited ja aborted – aivan kuten riviversion otsikossa. Nämä tiedot on jaettu useisiin tiedostoihin vain mukavuussyistä; palaamme tähän asiaan, kun keskustelemme jäädyttämisestä. Näitä tiedostoja käytetään sivu sivulta, aivan kuten kaikkia muita tiedostoja.

Joten kun tapahtuma vahvistetaan, XACT:iin asetetaan kyseisen tapahtuman "committed"-bitti. Ja siinä kaikki, mitä tapahtuman vahvistuksen yhteydessä tapahtuu (vaikka emme ole vielä käsitelleet ennakkoon kirjoitettavaa lokia).

Kun jokin muu tapahtuma käyttää juuri tarkastelemaamme taulukkosivua, sen on vastattava muutamaan kysymykseen.

  1. Onko xmin-tapahtuma suoritettu? Jos ei, luodun rivin version ei pitäisi olla näkyvissä.
    Tämä tarkistus suoritetaan tutkimalla instanssin jaetussa muistissa sijaitsevaa toista rakennetta nimeltä ProcArray. Se sisältää luettelon kaikista aktiivisista prosesseista, joista jokaisella on oma nykyinen (aktiivinen) tapahtumanumeronsa.
  2. Jos se päättyi, miten – commitin vai palautuksen seurauksena? Jos palautuksen seurauksena, riviversion ei myöskään pitäisi olla näkyvissä.
    Juuri tätä varten XACT on olemassa. Vaikka XACT:n viimeiset sivut tallennetaan RAM-puskureihin, XACT:n tarkistaminen joka kerta on kallista. Siksi, kun tapahtuman tila on määritetty, se kirjoitetaan riviversion xmin_committed- ja xmin_aborted-bitteihin. Jos jompikumpi näistä biteistä on asetettu, xmin-tapahtuman tila katsotaan tunnetuksi, eikä seuraavan tapahtuman enää tarvitse käyttää XACT:ia.

Miksi näitä bittejä ei aseta lisäyksen suorittava tapahtuma? Kun lisäys tapahtuu, tapahtuma ei vielä tiedä, onnistuuko se. Ja commit-vaiheessa ei ole enää selvää, mitä rivejä ja milläkin sivuilla on muokattu. Tällaisia ​​sivuja voi olla useita, ja niiden tallentaminen on epäkäytännöllistä. Lisäksi jotkin sivut on saatettu siirtää välimuistista levylle; niiden uudelleen lukeminen bittien muuttamiseksi hidastaisi merkittävästi commit-prosessia.

Tämän säästön haittapuolena on, että muutosten jälkeen mikä tahansa tapahtuma (jopa yksinkertainen luku - SELECT) saattaa alkaa muuttaa puskurivälimuistin datasivuja.

Eli kirjataan muutos muistiin.

=> COMMIT;

Sivulla ei ole tapahtunut mitään muutosta (mutta tiedämme, että tapahtuman tila on jo kirjoitettu XACT:iin):

=> 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ä siirtyneen 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 version xmax-kenttään kirjoitetaan nykyisen poistotapahtuman numero ja xmax_aborted-bitti nollataan.

Huomaa, että aktiiviselle tapahtumalle asetettu xmax-arvo toimii rivilukkona. Jos toinen tapahtuma yrittää päivittää tai poistaa tätä riviä, sen on odotettava xmax-tapahtuman valmistumista. Käsittelemme lukkoja tarkemmin myöhemmin. Toistaiseksi todetaan, että rivilukkojen määrä on rajoittamaton. Ne eivät vie RAM-muistia, eikä niiden lukumäärä vaikuta järjestelmän suorituskykyyn. Pitkillä tapahtumilla on kuitenkin muita haittapuolia, mutta käsittelemme niitä myöhemmin.

Poistetaan rivi.

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

Näemme, että tapahtumanumero kirjoitetaan xmax-kenttään, mutta informaatiobittejä 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 kumoaminen toimii samalla tavalla kuin committing, paitsi että keskeytetty bitti asetetaan XACT-muuttujaan tapahtumalle. Kumoaminen suoritetaan yhtä nopeasti kuin committing. Vaikka komentoa kutsutaan nimellä ROLLBACK, se ei peru mitään muutoksia: kaikki tapahtuman datasivuilla muokkaamat tiedot pysyvät 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 sivulle avataan, sen tila tarkistetaan ja riviversioon asetetaan xmax_aborted-vihjebitti. Itse xmax-numero pysyy sivulla, mutta kukaan ei enää 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 rivin nykyinen versio olisi ensin poistettu ja sitten lisätty uusi.

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

Kysely palauttaa yhden rivin (uuden version):

=> 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 merkitään nykyisen tapahtuman numerolla xmax-kentässä. Tämä arvo korvataan, koska edellinen tapahtuma peruutettiin. xmax_aborted-bitti tyhjennetään, koska nykyisen tapahtuman tila on edelleen tuntematon.

Rivin ensimmäinen versio viittaa nyt toiseen (kenttä t_ctid) uudempana.

Hakemistosivulle ilmestyy toinen osoitin ja toinen rivi, jotka viittaavat taulukkosivun toiseen versioon.

Kuten poistossa, rivin ensimmäisen version xmax-arvo toimii osoituksena siitä, että rivi on lukittu.

No niin, tehdään kauppa loppuun.

=> COMMIT;

Indeksit

Tähän asti olemme puhuneet vain taulukkosivuista. Mutta mitä tapahtuu indeksien sisällä?

Hakemistosivujen tiedot vaihtelevat suuresti hakemistotyypin mukaan. Jopa yhdellä hakemistotyypillä voi olla erityyppisiä sivuja. Esimerkiksi B-puussa on metatietosivu ja "tavallisia" sivuja.

Sivu sisältää kuitenkin tyypillisesti taulukon osoittimia riveihin ja itse riveihin (aivan kuten taulukkosivu). Lisäksi sivun lopussa on varattu tilaa erikoistiedoille.

Indeksien riveillä voi myös olla hyvin erilaisia ​​rakenteita indeksityypistä riippuen. Esimerkiksi B-puussa lehtisivuihin liittyvät rivit sisältävät indeksiavaimeen liittyvän arvon ja viittauksen (ctid) vastaavaan taulukon riviin. Yleisesti ottaen indeksi voi olla rakenteeltaan melko erilainen.

Tärkeintä on, että minkään tyyppisillä indekseillä ei ole riviversioita. Tai pikemminkin kutakin riviä voidaan pitää täsmälleen yhden version edustamana. Toisin sanoen indeksirivin otsikossa ei ole xmin- ja xmax-kenttiä. Indeksiviitteiden voidaan katsoa osoittavan kaikkiin taulukon riviversioihin – joten ainoa tapa määrittää, minkä version tapahtuma näkee, on katsoa taulukkoa. (Kuten tavallista, tämä ei ole koko totuus. Joissakin tapauksissa näkyvyyskartta voi optimoida prosessia, mutta käsittelemme tätä yksityiskohtaisemmin myöhemmin.)

Tässä tapauksessa hakemistosivulta löytyy viitteitä 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 tapahtumat

Käytännössä PostgreSQL käyttää optimointia, jonka avulla se voi "tallentaa" transaktiomääriä.

Jos tapahtuma vain lukee dataa, se ei vaikuta riviversioiden näkyvyyteen. Siksi taustajärjestelmä ensin määrittää tapahtumalle virtuaalisen tunnuksen (virtual xid). Tämä tunnus koostuu prosessitunnuksesta ja järjestysnumerosta.

Tämän numeron myöntäminen ei vaadi kaikkien prosessien välistä synkronointia ja on siksi erittäin nopeaa. Käsittelemme toista syytä virtuaalinumeroiden käytölle jäädyttämisen yhteydessä.

Virtuaalilukuja ei oteta millään tavalla huomioon datatilannekuvissa.

Järjestelmässä voi eri aikoina olla virtuaalisia tapahtumia, joiden numeroita on jo käytetty, ja tämä on normaalia. Tällaista numeroa ei kuitenkaan voida kirjoittaa datasivuille, koska seuraavalla kerralla, kun sivua käytetään, siitä voi tulla merkityksetön.

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

Säästä pisteitä

SQL:ssä määritellään säästä pisteitä (tallennuspisteet), joiden avulla voit kumota osan tapahtumasta keskeyttämättä sitä kokonaan. Tämä ei kuitenkaan sovi yllä olevaan kaavaan, koska tapahtumalla on yksi tila kaikille muutoksille, eikä fyysisiä tietoja peruta.

Tämän toiminnallisuuden toteuttamiseksi tallennuspisteen sisältävä tapahtuma jaetaan useisiin erillisiin tapahtumiin. sisäkkäiset tapahtumat (alitapahtuma), jonka tilaa voidaan hallita erikseen.

Alitapahtumilla on oma tapahtumanumeronsa (korkeampi kuin päätapahtuman numero). Alitapahtumien tila tallennetaan XACT:iin normaalisti, mutta niiden lopullinen tila riippuu päätapahtuman tilasta: jos se peruutetaan, kaikki alitapahtumat peruutetaan myös.

Transaktioiden sisäkkäisyyden tiedot tallennetaan tiedostoihin PGDATA/pg_subtrans-hakemistossa. Näihin tiedostoihin päästään käsiksi instanssin jaetun muistin puskureiden kautta, jotka on järjestetty samalla tavalla kuin XACT-puskurit.

Älä sekoita sisäkkäisiä tapahtumia autonomisiin tapahtumiin. Autonomiset tapahtumat ovat täysin riippumattomia toisistaan, kun taas sisäkkäiset tapahtumat ovat. Autonomisia tapahtumia ei ole olemassa tavallisessa PostgreSQL:ssä, ja ehkä parasta onkin, että niitä tarvitaan harvoin, ja niiden läsnäolo muissa tietokannan hallintajärjestelmissä houkuttelee väärinkäytöksiin, mikä puolestaan ​​johtaa laajalle levinneeseen väärinkäyttöön.

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)

Asetetaan nyt tallennuspiste ja lisätään uusi rivi.

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

Huomaa, että txid_current()-funktio palauttaa päätapahtuman numeron, ei sisäkkäisen tapahtuman numeroa.

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

Palataanpa tallennuskohtaan 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)

Sivulla näemme edelleen rivin, jonka takaisinperintötapahtuma on lisännyt.

Kirjaamme muutokset muistiin.

=> 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 on selvästi nähtävissä, että jokaisella sisäkkäisellä tapahtumalla on oma tilansa.

Huomaa, että sisäkkäisiä tapahtumia ei voida käyttää eksplisiittisesti SQL:ssä; eli uutta tapahtumaa ei voida aloittaa ilman nykyisen tapahtuman suorittamista loppuun. Tätä mekanismia käytetään implisiittisesti tallennuspisteitä käytettäessä sekä PL/pgSQL-poikkeuksia käsiteltäessä 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 atomisuus

Mitä tapahtuu, jos toiminnon aikana tapahtuu virhe? Esimerkiksi:

=> 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. Tapahtuma katsotaan nyt keskeytetyksi, eikä lisätoimintoja sallita:

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

Ja vaikka yrittäisitkin tehdä muutokset commit-tilaan, PostgreSQL raportoi peruutuksesta:

=> COMMIT;
ROLLBACK

Miksi emme voi jatkaa tapahtumaa epäonnistumisen jälkeen? Ongelmana on, että virhe olisi voinut tapahtua tavalla, joka olisi antanut meille pääsyn vain osaan muutoksista – ei pelkästään tapahtuman, vaan myös itse operaattorin atomisuus olisi rikottu. 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 huomattava, että psql:ssä on tila, joka sallii transaktion jatkua epäonnistumisen jälkeen, ikään kuin virheellisen lausekkeen toiminnot 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;

Kuten arvata saattaa, tässä tilassa psql asettaa implisiittisen tallennuspisteen ennen jokaista komentoa ja epäonnistumisen sattuessa käynnistää palautuksen siihen. Tätä tilaa ei käytetä oletusarvoisesti, koska tallennuspisteiden asettaminen (myös ilman palautusta) aiheuttaa merkittäviä lisäkustannuksia.

Jatkoa.

Lähde: will.com

Osta luotettava isännöinti sivustoille, joissa on DDoS-suojaus, VPS VDS -palvelimet 🔥 Osta luotettavaa verkkosivustojen hostingia DDoS-suojauksella, VPS VDS -palvelimilla | ProHoster