MVCC-3. Versionet e rreshtave

Pra, kemi shqyrtuar çështjet e lidhura me izolimin, dhe bĂ«ra njĂ« devijim pĂ«r organizimin e tĂ« dhĂ«nave nĂ« nivelin mĂ« tĂ« ulĂ«t. Dhe pĂ«rfundimisht arritĂ«m te mĂ« e interesante — te versionet e rreshtave.

Titulli

Siç kemi thĂ«nĂ« tashmĂ«, çdo rresht mund tĂ« jetĂ« i pranishĂ«m nĂ« bazĂ«n e tĂ« dhĂ«nave nĂ« disa versione njĂ«herĂ«sh. NjĂ« version duhet ndonjĂ«herĂ« tĂ« dallohet nga tjetri. PĂ«r kĂ«tĂ« qĂ«llim, çdo version ka dy shĂ«nime qĂ« pĂ«rcaktojnĂ« "kohĂ«n" e veprimit tĂ« kĂ«tij versioni (xmin dhe xmax). NĂ« thonjĂ«za — sepse nuk pĂ«rdoret koha si e tillĂ«, por njĂ« numĂ«rues special qĂ« rritet. Dhe ky numĂ«rues — Ă«shtĂ« numri i transaksionit.

(Si zakonisht, nĂ« tĂ« vĂ«rtetĂ« gjithçka Ă«shtĂ« mĂ« e komplikuar: numri i transaksioneve nuk mund tĂ« rritet vazhdimisht pĂ«r shkak tĂ« kufizimit tĂ« saktĂ«sisĂ« sĂ« numĂ«ruesit. Por kĂ«to detaje do t’i shqyrtojmĂ« nĂ« hollĂ«si kur tĂ« arrijmĂ« te ngrirja.)

Kur krijohet një rresht, vlera e xmin vendoset në numrin e transaksionit që ekzekutoi komandën INSERT, ndërsa xmax nuk plotësohet.

Kur një rresht fshihet, vlera xmax e versionit aktual shënohet me numrin e transaksionit që ekzekutoi DELETE.

Kur një rresht ndryshohet me komandën UPDATE, në të vërtetë kryhen dy operacione: DELETE dhe INSERT. Në versionin aktual, rreshti vendos xmax, i barabartë me numrin e transaksionit që kryen UPDATE-in. Më pas krijohet një version i ri i të njëjtit rresht; vlera xmin e tij përputhet me vlerën xmax të versionit të mëparshëm.

Fushat xmin dhe xmax janë pjesë e titullit të versionit të rreshtit. Përveç këtyre fushave, titulli përmban edhe të tjera, për shembull:

  • infomask — njĂ« seri bitĂ«sh qĂ« pĂ«rcaktojnĂ« pronat e kĂ«tij versioni. Ka shumĂ«, ne gradualisht do tĂ« shqyrtojmĂ« kryesorĂ«t.
  • ctid — njĂ« referencĂ« pĂ«r versionin tjetĂ«r, mĂ« tĂ« ri tĂ« tĂ« njĂ«jtit rresht. NĂ« versionin mĂ« tĂ« ri, aktual, rreshti ctid referon nĂ« vetĂ« kĂ«tĂ« version. Numri ka formĂ«n (x,y), ku x Ă«shtĂ« numri i faqes, y Ă«shtĂ« numri rendor i treguesit nĂ« vargun.
  • bitmapi i vlerave tĂ« panjohura — shĂ«non ato kolona tĂ« kĂ«tij versioni qĂ« pĂ«rmbajnĂ« njĂ« vlerĂ« tĂ« panjohur (NULL). NULL nuk Ă«shtĂ« njĂ« nga vlerat e zakonshme tĂ« tipave tĂ« tĂ« dhĂ«nave, prandaj shenja duhet tĂ« ruhet veçmas.

Si pĂ«rfundim, titulli del mjaft i madh — tĂ« paktĂ«n 23 byte pĂ«r çdo version tĂ« rendit, ndĂ«rsa zakonisht mĂ« shumĂ« pĂ«r shkak tĂ« hartĂ«s NULL. NĂ«se tabela Ă«shtĂ« "e ngushtĂ«" (domethĂ«nĂ« pĂ«rmban pak kolona), shpenzimet mund tĂ« zĂ«nĂ« mĂ« shumĂ« se informacioni i dobishĂ«m.

