MVCC-3. Versi string

Jadi, kami telah mempertimbangkan masalah yang berkaitan dengan isolasi, dan mundur pengorganisasian data pada tingkat rendah. Dan akhirnya kita sampai pada bagian yang paling menarik - versi string.

Judul

Seperti yang telah kami katakan, setiap baris bisa ada secara bersamaan di beberapa versi database. Satu versi harus dibedakan dari yang lain. Untuk tujuan ini, setiap versi memiliki dua tanda yang menentukan “waktu” tindakan versi ini (xmin dan xmax). Dalam tanda kutip - karena yang digunakan bukanlah waktu, tetapi penghitung peningkatan khusus. Dan counter ini adalah nomor transaksinya.

(Seperti biasa, kenyataannya lebih rumit: nomor transaksi tidak dapat bertambah sepanjang waktu karena terbatasnya kapasitas bit penghitung. Namun kita akan melihat detail ini secara mendetail saat kita sampai pada tahap pembekuan.)

Ketika sebuah baris dibuat, xmin diatur ke nomor transaksi yang mengeluarkan perintah INSERT, dan xmax dibiarkan kosong.

Ketika sebuah baris dihapus, nilai xmax versi saat ini ditandai dengan nomor transaksi yang melakukan DELETE.

Ketika sebuah baris diubah dengan perintah UPDATE, dua operasi sebenarnya dilakukan: DELETE dan INSERT. Versi baris saat ini menetapkan xmax sama dengan jumlah transaksi yang melakukan UPDATE. Versi baru dari string yang sama kemudian dibuat; nilai xminnya sama dengan nilai xmax versi sebelumnya.

Bidang xmin dan xmax disertakan dalam header versi baris. Selain bidang-bidang ini, header berisi bidang lain, misalnya:

  • infomask adalah serangkaian bit yang menentukan properti versi ini. Jumlahnya cukup banyak; Kami secara bertahap akan mempertimbangkan yang utama.
  • ctid adalah tautan ke versi berikutnya yang lebih baru dari baris yang sama. Untuk versi string terbaru dan terkini, ctid mengacu pada versi itu sendiri. Angka tersebut berbentuk (x,y), dimana x adalah nomor halaman, y adalah nomor indeks pada array.
  • bitmap nol - Menandai kolom-kolom pada versi tertentu yang berisi nilai nol (NULL). NULL bukan salah satu nilai tipe data normal, sehingga atributnya harus disimpan secara terpisah.

Hasilnya, headernya cukup besar - setidaknya 23 byte untuk setiap versi baris, dan biasanya lebih banyak karena bitmap NULL. Jika tabelnya "sempit" (yaitu berisi beberapa kolom), overhead mungkin memerlukan lebih dari informasi berguna.

menyisipkan

Mari kita lihat lebih dekat bagaimana operasi string tingkat rendah dilakukan, dimulai dengan penyisipan.

Untuk eksperimen, mari buat tabel baru dengan dua kolom dan indeks pada salah satunya:

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

Mari sisipkan satu baris setelah memulai transaksi.

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

Berikut nomor transaksi kami saat ini:

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

Mari kita lihat isi halamannya. Fungsi heap_page_items dari ekstensi pageinspect memungkinkan Anda mendapatkan informasi tentang pointer dan versi baris:

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

Perhatikan bahwa kata heap di PostgreSQL mengacu pada tabel. Ini adalah penggunaan istilah yang aneh lainnya - tumpukan diketahui struktur data, yang tidak memiliki kesamaan dengan tabel. Di sini kata tersebut digunakan dalam arti “semuanya disatukan”, bukan indeks yang dipesan.

Fungsi tersebut menampilkan data “sebagaimana adanya”, dalam format yang sulit dipahami. Untuk mengetahuinya, kami hanya akan menyisakan sebagian informasi dan menguraikannya:

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

Inilah yang kami lakukan:

  • Menambahkan angka nol pada nomor indeks agar terlihat sama dengan t_ctid: (nomor halaman, nomor indeks).
  • Menguraikan status penunjuk lp_flags. Ini dia "normal" - ini berarti penunjuk sebenarnya merujuk ke versi string. Kita akan melihat arti lainnya nanti.
  • Dari semua bit informasi, sejauh ini hanya dua pasang yang telah teridentifikasi. Bit xmin_commit dan xmin_aborted menunjukkan apakah nomor transaksi xmin dikomit (dibatalkan). Dua bit serupa mengacu pada nomor transaksi xmax.

Apa yang kita lihat? Saat Anda menyisipkan sebuah baris, indeks nomor 1 akan muncul di halaman tabel, menunjuk ke versi pertama dan satu-satunya dari baris tersebut.

Pada versi string, field xmin diisi dengan nomor transaksi saat ini. Transaksi masih aktif, sehingga bit xmin_commit dan xmin_aborted tidak disetel.

Bidang ctid versi baris mengacu pada baris yang sama. Artinya versi yang lebih baru tidak ada.

Bidang xmax diisi dengan angka tiruan 0 karena versi baris ini belum dihapus dan merupakan versi terkini. Transaksi tidak akan memperhatikan nomor ini karena bit xmax_aborted sudah diatur.

Mari kita mengambil satu langkah lagi untuk meningkatkan keterbacaan dengan menambahkan bit informasi ke nomor transaksi. Dan mari kita buat sebuah fungsi, karena kita memerlukan permintaan tersebut lebih dari satu kali:

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

Dalam formulir ini, lebih jelas apa yang terjadi di header versi baris:

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

Informasi serupa, namun secara signifikan kurang rinci, dapat diperoleh dari tabel itu sendiri, menggunakan kolom semu xmin dan xmax:

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

Fiksasi

Jika suatu transaksi berhasil diselesaikan, Anda perlu mengingat statusnya - perhatikan bahwa transaksi tersebut telah dilakukan. Untuk melakukan ini, digunakan struktur yang disebut XACT (dan sebelum versi 10 disebut CLOG (commit log) dan nama ini masih dapat ditemukan di tempat yang berbeda).

XACT bukan tabel katalog sistem; ini adalah file di direktori PGDATA/pg_xact. Mereka memiliki dua bit untuk setiap transaksi: dilakukan dan dibatalkan - seperti pada header versi baris. Informasi ini dibagi menjadi beberapa file semata-mata untuk kenyamanan; kami akan kembali ke masalah ini ketika kami mempertimbangkan pembekuan. Dan pengerjaan dengan file-file ini dilakukan halaman demi halaman, seperti yang lainnya.

Jadi, ketika suatu transaksi dilakukan di XACT, bit yang dikomit diatur untuk transaksi ini. Dan hanya ini yang terjadi selama melakukan (walaupun kita belum membicarakan tentang log pra-perekaman).

Ketika transaksi lain mengakses halaman tabel yang baru saja kita lihat, transaksi tersebut harus menjawab beberapa pertanyaan.

  1. Apakah transaksi xmin sudah selesai? Jika tidak, maka versi string yang dibuat tidak akan terlihat.
    Pemeriksaan ini dilakukan dengan melihat struktur lain, yang terletak di memori bersama instance dan disebut ProcArray. Ini berisi daftar semua proses yang aktif, dan untuk masing-masing proses tersebut nomor transaksinya saat ini (aktif) ditunjukkan.
  2. Jika sudah selesai, lalu bagaimana - dengan melakukan atau membatalkan? Jika dibatalkan, versi barisnya juga tidak akan terlihat.
    Inilah gunanya XACT. Namun, meskipun halaman terakhir XACT disimpan dalam buffer di RAM, masih mahal untuk memeriksa XACT setiap saat. Oleh karena itu, setelah status transaksi ditentukan, status tersebut ditulis ke bit xmin_commit dan xmin_aborted dari versi string. Jika salah satu bit ini disetel, maka status transaksi xmin dianggap diketahui dan transaksi berikutnya tidak perlu mengakses XACT.

Mengapa bit-bit ini tidak disetel oleh transaksi itu sendiri yang melakukan penyisipan? Ketika terjadi penyisipan, transaksi belum mengetahui apakah akan berhasil. Dan pada saat dilakukan, tidak lagi jelas baris mana di halaman mana yang diubah. Mungkin ada banyak halaman seperti itu, dan menghafalnya tidak menguntungkan. Selain itu, beberapa halaman mungkin dipindahkan dari cache buffer ke disk; membacanya lagi untuk mengubah bit akan memperlambat penerapan secara signifikan.

Kelemahan dari penghematan ini adalah setelah perubahan, transaksi apa pun (bahkan transaksi yang melakukan pembacaan sederhana - SELECT) dapat mulai mengubah halaman data di cache buffer.

Jadi, mari kita perbaiki perubahannya.

=> COMMIT;

Tidak ada yang berubah di halaman tersebut (tetapi kita tahu bahwa status transaksi sudah tercatat di XACT):

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

Sekarang transaksi yang mengakses halaman terlebih dahulu harus menentukan status transaksi xmin dan menuliskannya ke bit informasi:

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

Pemindahan

Ketika sebuah baris dihapus, nomor transaksi penghapusan saat ini ditulis ke bidang xmax versi saat ini, dan bit xmax_aborted dihapus.

Perhatikan bahwa nilai xmax yang ditetapkan terkait dengan transaksi aktif bertindak sebagai kunci baris. Jika transaksi lain ingin memperbarui atau menghapus baris ini, maka akan terpaksa menunggu hingga transaksi xmax selesai. Kami akan berbicara lebih banyak tentang pemblokiran nanti. Untuk saat ini, kami hanya mencatat bahwa jumlah kunci baris tidak terbatas. Mereka tidak memakan ruang di RAM dan kinerja sistem tidak terpengaruh oleh jumlahnya. Benar, transaksi “panjang” memiliki kelemahan lain, tetapi akan dibahas lebih lanjut nanti.

Mari kita hapus barisnya.

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

Kita melihat bahwa nomor transaksi tertulis di kolom xmax, tetapi bit informasi tidak disetel:

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

Membatalkan

Membatalkan perubahan cara kerjanya mirip dengan melakukan, hanya di XACT bit yang dibatalkan diatur untuk transaksi. Membatalkan sama cepatnya dengan melakukan. Meskipun perintahnya disebut ROLLBACK, perubahan tidak dibatalkan: segala sesuatu yang berhasil diubah oleh transaksi di halaman data tetap tidak berubah.

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

Ketika halaman diakses, statusnya akan diperiksa dan bit petunjuk xmax_aborted akan disetel ke versi baris. Nomor xmax itu sendiri tetap ada di halaman, tapi tidak ada yang akan melihatnya.

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

Memperbarui

Pembaruan berfungsi seolah-olah pertama kali menghapus versi baris saat ini dan kemudian menyisipkan yang baru.

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

Kueri menghasilkan satu baris (versi baru):

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

Namun di halaman tersebut kami melihat kedua versi:

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

Versi yang dihapus ditandai dengan nomor transaksi saat ini di kolom xmax. Apalagi nilai ini tertulis di atas nilai lama, karena transaksi sebelumnya dibatalkan. Dan bit xmax_aborted terhapus karena status transaksi saat ini belum diketahui.

Versi pertama dari baris tersebut sekarang mengacu pada baris kedua (bidang t_ctid) sebagai yang lebih baru.

Indeks kedua muncul di halaman indeks dan baris kedua merujuk pada versi kedua di halaman tabel.

Sama seperti penghapusan, nilai xmax pada baris versi pertama merupakan indikasi bahwa baris tersebut terkunci.

Baiklah, mari kita selesaikan transaksinya.

=> COMMIT;

Indeks

Sejauh ini kita hanya membicarakan halaman tabel. Apa yang terjadi di dalam indeks?

Informasi di halaman indeks sangat bervariasi tergantung pada jenis indeks tertentu. Dan bahkan satu jenis indeks pun memiliki jenis halaman yang berbeda-beda. Misalnya, B-tree memiliki halaman metadata dan halaman "biasa".

Namun, halaman tersebut biasanya memiliki array pointer ke baris dan baris itu sendiri (seperti halaman tabel). Selain itu, di akhir halaman terdapat ruang untuk data khusus.

Baris dalam indeks juga dapat memiliki struktur yang sangat berbeda bergantung pada jenis indeks. Misalnya, untuk B-tree, baris yang terkait dengan halaman daun berisi nilai kunci pengindeksan dan referensi (ctid) ke baris tabel terkait. Secara umum, indeks dapat disusun dengan cara yang sangat berbeda.

Hal yang paling penting adalah tidak ada versi baris dalam indeks jenis apa pun. Atau kita dapat berasumsi bahwa setiap baris diwakili oleh tepat satu versi. Dengan kata lain, tidak ada kolom xmin dan xmax di header baris indeks. Kita dapat berasumsi bahwa tautan dari indeks mengarah ke semua versi baris tabel - sehingga Anda dapat mengetahui versi mana yang akan dilihat transaksi hanya dengan melihat tabel. (Seperti biasa, hal ini tidak sepenuhnya benar. Dalam beberapa kasus, peta visibilitas dapat mengoptimalkan proses, namun kita akan melihatnya lebih detail nanti.)

Pada saat yang sama, di halaman indeks kami menemukan petunjuk ke kedua versi, baik versi saat ini maupun versi lama:

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

Transaksi maya

Dalam praktiknya, PostgreSQL menggunakan pengoptimalan yang memungkinkannya “menyimpan” nomor transaksi.

Jika suatu transaksi hanya membaca data, hal itu tidak berpengaruh pada visibilitas versi baris. Oleh karena itu, proses layanan terlebih dahulu mengeluarkan xid virtual untuk transaksi tersebut. Nomor tersebut terdiri dari ID proses dan nomor urut.

Penerbitan nomor ini tidak memerlukan sinkronisasi antara semua proses dan karenanya sangat cepat. Kita akan mengetahui alasan lain untuk menggunakan nomor virtual ketika kita berbicara tentang pembekuan.

Nomor virtual tidak diperhitungkan dengan cara apa pun dalam snapshot data.

Di waktu yang berbeda, mungkin ada transaksi virtual di sistem dengan nomor yang sudah digunakan, dan ini normal. Namun nomor tersebut tidak dapat dituliskan ke dalam halaman data, karena pada saat halaman tersebut diakses lagi, halaman tersebut mungkin kehilangan maknanya.

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

Jika suatu transaksi mulai mengubah data, maka diberikan nomor transaksi yang nyata dan unik.

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

=> COMMIT;

Transaksi Bersarang

Simpan poin

Didefinisikan dalam SQL menyimpan poin (savepoint), yang memungkinkan Anda membatalkan sebagian transaksi tanpa mengganggu transaksi sepenuhnya. Namun hal ini tidak sesuai dengan diagram di atas, karena transaksi memiliki status yang sama untuk semua perubahannya, dan secara fisik tidak ada data yang dibatalkan.

Untuk mengimplementasikan fungsi ini, transaksi dengan savepoint dibagi menjadi beberapa terpisah transaksi bersarang (subtransaksi) yang statusnya dapat dikelola tersendiri.

Transaksi bertingkat mempunyai nomor tersendiri (lebih tinggi dari nomor transaksi utama). Status transaksi bertumpuk dicatat dengan cara biasa di XACT, namun status akhir bergantung pada status transaksi utama: jika dibatalkan, maka semua transaksi bertumpuk juga dibatalkan.

Informasi tentang sarang transaksi disimpan dalam file di direktori PGDATA/pg_subtrans. File diakses melalui buffer di memori bersama instance, yang diatur dengan cara yang sama seperti buffer XACT.

Jangan bingung membedakan transaksi bersarang dengan transaksi otonom. Transaksi otonom tidak bergantung satu sama lain dengan cara apa pun, namun transaksi bersarang bergantung satu sama lain. Tidak ada transaksi otonom di PostgreSQL biasa, dan, mungkin, yang terbaik: transaksi tersebut sangat, sangat jarang dibutuhkan, dan kehadirannya di DBMS lain memicu penyalahgunaan, yang kemudian diderita semua orang.

Mari kita bersihkan tabelnya, mulai transaksi dan masukkan baris:

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

Sekarang mari kita letakkan titik simpan dan masukkan baris lain.

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

Perhatikan bahwa fungsi txid_current() mengembalikan nomor transaksi utama, bukan nomor transaksi bertumpuk.

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

Mari kita kembali ke titik penyimpanan dan masukkan baris ketiga.

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

Di halaman tersebut kita terus melihat baris yang ditambahkan oleh transaksi bersarang yang dibatalkan.

Kami memperbaiki perubahannya.

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

Sekarang Anda dapat melihat dengan jelas bahwa setiap transaksi bertingkat memiliki statusnya sendiri.

Perhatikan bahwa transaksi bertumpuk tidak dapat digunakan secara eksplisit dalam SQL, artinya Anda tidak dapat memulai transaksi baru tanpa menyelesaikan transaksi saat ini. Mekanisme ini diaktifkan secara implisit ketika menggunakan savepoint, serta ketika menangani pengecualian PL/pgSQL dan dalam sejumlah kasus lain yang lebih eksotik.

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

Kesalahan dan atomisitas operasi

Apa yang terjadi jika terjadi kesalahan saat melakukan suatu operasi? Misalnya seperti ini:

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

Sebuah kesalahan telah terjadi. Sekarang transaksi dianggap dibatalkan dan tidak ada operasi yang diperbolehkan di dalamnya:

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

Dan bahkan jika Anda mencoba melakukan perubahan, PostgreSQL akan melaporkan pembatalan:

=> COMMIT;
ROLLBACK

Mengapa transaksi tidak dapat dilanjutkan setelah gagal? Faktanya adalah bahwa kesalahan dapat muncul sedemikian rupa sehingga kita akan mendapatkan akses ke sebagian perubahan - atomisitas bahkan bukan transaksinya, tetapi operatornya akan dilanggar. Seperti pada contoh kita, di mana operator berhasil memperbarui satu baris sebelum kesalahan:

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

Harus dikatakan bahwa psql memiliki mode yang masih memungkinkan transaksi dilanjutkan setelah kegagalan seolah-olah tindakan operator yang salah telah dibatalkan.

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

Tidak sulit untuk menebak bahwa dalam mode ini, psql sebenarnya menempatkan titik penyimpanan implisit sebelum setiap perintah, dan jika terjadi kegagalan, ia akan melakukan rollback ke perintah tersebut. Mode ini tidak digunakan secara default, karena pengaturan titik penyimpanan (bahkan tanpa mengembalikannya) memerlukan overhead yang signifikan.

Untuk dilanjutkan.

Sumber: www.habr.com

Tambah komentar