MVCC-3. Լարային տարբերակներ

Այսպիսով, մենք քննարկել ենք հետ կապված հարցեր մեկուսացում, և նահանջեց մոտ տվյալների կազմակերպում ցածր մակարդակով. Եվ վերջապես հասանք ամենահետաքրքիր մասին՝ լարային տարբերակներին։

Վերնագիր

Ինչպես արդեն ասացինք, յուրաքանչյուր տող կարող է միաժամանակ գոյություն ունենալ տվյալների բազայում մի քանի տարբերակներով։ Մեկ տարբերակը պետք է ինչ-որ կերպ տարբերվի մյուսից: Այդ նպատակով յուրաքանչյուր տարբերակ ունի երկու նշան, որոնք որոշում են այս տարբերակի գործողության «ժամանակը» (xmin և xmax): Չակերտներում - քանի որ որպես այդպիսին օգտագործվում է ոչ թե ժամանակը, այլ հատուկ աճող հաշվիչը: Եվ այս հաշվիչը գործարքի համարն է:

(Ինչպես սովորաբար, իրականությունն ավելի բարդ է. գործարքի համարը չի կարող անընդհատ աճել հաշվիչի սահմանափակ բիթային հզորության պատճառով: Բայց մենք մանրամասն կանդրադառնանք այս մանրամասներին, երբ հասնենք սառեցմանը:)

Երբ ստեղծվում է տող, xmin-ը սահմանվում է գործարքի համարին, որը թողարկել է INSERT հրամանը, և xmax-ը մնում է դատարկ:

Երբ տողը ջնջվում է, ընթացիկ տարբերակի xmax արժեքը նշվում է DELETE կատարած գործարքի թվով:

Երբ տողը փոփոխվում է UPDATE հրամանով, իրականում կատարվում է երկու գործողություն՝ DELETE և INSERT: Շարքի ընթացիկ տարբերակը սահմանում է xmax-ը, որը հավասար է ԹԱՐՄԱՑՄԱՆ կատարած գործարքի թվին: Այնուհետև ստեղծվում է նույն տողի նոր տարբերակը. դրա xmin արժեքը համընկնում է նախորդ տարբերակի xmax արժեքի հետ:

Xmin և xmax դաշտերը ներառված են տողի տարբերակի վերնագրում: Բացի այս դաշտերից, վերնագիրը պարունակում է ուրիշներ, օրինակ.

  • infomask-ը բիթերի շարք է, որոնք սահմանում են այս տարբերակի հատկությունները: Դրանք բավականին շատ են. Մենք աստիճանաբար կդիտարկենք հիմնականները.
  • ctid-ը հղում է նույն տողի հաջորդ, ավելի նոր տարբերակին: Լարի ամենանոր, ամենաարդի տարբերակի համար ctid-ը վերաբերում է հենց այս տարբերակին: Թիվն ունի (x,y) ձևը, որտեղ x-ը էջի համարն է, y-ը զանգվածի ինդեքսի համարն է։
  • null bitmap - Նշում է տվյալ տարբերակի այն սյունակները, որոնք պարունակում են զրոյական արժեք (NULL): NULL-ը սովորական տվյալների տիպի արժեքներից չէ, ուստի հատկանիշը պետք է առանձին պահվի:

Արդյունքում, վերնագիրը բավականին մեծ է՝ տողի յուրաքանչյուր տարբերակի համար առնվազն 23 բայթ, և սովորաբար ավելի շատ՝ NULL bitmap-ի շնորհիվ: Եթե ​​աղյուսակը «նեղ» է (այսինքն՝ պարունակում է մի քանի սյունակներ), ապա վերին ծախսը կարող է ավելին զբաղեցնել, քան օգտակար տեղեկությունը:

տեղադրել

Եկեք մանրամասն նայենք, թե ինչպես են կատարվում ցածր մակարդակի լարային գործողությունները՝ սկսած ներդիրից:

Փորձերի համար եկեք ստեղծենք նոր աղյուսակ երկու սյունակով և դրանցից մեկի ինդեքսով.

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

Եկեք նայենք էջի բովանդակությանը. Pageinspect ընդլայնման heap_page_items ֆունկցիան թույլ է տալիս տեղեկատվություն ստանալ ցուցիչների և տողերի տարբերակների մասին.

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

Նշենք, որ PostgreSQL-ում heap բառը վերաբերում է աղյուսակներին: Սա տերմինի ևս մեկ տարօրինակ օգտագործում է՝ կույտ հայտնի է տվյալների կառուցվածքը, որը ոչ մի ընդհանուր բան չունի սեղանի հետ։ Այստեղ բառն օգտագործվում է «ամեն ինչ միաձուլված է» իմաստով, ի տարբերություն պատվիրված ցուցիչների։

Ֆունկցիան ցույց է տալիս տվյալները «ինչպես կան», դժվար հասկանալի ձևաչափով: Դա պարզելու համար մենք կթողնենք տեղեկատվության միայն մի մասը և կվերծանենք այն.

=> 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 ցուցիչի վիճակը: Այստեղ այն «նորմալ» է. սա նշանակում է, որ ցուցիչը իրականում վերաբերում է տողի տարբերակին: Մենք ավելի ուշ կանդրադառնանք այլ իմաստներին:
  • Բոլոր տեղեկատվական բիթերից մինչ այժմ հայտնաբերվել են միայն երկու զույգ: 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 գրացուցակի ֆայլերն են: Նրանք ունեն երկու բիթ յուրաքանչյուր գործարքի համար՝ կատարված և ընդհատված, ճիշտ այնպես, ինչպես տողի տարբերակի վերնագրում: Այս տեղեկատվությունը բաժանված է մի քանի ֆայլերի՝ բացառապես հարմարության համար, մենք կանդրադառնանք այս հարցին, երբ քննարկենք սառեցումը: Եվ այս ֆայլերի հետ աշխատանքը կատարվում է էջ առ էջ, ինչպես մյուս բոլորի հետ:

Այսպիսով, երբ գործարքը կատարվում է XACT-ում, պարտավորված բիթը սահմանվում է այս գործարքի համար: Եվ սա այն ամենն է, ինչ տեղի է ունենում կատարման ընթացքում (թեև մենք դեռ չենք խոսում նախնական ձայնագրման մատյանի մասին):

Երբ մեկ այլ գործարք մուտք է գործում աղյուսակի էջ, որը մենք հենց նոր նայեցինք, այն պետք է պատասխանի մի քանի հարցերի:

  1. Ավարտվե՞լ է xmin գործարքը: Եթե ​​ոչ, ապա տողի ստեղծված տարբերակը չպետք է տեսանելի լինի։
    Այս ստուգումն իրականացվում է մեկ այլ կառույցի դիտմամբ, որը գտնվում է օրինակի ընդհանուր հիշողության մեջ և կոչվում է ProcArray: Այն պարունակում է բոլոր ակտիվ գործընթացների ցանկը, և յուրաքանչյուրի համար նշվում է նրա ընթացիկ (ակտիվ) գործարքի համարը:
  2. Եթե ​​ավարտված է, ապա ինչպե՞ս՝ կատարելով կամ չեղարկելով: Եթե ​​չեղարկվի, ապա տողի տարբերակը նույնպես չպետք է տեսանելի լինի:
    Սա հենց այն է, ինչի համար է XACT-ը: Բայց, չնայած XACT-ի վերջին էջերը պահվում են RAM-ի բուֆերներում, այնուամենայնիվ, ամեն անգամ 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 գործարքի ավարտին: Արգելափակման մասին ավելի ուշ կխոսենք: Առայժմ մենք պարզապես նշում ենք, որ շարքի կողպեքների քանակը անսահմանափակ է: Նրանք տեղ չեն զբաղեցնում RAM-ում, և համակարգի կատարումը չի տուժում դրանց քանակից: Ճիշտ է, «երկար» գործարքներն ունեն այլ թերություններ, բայց դրա մասին ավելի ուշ:

Եկեք ջնջենք տողը.

=> 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-ում ընդհատված բիթը սահմանված է գործարքի համար: Չեղարկելը նույնքան արագ է, որքան պարտավորելը: Չնայած հրամանը կոչվում է 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-tree-ն ունի մետատվյալների էջ և «սովորական» էջեր:

Այնուամենայնիվ, էջը սովորաբար ունի տողերի և հենց տողերի ցուցիչների զանգված (ինչպես աղյուսակի էջը): Բացի այդ, էջի վերջում տեղ կա հատուկ տվյալների համար։

Ինդեքսների տողերը կարող են նաև ունենալ շատ տարբեր կառուցվածքներ՝ կախված ինդեքսի տեսակից: Օրինակ, 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-ն օգտագործում է օպտիմալացումներ, որոնք թույլ են տալիս «փրկել» գործարքների համարները:

Եթե ​​գործարքը միայն կարդում է տվյալներ, դա չի ազդում տողերի տարբերակների տեսանելիության վրա: Հետևաբար, սպասարկման գործընթացը նախ գործարքին տալիս է վիրտուալ xid: Համարը բաղկացած է գործընթացի ID-ից և հաջորդական համարից:

Այս համարի թողարկումը չի պահանջում համաժամացում բոլոր գործընթացների միջև և, հետևաբար, շատ արագ է: Վիրտուալ համարների օգտագործման մեկ այլ պատճառի կծանոթանանք, երբ խոսենք սառեցման մասին։

Վիրտուալ թվերը ոչ մի կերպ հաշվի չեն առնվում տվյալների ակնոցներում:

Ժամանակի տարբեր կետերում համակարգում կարող են լինել վիրտուալ գործարքներ արդեն իսկ օգտագործված թվերով, և դա նորմալ է: Բայց նման թիվը չի կարող գրվել տվյալների էջերում, քանի որ հաջորդ անգամ էջ մուտք գործելիս այն կարող է կորցնել ամբողջ իմաստը:

=> 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), որը թույլ է տալիս չեղարկել գործարքի մի մասը՝ առանց այն ամբողջությամբ ընդհատելու: Բայց դա չի տեղավորվում վերը նշված գծապատկերում, քանի որ գործարքն ունի նույն կարգավիճակը իր բոլոր փոփոխությունների համար, և ֆիզիկապես ոչ մի տվյալ հետ չի վերադարձվում:

Այս գործառույթն իրականացնելու համար պահման կետով գործարքը բաժանվում է մի քանի առանձինների ներդիր գործարքներ (ենթ գործարքը), որի կարգավիճակը կարելի է կառավարել առանձին։

Ներդրված գործարքներն ունեն իրենց համարը (ավելի բարձր, քան հիմնական գործարքի համարը): Ներդրված գործարքների կարգավիճակը գրանցվում է սովորական ձևով XACT-ում, սակայն վերջնական կարգավիճակը կախված է հիմնական գործարքի կարգավիճակից. եթե այն չեղարկված է, ապա բոլոր ներդիր գործարքները նույնպես չեղարկվում են:

Գործարքների տեղադրման մասին տեղեկատվությունը պահվում է PGDATA/pg_subtrans գրացուցակում գտնվող ֆայլերում: Ֆայլերը հասանելի են բուֆերների միջոցով օրինակի ընդհանուր հիշողության մեջ, որոնք կազմակերպված են այնպես, ինչպես XACT բուֆերները:

Մի շփոթեք ներդիր գործարքները ինքնավար գործարքների հետ: Ինքնավար գործարքները ոչ մի կերպ կախված չեն միմյանցից, բայց ներդիր գործարքները կախված են: Սովորական PostgreSQL-ում չկան ինքնավար գործարքներ, և, հավանաբար, լավագույն դեպքում. դրանք շատ, շատ հազվադեպ են անհրաժեշտ, և դրանց առկայությունը այլ DBMS-ներում առաջացնում է չարաշահում, որից հետո տուժում են բոլորը:

Եկեք մաքրենք աղյուսակը, սկսենք գործարք և տեղադրենք տողը.

=> 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-ն իրականում յուրաքանչյուր հրամանից առաջ դնում է անուղղակի պահպանման կետ, իսկ ձախողման դեպքում նախաձեռնում է հետադարձ վերադարձ։ Այս ռեժիմը լռելյայն չի օգտագործվում, քանի որ պահման կետերի կարգավորումը (նույնիսկ առանց դրանց հետ վերադառնալու) ներառում է զգալի ծախսեր:

Շարունակություն:

Source: www.habr.com

Добавить комментарий