MVCC-3. Versiuni de rânduri

Așadar, am analizat problemele legate de izolație, și am făcut o deviație despre organizarea datelor la un nivel de bază. Și în sfârșit am ajuns la cel mai interesant — versiunile de rânduri.

Titlu

După cum am spus, fiecare rând poate exista simultan în baza de date în mai multe versiuni. O versiune trebuie deosebită de alta. În acest scop, fiecare versiune are două marcaje care definesc «timpul» de valabilitate al acestei versiuni (xmin și xmax). În ghilimele — pentru că nu se folosește timpul propriu-zis, ci un contor special care crește. Iar acest contor este numărul de tranzacție.

(După cum se știe, de fapt, totul este mai complex: numărul tranzacțiilor nu poate crește tot timpul din cauza limitării dimensiunii contorului. Dar aceste detalii le vom analiza în detaliu când vom ajunge la înghețare.)

Când un rând este creat, valoarea xmin este setată la numărul tranzacției care a executat comanda INSERT, iar xmax nu este completată.

Când un rând este șters, valoarea xmax a versiunii curente este marcată cu numărul tranzacției care a efectuat DELETE.

Când un rând este modificat prin comanda UPDATE, se efectuează de fapt două operațiuni: DELETE și INSERT. În versiunea curentă a rândului se setează xmax, care este egal cu numărul tranzacției care a executat UPDATE. Apoi, se creează o nouă versiune a aceluiași rând; valoarea xmin a acesteia coincide cu valoarea xmax a versiunii anterioare.

Câmpurile xmin și xmax fac parte din capul versiunii rândului. Pe lângă aceste câmpuri, capul conține și altele, de exemplu:

  • infomask — un șir de biți care definesc proprietățile acestei versiuni. Există destule; principalele dintre ele le vom analiza treptat.
  • ctid — un link către versiunea ulterioară, mai nouă, a aceluiași rând. La cea mai recentă, versiunea actualizată a rândului, ctid se referă la această versiune. Numărul are forma (x,y), unde x este numărul paginii, iar y este numărul de ordine al indicatorului în matrice.
  • harta de biți a valorilor nesigure — marchează coloanele acestei versiuni care conțin valori nesigure (NULL). NULL nu este unul dintre valorile obișnuite ale tipurilor de date, deci indicatorul trebuie să fie stocat separat.

Ca urmare, capul devine destul de mare — minimum 23 de octeți pentru fiecare versiune a rândului, iar adesea mai mult din cauza hărții de biți NULL. Dacă tabela este „îngustă” (adică conține puține coloane), costurile de overhead pot depăși informația utilă.

Inserare

Să analizăm mai în detaliu cum se efectuează operațiunile pe șiruri la un nivel redus și să începem cu inserarea.

Pentru experimente, vom crea un nou tabel cu două coloane și un index pe una dintre ele:

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

Vom insera un șir, începând mai întâi tranzacția.

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

Iată numărul tranzacției noastre curente:

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

Să aruncăm o privire asupra conținutului paginii. Funcția heap_page_items a extensiei pageinspect permite obținerea de informații despre pointeri și versiuni de rânduri:

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

Observăm că termenul heap în PostgreSQL se referă la tabele. Aceasta este o altă utilizare ciudată a termenului — un heap este o structură de date cunoscută , care nu are nimic în comun cu o tabelă. Aici, cuvântul este folosit în sensul de „totul este aruncat într-un heap”, spre deosebire de indicii ordonați.Funcția arată datele „așa cum sunt”, într-un format greu de interpretat. Pentru a înțelege, vom păstra doar o parte a informațiilor și le vom clarifica:

=> SELECT '(0,'||lp||')' AS ctid, CASE lp_flags WHEN 0 THEN 'neutilizat' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirecționat la '||lp_off WHEN 3 THEN 'mort' 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)
Iată ce am făcut:

Am adăugat zero la numărul pointerului, pentru a-l aduce în același format ca t_ctid: (numărul paginii, numărul pointerului).

  • Am decodificat starea pointerului lp_flags. Aici este „normal” — ceea ce înseamnă că pointerul se referă cu adevărat la versiunea rândului. Alte valori vor fi discutate mai târziu.
  • Dintre toți bitii informației, am scos deocamdată doar două perechi. Biti xmin_committed și xmin_aborted arată dacă tranzacția cu numărul xmin a fost confirmată (sau anulată). Două bite similar se referă la tranzacția cu numărul xmax.
  • Из всех информационных битов выделили пока только две пары. Биты xmin_committed и xmin_aborted показывают, зафиксирована ли (отменена ли) транзакция с номером xmin. Два аналогичных бита относятся к транзакции с номером xmax.

Ce vedem? Atunci când inserăm o linie într-o pagină tabelară, va apărea un pointer cu numărul 1, referindu-se la prima și singura versiune a liniei.

În versiunea liniei, câmpul xmin este completat cu numărul tranzacției curente. Tranzacția este încă activă, astfel încât ambele biți xmin_committed și xmin_aborted nu sunt setați.

Câmpul ctid al versiunii liniei se referă la această linie. Acest lucru înseamnă că nu există o versiune mai recentă.

Câmpul xmax este completat cu un număr fictiv 0, deoarece această versiune a liniei nu a fost ștearsă și este actuală. Tranzacțiile nu vor lua în considerare acest număr, deoarece bitul xmax_aborted este setat.

Să facem încă un pas către îmbunătățirea lizibilității, adăugând biți informaționali la numerele tranzacțiilor. Și să creăm o funcție, deoarece interogarea ne va fi necesară din nou:

=> 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 'nefolosit'
         WHEN 1 THEN 'normal'
         WHEN 2 THEN 'redirecționat către '||lp_off
         WHEN 3 THEN 'mort'
       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;

În această formă este mult mai clar ce se întâmplă în antetul versiunii liniei:

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

Informații similare, dar considerabil mai puțin detaliate, pot fi obținute și din tabelul în sine, folosind pseudocolonelurile xmin și xmax:

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

Confirmare

După finalizarea cu succes a tranzacției, trebuie să ne amintim de statutul acesteia — să marcăm că a fost confirmată. Pentru aceasta se folosește o structură numită XACT (iar înainte de versiunea 10 se numea CLOG (commit log) și acest nume poate apărea în diferite locuri).

XACT — nu este un tabel de catalog de sistem; acestea sunt fișiere în directorul PGDATA/pg_xact. În acestea, pentru fiecare tranzacție sunt prevăzute două biți: committed și aborted — exact așa cum se află în antetul versiunii liniei. Informațiile sunt împărțite în mai multe fișiere exclusiv pentru comoditate, ne vom întoarce înapoi la această problemă atunci când vom analiza înghețarea. Lucrul cu aceste fișiere se face pagină cu pagină, la fel ca și cu toate celelalte.

Așadar, când o tranzacție este confirmată în XACT, bitul committed pentru această tranzacție este setat. Și acesta este tot ce se întâmplă în timpul confirmării (deși momentan nu discutăm despre jurnalul de preînregistrare).

Când o altă tranzacție accesează pagina tabelului pe care tocmai l-am analizat, aceasta va trebui să răspundă la câteva întrebări.

  1. A încheiat tranzacția xmin? Dacă nu, atunci versiunea creată a rândului nu ar trebui să fie vizibilă.
    Această verificare se face prin vizualizarea unei alte structuri, care se află în memoria comună a instanței și se numește ProcArray. Aceasta conține o listă a tuturor proceselor active, iar pentru fiecare este specificat numărul tranzacției sale curente (active).
  2. Dacă s-a încheiat, cum — prin confirmare sau anulare? Dacă prin anulare, atunci versiunea rândului nu ar trebui să fie vizibilă.
    Acesta este motivul pentru care avem XACT. Totuși, deși ultimele pagini XACT sunt păstrate în bufere în memorie, verificarea XACT de fiecare dată este costisitoare. De aceea, starea tranzacției stabilită o dată este înregistrată în biții xmin_committed și xmin_aborted ai versiunii rândului. Dacă oricare dintre acești biți este setat, atunci starea tranzacției xmin este considerată cunoscută și următoarei tranzacții nu va trebui să acceseze XACT.

