MVCC-3. Wersje stringowe

Rozważaliśmy więc kwestie związane z izolacjai wycofał się ok organizowanie danych na niskim poziomie. I w końcu dotarliśmy do najciekawszej części – wersji smyczkowych.

Tytuł

Jak już powiedzieliśmy, każdy wiersz może jednocześnie istnieć w kilku wersjach w bazie danych. Jedną wersję trzeba jakoś odróżnić od drugiej.W tym celu każda wersja posiada dwa oznaczenia określające „czas” działania tej wersji (xmin i xmax). W cudzysłowie - ponieważ nie używa się czasu jako takiego, ale specjalnego licznika rosnącego. A ten licznik to numer transakcji.

(Jak zwykle rzeczywistość jest bardziej skomplikowana: liczba transakcji nie może stale rosnąć ze względu na ograniczoną pojemność bitową licznika. Ale szczegółowo przyjrzymy się tym szczegółom, gdy dojdziemy do zamrożenia.)

Kiedy tworzony jest wiersz, xmin jest ustawiany na numer transakcji, która wydała polecenie INSERT, a xmax pozostaje puste.

Po usunięciu wiersza wartość xmax aktualnej wersji jest oznaczona numerem transakcji, która wykonała DELETE.

Kiedy wiersz jest modyfikowany poleceniem UPDATE, w rzeczywistości wykonywane są dwie operacje: DELETE i INSERT. Bieżąca wersja wiersza ustawia xmax równy numerowi transakcji, która przeprowadziła AKTUALIZACJĘ. Następnie tworzona jest nowa wersja tego samego ciągu; jego wartość xmin pokrywa się z wartością xmax poprzedniej wersji.

Pola xmin i xmax znajdują się w nagłówku wersji wiersza. Oprócz tych pól nagłówek zawiera inne, na przykład:

  • infomask to seria bitów definiujących właściwości tej wersji. Jest ich całkiem sporo; Stopniowo będziemy rozważać główne.
  • ctid jest łączem do następnej, nowszej wersji tej samej linii. W przypadku najnowszej, najbardziej aktualnej wersji ciągu znaków ctid odnosi się do samej tej wersji. Liczba ma postać (x,y), gdzie x to numer strony, y to numer indeksu w tablicy.
  • null bitmap - Zaznacza te kolumny danej wersji, które zawierają wartość null (NULL). NULL nie jest jedną z normalnych wartości typu danych, dlatego atrybut musi być przechowywany oddzielnie.

W rezultacie nagłówek jest dość duży - co najmniej 23 bajty na każdą wersję linii, a zwykle więcej ze względu na bitmapę NULL. Jeśli tabela jest „wąska” (to znaczy zawiera kilka kolumn), narzut może pochłonąć więcej niż przydatne informacje.

wstawić

Przyjrzyjmy się bliżej sposobowi wykonywania operacji na ciągach niskiego poziomu, zaczynając od wstawienia.

Na potrzeby eksperymentów utwórzmy nową tabelę z dwiema kolumnami i indeksem w jednej z nich:

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

Wstawmy jeden wiersz po rozpoczęciu transakcji.

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

Oto nasz aktualny numer transakcji:

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

Przyjrzyjmy się zawartości strony. Funkcja heap_page_items rozszerzenia pageinspect pozwala uzyskać informacje o wskaźnikach i wersjach wierszy:

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

Zauważ, że słowo sterta w PostgreSQL odnosi się do tabel. To kolejne dziwne użycie terminu – znana jest sterta struktura danych, który nie ma nic wspólnego z tabelą. Tutaj słowo to jest użyte w znaczeniu „wszystko jest zrzucone w jedną całość”, w przeciwieństwie do uporządkowanych indeksów.

Funkcja pokazuje dane „takie jakie są”, w trudnym do zrozumienia formacie. Aby to rozgryźć, pozostawimy tylko część informacji i rozszyfrujemy ją:

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

Oto co zrobiliśmy:

  • Dodano zero do numeru indeksu, aby wyglądał tak samo jak t_ctid: (numer strony, numer indeksu).
  • Odszyfrowano stan wskaźnika lp_flags. Tutaj jest to „normalne” – oznacza to, że wskaźnik faktycznie odnosi się do wersji ciągu. Później przyjrzymy się innym znaczeniom.
  • Ze wszystkich bitów informacji zidentyfikowano dotychczas tylko dwie pary. Bity xmin_committed i xmin_aborted wskazują, czy transakcja o numerze xmin została zatwierdzona (przerwana). Dwa podobne bity odnoszą się do numeru transakcji xmax.

Co widzimy? Po wstawieniu wiersza na stronie tabeli pojawi się indeks o numerze 1, wskazujący pierwszą i jedyną wersję wiersza.

W wersji string pole xmin wypełniane jest aktualnym numerem transakcji. Transakcja jest nadal aktywna, więc bity xmin_committed i xmin_aborted nie są ustawione.

Pole ctid wersji wiersza odnosi się do tego samego wiersza. Oznacza to, że nowsza wersja nie istnieje.

Pole xmax jest wypełnione fikcyjną liczbą 0, ponieważ ta wersja wiersza nie została usunięta i jest aktualna. Transakcje nie będą zwracać uwagi na tę liczbę, ponieważ ustawiony jest bit xmax_aborted.

Zróbmy jeszcze jeden krok w kierunku poprawy czytelności, dodając bity informacyjne do numerów transakcji. I utwórzmy funkcję, ponieważ żądanie będzie nam potrzebne więcej niż raz:

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

W tej formie znacznie wyraźniej widać, co dzieje się w nagłówku wersji wiersza:

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

Podobne, ale znacznie mniej szczegółowe informacje można uzyskać z samej tabeli, używając pseudokolumn xmin i xmax:

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

Fiksacja

Jeśli transakcja zakończy się pomyślnie, musisz zapamiętać jej status - pamiętaj, że została zatwierdzona. W tym celu używana jest struktura o nazwie XACT (a przed wersją 10 nazywała się CLOG (dziennik zatwierdzeń) i tę nazwę nadal można znaleźć w różnych miejscach).

XACT nie jest tabelą katalogu systemowego; są to pliki w katalogu PGDATA/pg_xact. Dla każdej transakcji mają dwa bity: zatwierdzony i przerwany - tak jak w nagłówku wersji wiersza. Informacje te zostały podzielone na kilka plików wyłącznie dla wygody; do tej kwestii powrócimy, gdy będziemy rozważać zamrożenie. Praca z tymi plikami odbywa się strona po stronie, tak jak w przypadku wszystkich innych.

Tak więc, gdy transakcja zostanie zatwierdzona w XACT, bit zatwierdzenia jest ustawiony dla tej transakcji. I to wszystko, co dzieje się podczas zatwierdzania (choć nie mówimy jeszcze o dzienniku przed nagraniem).

Gdy inna transakcja uzyska dostęp do strony tabeli, którą właśnie przeglądaliśmy, będzie musiała odpowiedzieć na kilka pytań.

  1. Czy transakcja xmin została zakończona? Jeśli nie, to utworzona wersja ciągu nie powinna być widoczna.
    Sprawdzanie to odbywa się poprzez sprawdzenie innej struktury, która znajduje się w pamięci współdzielonej instancji i nazywa się ProcArray. Zawiera listę wszystkich aktywnych procesów, a dla każdego z nich wskazany jest numer jego bieżącej (aktywnej) transakcji.
  2. Jeśli zostało to zakończone, to w jaki sposób - zatwierdzając lub anulując? W przypadku anulowania wersja wiersza również nie powinna być widoczna.
    Właśnie do tego służy XACT. Ale chociaż ostatnie strony XACT są przechowywane w buforach w pamięci RAM, sprawdzanie XACT za każdym razem jest nadal kosztowne. Dlatego po ustaleniu statusu transakcji jest on zapisywany do bitów xmin_committed i xmin_aborted wersji ciągu. Jeśli jeden z tych bitów jest ustawiony, stan transakcji xmin jest uważany za znany i następna transakcja nie będzie musiała uzyskać dostępu do XACT.

Dlaczego te bity nie są ustawiane przez samą transakcję podczas wstawiania? Kiedy następuje wstawienie, transakcja nie wie jeszcze, czy się powiedzie. A w momencie zatwierdzenia nie jest już jasne, w których wierszach które strony zostały zmienione. Takich stron może być wiele, a zapamiętywanie ich jest nieopłacalne. Ponadto niektóre strony można usunąć z bufora pamięci podręcznej na dysk; ponowne ich przeczytanie w celu zmiany bitów znacznie spowolniłoby zatwierdzenie.

Wadą oszczędności jest to, że po zmianach każda transakcja (nawet ta wykonująca prosty odczyt - SELECT) może zacząć zmieniać strony danych w buforze pamięci podręcznej.

Zatem naprawmy tę zmianę.

=> COMMIT;

Na stronie nic się nie zmieniło (ale wiemy, że status transakcji jest już zarejestrowany w XACT):

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

Teraz transakcja, która jako pierwsza uzyska dostęp do strony, będzie musiała określić status transakcji xmin i zapisać go w bitach informacyjnych:

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

Usuwanie

Po usunięciu wiersza numer bieżącej transakcji usuwania jest zapisywany w polu xmax bieżącej wersji, a bit xmax_aborted jest kasowany.

Należy pamiętać, że ustawiona wartość xmax odpowiadająca aktywnej transakcji działa jak blokada wiersza. Jeśli inna transakcja będzie chciała zaktualizować lub usunąć ten wiersz, będzie zmuszona poczekać na zakończenie transakcji xmax. Porozmawiamy więcej o blokowaniu później. Na razie zauważamy tylko, że liczba blokad rzędowych jest nieograniczona. Nie zajmują miejsca w pamięci RAM, a ich liczba nie ucierpi na wydajności systemu. Co prawda „długie” transakcje mają inne wady, ale o tym później.

Usuńmy linię.

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

Widzimy, że w polu xmax zapisany jest numer transakcji, ale bity informacyjne nie są ustawione:

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

anulowanie

Przerywanie zmian działa podobnie do zatwierdzania, tyle że w XACT dla transakcji ustawiony jest bit przerwania. Cofnięcie działania jest równie szybkie jak zatwierdzenie. Chociaż polecenie nazywa się ROLLBACK, zmiany nie są cofane: wszystko, co transakcja zdążyła zmienić na stronach danych, pozostaje niezmienione.

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

Po uzyskaniu dostępu do strony status zostanie sprawdzony, a bit podpowiedzi xmax_aborted zostanie ustawiony na wersję wiersza. Sama liczba xmax pozostaje na stronie, ale nikt na nią nie spojrzy.

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

Aktualizuj

Aktualizacja działa tak, jakby najpierw usunęła bieżącą wersję wiersza, a następnie dodała nową.

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

Zapytanie generuje jedną linię (nowa wersja):

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

Ale na stronie widzimy obie wersje:

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

Usunięta wersja oznaczona jest aktualnym numerem transakcji w polu xmax. Co więcej, ta wartość jest nadpisana na starą, ponieważ poprzednia transakcja została anulowana. A bit xmax_aborted zostaje wyczyszczony, ponieważ status bieżącej transakcji nie jest jeszcze znany.

Pierwsza wersja linii odnosi się teraz do drugiej (pole t_ctid) jako do nowszej.

Drugi indeks pojawia się na stronie indeksu, a drugi wiersz odnosi się do drugiej wersji na stronie tabeli.

Podobnie jak przy usuwaniu, wartość xmax w pierwszej wersji wiersza wskazuje, że wiersz jest zablokowany.

Cóż, sfinalizujmy transakcję.

=> COMMIT;

wskaźniki

Do tej pory mówiliśmy tylko o stronach tabel. Co dzieje się wewnątrz indeksów?

Informacje zawarte na stronach indeksu różnią się znacznie w zależności od konkretnego typu indeksu. Nawet jeden typ indeksu ma różne typy stron. Na przykład drzewo B ma stronę metadanych i „zwykłe” strony.

Jednak strona zwykle zawiera tablicę wskaźników do wierszy i samych wierszy (podobnie jak strona tabeli). Dodatkowo na końcu strony znajduje się miejsce na dane specjalne.

Wiersze w indeksach mogą mieć również bardzo różną strukturę w zależności od typu indeksu. Na przykład w przypadku drzewa B wiersze powiązane ze stronami liści zawierają wartość klucza indeksującego i odwołanie (ctid) do odpowiedniego wiersza tabeli. Generalnie indeks można skonstruować zupełnie inaczej.

Najważniejszą kwestią jest to, że w indeksach dowolnego typu nie ma wersji wierszy. Cóż, albo możemy założyć, że każda linia jest reprezentowana przez dokładnie jedną wersję. Innymi słowy, w nagłówku wiersza indeksu nie ma pól xmin i xmax. Możemy założyć, że linki z indeksu prowadzą do wszystkich wersji wierszy tabeli - więc tylko patrząc na tabelę, możesz dowiedzieć się, którą wersję zobaczy transakcja. (Jak zawsze nie jest to cała prawda. W niektórych przypadkach mapa widoczności może zoptymalizować proces, ale przyjrzymy się temu bardziej szczegółowo później.)

Jednocześnie na stronie indeksowej znajdziemy odnośniki do obu wersji, zarówno tej aktualnej, jak i starej:

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

Transakcje wirtualne

W praktyce PostgreSQL stosuje optymalizacje, które pozwalają mu „zapisywać” numery transakcji.

Jeśli transakcja tylko odczytuje dane, nie ma to wpływu na widoczność wersji wierszy. Dlatego proces obsługi najpierw wystawia wirtualny identyfikator transakcji. Numer składa się z identyfikatora procesu i numeru kolejnego.

Nadanie tego numeru nie wymaga synchronizacji pomiędzy wszystkimi procesami i dlatego jest bardzo szybkie. Z innym powodem używania numerów wirtualnych zapoznamy się, gdy będziemy mówić o zamrażaniu.

Wirtualne liczby nie są w żaden sposób uwzględniane w migawkach danych.

W różnych momentach w systemie mogą znajdować się wirtualne transakcje z numerami, które zostały już wykorzystane, i jest to normalne. Ale takiej liczby nie można zapisać na stronach danych, ponieważ przy następnym otwarciu strony może ona stracić wszelkie znaczenie.

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

Jeżeli transakcja zaczyna zmieniać dane, nadawany jest prawdziwy, unikalny numer transakcji.

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

=> COMMIT;

Zagnieżdżone transakcje

Oszczędzaj punkty

Zdefiniowane w SQL oszczędzaj punkty (savepoint), które umożliwiają anulowanie części transakcji bez jej całkowitego przerywania. Nie pasuje to jednak do powyższego diagramu, ponieważ transakcja ma ten sam status dla wszystkich zmian i fizycznie żadne dane nie są wycofywane.

Aby zaimplementować tę funkcjonalność, transakcja z punktem zapisu jest dzielona na kilka oddzielnych transakcje zagnieżdżone (podtransakcja), której statusem można zarządzać osobno.

Transakcje zagnieżdżone mają swój własny numer (wyższy niż numer transakcji głównej). Status transakcji zagnieżdżonych jest rejestrowany w XACT w zwykły sposób, ale ostateczny status zależy od statusu transakcji głównej: jeśli zostanie ona anulowana, wszystkie transakcje zagnieżdżone również zostaną anulowane.

Informacje o zagnieżdżeniu transakcji przechowywane są w plikach w katalogu PGDATA/pg_subtrans. Dostęp do plików odbywa się poprzez bufory we współdzielonej pamięci instancji, zorganizowane w taki sam sposób jak bufory XACT.

Nie myl transakcji zagnieżdżonych z transakcjami autonomicznymi. Transakcje autonomiczne nie są od siebie w żaden sposób zależne, ale transakcje zagnieżdżone tak. W zwykłym PostgreSQL nie ma autonomicznych transakcji i, być może, najlepiej: są one potrzebne bardzo, bardzo rzadko, a ich obecność w innych DBMS-ach prowokuje nadużycia, z powodu których wszyscy wtedy cierpią.

Wyczyśćmy tabelę, rozpocznijmy transakcję i wstawmy wiersz:

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

Teraz umieśćmy punkt zapisu i wstawmy kolejną linię.

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

Należy zauważyć, że funkcja txid_current() zwraca numer transakcji głównej, a nie numer transakcji zagnieżdżonej.

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

Wróćmy do punktu zapisu i wstawmy trzecią linię.

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

Na stronie nadal widzimy wiersz dodany przez anulowaną zagnieżdżoną transakcję.

Naprawiamy zmiany.

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

Teraz wyraźnie widać, że każda zagnieżdżona transakcja ma swój własny status.

Należy pamiętać, że transakcji zagnieżdżonych nie można jawnie używać w języku SQL, co oznacza, że ​​nie można rozpocząć nowej transakcji bez zakończenia bieżącej. Mechanizm ten jest aktywowany domyślnie podczas korzystania z punktów zapisu, a także podczas obsługi wyjątków PL/pgSQL i w wielu innych, bardziej egzotycznych przypadkach.

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

Błędy i atomowość operacji

Co się stanie, jeśli podczas wykonywania operacji wystąpi błąd? Na przykład tak:

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

Wystąpił błąd. Teraz transakcja jest uważana za przerwaną i nie można w niej wykonywać żadnych operacji:

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

Nawet jeśli spróbujesz zatwierdzić zmiany, PostgreSQL zgłosi przerwanie:

=> COMMIT;
ROLLBACK

Dlaczego transakcja nie może być kontynuowana po niepowodzeniu? Faktem jest, że błąd mógłby powstać w taki sposób, że uzyskalibyśmy dostęp do części zmian – naruszona zostałaby nawet nie atomowość transakcji, a operator. Tak jak w naszym przykładzie, gdzie operatorowi udało się zaktualizować jedną linię przed wystąpieniem błędu:

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

Trzeba powiedzieć, że psql ma tryb, który nadal pozwala na kontynuację transakcji po awarii, tak jakby działania błędnego operatora zostały wycofane.

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

Nietrudno zgadnąć, że w tym trybie psql faktycznie umieszcza ukryty punkt zapisu przed każdym poleceniem, a w przypadku niepowodzenia inicjuje powrót do niego. Ten tryb nie jest domyślnie używany, ponieważ ustawianie punktów zapisu (nawet bez cofania się do nich) wiąże się ze znacznym obciążeniem.

Kontynuowane.

Źródło: www.habr.com

Dodaj komentarz