Shtimi

Le të shqyrtojmë në detaje se si kryhen operacionet me vargjet në nivel të ulët, dhe le të fillojmë me shtimin.

Për eksperimente, do të krijojmë një tabelë të re me dy kolona dhe një indeks mbi njërën prej tyre:

=> KRIJO TABELË t(
  id serial,
  s tekst
);
=> KRIJO INDËS NË t(s);

Do të shtojmë një rresht, duke filluar fillimisht transaksionin.

=> FILLO;
=> SHTO NË t(s) VLERAT ('FOO');

Ja numri i transaksionit tonë aktual:

=> ZGJEDH txid_current();
 txid_current 
--------------
         3664
(1 rresht)

Le të shohim përmbajtjen e faqes. Funksioni heap_page_items i shtesës pageinspect lejon marrjen e informacionit mbi treguesit dhe versionet e rreshtave:

=> ZGJEDH * NGA heap_page_items(get_raw_page('t',0)) gx
-[ REGJISTRI 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

VĂ«mĂ« re se me fjalĂ«n heap (grumbull) nĂ« PostgreSQL i referohemi tabelave. Ky Ă«shtĂ« njĂ« pĂ«rdorim tjetĂ«r i çuditshĂ«m i termit — grumbulli Ă«shtĂ« njĂ« strukturĂ« e njohur tĂ« dhĂ«nash, e cila nuk ka lidhje me tabelĂ«n. KĂ«tu kjo fjalĂ« pĂ«rdoret nĂ« kuptimin "gjithçka Ă«shtĂ« hedhur nĂ« njĂ« grumbull", ndryshe nga indekset e renditura.

Funksioni tregon të dhënat "siç janë", në një format që është i komplikuar për t'u kuptuar. Për të sqaruar, ne do të lëmë vetëm një pjesë të informacionit dhe do ta shpjegojmë atë:

=> SELECT '(0,'||lp||')' AS ctid,
       CASE lp_flags
         WHEN 0 THEN 'e papërdorur'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'direktim në '||lp_off
         WHEN 3 THEN 'të vdekur'
       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
-[ REGJISTRI 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)

Këtu është ajo që bëmë:

  • Shtuam njĂ« zero nĂ« numrin e treguesit, pĂ«r ta sjellĂ« atĂ« nĂ« tĂ« njĂ«jtin format si t_ctid: (numri i faqeve, numri i treguesit).
  • Ne kemi dekoduar statusin e treguesit lp_flags. KĂ«tu ai Ă«shtĂ« "normal" — kjo do tĂ« thotĂ« se treguesi vĂ«rtet i referohet versionit tĂ« rreshtit. Vlera tĂ« tjera do tĂ« shqyrtohen mĂ« vonĂ«.
  • Nga tĂ« gjitha informacionet e bitĂ«ve, deri tani kemi nxjerrĂ« vetĂ«m dy çifte. BitĂ«t xmin_committed dhe xmin_aborted tregojnĂ« nĂ«se transaksioni me numrin xmin Ă«shtĂ« konfirmuar (apo anuluar). Dy bitĂ« e ngjashĂ«m i pĂ«rkasin transaksionit me numrin xmax.

ÇfarĂ« shohim? Kur futet njĂ« rresht nĂ« faqen e tabelĂ«s, do tĂ« shfaqet njĂ« tregues me numrin 1, qĂ« i referohet versionit tĂ« parĂ« dhe tĂ« vetĂ«m tĂ« rreshtit.

Në versionin e rreshtit, fusha xmin është mbushur me numrin e transaksionit aktual. Transaksioni është ende aktiv, prandaj të dy bitët xmin_committed dhe xmin_aborted nuk janë vendosur.

Fusha ctid e versionit të rreshtit i referohet këtij rreshti të njëjtë. Kjo do të thotë që nuk ekziston një version më i ri.

Fusha xmax është mbushur me numrin fiktiv 0, pasi ky version i rreshtit nuk është fshirë dhe është aktual. Transaksionet nuk do të kenë parasysh këtë numër, pasi biti xmax_aborted është vendosur.

Të bëjmë një hap tjetër për të përmirësuar lexueshmërinë duke shtuar bitët informues në numrat e transaksioneve. Dhe do të krijojmë një funksion, pasi kërkesa do të na nevojitet përsëri:

=> KRIJONI FUNKCIÓN heap_page(relname text, pageno integer)
KTHEN TABEL(ctid tid, state text, xmin text, xmax text, t_ctid tid)
SI $$
ZBULLO (pageno,lp)::text::tid SI ctid,
       RAST CASE lp_flags
         KUR 0 ATËR 'tĂ« pa pĂ«rdorur'
         KUR 1 ATËR 'normal'
         KUR 2 ATËR 'redirigjo nĂ« '||lp_off
         KUR 3 ATËR 'i vdekur'
       FUND RAST si state,
       t_xmin || RAST
         KUR (t_infomask & 256) > 0 ATËR ' (c)'
         KUR (t_infomask & 512) > 0 ATËR ' (a)'
         KUR TJETRA ''
       FUND RAST si xmin,
       t_xmax || RAST
         KUR (t_infomask & 1024) > 0 ATËR ' (c)'
         KUR (t_infomask & 2048) > 0 ATËR ' (a)'
         KUR TJETRA ''
       FUND RAST si xmax,
       t_ctid
BRAHE heap_page_items(merr_raw_page(relname,pageno))
RENDA NGA lp;
$$ GJUHA SQL;

Në këtë formë është dukshëm më e qartë se çfarë ndodh në titullin e versionit të rreshtit:

=> ZGJIDH * NGA heap_page('t',0);
 ctid  | status  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 rresht)

Informacione të ngjashme, por dukshëm më pak të detajuara, mund të merren edhe nga tabela e vet, duke përdorur kolona të rrejshme xmin dhe xmax:

=> ZGJIDH xmin, xmax, * NGA t;
 xmin | xmax | id |  s  
------+------+----+-----
 3664 |    0 |  1 | FOO
(1 rresht)

Përditësimi

Pas pĂ«rfundimit tĂ« suksesshĂ«m tĂ« transaksionit, Ă«shtĂ« e nevojshme tĂ« rikujtohet statusi i tij — tĂ« shĂ«nohet se Ă«shtĂ« pĂ«rmbyllur. PĂ«r kĂ«tĂ« pĂ«rdoret njĂ« strukturĂ« e quajtur XACT (deri nĂ« versionin 10 quhej CLOG (regjistri i pĂ«rmbylljes) dhe ky emĂ«r ende mund tĂ« haset nĂ« vende tĂ« ndryshme).

XACT — nuk Ă«shtĂ« njĂ« tabelĂ« e katalogut sistemor; kĂ«to janĂ« skedarĂ« nĂ« katalogun PGDATA/pg_xact. NĂ« to, pĂ«r secilĂ«n transaksion i jepen dy bita: i konfirmuar dhe i anuluar — ashtu si nĂ« titullin e versionit tĂ« rreshtit. Kjo informacion Ă«shtĂ« ndarĂ« nĂ« disa skedare vetĂ«m pĂ«r lehtĂ«si, dhe ne do tĂ« kthehemi nĂ« kĂ«tĂ« çështje kur tĂ« shqyrtojmĂ« ngrirjen. Punimi me kĂ«to skedare bĂ«het faqe pas faqe si me tĂ« gjitha tĂ« tjerat.

Pra, kur një transaksion konfirmohet në XACT, vendoset bita e konfirmuar për këtë transaksion. Dhe kjo është e gjitha që ndodh gjatë konfirmimit (në fakt, ne nuk po flasim akoma për regjistrin e para-shkrimit).

Kur ndonjë transaksion tjetër t'i qaset faqes tabelare që sapo kemi shqyrtuar, ajo do t'i duhet të përgjigjet disa pyetjeve.

  1. A ka përfunduar transaksioni xmin? Nëse jo, versioni i krijuar i rreshtit nuk duhet të jetë i dukshëm.
    Një kontroll i tillë realizohet duke parë një strukturë tjetër, e cila ndodhet në memorjen e përbashkët të instancës dhe quhet ProcArray. Ajo përmban listën e të gjithë proceseve aktivë, dhe për secilin specifikohet numri i transaksionit të tij aktual (aktiv).
  2. NĂ«se ka pĂ«rfunduar, si — me njĂ« regjistrim ose njĂ« anulim? NĂ«se me njĂ« anulim, atĂ«herĂ« versioni i rreshtit nuk duhet tĂ« jetĂ« i dukshĂ«m gjithashtu.
    Pikërisht për këtë është i nevojshëm XACT. Megjithatë, ndonëse faqet e fundit të XACT ruajnë në pamje të përkohshme në memorien RAM, kontrollimi i XACT-it çdo herë është i ngarkuar. Prandaj, statusi i transaksionit të zbuluar një herë regjistrohet në bitet xmin_committed dhe xmin_aborted të versionit të rreshtit. Nëse një nga këto bita është i vendosur, atëherë gjendja e transaksionit xmin konsiderohet e njohur dhe transaksioni tjetër nuk do të duhet të kthehet më në XACT.

Pse këto bita nuk vendosen nga vetë transaksioni që realizon insertimin? Kur ndodh insertimi, transaksioni ende nuk di nëse do të përfundojë me sukses. Dhe në momentin e regjistrimit, nuk është e qartë se cilat saktësisht rreshta në cilat saktësisht faqe janë ndryshuar. Mund të ketë shumë faqe të tilla, dhe të mbash mend ato nuk është e dobishme. Për më tepër, disa faqe mund të jenë dëbuar nga cache buffer në disk; rileximi i tyre për të ndryshuar bitet do të thoshte një ngadalësim të konsiderueshëm të regjistrimit.

Ankthi pasĂ« kursimi Ă«shtĂ« se pas ndryshimeve, çdo transaksion (edhe ai qĂ« bĂ«n thjesht njĂ« lexim — SELECT) mund tĂ« fillojĂ« tĂ« ndryshojĂ« faqet e tĂ« dhĂ«nave nĂ« memorie.

Pra, le të regjistrojmë ndryshimin.

=> COMMIT;

Nuk ka pasur ndryshime në faqe (por ne e dimë se statusi i transaksionit është regjistruar tashmë në XACT):

=> ZGJIDH * NGA heap_page('t',0);
 ctid  | status  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3664 | 0 (a) | (0,1)
(1 rresht)

Tani, transaksioni që u drejtua i pari në faqe do të duhet të përcaktojë statusin e transaksionit xmin dhe do ta regjistrojë atë në bitet informative:

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 rresht)

=> ZGJIDH * NGA heap_page('t',0);
 ctid  | state  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 0 (a) | (0,1)
(1 rresht)

Fshirja

Kur fshihet një rresht, numri i transaksionit që po fshin regjistrohet në fushën xmax të versionit aktual, ndërsa biti xmax_aborted reset-ohen.

Vërejmë se vlera e vendosur xmax, e cila i përket transaksionit aktiv, vepron si një bllokim i rreshtit. Nëse një transaksion tjetër do të përpiqet të përditësojë ose fshijë këtë rresht, ai do të detyrohet të presë për përfundimin e transaksionit xmax. Më shumë për bllokimet do të flasim më vonë. Deri atëherë, le të theksojmë vetëm se numri i bllokimeve të rreshtit nuk ka kufizime. Ato nuk zënë vend në kujtesën e operativit dhe performanca e sistemit nuk vuajnë nga numri i tyre. Megjithatë, transaksionet 'e gjata' kanë disavantazhe të tjera, por për këtë do të flasim më vonë.

Të fshijmë rreshtin.

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

Vërejmë se numri i transaksionit është shkruar në fushën xmax, por bitet informuese nuk janë vendosur:

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

Anulimi

Anulimi i ndryshimeve funksionon në mënyrë të ngjashme si fiksa, vetëm se në XACT për transaksionin vendoset biti aborted. Anulimi kryhet po aq shpejt sa edhe fiksa. Edhe pse komanda quhet ROLLBACK, nuk ndodh një rikthim i ndryshimeve: gjithçka që transaksioni arriti të ndryshonte në faqet e të dhënave mbetet pa ndryshim.

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

Kur kërkohet një faqe, do të kontrollohet statusi dhe në versionin e rreshtit do të vendoset bita e sugjerimit xmax_aborted. Numri vetë xmax do të mbetet në faqe, por askush nuk do ta shikojë atë më.

=> SELECT * FROM t;
 id |  s  
----+-----
  1 | FOO
(1 rresht)

=> ZGJIDH * NGA heap_page('t',0);
 ctid  | gjendja |   xmin   |   xmax   | t_ctid 
-------+--------+----------+----------+--------
 (0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
(1 rresht)

Përditësimi

Përditësimi funksionon sikur fillimisht të ketë ndodhur eliminimi i versionit aktual të rreshtit, dhe më pas futja e një të ri.

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

Kërkesa jep një rresht (version të ri):

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

Por në faqe shohim të dy versionet:

=> ZGJIDH * NGA heap_page('t',0);
 ctid  | gjendja |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 3666  | (0,2)
 (0,2) | normal | 3666     | 0 (a) | (0,2)
(2 rreshta)

Versioni i fshirë është shënuar me numrin e transaksionit aktual në fushën xmax. Për më tepër, kjo vlerë është përshkruar sipër asaj të vjetër, pasi transaksioni i mëparshëm ishte anuluar. Ndërsa bita xmax_aborted është resetuar, pasi statusi i transaksionit aktual ende nuk është i njohur.

Versioni i parë i rreshtit tani i referohet të dytit (fusha t_ctid), si një më të ri.

Në faqen e indeksit shfaqet një tregues i dytë dhe një rresht i dytë, që i referohet versionit të dytë në faqen tabelë.

Ashtu siç ndodh me fshirjen, vlera xmax në versionin e parë të rreshtit shërben si tregues që rreshti është i bllokuar.

Dhe tani do ta përfundojmë transaksionin.

=> COMMIT;

Indeksat

Deri tani kemi folur vetëm për faqet tabelë. Por çfarë ndodh brenda indekseve?

Informacioni nĂ« faqet e indeksit varet shumĂ« nga lloji i veçantĂ« tĂ« indeksit. Edhe pĂ«r njĂ« tip tĂ« vetĂ«m indeksi mund tĂ« ketĂ« lloje tĂ« ndryshme faqesh. PĂ«r shembull, B-pema ka njĂ« faqe me metad-data dhe ‘faqe tĂ« zakonshme’.

Megjithatë, zakonisht në faqe ka një varg treguesish për rreshtat dhe vetë rreshtat (ashtu si edhe në faqen tabelë). Për më tepër, në fund të faqes rezervohen hapësira për të dhëna speciale.

Rreshtat në indekse gjithashtu mund të kenë strukturë shumë të ndryshme në varësi të tipit të indeksit. Për shembull, për B-pemat, rreshtat që i përkasin faqeve me gjethe përmbajnë vlerën e çelësit të indeksimit dhe një referencë (ctid) në rreshtin përkatës të tabelës. Në përgjithësi, indeksi mund të jetë organizuar në një mënyrë krejt tjetër.

Pika mĂ« e rĂ«ndĂ«sishme Ă«shtĂ« se nĂ« indekset e çdo lloji nuk ka versione tĂ« rreshtave. Ose mund ta konsideroni se çdo rresht paraqitet me njĂ« version tĂ« vetĂ«m. NĂ« terma tĂ« tjerĂ«, nĂ« titullin e rreshtit tĂ« indeksit nuk ka fushat xmin dhe xmax. Mund ta mendoni se lidhjet nga indeksi pĂ«rfshijnĂ« tĂ« gjitha versionet e tabelĂ«s sĂ« rreshtave — kĂ«shtu qĂ« pĂ«r tĂ« kuptuar cilin version do ta shohĂ« transaksioni, mund tĂ« kĂ«rkoni nĂ« tabelĂ«. (Si zakonisht, kjo nuk Ă«shtĂ« e gjitha e vĂ«rteta. NĂ« disa raste, harta e dukshmĂ«risĂ« lejon optimizimin e procesit, por do ta shqyrtojmĂ« kĂ«tĂ« mĂ« vonĂ«.)

Në këtë mënyrë, në faqen e indeksit gjejmë tregues për të dy versionet, si për atë aktuale ashtu edhe për të vjetra:

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

Transaksione virtuale

Në praktikë, PostgreSQL përdor optimizimin që lejon "kursimin" e numrave të transaksioneve.

Nëse një transaksion vetëm lexon të dhëna, atëherë ai nuk ndikon në dukshmërinë e versioneve të rreshtave. Prandaj, në fillim, procesi shërbues jep një numër virtual transaksioni (virtual xid). Numri përbëhet nga identifikuesi i procesit dhe një numër rendor.

Shpërndarja e këtij numri nuk kërkon sinkronizim midis të gjithë proceseve dhe prandaj realizohet shumë shpejt. Një arsye tjetër për përdorimin e numrave virtualë do të diskutohet kur të flasim për ngrirjen.

Numrat virtualë nuk përfshihen në pamjet e të dhënave.

Në momente të ndryshme, sistemi mund të ketë transaksione virtuale me numra që janë përdorur tashmë, dhe kjo është normale. Por një numër i tillë nuk mund të regjistrohet në faqet e të dhënave, sepse në qasjen e ardhshme ndaj faqes, ai mund të humbasë çdo kuptim.

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

(1 row)

Nëse transaksioni fillon të ndryshojë të dhënat, i jepet një numër transaksioni unik, të vërtetë.

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

=> COMMIT;

Transaksionet e ngjitura

Pikat e ruajtjes

Në SQL janë të përcaktuara pikat e ruajtjes (savepoint), që lejojnë anulimin e pjesës së operacioneve të transaksionit, pa e ndërprerë plotësisht atë. Por kjo nuk përputhet me skemën e mësipërme, pasi statusi i transaksionit është i njëjtë për të gjitha ndryshimet e tij, dhe fizikisht nuk ka të dhëna që kthehen mbrapsht.

Për të realizuar një funksionalitet të tillë, transaksioni me pikë ruajtjeje ndahet në disa transaksione të brendshme (subtransaction), statusin e të cilave mund të menaxhohen veçmas.

Transaksionet e brendshme kanë numrin e tyre (më të madh se numri i transaksionit kryesor). Statusi i transaksioneve të brendshme regjistrohet në mënyrë normale në XACT, megjithatë statusi përfundimtar varet nga statusi i transaksionit kryesor: nëse ai anulohet, gjithashtu anulohen të gjitha transaksionet e brendshme.

Informacioni mbi thellësinë e transaksioneve ruhet në skedarët në katalogun PGDATA/pg_subtrans. Qasja në skedarët ndodh përmes bufereve në memorjen e përbashkët të instancës, të organizuara ashtu si bufert XACT.

Mos e ngatërroni transaksionet e paluara me transaksionet autonome. Transaksionet autonome nuk varen njëra nga tjetra, ndërsa ato të paluara varen. Transaksionet autonome nuk janë të pranishme në PostgreSQL-në e zakonshme, dhe ndoshta, për mirë: ato janë të nevojshme shumë dhe shumë rrallë, dhe pranimi i tyre në DBMS të tjera provokon abuzime që më pas i dëmtojnë të gjithë.

Të pastrojmë tavolinën, të fillojmë një transaksion dhe të fusim një rresht:

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

=> ZGJIDH xmin, xmax, * NGA t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
(1 rresht)

=> ZGJIDH * NGA heap_page('t',0);
 ctid  | gjendja  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
(1 rresht)

Tani vendosim një pikë ruajtjeje dhe fusim një rresht tjetër.

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

Vini re se funksioni txid_current() jep numrin e transaksionit kryesor, jo të paluar.

=> ZGJIDH xmin, xmax, * NGA t;
 xmin | xmax | id |  s  
------+------+----+-----
 3669 |    0 |  2 | FOO
 3670 |    0 |  3 | XYZ
(2 rreshta)

=> ZGJIDH * NGA heap_page('t',0);
 ctid  | gjendja  | xmin | xmax  | t_ctid 
-------+--------+------+-------+--------
 (0,1) | normal | 3669 | 0 (a) | (0,1)
 (0,2) | normal | 3670 | 0 (a) | (0,2)
(2 rreshta)

Të kthehemi në pikën e ruajtjes dhe të fusim rreshtin e tretë.

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

=> ZGJIDH * NGA 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 rresht)