De ce acești biți nu sunt setați de tranzacția care efectuează inserția? Când are loc inserția, tranzacția nu știe încă dacă va avea succes. Iar în momentul confirmării, deja nu este clar care rânduri din care pagini au fost modificate. Pot exista multe astfel de pagini, iar memorarea acestora nu este rentabilă. În plus, unele pagini pot fi evacuate din cache-ul de buffer pe disc; reluarea citirii acestora pentru a modifica biții ar însemna o încetinire substanțială a confirmării.

Partea opusă a economisirii este că, după modificări, orice tranzacție (chiar și cea care efectuează o simplă citire — SELECT) poate începe să modifice paginile de date în cache-ul de buffer.

Așadar, să confirmăm modificarea.

=> COMMIT;

În pagină nu s-a schimbat nimic (dar știm că starea tranzacției este deja înregistrată în XACT):

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

Acum, tranzacția care a accesat prima pagina va trebui să determine starea tranzacției xmin și o va înregistra în biții informaționali:

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

Ștergere

Când se șterge o linie în câmpul xmax, versiunea curentă înregistrează numărul tranzacției de ștergere active, iar bitul xmax_aborted este resetat.

Observăm că valoarea xmax, corespunzătoare tranzacției active, funcționează ca o blocare a liniei. Dacă o altă tranzacție încearcă să actualizeze sau să șteargă această linie, va trebui să aștepte finalizarea tranzacției xmax. Detalii despre blocaje vom discuta mai târziu. Deocamdată, să subliniem doar că numărul de blocaje ale liniilor nu este limitat. Acestea nu ocupă spațiu în memorie și performanța sistemului nu este afectată de numărul acestora. Totuși, tranzacțiile „lungi” au alte dezavantaje, dar și despre acestea vom vorbi mai târziu.

Să ștergem o linie.

=> ÎNCEPERE;
=> ȘTERGE DIN t;
=> SELECT txid_current();
 txid_current 
--------------
         3665
(1 rând)

Vedem că numărul tranzacției a fost înregistrat în câmpul xmax, dar biții informaționali nu sunt setați:

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

Anulare

Anularea modificărilor funcționează similar cu confirmarea, doar că în XACT pentru tranzacție se setează bitul aborted. Anularea se efectuează la fel de repede ca și confirmarea. Deși comanda se numește ROLLBACK, modificările nu sunt inversate: tot ceea ce tranzacția a reușit să schimbe în paginile de date rămâne neschimbat.

=> ROLLBACK;
=> SELECT * DIN heap_page('t',0);
 ctid  | stare  |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 rând)

La accesarea paginii, se va verifica starea și în versiunea liniei va fi setat bitul indicativ xmax_aborted. Numărul xmax rămâne pe pagină, dar nimeni nu-l va lua în considerare.

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

=> SELECT * FROM heap_page('t',0);
 ctid  | stare  |   xmin   |   xmax   | t_ctid 
-------+--------+----------+----------+--------
 (0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
(1 rând)

Actualizare

Actualizarea funcționează de parcă s-ar fi efectuat mai întâi o ștergere a versiunii curente a liniei, iar apoi o inserare a uneia noi.

=> ÎNCEPERE;
=> ACTUALIZEAZĂ t SET s = 'BAR';
=> SELECT txid_current();
 txid_current 
--------------
         3666
(1 rând)

Interogarea returnează un singur rând (noua versiune):

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

Dar pe pagină vedem ambele versiuni:

=> SELECT * FROM heap_page('t',0);
 ctid  | stare  |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normal | 3664 (c) | 3666  | (0,2)
 (0,2) | normal | 3666     | 0 (a) | (0,2)
(2 rânduri)

Versiunea ștearsă este marcată cu numărul tranzacției curente în câmpul xmax. Însă, această valoare a fost înregistrată peste cea veche, deoarece tranzacția anterioară a fost anulată. Iar bitul xmax_aborted a fost resetat, deoarece starea tranzacției curente este încă necunoscută.

Prima versiune a liniei face acum referire la a doua (câmpul t_ctid), ca fiind mai nouă.

Pe pagina index apare a doua referință și a doua linie, care face referire la a doua versiune din pagina tabelară.

La fel ca și în cazul eliminării, valoarea xmax din prima versiune a liniei servește ca un semn că linia este blocată.

Și acum să încheiem tranzacția.

=> COMMIT;

Indecși

Până acum am vorbit doar despre paginile tabelare. Ce se întâmplă în interiorul indicelui?

Informațiile din paginile index depind foarte mult de tipul specific al indexului. Chiar și pentru un singur tip de index, pot exista diferite tipuri de pagini. De exemplu, un arbore B are o pagină cu metadate și pagini «obișnuite».

Cu toate acestea, de obicei, o pagină conține un array de referințe la linii și liniile însele (la fel ca în pagina tabelară). În plus, la sfârșitul paginii se rezervă un loc pentru date speciale.

Liniile din indici pot avea, de asemenea, o structură foarte variată în funcție de tipul indexului. De exemplu, pentru un arbore B, liniile aferente paginilor frunzelor conțin valoarea cheii de indexare și o referință (ctid) la linia corespunzătoare din tabel. În general, un index poate fi organizat complet diferit.

Cel mai important aspect este că în indicii de orice tip nu există versiuni ale liniilor. Sau se poate considera că fiecare linie este reprezentată exact de o singură versiune. Cu alte cuvinte, în antetul unei linii de index nu există câmpuri xmin și xmax. Se poate considera că referințele din index duc la toate versiunile tabelului liniilor - astfel încât să înțelegem ce versiune va vedea tranzacția, putem verifica doar în tabel. (Ca de obicei, aceasta nu este întreaga adevăr. În unele cazuri, harta vizibilității permite optimizarea procesului, dar vom detalia acest aspect mai târziu.)

În acest fel, în pagina index descoperim referințe către ambele versiuni, atât cea actuală, cât și cea veche:

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

Tranzacții virtuale

În practică, PostgreSQL folosește o optimizare care permite „economisirea” numerelor de tranzacție.

Dacă tranzacția doar citește date, atunci nu afectează vizibilitatea versiunilor liniilor. Prin urmare, la început, procesul de servicii atribuie tranzacțiilor un număr virtual (virtual xid). Numărul este format din identificatorul procesului și un număr secvențial.

Alocarea acestui număr nu necesită sincronizare între toate procesele și, prin urmare, se desfășoară foarte rapid. O altă utilizare a numerelor virtuale va fi discutată când vom vorbi despre înghețare.

Numerele virtuale nu sunt incluse în instantaneele de date.

În diferite momente, sistemul poate conține tranzacții virtuale cu numere care au fost deja folosite, și aceasta este normal. Însă un astfel de număr nu poate fi scris în paginile de date, deoarece la următoarea accesare a paginii, poate pierde orice semnificație.

=> ÎNCEPUT;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                         
(1 rând)

Dacă tranzacția începe să modifice date, i se atribuie un număr real, unic de tranzacție.

=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                     3667
(1 rând)

=> COMMIT;

Tranzacții încorporate

Puncte de salvare

În SQL sunt definite puncte de salvare (savepoint), care permit anularea unei părți a operațiunii tranzacției, fără a o întrerupe complet. Dar acest lucru nu se încadrează în schema de mai sus, deoarece statutul tranzacției este unul pentru toate modificările sale, iar fizic, nicio dată nu este retrogradată.

Pentru a implementa o astfel de funcționalitate, tranzacția cu punct de salvare este împărțită în mai multe tranzacții încorporate (subtransaction), ale căror stări pot fi gestionate separat.

Tranzacțiile încorporate au propriul număr (mai mare decât numărul tranzacției principale). Statutul tranzacțiilor încorporate este înregistrat în mod obișnuit în XACT, cu toate acestea, statutul final depinde de statutul tranzacției principale: dacă aceasta este anulată, atunci toate tranzacțiile încorporate sunt de asemenea anulate.

Informațiile despre încorporarea tranzacțiilor sunt stocate în fișiere în directorul PGDATA/pg_subtrans. Accesarea fișierelor se face prin module în memoria partajată a instanței, organizate la fel ca modulele XACT.

Nu confundați tranzacțiile încorporate și tranzacțiile autonome. Tranzacțiile autonome nu depind una de cealaltă, în timp ce tranzacțiile încorporate depind. Tranzacțiile autonome nu există în PostgreSQL obișnuit și, probabil, bine fac: de fapt, acestea sunt necesare foarte rar, iar prezența lor în alte SGBD-uri provoacă abuzuri de care toată lumea suferă ulterior.

Vom curăța tabelul, vom începe tranzacția și vom insera un rând:

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

Acum vom plasa un punct de salvare și vom insera o altă linie.

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

Observați că funcția txid_current() returnează numărul tranzacției principale, nu al celor încorporate.

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

Ne vom întoarce la punctul de salvare și vom insera a treia linie.

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

Pe pagină continuăm să vedem liniile adăugate de tranzacția încorporată anulată.

Fixăm modificările.

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

Acum este clar că fiecare tranzacție încorporată are propriul său statut.

Observăm că tranzacțiile încorporate nu pot fi folosite explicit în SQL, ceea ce înseamnă că nu putem începe o nouă tranzacție fără a termina pe cea curentă. Acest mecanism este activat implicit atunci când folosim puncte de salvare și în procesarea excepțiilor PL/pgSQL, dar și în alte cazuri mai exotice.

=*> BEGIN;
BEGIN
=*> BEGIN;
AVERTISMENT:  există deja o tranzacție în desfășurare
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
AVERTISMENT:  nu există nicio tranzacție în desfășurare
COMMIT

Erori și atomicitatea operațiunilor

Ce se va întâmpla dacă se produce o eroare în cadrul execuției unei operațiuni? De exemplu, astfel:

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

=*> UPDATE t SET s = repeat('X', 1/(id-4));
EROARE:  diviziune cu zero

A apărut o eroare. Acum tranzacția este considerată întreruptă și nici o operațiune nu este permisă în interiorul acesteia:

=> SELECT * FROM t;
EROARE:  tranzacția curentă este anulată, comenzile sunt ignorate până la sfârșitul blocului de tranzacție

Și chiar dacă încercăm să fixăm modificările, PostgreSQL ne va anunța despre anulare:

=> COMMIT;
ROLLBACK

De ce nu se poate continua executarea unei tranzacții după o eroare? Problema este că eroarea ar fi putut apărea astfel încât am fi avut acces la o parte din modificări — ar fi fost încălcată atomicitatea, chiar și a operatorului, nu doar a tranzacției. Ca în exemplul nostru, unde operatorul a reușit să actualizeze o linie înainte de eroare:

=> SELECT * FROM heap_page('t',0);
 ctid  | stare  |   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 rânduri)

Trebuie spus că în psql există un mod care totuși permite continuarea lucrului cu tranzacția după o eroare, ca și cum acțiunile operatorului eronat ar fi fost anulate.

= > 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));
EROARE:  diviziune cu zero

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

=> COMMIT;

Nu este greu de ghicit că în acest mod psql, de fapt, plasează în fața fiecărei comenzi un punct de salvare implicit, iar în caz de eroare inițiază revenirea la acesta. Acest mod nu este folosit implicit, deoarece stabilirea punctelor de salvare (chiar și fără revenirea la ele) implică costuri generale semnificative.

Continuare.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster