MVCC-3. Версіі радкоў

Такім чынам, мы разгледзелі пытанні, звязаныя з ізаляцыяй, і зрабілі адступленне аб арганізацыі дадзеных на нізкім узроўні. І нарэшце дабраліся да самага цікавага - да версій радкоў.

Загаловак

Як мы ўжо казалі, кожны радок можа адначасова прысутнічаць у базе дадзеных у некалькіх версіях. Адну версію ад іншай трэба неяк адрозніваць З гэтай мэтай кожная версія мае дзве адзнакі, якія вызначаюць «час» дзеяння дадзенай версіі (xmin і xmax). У двукоссі - таму, што выкарыстоўваецца не час як такое, а спецыяльны які павялічваецца лічыльнік. І гэты лічыльнік - нумар транзакцыі.

(Як звычайна, насамрэч усё складаней: нумар транзакцый не можа ўвесь час павялічвацца з-за абмежаванай разраднасці лічыльніка. Але гэтыя дэталі мы разгледзім падрабязна, калі дойдзем да замаразкі.)

Калі радок ствараецца, значэнне xmin усталёўваецца ў нумар транзакцыі, якая выканала каманду INSERT, а xmax не запаўняецца.

Калі радок выдаляецца, значэнне xmax бягучай версіі пазначаецца нумарам транзакцыі, якая выканала DELETE.

Калі радок змяняецца камандай UPDATE, фактычна выконваюцца дзве аперацыі: DELETE і INSERT. У бягучай версіі радка усталёўваецца xmax, роўны нумару транзакцыі, якая выканала UPDATE. Затым ствараецца новая версія таго ж радка; значэнне xmin у яе супадае са значэннем xmax папярэдняй версіі.

Палі xmin і xmax уваходзяць у загаловак версіі радка. Акрамя гэтых палёў, загаловак утрымоўвае і іншыя, напрыклад:

  • infomask - шэраг бітаў, якія вызначаюць уласцівасці дадзенай версіі. Іх даволі шмат; асноўныя з іх мы паступова разгледзім.
  • ctid - спасылка на наступную, навейшую, версію таго ж радка. У самай новай, актуальнай, версіі радка ctid спасылаецца на саму гэтую версію. Нумар мае выгляд (x, y), дзе x - нумар старонкі, y - парадкавы нумар паказальніка ў масіве.
  • бітавая карта нявызначаных значэнняў - адзначае тыя слупкі дадзенай версіі, якія ўтрымоўваюць нявызначанае значэнне (NULL). NULL не з'яўляецца адным са звычайных значэнняў тыпаў дадзеных, таму прыкмета даводзіцца захоўваць асобна.

У выніку загаловак атрымліваецца даволі вялікі - мінімум 23 байта на кожную версію радка, а звычайна больш з-за бітавай карты NULL-ов. Калі табліца "вузкая" (гэта значыць утрымоўвае мала слупкоў), накладныя выдаткі могуць займаць больш, чым карысная інфармацыя.

устаўка

Разгледзім падрабязней, як выконваюцца аперацыі са радкамі на нізкім узроўні, і пачнем са ўстаўкі.

Для эксперыментаў створым новую табліцу з двума слупкамі і азначнік па адным з іх:

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

Уставім адзін радок, папярэдне пачаўшы транзакцыю.

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

Вось нумар нашай бягучай транзакцыі:

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

Зазірнем у змесціва старонкі. Функцыя heap_page_items пашырэння pageinspect дазваляе атрымаць інфармацыю аб паказальніках і версіях радкоў:

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

Заўважым, што словам heap (куча) у PostgreSQL абазначаюцца табліцы. Гэта яшчэ адно дзіўнае ўжыванне тэрміна - куча з'яўляецца вядомай структурай дадзеных, якая не мае з табліцай нічога агульнага. Тут гэтае слова ўжываецца ў сэнсе "ўсё звалена ў кучу", у адрозненне ад спарадкаваных азначнікаў.

Функцыя паказвае дадзеныя "як ёсць", у фармаце, складаным для ўспрымання. Каб разабрацца, мы пакінем толькі частку інфармацыі і расшыфруем яе:

=> 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_ctid: (нумар старонкі, нумар паказальніка).
  • Расшыфравалі стан паказальніка lp_flags. Тут ён "normal" - гэта значыць, што паказальнік сапраўды спасылаецца на версію радка. Іншыя значэнні разгледзім пазней.
  • З усіх інфармацыйных бітаў вылучылі пакуль толькі дзве пары. Біты xmin_committed і xmin_aborted паказваюць, ці зафіксаваная (адменена ці) транзакцыя з нумарам xmin. Два аналагічныя біты адносяцца да транзакцыі з нумарам xmax.

Што ж мы бачым? Пры ўстаўцы радка ў таблічнай старонцы з'явіцца паказальнік з нумарам 1, які спасылаецца на першую і адзіную версію радка.

У версіі радка поле xmin запоўнена нумарам бягучай транзакцыі. Транзакцыя яшчэ актыўная, таму абодва біты xmin_committed і xmin_aborted не ўсталяваныя.

Поле ctid версіі радка спасылаецца на гэты ж радок. Гэта азначае, што навейшай версіі не існуе.

Поле xmax запоўнена фіктыўным нумарам 0, паколькі дадзеная версія радка не выдалена і з'яўляецца актуальнай. Транзакцыі не будуць зважаць на гэты нумар, паколькі ўсталяваны біт xmax_aborted.

Зробім яшчэ адзін крок да паляпшэння чытальнасці, дапісаўшы інфармацыйныя біты да нумароў транзакцый. І створым функцыю, паколькі запыт нам яшчэ не раз спатрэбіцца:

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

У такім выглядзе значна больш зразумела, што робіцца ў загалоўку версіі радка:

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

Падобную, але істотна меней дэталёвую, інфармацыю можна атрымаць і з самой табліцы, выкарыстаючы псевдостолбцы xmin і xmax:

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

фіксацыя

Пры паспяховым завяршэнні транзакцыі трэба запомніць яе статус - адзначыць, што яна зафіксаваная. Для гэтага выкарыстоўваецца структура, званая XACT (а да версіі 10 яна звалася CLOG (commit log) і гэтая назва яшчэ можа сустракацца ў розных месцах).

XACT - не табліца сістэмнага каталога; гэта файлы ў каталогу PGDATA/pg_xact. У іх для кожнай транзакцыі адведзена два біты: committed і aborted - сапраўды гэтак жа, як у загалоўку версіі радка. На некалькі файлаў гэтая інфармацыя разбіта выключна для зручнасці, мы яшчэ вернемся да гэтага пытання, калі будзем разглядаць замарозку. А праца з гэтымі файламі вядзецца пастаронкава, як і са ўсімі іншымі.

Такім чынам, пры фіксацыі транзакцыі ў XACT выстаўляецца біт committed для дадзенай транзакцыі. І гэта ўсё, што адбываецца пры фіксацыі (праўда, мы пакуль не гаворым пра часопіс перадзапісу).

Калі якая-небудзь іншая транзакцыя звернецца да таблічнай старонкі, на якую мы толькі што глядзелі, ёй давядзецца адказаць на некалькі пытанняў.

  1. Ці скончылася транзакцыя xmin? Калі не, то створаная версія радка не павінна быць бачная.
    Такая праверка выконваецца праглядам яшчэ адной структуры, якая размяшчаецца ў агульнай памяці асобніка і завецца ProcArray. У ёй знаходзіцца спіс усіх актыўных працэсаў, і для кожнага пазначаны нумар яго бягучай (актыўнай) транзакцыі.
  2. Калі завяршылася, то як - фіксацыяй або адменай? Калі адменай, то версія радка таксама не павінны быць бачная.
    Вось для гэтага як раз і патрэбен XACT. Але, хоць апошнія старонкі XACT захоўваюцца ў буферах у аператыўнай памяці, усё ж кожны раз правяраць XACT накладна. Таму высветлены аднойчы статут транзакцыі запісваецца ў біты xmin_committed і xmin_aborted версіі радка. Калі адзін з гэтых бітаў усталяваны, то стан транзакцыі xmin лічыцца вядомым і наступнай транзакцыі ўжо не давядзецца звяртацца да XACT.

Чаму гэтыя біты не ўстанаўліваюцца самой транзакцыяй, якая выконвае ўстаўку? Калі адбываецца ўстаўка, транзакцыя яшчэ не ведае, ці завершыцца яна паспяхова. А ў момант фіксацыі ўжо незразумела, якія менавіта радкі ў якіх менавіта старонках былі змененыя. Такіх старонак можа аказацца шмат, і запамінаць іх нявыгадна. Да таго ж частка старонак можа быць выцесненая з буфернага кэша на дыск; чытаць іх нанова, каб змяніць біты, азначала б істотна запаволіць фіксацыю.

Адваротны бок эканоміі складаецца ў тым, што пасля змен любая транзакцыя (нават якая выконвае простае чытанне - SELECT) можа пачаць змяняць старонкі дадзеных у буферным кэшы.

Такім чынам, зафіксуем змену.

=> COMMIT;

У старонцы нічога не змянілася (але мы ведаем, што статут транзакцыі ўжо запісаны ў XACT):

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

Цяпер транзакцыя, якая першай звярнулася да старонкі, павінна будзе вызначыць статус транзакцыі xmin і запіша яго ў інфармацыйныя біты:

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

Выдаленне

Пры выдаленні радка ў поле xmax актуальнай версіі запісваецца нумар бягучай выдаляючай транзакцыі, а біт xmax_aborted скідаецца.

Заўважым, што ўсталяванае значэнне xmax, якое адпавядае актыўнай транзакцыі, выступае ў якасці блакавання радка. Калі іншая транзакцыя збіраецца абнавіць ці выдаліць гэты радок, яна будзе вымушана дачакацца завяршэння транзакцыі xmax. Больш падрабязна пра блакіроўкі мы будзем казаць пазней. Пакуль адзначым толькі, што колькасць блакіровак радкоў нічым не абмежавана. Яны не займаюць месца ў аператыўнай памяці і прадукцыйнасць сістэмы не церпіць ад іх колькасці. Праўда, у "доўгіх" транзакцый ёсць іншыя мінусы, але пра гэта таксама пазней.

Выдалім радок.

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

Бачым, што нумар транзакцыі запісаўся ў полі xmax, але інфармацыйныя біты не ўсталяваныя:

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

Адмена

Адмена зменаў працуе аналагічна фіксацыі, толькі ў XACT для транзакцыі выстаўляецца біт aborted. Адмена выконваецца гэтак жа хутка, як і фіксацыя. Хоць каманда і завецца ROLLBACK, адкату змен не адбываецца: усё, што транзакцыя паспела змяніць у старонках дадзеных, застаецца без змен.

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

Пры звароце да старонкі будзе правераны статус і ў версію радка будзе ўсталяваны біт падказкі xmax_aborted. Сам нумар 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)

Абнаўленне

Абнаўленне працуе так, як быццам спачатку выканалася выдаленне бягучай версіі радка, а затым устаўка новай.

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

Запыт выдае адзін радок (новую версію):

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

Але ў старонцы мы бачым абедзве версіі:

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

Выдаленая версія пазначана нумарам бягучай транзакцыі ў полі xmax. Прычым гэтае значэнне запісана па-над старым, паколькі папярэдняя транзакцыя была адменена. А біт xmax_aborted скінуты, бо статут бягучай транзакцыі яшчэ невядомы.

Першая версія радка спасылаецца зараз на другую (поле t_ctid), як на навейшую.

У індэкснай старонцы з'яўляецца другі паказальнік і другі радок, якая спасылаецца на другую версію ў таблічнай старонцы.

Гэтак жа, як і пры выдаленні, значэнне xmax у першай версіі радка служыць прыкметай таго, што радок заблакаваная.

Ну і завершым транзакцыю.

=> COMMIT;

Індэксы

Да гэтага часу мы казалі толькі аб таблічных старонках. А што адбываецца ўнутры азначнікаў?

Інфармацыя ў індэксных старонках моцна залежыць ад канкрэтнага тыпу індэкса. І нават у аднаго тыпу азначніка бываюць розныя выгляды старонак. Напрыклад, у B-дрэва ёсць старонка з метададзенымі і "звычайныя" старонкі.

Тым не менш, звычайна ў старонцы ёсць масіў паказальнікаў на радкі і самі радкі (гэтак жа, як і ў таблічнай старонцы). Акрамя таго, у канцы старонкі адводзіцца месца пад спецыяльныя дадзеныя.

Радкі ў індэксах таксама могуць мець вельмі розную структуру ў залежнасці ад тыпу індэкса. Напрыклад, для B-дрэва радкі, якія адносяцца да ліставых старонак, утрымоўваюць значэнне ключа індэксавання і спасылку (ctid) на які адпавядае радок табліцы. У агульным выпадку індэкс можа быць уладкованы зусім іншай выявай.

Найважнейшы момант складаецца ў тым, што ў азначніках любога тыпу не бывае версій радкоў. Ну ці можна лічыць, што кожны радок прадстаўлены роўна адной версіяй. Іншымі словамі, у загалоўку індэкснага радка не бывае палёў xmin і xmax. Можна лічыць, што спасылкі з азначніка вядуць на ўсе таблічныя версіі радкоў — так што разабрацца, якую з версій убачыць транзакцыя, можна толькі зазірнуўшы ў табліцу. (Як звычайна, гэта не ўся праўда. У некаторых выпадках карта бачнасці дазваляе аптымізаваць працэс, але падрабязней разгледзім гэта пазней.)

Пры гэтым у індэкснай старонцы выяўляем паказальнікі на абедзве версіі, як на актуальную, так і на старую:

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

Віртуальныя транзакцыі

На практыцы PostgreSQL выкарыстоўвае аптымізацыю, якая дазваляе "эканоміць" нумары транзакцый.

Калі транзакцыя толькі чытае дадзеныя, то яна ніяк не ўплывае на бачнасць версій радкоў. Таму спачатку абслуговы працэс выдае транзакцыі віртуальны нумар (virtual xid). Нумар складаецца з ідэнтыфікатара працэсу і паслядоўнага ліку.

Выдача гэтага нумара не патрабуе сінхранізацыі паміж усімі працэсамі, і таму выконваецца вельмі хутка. З іншай прычынай выкарыстання віртуальных нумароў мы пазнаёмімся, калі будзем казаць аб замарозцы.

Віртуальныя нумары ніяк не ўлічваюцца ў здымках звестак.

У розныя моманты часу ў сістэме цалкам могуць аказацца віртуальныя транзакцыі з нумарамі, якія ўжо выкарыстоўваліся, і гэта нармальна. Але такі нумар нельга запісваць у старонкі дадзеных, таму што пры наступным звароце да старонкі ён можа страціць усялякі сэнс.

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

Калі ж транзакцыя пачынае мяняць дадзеныя, ёй выдаецца сапраўдны, унікальны нумар транзакцыі.

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

=> COMMIT;

Укладзеныя транзакцыі

Кропкі захавання

У SQL вызначаны кропкі захавання (savepoint), якія дазваляюць адмяніць частку аперацыяй транзакцыі, не перарываючы яе поўнасцю. Але гэта не ўкладваецца ў прыведзеную вышэй схему, паколькі статут у транзакцыі адзін на ўсе яе змены, а фізічна ніякія дадзеныя не адкочваюцца.

Каб рэалізаваць такі функцыянал, транзакцыя з кропкай захавання разбіваецца на некалькі асобных. укладзеных транзакцый (subtransaction), статутам якіх можна кіраваць асобна.

Укладзеныя транзакцыі маюць свой уласны нумар (большы за нумар асноўнай транзакцыі). Статус укладзеных транзакцый запісваецца звычайнай выявай у XACT, аднак фінальны статут залежыць ад статуту асноўнай транзакцыі: калі яна адмененая, то адмяняюцца таксама і ўсе ўкладзеныя транзакцыі.

Інфармацыя аб укладзенасці транзакцый захоўваецца ў файлах у каталогу PGDATA/pg_subtrans. Зварот да файлаў адбываецца праз буферы ў агульнай памяці асобніка, арганізаваныя гэтак жа, як і буферы XACT.

Не блытайце ўкладзеныя транзакцыі і аўтаномныя транзакцыі. Аўтаномныя транзакцыі ніяк не залежаць адзін ад аднаго, а ўкладзеныя - залежаць. Аўтаномных транзакцый у звычайным PostgreSQL няма, і, мабыць, да лепшага: па справе яны патрэбны вельмі і вельмі рэдка, а іх наяўнасць у іншых СКБД правакуе злоўжыванне, ад якога потым усё пакутуюць.

Ачысцім табліцу, пачнем транзакцыю і ўставім радок:

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

Цяпер паставім кропку захавання і ўставім яшчэ адзін радок.

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

Заўважце, што функцыя txid_current() выдае нумар асноўнай, а не ўкладзенай, транзакцыі.

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

Адкоцімся да кропкі захавання і ўставім трэці радок.

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

У старонцы мы працягваем бачыць радок, дададзены адмененай укладзенай транзакцыяй.

Фіксуем змены.

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

Цяпер добра бачна, што кожная ўкладзеная транзакцыя мае ўласны статус.

Заўважым, што ўкладзеныя транзакцыі нельга выкарыстоўваць у SQL відавочна, гэта значыць нельга пачаць новую транзакцыю, не завяршыўшы бягучую. Гэты механізм задзейнічаецца няяўна пры выкарыстанні кропак захавання, а яшчэ пры апрацоўцы выключэнняў PL/pgSQL і ў шэрагу іншых, больш экзатычных выпадкаў.

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

Памылкі і атамарнасць аперацый

Што адбудзецца, калі пры выкананні аперацыі адбудзецца памылка? Напрыклад, так:

=> 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;
ERROR:  current transaction is aborted, commands ignored until end of transaction block

І нават калі паспрабаваць зафіксаваць змены, PostgreSQL паведаміць аб адмене:

=> COMMIT;
ROLLBACK

Чаму нельга прадоўжыць выкананне транзакцыі пасля збою? Справа ў тым, што памылка магла ўзнікнуць так, што мы атрымалі б доступ да часткі змен - была б парушана атамарнасць нават не транзакцыі, а аператара. Як у нашым прыкладзе, дзе аператар да памылкі паспеў абнавіць адзін радок:

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

Трэба сказаць, што ў psql маецца рэжым, які ўсёткі дазваляе працягваць працу транзакцыі пасля збою так, як быццам дзеянні памылковага аператара адкочваюцца.

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

Няцяжка здагадацца, што ў такім рэжыме psql фактычна ставіць перад кожнай камандай няяўную кропку захавання, а ў выпадку збою ініцыюе адкат да яе. Такі рэжым не выкарыстоўваецца па змаўчанні, паколькі ўсталёўка кропак захавання (нават без адкату да іх) спалучана з істотнымі накладнымі выдаткамі.

Працяг.

Крыніца: habr.com

Дадаць каментар