MVCC-3. Verze řádků

Takže jsme se podívali na otázky spojené s izolací, a udělali jsme odbočku k organizaci dat na nízké úrovni. A nakonec jsme se dostali k tomu nejzajímavějšímu — k verzím řádků.

Nadpis

Jak jsme již zmínili, každý řádek může zároveň existovat v databázi v několika verzích. Jednu verzi od druhé je třeba nějak odlišit. K tomuto účelu má každá verze dvě značky, které určují „čas“ platnosti této verze (xmin a xmax). V úvozovkách — protože se nepoužívá čas jako takový, ale speciální rostoucí čítač. A tento čítač je číslo transakce.

(Jak obvykle, ve skutečnosti je to složitější: čísla transakcí nemohou neustále růst kvůli omezené velikosti čítače. Ale tyto detaily probereme podrobně, až se dostaneme k zamrzání.)

Když je řádek vytvořen, hodnota xmin je nastavena na číslo transakce, která provedla příkaz INSERT, a xmax není vyplněn.

Když je řádek smazán, hodnota xmax aktuální verze je označena číslem transakce, která provedla DELETE.

Když je řádek změněn příkazem UPDATE, ve skutečnosti se provádějí dvě operace: DELETE a INSERT. V aktuální verzi řádku se nastaví xmax, rovné číslu transakce, která provedla UPDATE. Poté se vytvoří nová verze téhož řádku; hodnota xmin u ní odpovídá hodnotě xmax předchozí verze.

Pole xmin a xmax jsou součástí hlavičky verze řádku. Kromě těchto polí hlavička obsahuje i další, například:

  • infomask — řada bitů, které určují vlastnosti dané verze. Je jich poměrně mnoho; základní z nich si postupně ukážeme.
  • ctid — odkaz na další, novější, verzi téhož řádku. U nejnovější verze řádku ctid odkazuje na tuto verzi. Číslo má formát (x,y), kde x je číslo stránky, y je pořadové číslo ukazatele v poli.
  • bitová mapa neurčitých hodnot — označuje ty sloupce této verze, které obsahují neurčitou hodnotu (NULL). NULL není jednou z běžných hodnot datových typů, proto musí být příznak uchováván samostatně.

V důsledku toho je hlavička poměrně velká — minimálně 23 bajtů na každou verzi řádku, a obvykle více kvůli bitové mapě NULL. Pokud je tabulka „úzká“ (to znamená obsahuje málo sloupců), režie mohou zabírat více než užitečné informace.

Vložení

Podíváme se podrobněji, jak se provádějí operace se zpětnými řetězci na nízké úrovni, a začneme s vkládáním.

Pro experimenty vytvoříme novou tabulku se dvěma sloupci a index na jeden z nich:

=> VYTVOŘIT TABULKU t(
  id serial,
  s text
);
=> VYTVOŘIT INDEX ON t(s);

Vložíme jeden řádek, předtím zahájíme transakci.

=> ZAČÍT;
=> VLOŽIT DO t(s) HODNOTY ('FOO');

Tady je číslo naší aktuální transakce:

=> VYBRAT txid_current();
 txid_current 
--------------
         3664
(1 řádek)

Podíváme se na obsah stránky. Funkce heap_page_items z rozšíření pageinspect umožňuje získat informace o ukazatelích a verzích řádků:

=> VYBRAT * Z heap_page_items(get_raw_page('t',0)) gx
-[ ZÁZNAM 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šimněme si, že slovo heap (hromada) v PostgreSQL označuje tabulky. To je další podivné užití termínu — hromada je známá datová struktura, která nemá s tabulkou nic společného. Zde se toto slovo používá ve smyslu "vše je hromadně naskládáno", na rozdíl od uspořádaných indexů.

Funkce zobrazuje data „tak, jak jsou“, v formátu, který je obtížný na vnímání. Abychom se vyznali, ponecháme pouze část informací a vysvětlíme ji:

=> VYBRAT '(0,'||lp||')' JAKO ctid,
       PŘÍPAD lp_flags
         KDYŽ 0 POTOM 'nepoužité'
         KDYŽ 1 POTOM 'normální'
         KDYŽ 2 POTOM 'přesměrování na '||lp_off
         KDYŽ 3 POTOM 'neplatné'
       KONEC JAKO stav,
       t_xmin jako xmin,
       t_xmax jako xmax,
       (t_infomask & 256) > 0  JAKO xmin_commited,
       (t_infomask & 512) > 0  JAKO xmin_aborted,
       (t_infomask & 1024) > 0 JAKO xmax_commited,
       (t_infomask & 2048) > 0 JAKO xmax_aborted,
       t_ctid
Z heap_page_items(get_raw_page('t',0)) gx
-[ ZÁZNAM 1 ]-+-------
ctid          | (0,1)
stav          | normální
xmin          | 3664
xmax          | 0
xmin_commited | f
xmin_aborted  | f
xmax_commited | f
xmax_aborted  | t
t_ctid        | (0,1)

Toto je to, co jsme udělali:

  • Přidali jsme k číslu ukazatele nulu, abychom ho přivedli do stejné podoby jako t_ctid: (číslo stránky, číslo ukazatele).
  • Vysvětlili jsme stav ukazatele lp_flags. Zde je „normální“ — to znamená, že ukazatel skutečně odkazuje na verzi řádku. Další hodnoty probereme později.
  • Z všech informačních bitů jsme zatím vybrali pouze dvě dvojice. Bity xmin_committed a xmin_aborted ukazují, zda byla (zrušena) transakce s číslem xmin. Dva obdobné bity se vztahují k transakci s číslem xmax.

Co tedy vidíme? Při vložení řádku na tabulkovou stránku se zobrazí ukazatel s číslem 1, který odkazuje na první a jedinou verzi řádku.

V verzi řádku je pole xmin vyplněno číslem aktuální transakce. Transakce je ještě aktivní, proto nejsou nastavena obě bity xmin_committed a xmin_aborted.

Pole ctid verze řádku odkazuje na tento stejný řádek. To znamená, že novější verze neexistuje.

Pole xmax je vyplněno fiktivním číslem 0, protože tato verze řádku nebyla odstraněna a je aktuální. Transakce si na toto číslo nebudou brát ohled, protože je nastaven bit xmax_aborted.

Uděláme další krok ke zlepšení čitelnosti a přidáme informační bity k číslům transakcí. A vytvoříme funkci, protože tento dotaz budeme potřebovat znovu:

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

V této podobě je daleko jasnější, co se děje v hlavičce verze řádku:

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

Podobné, ale mnohem méně podrobné informace lze získat také z tabulky samotné pomocí pseudokolon xlm a xmax:

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

Potvrzení

Při úspěšném dokončení transakce je třeba si pamatovat její stav - označit ji jako potvrzenou. K tomu se používá struktura zvaná XACT (a před verzí 10 se nazývala CLOG (commit log) a tento název se ještě může objevovat na různých místech).

XACT - není tabulka systémového katalogu; jsou to soubory v adresáři PGDATA/pg_xact. Každé transakci jsou přiděleny dva bity: committed a aborted - přesně tak jako v hlavičce verze řádku. Tato informace je rozdělena na několik souborů pouze pro pohodlí, k této otázce se ještě vrátíme, až budeme zkoumat zmrazování. Práce s těmito soubory probíhá po stránkách, stejně jako se všemi ostatními.

Při potvrzení transakce v XACT se pro tuto transakci nastaví bit committed. A to je vše, co se při potvrzení uděje (i když zatím nemluvíme o protokolu předzáznamu).

Když nějaká jiná transakce přistoupí k tabulkové stránce, na kterou jsme se právě dívali, musí odpovědět na několik otázek.

  1. Byla transakce xmin dokončena? Pokud ne, verze řádku by neměla být viditelná.
    Tato kontrola se provádí prohlížením další struktury, která se nachází ve sdílené paměti instance a nazývá se ProcArray. Obsahuje seznam všech aktivních procesů a pro každý je uvedeno číslo jeho aktuální (aktivní) transakce.
  2. Pokud byla dokončena, jak — potvrzením nebo zrušením? Pokud zrušením, verze řádku by také neměla být viditelná.
    K tomu je právě potřebný XACT. I když poslední stránky XACT jsou udržovány v paměťových bufferech, každé ověřování XACT je náročné. Proto se jednou zjištěný stav transakce zapisuje do bitů xmin_committed a xmin_aborted verze řádku. Pokud je jeden z těchto bitů nastaven, považuje se stav transakce xmin za známý a další transakce již nemusí kontrolovat XACT.

Proč tyto bity nenastavuje samotná transakce, která provádí vkládání? Při vkládání transakce ještě neví, zda bude úspěšná. A v okamžiku potvrzení už není jasné, které konkrétní řádky byly na jakých stránkách změněny. Takových stránek může být mnoho a jejich zapamatování by nebylo ekonomické. Navíc může být část stránek vyřazena z buffer cache na disk; znovu je číst pro změnu bitů by znamenalo podstatně zpomalit potvrzení.

Druhá strana úspor spočívá v tom, že po změnách může jakákoli transakce (dokonce provádějící jednoduché čtení — SELECT) začít měnit stránky dat v buffer cache.

Pojďme tedy potvrdit změnu.

=> COMMIT;

Na stránce se nic nezměnilo (ale víme, že stav transakce je již zapsán v XACT):

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

Nyní transakce, která jako první přistoupila ke stránce, musí určit stav transakce xmin a zapíše jej do informačních bitů:

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

Odstranit

Při odstranění řádku v poli xmax aktuální verze se zapíše číslo aktuální transakce, která provádí odstranění, a bit xmax_aborted se resetuje.

Všimněte si, že nastavená hodnota xmax, odpovídající aktivní transakci, funguje jako zámek řádku. Pokud se jiná transakce chystá aktualizovat nebo odstranit tento řádek, bude muset počkat na dokončení transakce xmax. O zámcích budeme hovořit později. Zatím si pouze poznamenejme, že počet zámků řádků není ničím omezen. Nepřijímají místo v operační paměti a výkon systému netrpí jejich množstvím. Pravda, že „dlouhé“ transakce mají jiné nevýhody, ale o tom také později.

Odstraníme řádek.

=> ZAČÍT;
=> ODSTRANIT Z t;
=> VYBRAT txid_current();
 txid_current 
--------------
         3665
(1 řádek)

Vidíme, že číslo transakce se zapsalo do pole xmax, ale informační bity nejsou nastaveny:

=> SELECT * FROM heap_page('t',0);
 ctid  | stav   |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normální | 3664 (c) | 3665 | (0,1)
(1 řádek)

Zpět

Zrušení změn funguje podobně jako potvrzení, jen v XACT pro transakci je nastaven bit aborted. Zrušení probíhá stejně rychle jako potvrzení. Ačkoliv se příkaz jmenuje ROLLBACK, k návratu změn nedochází: vše, co transakce stihla změnit na datových stránkách, zůstává beze změn.

=> ROLLBACK;
=> VYBRAT * Z heap_page('t',0);
 ctid  | stav   |   xmin   | xmax | t_ctid 
-------+--------+----------+------+--------
 (0,1) | normální | 3664 (c) | 3665 | (0,1)
(1 řádek)

Při přístupu na stránku bude zkontrolován stav a do verze řádku bude nastaven bit nápovědy xmax_aborted. Číslo xmax při tom zůstává na stránce, ale nikdo se na něj již dívat nebude.

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

=> SELECT * FROM heap_page('t',0);
 ctid  | stav   |   xmin   |   xmax   | t_ctid 
-------+--------+----------+----------+--------
 (0,1) | normální | 3664 (c) | 3665 (a) | (0,1)
(1 řádek)

Aktualizace

Aktualizace funguje tak, jako by nejprve došlo k odstranění aktuální verze řádku a poté k vložení nové.

=> ZAČÍT;
=> AKTUALIZOVAT t SET s = 'BAR';
=> VYBRAT txid_current();
 txid_current 
--------------
         3666
(1 řádek)

Dotaz vrací jeden řádek (nová verze):

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

Ale na stránce vidíme obě verze:

=> SELECT * FROM heap_page('t',0);
 ctid  | stav   |   xmin   | xmax  | t_ctid 
-------+--------+----------+-------+--------
 (0,1) | normální | 3664 (c) | 3666  | (0,2)
 (0,2) | normální | 3666     | 0 (a) | (0,2)
(2 řádky)

Odstraněná verze je označena číslem aktuální transakce v poli xmax. Toto hodnotu bylo zapsáno na místo starého, protože předchozí transakce byla zrušena. A bit xmax_aborted byl resetován, jelikož stav aktuální transakce je dosud neznámý.

První verze řádku nyní odkazuje na druhou (pole t_ctid), jako na novější.

Na indexové stránce se objeví druhý ukazatel a druhý řádek, který odkazuje na druhou verzi v tabulkové stránce.

Stejně jako při mazání, hodnota xmax v první verzi řádku slouží jako znak toho, že řádek je zablokován.

A tak uzavřeme transakci.

=> COMMIT;

Indexy

Dosud jsme hovořili pouze o tabulkových stránkách. A co se děje uvnitř indexů?

Informace na indexových stránkách do značné míry závisí na konkrétním typu indexu. A dokonce i u jednoho typu indexu mohou existovat různé typy stránek. Například B-strom má stránku s metadaty a "běžné" stránky.

Nicméně obvykle na stránce existuje pole ukazatelů na řádky a samotné řádky (stejně jako v tabulkové stránce). Navíc je na konci stránky vyhrazen prostor pro speciální údaje.

Řádky v indexech mohou mít také velmi různou strukturu v závislosti na typu indexu. Například pro B-strom řádky, které se vztahují k listovým stránkám, obsahují hodnotu klíče indexování a odkaz (ctid) na odpovídající řádek tabulky. V obecnosti může být index strukturován zcela jinak.

Nejdůležitější na tom je, že v indexech jakéhokoli typu není verze řádků. Nebo lze říct, že každý řádek je představován právě jednou verzí. Jinými slovy, v hlavičce indexového řádku neexistují pole xmin a xmax. Lze považovat, že odkazy z indexu vedou na všechny tabulkové verze řádků — takže zjistit, kterou verzi transakce uvidí, lze pouze nahlédnutím do tabulky. (Jak je obvyklé, to není celá pravda. V některých případech mapa viditelnosti umožňuje optimalizovat proces, ale podrobněji to probereme později.)

Přitom na indexové stránce zjistíme ukazatele na obě verze, jak na aktuální, tak na starou:

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

Virtuální transakce

V praxi PostgreSQL používá optimalizaci, která umožňuje "šetřit" čísla transakcí.

Pokud transakce pouze čte data, pak nijak neovlivňuje viditelnost verzí řádků. Proto na začátku obslužný proces vydá transakcím virtuální číslo (virtual xid). Číslo se skládá z identifikátoru procesu a sekvenčního čísla.

Vydání tohoto čísla nevyžaduje synchronizaci mezi všemi procesy, a proto je velmi rychlé. Další důvod, proč se používají virtuální čísla, probereme, až se budeme zabývat mražením.

Virtuální čísla se v datech nijak nezapočítávají.

V různých časových okamžicích mohou v systému existovat virtuální transakce s čísly, která již byla použita, a to je v pořádku. Ale takové číslo nelze zapisovat na datové stránky, protože při příštím přístupu na stránku může ztratit veškerý smysl.

=> BEGIN;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                         
(1 řádek)

Pokud však transakce začne měnit data, je jí přidělen skutečný, unikátní číslo transakce.

=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
 txid_current_if_assigned 
--------------------------
                     3667
(1 řádek)

=> COMMIT;

Vnořené transakce

Bodové uložení

V SQL jsou definovány bodové uložení (savepoint), které umožňují vrátit část transakčních operací, aniž by se přerušila celá transakce. Ale to se nehodí do předchozí schématu, protože status transakce je jeden pro všechny její změny, a fyzicky se žádná data nevracejí.

Aby bylo možné takovou funkčnost realizovat, je transakce s bodem uložením rozdělena na několik samostatných vnořených transakcí (subtransaction), jejichž stav lze spravovat samostatně.

Vnořené transakce mají své vlastní číslo (vyšší než číslo hlavní transakce). Stav vnořených transakcí se zapisuje běžným způsobem do XACT, ale konečný stav závisí na stavu hlavní transakce: pokud je zrušena, zruší se také všechny vnořené transakce.

Informace o vnořenosti transakcí jsou uchovávány v souborech v adresáři PGDATA/pg_subtrans. Přístup k souborům probíhá prostřednictvím sdílených paměťových bufferů instance, organizovaných stejně jako buffery XACT.

Nepleťte si vnořené transakce a autonomní transakce. Autonomní transakce na sobě nezávisí, zatímco vnořené závisí. Autonomní transakce nejsou v běžném PostgreSQL, a pravděpodobně je to lépe: jsou potřeba velmi zřídka a jejich přítomnost v jiných DBMS vede k zneužití, z něhož pak všichni trpí.

Vyčistíme tabulku, zahájíme transakci a vložíme řádek:

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

Nyní nastavíme bod obnovení a vložíme další řádek.

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

Všimněte si, že funkce txid_current() vrací číslo hlavní, nikoli vnořené, transakce.

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

Vrátíme se k bodu obnovení a vložíme třetí řádek.

=> 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 stránce stále vidíme řádek přidaný zrušenou vnořenou transakcí.

Zafixujeme změny.

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

Nyní je jasně vidět, že každá vnořená transakce má svůj vlastní stav.

Všimněte si, že vnořené transakce nelze používat v SQL explicitně, tedy nelze začít novou transakci, aniž bychom dokončili aktuální. Tento mechanismus se nevědomky aktivuje při používání bodů obnovení a také při zpracování výjimek PL/pgSQL a v řadě dalších, exotických případů.

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

Chyby a atomarita operací

Co se stane, pokud během provádění operace dojde k chybě? Například takto:

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

Došlo k chybě. Nyní se transakce považuje za přerušenou a žádná operace v ní není povolena:

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

A i když se pokusíte potvrdit změny, PostgreSQL oznámí zrušení:

=> COMMIT;
ROLLBACK

Proč nelze pokračovat v provádění transakce po chybě? Jde o to, že chyba mohla vzniknout tak, že bychom získali přístup k části změn — byla by porušena atomarita nejen transakce, ale i operátora. Jako v našem příkladu, kde operátor před chybou stihl aktualizovat jeden řádek:

=> SELECT * FROM heap_page('t',0);
 ctid  | stav    |   xmin   | xmax  | t_ctid 
-------+---------+----------+-------+--------
 (0,1) | normální | 3669 (c) | 3672  | (0,4)
 (0,2) | normální | 3670 (a) | 0 (a) | (0,2)
 (0,3) | normální | 3671 (c) | 0 (a) | (0,3)
 (0,4) | normální | 3672     | 0 (a) | (0,4)
(4 řádky)

Je třeba říci, že v psql existuje režim, který přeci jen umožňuje pokračovat v práci s transakcí po chybě tak, jako by akce chybného operátora byly vráceny.

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

Není těžké uhodnout, že v takovém režimu psql v podstatě staví před každý příkaz neimplicitní bod obnovení, a v případě chyby iniciuje návrat k němu. Tento režim se nepoužívá jako výchozí, protože nastavování bodů obnovení (i bez návratu k nim) je spojeno s významnými režijními náklady.

Pokračování.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster