Quindi abbiamo esaminato i temi relativi a , e abbiamo fatto una digressione su . E infine siamo arrivati alla parte più interessante: le versioni delle righe.
Titolo
Come già detto, ogni riga può esistere contemporaneamente nel database in diverse versioni. È necessario distinguere un versione dall'altra. A tal fine, ogni versione ha due marcatori che definiscono il "tempo" di validità di tale versione (xmin e xmax). Tra virgolette, perché non si utilizza il tempo in senso stretto, ma un contatore speciale che cresce. E questo contatore è il numero della transazione.
(Come spesso accade, in realtà è tutto più complesso: il numero delle transazioni non può crescere continuamente a causa della limitata ampiezza del contatore. Ma esamineremo questi dettagli in modo approfondito quando arriveremo al congelamento.)
Quando viene creata una riga, il valore xmin è impostato sul numero della transazione che ha eseguito il comando INSERT, mentre xmax non viene compilato.
Quando una riga viene eliminata, il valore xmax della versione attuale è contrassegnato con il numero della transazione che ha eseguito il DELETE.
Quando una riga viene modificata tramite il comando UPDATE, in realtà vengono eseguite due operazioni: DELETE e INSERT. Nella versione attuale della riga viene impostato xmax, pari al numero della transazione che ha eseguito l'UPDATE. Successivamente, viene creata una nuova versione della stessa riga; il valore xmin di essa coincide con il valore xmax della versione precedente.
I campi xmin e xmax fanno parte dell'intestazione della versione della riga. Oltre a questi campi, l'intestazione contiene anche altri, ad esempio:
- infomask — un insieme di bit che definisce le proprietà di questa versione. Ce ne sono abbastanza; alcuni dei principali verranno esaminati gradualmente.
- ctid — un riferimento alla versione successiva, più recente, della stessa riga. Nella versione più recente e attuale della riga, ctid si riferisce a questa versione stessa. Il numero ha la forma (x,y), dove x è il numero della pagina, y è il numero sequenziale del puntatore nell'array.
- una mappa dei valori non definiti — indica quelle colonne di questa versione che contengono un valore indefinito (NULL). NULL non è uno dei normali valori dei tipi di dati, quindi la caratteristica deve essere memorizzata separatamente.
Di conseguenza, l'intestazione risulta piuttosto grande: almeno 23 byte per ogni versione della riga, e di solito di più a causa della mappa dei NULL. Se la tabella è "stretta" (cioè contiene poche colonne), i costi aggiuntivi possono superare le informazioni utili.
Inserimento
Esaminiamo più dettagliatamente come vengono eseguite le operazioni sulle stringhe a un livello basso e iniziamo con l'inserimento.
Per esperimenti, creeremo una nuova tabella con due colonne e un indice su una di esse:
=> CREATE TABLE t(
id serial,
s text
);
=> CREATE INDEX ON t(s);
Inseriamo una riga, iniziando preliminarmente la transazione.
=> BEGIN;
=> INSERT INTO t(s) VALUES ('FOO');
Ecco il numero della nostra transazione corrente:
=> SELECT txid_current();
txid_current
--------------
3664
(1 row)
Diamo un'occhiata al contenuto della pagina. La funzione heap_page_items dell'estensione pageinspect consente di ottenere informazioni sui puntatori e sulle versioni delle righe:
=> 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
Notiamo che in PostgreSQL il termine heap (coda) indica tabelle. Questo è un altro strano uso del termine: la coda è una struttura di dati conosciuta La funzione mostra i dati 'così come sono', in un formato difficile da interpretare. Per chiarire, lasceremo solo una parte delle informazioni e la decifreremo:
=> SELECT '(0,'||lp||')' AS ctid, CASE lp_flags WHEN 0 THEN 'non utilizzato' WHEN 1 THEN 'normale' WHEN 2 THEN 'reindirizza a '||lp_off WHEN 3 THEN 'morto' END AS stato, 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)
stato | normale
xmin | 3664
xmax | 0
xmin_commited | f
xmin_aborted | f
xmax_commited | f
xmax_aborted | t
t_ctid | (0,1)
Ecco cosa abbiamo fatto:
Abbiamo aggiunto uno zero al numero del puntatore per portarlo allo stesso formato di t_ctid: (numero pagina, numero puntatore).
- Abbiamo decifrato lo stato del puntatore lp_flags. Qui è 'normale' — significa che il puntatore riferisce effettivamente a una versione della riga. Altri valori saranno esaminati in seguito.
- Tra tutti i bit informativi, per ora abbiamo isolato solo due coppie. I bit xmin_committed e xmin_aborted mostrano se la transazione con numero xmin è stata registrata (o annullata). Due bit simili si riferiscono alla transazione con numero xmax.
- Due bit simili si riferiscono alla transazione con numero xmax.
Cosa vediamo? Quando si inserisce una riga in una pagina tabellare, appare un puntatore con il numero 1, che fa riferimento alla prima e unica versione della riga.
Nella versione della riga, il campo xmin è compilato con il numero della transazione corrente. La transazione è ancora attiva, quindi entrambi i bit xmin_committed e xmin_aborted non sono impostati.
Il campo ctid della versione della riga si riferisce a questa stessa riga. Questo significa che non esiste una versione più recente.
Il campo xmax è compilato con un numero fittizio 0, poiché questa versione della riga non è stata eliminata ed è attuale. Le transazioni non faranno attenzione a questo numero, poiché il bit xmax_aborted è impostato.
Facciamo un ulteriore passo avanti per migliorare la leggibilità, aggiungendo bit informativi ai numeri delle transazioni. Creiamo una funzione, poiché ci servirà più volte:
=> 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;
In questo modo è molto più chiaro cosa succede nell'intestazione della versione della riga:
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)
È possibile ottenere informazioni simili, ma notevolmente meno dettagliate, direttamente dalla tabella stessa, utilizzando i pseudocolon XMIN e XMAX:
=> SELECT xmin, xmax, * FROM t;
xmin | xmax | id | s
------+------+----+-----
3664 | 0 | 1 | FOO
(1 row)
Registrazione
Al termine con successo della transazione, è necessario memorizzare il suo stato — segnalare che è stata registrata. Per questo si utilizza una struttura chiamata XACT (e fino alla versione 10 era conosciuta come CLOG (commit log) e questo nome potrebbe ancora apparire in vari contesti).
XACT non è una tabella del catalogo di sistema; è un file nella directory PGDATA/pg_xact. In esso, per ogni transazione, sono riservati due bit: committed e aborted — proprio come nell'intestazione della versione della riga. Queste informazioni sono suddivise in più file esclusivamente per comodità; torneremo su questo tema quando tratteremo il congelamento. E il lavoro con questi file avviene pagina per pagina, così come con tutti gli altri.
Quindi, quando una transazione viene registrata in XACT, viene impostato il bit committed per quella transazione. E questo è tutto ciò che avviene durante la registrazione (anche se per ora non parliamo del log di pre-scrittura).
Quando un'altra transazione accede alla pagina della tabella che abbiamo appena esaminato, deve rispondere a diverse domande.
- La transazione xmin è completata? Se no, la versione della riga creata non deve essere visibile.
Questa verifica viene eseguita esaminando un'altra struttura, che si trova nella memoria condivisa dell'istanza e si chiama ProcArray. Qui è presente un elenco di tutti i processi attivi, e per ciascuno è indicato il numero della sua transazione attuale (attiva). - Se è completata, come — tramite registrazione o annullamento? Se tramite annullamento, la versione della riga non deve essere visibile.
Ecco a cosa serve esattamente XACT. Tuttavia, sebbene le ultime pagine di XACT siano mantenute nei buffer in memoria, controllare XACT ogni volta è oneroso. Pertanto, lo stato della transazione una volta determinato viene registrato nei bit xmin_committed e xmin_aborted della versione della riga. Se uno di questi bit è impostato, lo stato della transazione xmin è considerato noto e la transazione successiva non dovrà più fare riferimento a XACT.
Perché questi bit non vengono impostati dalla transazione che esegue l'inserimento? Quando avviene l'inserimento, la transazione non sa ancora se avrà successo. E nel momento della registrazione non è chiaro quali righe in quali pagine sono state modificate. Potrebbe esserci un gran numero di queste pagine, e sarebbe svantaggioso memorizzarle. Inoltre, alcune pagine potrebbero essere espulse dalla cache dei buffer su disco; rileggere queste pagine per modificare i bit significherebbe rallentare notevolmente la registrazione.
La controparte del risparmio è che, dopo le modifiche, qualsiasi transazione (anche una semplice lettura — SELECT) può cominciare a modificare le pagine dei dati nella cache dei buffer.
Quindi, registriamo la modifica.
=> COMMIT;
Nella pagina non è cambiato nulla (ma sappiamo che lo stato della transazione è già stato registrato in XACT):
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+------+-------+--------
(0,1) | normal | 3664 | 0 (a) | (0,1)
(1 row)
Ora, la transazione che per prima accede alla pagina dovrà determinare lo stato della transazione xmin e lo registrerà nei bit informativi:
=> SELECT * FROM t;
id | s
----+-----
1 | FOO
(1 riga)
=> SELECT * FROM heap_page('t',0);
ctid | stato | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normale | 3664 (c) | 0 (a) | (0,1)
(1 riga)
Elimina
Quando si elimina una riga, il campo xmax della versione corrente registra il numero della transazione di eliminazione attuale, mentre il bit xmax_aborted viene azzerato.
Notiamo che il valore xmax impostato, corrispondente alla transazione attiva, funge da blocco per la riga. Se un'altra transazione sta per aggiornare o eliminare questa riga, sarà costretta ad attendere il completamento della transazione xmax. Parleremo in dettaglio dei blocchi più avanti. Per ora, notiamo solo che il numero di blocchi sulle righe non è limitato. Non occupano spazio nella memoria operativa e le prestazioni del sistema non risentono del loro numero. Tuttavia, le "lunghe" transazioni hanno altri svantaggi, ma ne parleremo più tardi.
Eliminiamo la riga.
=> BEGIN;
=> DELETE FROM t;
=> SELECT txid_current();
txid_current
--------------
3665
(1 row)
Vediamo che il numero della transazione è stato registrato nel campo xmax, ma i bit informativi non sono impostati:
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+------+--------
(0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)
Annulla
L'annullamento delle modifiche funziona in modo analogo al commit, solo che nel XACT per la transazione viene impostato il bit aborted. L'annullamento avviene altrettanto rapidamente quanto il commit. Anche se il comando si chiama ROLLBACK, non avviene alcun rollback delle modifiche: tutto ciò che la transazione è riuscita a modificare nelle pagine dei dati rimane invariato.
=> ROLLBACK;
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+------+--------
(0,1) | normal | 3664 (c) | 3665 | (0,1)
(1 row)
Quando si accede alla pagina, verrà verificato lo stato e nel campo della versione della riga verrà impostato il bit di suggerimento xmax_aborted. Il numero xmax rimane nella pagina, ma nessuno lo guarderà più.
=> SELECT * FROM t;
id | s
----+-----
1 | FOO
(1 riga)
=> SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax | t_ctid
-------+--------+----------+----------+--------
(0,1) | normal | 3664 (c) | 3665 (a) | (0,1)
(1 row)
Aggiornamento
L'aggiornamento funziona come se inizialmente fosse stata eseguita l'eliminazione della versione corrente della riga, seguita dall'inserimento di una nuova.
=> BEGIN;
=> UPDATE t SET s = 'BAR';
=> SELECT txid_current();
txid_current
--------------
3666
(1 row)
La query restituisce una riga (nuova versione):
=> SELECT * FROM t;
id | s
----+-----
1 | BAR
(1 row)
Ma nella pagina vediamo entrambe le versioni:
=> 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)
La versione eliminata è contrassegnata con il numero della transazione corrente nel campo xmax. Inoltre, questo valore è stato registrato sopra il vecchio, poiché la transazione precedente è stata annullata. E il bit xmax_aborted è stato azzerato, poiché lo stato della transazione corrente è ancora sconosciuto.
La prima versione della riga ora fa riferimento alla seconda (campo t_ctid), come a una versione più nuova.
Nella pagina dell'indice appare un secondo puntatore e una seconda riga che rimanda alla seconda versione nella pagina della tabella.
Come nel caso della cancellazione, il valore xmax nella prima versione della riga indica che la riga è bloccata.
E infine concludiamo la transazione.
=> COMMIT;
Indici
Fino ad ora abbiamo parlato solo delle pagine delle tabelle. E cosa succede all'interno degli indici?
Le informazioni nelle pagine degli indici dipendono fortemente dal tipo specifico di indice. E anche all'interno dello stesso tipo di indice ci possono essere vari tipi di pagine. Ad esempio, un B-albero ha una pagina di metadati e pagine 'normali'.
Tuttavia, normalmente in una pagina è presente un array di puntatori alle righe e alle righe stesse (proprio come nella pagina della tabella). Inoltre, alla fine della pagina viene riservato spazio per dati speciali.
Le righe negli indici possono avere una struttura molto diversa a seconda del tipo di indice. Ad esempio, per i B-alberi, le righe relative alle pagine foglia contengono il valore della chiave di indicizzazione e un riferimento (ctid) alla corrispondente riga della tabella. In generale, l'indice può essere organizzato in modo completamente diverso.
Il punto più importante è che negli indici di qualsiasi tipo non esistono versioni delle righe. O si può considerare che ogni riga è rappresentata da un'unica versione. In altre parole, nell'intestazione della riga dell'indice non ci sono campi xmin e xmax. Si può considerare che i riferimenti dall'indice puntano a tutte le versioni delle righe nella tabella — quindi capire quale versione vedrà la transazione è possibile solo guardando la tabella. (Come al solito, non è tutta la verità. In alcuni casi, la mappa di visibilità consente di ottimizzare il processo, ma lo esamineremo più avanti.)
Inoltre, nella pagina dell'indice troviamo puntatori a entrambe le versioni, sia quella attuale che quella precedente:
=> SELECT itemoffset, ctid FROM bt_page_items('t_s_idx',1);
itemoffset | ctid
------------+-------
1 | (0,2)
2 | (0,1)
(2 righe)
Transazioni virtuali
In pratica, PostgreSQL utilizza un'ottimizzazione che consente di 'risparmiare' i numeri delle transazioni.
Se una transazione legge solo i dati, allora non influisce sulla visibilità delle versioni delle righe. Pertanto, all'inizio, il processo di servizio assegna una numero virtuale alla transazione (virtual xid). Il numero è composto dall'identificatore del processo e da un numero sequenziale.
Il rilascio di questo numero non richiede sincronizzazione tra tutti i processi e quindi avviene molto rapidamente. Scopriremo un'altra ragione per l'uso dei numeri virtuali quando parleremo del congelamento.
I numeri virtuali non vengono affatto considerati negli snapshot dei dati.
In momenti diversi, nel sistema possono esserci transazioni virtuali con numeri già utilizzati, e ciò è normale. Tuttavia, un numero di questo tipo non può essere registrato nelle pagine dei dati, poiché alla successiva richiesta della pagina potrebbe perdere ogni significato.
=> BEGIN;
=> SELECT txid_current_if_assigned();
txid_current_if_assigned
--------------------------
(1 row)
Se la transazione inizia a modificare i dati, le viene assegnato un vero, unico numero di transazione.
=> UPDATE accounts SET amount = amount - 1.00;
=> SELECT txid_current_if_assigned();
txid_current_if_assigned
--------------------------
3667
(1 row)
=> COMMIT;
Transazioni annidate
Punti di salvataggio
In SQL sono definiti punti di salvataggio (savepoint), che consentono di annullare parte delle operazioni della transazione, senza interromperla completamente. Ma questo non si adatta allo schema sopra riportato, poiché lo stato di una transazione è uno per tutte le sue modifiche, e fisicamente nessun dato viene ripristinato.
Per implementare tale funzionalità, la transazione con un punto di salvataggio viene suddivisa in diverse transazioni annidate (subtransaction), il cui stato può essere gestito separatamente.
Le transazioni annidate hanno il proprio numero (maggiore di quello della transazione principale). Lo stato delle transazioni annidate viene registrato normalmente in XACT, tuttavia lo stato finale dipende dallo stato della transazione principale: se questa viene annullata, anche tutte le transazioni annidate vengono annullate.
Le informazioni sulla nidificazione delle transazioni sono memorizzate nei file nella directory PGDATA/pg_subtrans. L'accesso ai file avviene tramite buffer nella memoria condivisa dell'istanza, organizzati come i buffer XACT.
Non confondete le transazioni annidate con le transazioni autonome. Le transazioni autonome non dipendono l'una dall'altra, mentre le annidate sì. In PostgreSQL normale non ci sono transazioni autonome, e per fortuna: in genere sono necessarie molto raramente, e la loro presenza in altri DBMS provoca abusi che poi ricadono su tutti.
Puliamo la tabella, iniziamo la transazione e inseriamo una riga:
=> 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)
Adesso impostiamo un punto di salvataggio e inseriamo un'altra riga.
=> SAVEPOINT sp;
=> INSERT INTO t(s) VALUES ('XYZ');
=> SELECT txid_current();
txid_current
--------------
3669
(1 row)
Si noti che la funzione txid_current() restituisce il numero della transazione principale, non quella annidata.
=> 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)
Torniamo al punto di salvataggio e inseriamo una terza riga.
=> 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)
Nella pagina continuiamo a vedere la riga aggiunta dalla transazione annidata annullata.
Fissiamo le modifiche.
=> 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)
Ora è chiaro che ogni transazione annidata ha il proprio stato.
Si noti che le transazioni annidate non possono essere utilizzate esplicitamente in SQL, non è possibile avviare una nuova transazione senza completare quella attuale. Questo meccanismo è attivato implicitamente quando si utilizzano i punti di salvataggio e anche nella gestione delle eccezioni PL/pgSQL e in alcuni altri casi più esotici.
=> BEGIN;
BEGIN
=> BEGIN;
ATTENZIONE: c'è già una transazione in corso
BEGIN
=> COMMIT;
COMMIT
=> COMMIT;
ATTENZIONE: non c'è nessuna transazione in corso
COMMIT
Errori e atomicità delle operazioni
Cosa succede se si verifica un errore durante l'esecuzione di un'operazione? Ad esempio, così:
=> BEGIN;
=> SELECT * FROM t;
id | s
----+-----
2 | FOO
4 | BAR
(2 rows)
=> UPDATE t SET s = repeat('X', 1/(id-4));
ERRORE: divisione per zero
Si è verificato un errore. Ora la transazione è considerata interrotta e nessuna operazione in essa è consentita:
=> SELECT * FROM t;
ERRORE: la transazione corrente è stata annullata, comandi ignorati fino alla fine del blocco della transazione
E anche se si cerca di confermare le modifiche, PostgreSQL segnalerà l'annullamento:
=> COMMIT;
ROLLBACK
Perché non è possibile continuare l'esecuzione della transazione dopo un guasto? Il motivo è che l'errore potrebbe essersi verificato in modo tale che avremmo avuto accesso a una parte delle modifiche: l'atomicità sarebbe stata violata non solo nella transazione, ma anche nell'operatore. Come nel nostro esempio, dove l'operatore è riuscito ad aggiornare una riga prima dell'errore:
=> SELECT * FROM heap_page('t',0);
ctid | stato | xmin | xmax | t_ctid
-------+--------+----------+-------+--------
(0,1) | normale | 3669 (c) | 3672 | (0,4)
(0,2) | normale | 3670 (a) | 0 (a) | (0,2)
(0,3) | normale | 3671 (c) | 0 (a) | (0,3)
(0,4) | normale | 3672 | 0 (a) | (0,4)
(4 righe)
È importante notare che in psql esiste una modalità che consente di continuare la transazione dopo un guasto, come se le azioni dell'operatore errato venissero annullate.
=> 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));
ERRORE: divisione per zero
=> SELECT * FROM t;
id | s
----+-----
2 | FOO
4 | BAR
(2 rows)
=> COMMIT;
Non è difficile intuire che in questa modalità psql effettivamente imposta un punto di salvataggio implicito prima di ogni comando e, in caso di guasto, avvia un ripristino a quel punto. Questa modalità non è utilizzata di default, poiché impostare punti di salvataggio (anche senza un ripristino) comporta costi notevoli.
Fonte: habr.com