Në faqen po vazhdojmë të shohim rreshtin e shtuar nga transaksioni i anuluar.

Po regjistrojmë ndryshimet.

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

=> ZGJIDH * NGA 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 rresht)

Tani është qartë se secila transaksion i brendshëm ka statusin e vet.

Vlen të theksohet se transaksionet e brendshme nuk mund të përdoren në SQL në mënyrë të drejtpërdrejtë, pra nuk mund të filloni një transaksion të ri pa përfunduar atë aktual. Ky mekanizëm aktivizohet në mënyrë implicite kur përdoren pika shpëtimi, si dhe gjatë trajtimit të përjashtimeve PL/pgSQL dhe në disa raste të tjera më ekzotike.

=> BEGIN;
FILLIM
=> BEGIN;
KËSHILLIM:  ekziston tashmĂ« njĂ« transaksion nĂ« proces
FILLIM
=> COMMIT;
COMMIT
=> COMMIT;
KËSHILLIM:  nuk ka transaksion nĂ« proces
COMMIT

Gabimet dhe atomikësia e operacioneve

ÇfarĂ« do tĂ« ndodhĂ« nĂ«se ndodh njĂ« gabim gjatĂ« ekzekutimit tĂ« operacionit? PĂ«r shembull, kĂ«shtu:

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

=> UPDATE t SET s = repeat('X', 1/(id-4));
Gabim:  ndarje me zero

Ndodhi një gabim. Tani transaksioni konsiderohet i ndërprerë dhe asnjë operacion në të nuk lejohet:

=> SELECT * FROM t;
GABIM: transaksioni aktual është i ndërprerë, komandat injorohen deri në fund të bllokut të transaksionit

Dhe edhe nëse përpiqemi të konfirmojmë ndryshimet, PostgreSQL do të raportojë për ndalimin:

=> COMMIT;
RIVENDOS

Pse nuk mund tĂ« vazhdojmĂ« me ekzekutimin e transaksionit pas dĂ«shtimit? Arsyeja Ă«shtĂ« se gabimi mund tĂ« ketĂ« ndodhur nĂ« njĂ« mĂ«nyrĂ« qĂ« do tĂ« na kishte dhĂ«nĂ« qasje nĂ« njĂ« pjesĂ« tĂ« ndryshimeve — do tĂ« ishte shkelur atomikshmĂ«ria e even as transaksionit, as operatorit. Si nĂ« shembullin tonĂ«, ku operatori kishte arritur tĂ« pĂ«rditĂ«sonte njĂ« rresht pĂ«rpara gabimit:

=> ZGJIDH * NGA heap_page('t',0);
 ctid  | gjendja  |   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 rreshta)

Duhet thënë se në psql ekziston një mod për këtë që megjithatë lejon vazhdimin e transaksionit pas dështimit ashtu siç do të ishte veprimet e operatorit të gabuar.

=> set ON_ERROR_ROLLBACK on
=> FILLIM;
=> ZGJIDH * NGA t;
 id |  s  
----+-----
  2 | FOO
  4 | BAR
(2 rresht)

=> UPDATE t SET s = repeat('X', 1/(id-4));
Gabim:  ndarje me zero

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

=> COMMIT;

Nuk është e vështirë të kuptohet se në këtë mod, psql në fakt vendos një pikë ruajtjeje të paqartë para çdo komande, dhe në rast dështimi nismon një rivotim te ajo. Ky mod nuk përdoret si parazgjedhje, pasi vendosja e pikave të ruajtjes (edhe pa u rikthyer te ato) është e lidhur me kosto të konsiderueshme.

Vazhdimi.

Burimi: habr.com

Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster