ROLLBACK dari chapter sebelumnya bekerja seperti tombol darurat universal: satu sentuhan, seluruh transaksi batal. Praktik sehari-hari sering butuh kendali yang lebih halus. Satu transaksi bisa berisi langkah wajib — memindahkan saldo, menyimpan pesanan — dan langkah pelengkap yang boleh gagal — mengirim notifikasi, mencatat log, memanggil layanan luar. Jika langkah pelengkap error di urutan ke-10, membatalkan sembilan langkah sebelumnya lalu mengulang semuanya dari awal itu pemborosian. SAVEPOINT mengatasi ini dengan menaruh penanda di dalam transaksi: ROLLBACK TO SAVEPOINT memundurkan transaksi ke penanda itu saja, sementara pekerjaan sebelum penanda tetap aman dan transaksi tetap berjalan.

Menyiapkan Tabel Latihan #

Demo chapter ini memakai tiga tabel: rekening yang sudah akrab, mutasi sebagai jurnal pergerakan saldo, dan notifikasi untuk kirim pesan ke nasabah. Kolom kanal pada notifikasi sengaja dibatasi CHECK ke sms dan email — nilai lain akan ditolak, dan penolakan inilah yang kita pakai untuk mensimulasikan kegagalan langkah pelengkap:

sql buat-tabel.sql
CREATE TABLE rekening (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    jenis text NOT NULL,
    saldo numeric(12, 2) NOT NULL CHECK (saldo >= 0),
    CONSTRAINT saldo_ewallet_maks
        CHECK (jenis <> 'e-wallet' OR saldo <= 1000000)
);

CREATE TABLE mutasi (
    id          integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    rekening_id integer NOT NULL REFERENCES rekening(id),
    arah        text NOT NULL CHECK (arah IN ('debit', 'kredit')),
    jumlah      numeric(12, 2) NOT NULL CHECK (jumlah > 0),
    keterangan  text NOT NULL
);

CREATE TABLE notifikasi (
    id          integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    rekening_id integer NOT NULL REFERENCES rekening(id),
    kanal       text NOT NULL CHECK (kanal IN ('sms', 'email')),
    pesan       text NOT NULL
);

INSERT INTO rekening (id, nama, jenis, saldo) VALUES
    (1, 'Budi Santoso', 'tabungan', 500000),
    (2, 'Siti Aminah', 'tabungan', 300000),
    (3, 'Dewi Lestari', 'e-wallet', 950000);

SELECT id, nama, jenis, saldo FROM rekening ORDER BY id;

Output yang Dihasilkan:

text
CREATE TABLE
CREATE TABLE
CREATE TABLE
INSERT 0 3
 id |     nama     |  jenis   |   saldo   
----+--------------+----------+-----------
  1 | Budi Santoso | tabungan | 500000.00
  2 | Siti Aminah  | tabungan | 300000.00
  3 | Dewi Lestari | e-wallet | 950000.00
(3 rows)

SAVEPOINT: Penanda di Dalam Transaksi #

Contoh pertama memperlihatkan mekanisme dasarnya tanpa error sama sekali. Transfer Rp100.000 dari Budi ke Siti dibungkus transaksi, lalu pencatatan jurnal dijadikan langkah terpisah: SAVEPOINT catat_mutasi dipasang tepat sebelum pencatatan, satu baris jurnal sengaja dimasukkan dengan jumlah yang salah, dan ROLLBACK TO SAVEPOINT membuang baris itu tanpa menyentuh transfernya:

BEGIN;

UPDATE rekening SET saldo = saldo - 100000 WHERE id = 1;

UPDATE rekening SET saldo = saldo + 100000 WHERE id = 2;

SAVEPOINT catat_mutasi;

INSERT INTO mutasi (rekening_id, arah, jumlah, keterangan) VALUES
    (1, 'debit', 10000, 'Transfer ke Siti Aminah');

SELECT id, rekening_id, arah, jumlah, keterangan FROM mutasi ORDER BY id;

ROLLBACK TO SAVEPOINT catat_mutasi;

INSERT INTO mutasi (rekening_id, arah, jumlah, keterangan) VALUES
    (1, 'debit', 100000, 'Transfer ke Siti Aminah'),
    (2, 'kredit', 100000, 'Transfer dari Budi Santoso');

COMMIT;

SELECT id, rekening_id, arah, jumlah, keterangan FROM mutasi ORDER BY id;

Output yang Dihasilkan:

text
BEGIN
UPDATE 1
UPDATE 1
SAVEPOINT
INSERT 0 1
 id | rekening_id | arah  |  jumlah  |       keterangan        
----+-------------+-------+----------+-------------------------
  1 |           1 | debit | 10000.00 | Transfer ke Siti Aminah
(1 row)

ROLLBACK
INSERT 0 2
COMMIT
 id | rekening_id |  arah  |  jumlah   |         keterangan         
----+-------------+--------+-----------+----------------------------
  2 |           1 | debit  | 100000.00 | Transfer ke Siti Aminah
  3 |           2 | kredit | 100000.00 | Transfer dari Budi Santoso
(2 rows)

Mari kita bedah peran setiap bagian:

  1. Dua UPDATE menjalankan transfer Budi ke Siti — langkah wajib yang harus tetap aman apa pun yang terjadi pada pencatatan.
  2. SAVEPOINT catat_mutasi memasang penanda bernama catat_mutasi pada titik ini. Nama savepoint bebas; PostgreSQL mencatatnya dalam daftar penanda transaksi berjalan.
  3. INSERT pertama mencatat jurnal dengan jumlah salah: Rp10.000, seharusnya Rp100.000. SELECT di tengah transaksi memperlihatkan baris keliru itu dengan id = 1.
  4. ROLLBACK TO SAVEPOINT catat_mutasi memundurkan transaksi ke penanda — semua efek setelah penanda dibatalkan, termasuk baris jurnal yang salah. Tag yang muncul tetap ROLLBACK, jadi andalkan konteks perintahnya untuk membedakannya dari ROLLBACK penuh.
  5. INSERT kedua mencatat ulang jurnal dengan jumlah benar, lalu COMMIT mengunci semuanya: transfer maupun jurnal yang sudah dikoreksi.

SELECT terakhir menampilkan dua baris jurnal final — perhatikan bahwa id-nya adalah 2 dan 3. Baris keliru tadi sempat mengonsumsi id = 1, dan penomoran identity tidak ikut dibatalkan oleh ROLLBACK TO SAVEPOINT. Perilaku ini dibahas lebih lanjut di callout di bawah.

AspekROLLBACKROLLBACK TO SAVEPOINT nama
Cakupan pembatalanSeluruh transaksiHanya perintah setelah penanda nama
Status transaksi setelahnyaBerakhir — sesi kembali ke autocommitTetap berjalan — lanjutkan atau akhiri manual
Perintah wajib setelahnyaTidak adaCOMMIT atau ROLLBACK di akhir
Kasus pakaiMembatalkan unit kerja gagal totalMembuang cabang pekerjaan, lalu lanjut

Pulih dari Error Tanpa Membuang Transaksi #

Skenario kedua adalah kasus pakai SAVEPOINT yang paling umum: langkah pelengkap mengalami error, dan Anda ingin transaksi intinya tetap tersimpan. Aplikasi transfer mengirim notifikasi WhatsApp — nilai yang tidak diizinkan kolom kanal — sehingga INSERT ke notifikasi gagal di tengah transaksi. Tanpa savepoint, error ini membuat seluruh transaksi berstatus aborted dan transfer ikut batal. Dengan savepoint dipasang sebelum notifikasi, kerusakannya terlokalisir. Demo dijalankan sebagai sesi psql interaktif agar error dan pemulihannya terlihat utuh:

psql -X 2>&1 <<'SQL'
BEGIN;
UPDATE rekening SET saldo = saldo - 50000 WHERE id = 1;
UPDATE rekening SET saldo = saldo + 50000 WHERE id = 2;
SAVEPOINT sebelum_notifikasi;
INSERT INTO notifikasi (rekening_id, kanal, pesan) VALUES (1, 'whatsapp', 'Transfer Rp50000 ke Siti Aminah');
ROLLBACK TO SAVEPOINT sebelum_notifikasi;
INSERT INTO notifikasi (rekening_id, kanal, pesan) VALUES (1, 'sms', 'Transfer Rp50000 ke Siti Aminah');
COMMIT;
SELECT id, rekening_id, kanal, pesan FROM notifikasi ORDER BY id;
SELECT id, nama, saldo FROM rekening WHERE id IN (1, 2) ORDER BY id;
SQL

Output yang Dihasilkan:

text
BEGIN
UPDATE 1
UPDATE 1
SAVEPOINT
ERROR:  new row for relation "notifikasi" violates check constraint "notifikasi_kanal_check"
DETAIL:  Failing row contains (1, 1, whatsapp, Transfer Rp50000 ke Siti Aminah).
ROLLBACK
INSERT 0 1
COMMIT
 id | rekening_id | kanal |              pesan              
----+-------------+-------+---------------------------------
  2 |           1 | sms   | Transfer Rp50000 ke Siti Aminah
(1 row)

 id |     nama     |   saldo   
----+--------------+-----------
  1 | Budi Santoso | 350000.00
  2 | Siti Aminah  | 450000.00
(2 rows)

Alurnya berjalan persis seperti desainnya:

  1. Transfer dua UPDATE dieksekusi lebih dulu — ini langkah wajib.
  2. SAVEPOINT sebelum_notifikasi dipasang sebelum langkah yang berisiko gagal.
  3. INSERT notifikasi WhatsApp ditolak constraint notifikasi_kanal_check. Error ini tidak membuat transaksi aborted permanen, karena ROLLBACK TO SAVEPOINT segera memundurkan keadaan ke titik sebelum kegagalan — transaksi kembali sehat dan bisa dilanjutkan.
  4. Notifikasi dicoba ulang lewat kanal sms yang valid, lalu COMMIT menutup transaksi.
  5. SELECT akhir membuktikan hasil akhirnya: transfer tersimpan (saldo Budi berkurang Rp50.000, saldo Siti bertambah Rp50.000) dan satu notifikasi sms tercatat.

Savepoint Berlapis #

Satu transaksi boleh memuat banyak savepoint, dan penanda bisa dipasang bertingkat. Aturan mainnya: ROLLBACK TO SAVEPOINT x membatalkan semua perintah setelah x — termasuk savepoint-savepoint yang dipasang setelah x beserta efeknya. Contoh berikut menerapkan dua potongan biaya pada rekening Budi, membuktikan bahwa rollback ke penanda dalam tidak menyentuh pekerjaan luar, lalu rollback ke penanda luar menyeret semuanya:

BEGIN;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

SAVEPOINT sebelum_biaya_admin;

UPDATE rekening SET saldo = saldo - 25000 WHERE id = 1;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

SAVEPOINT sebelum_pajak;

UPDATE rekening SET saldo = saldo - 50000 WHERE id = 1;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

ROLLBACK TO SAVEPOINT sebelum_pajak;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

ROLLBACK TO SAVEPOINT sebelum_biaya_admin;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

COMMIT;

Output yang Dihasilkan:

text
BEGIN
 id |     nama     |   saldo   
----+--------------+-----------
  1 | Budi Santoso | 350000.00
(1 row)

SAVEPOINT
UPDATE 1
 id |     nama     |   saldo   
----+--------------+-----------
  1 | Budi Santoso | 325000.00
(1 row)

SAVEPOINT
UPDATE 1
 id |     nama     |   saldo   
----+--------------+-----------
  1 | Budi Santoso | 275000.00
(1 row)

ROLLBACK
 id |     nama     |   saldo   
----+--------------+-----------
  1 | Budi Santoso | 325000.00
(1 row)

ROLLBACK
 id |     nama     |   saldo   
----+--------------+-----------
  1 | Budi Santoso | 350000.00
(1 row)

COMMIT

Urutan saldo Budi membentuk tangga yang naik-turun: Rp350.000 di awal, turun ke Rp325.000 setelah biaya admin, turun lagi ke Rp275.000 setelah pajak, naik kembali ke Rp325.000 saat pajak dibatalkan dengan rollback ke sebelum_pajak, dan kembali ke Rp350.000 saat biaya admin pun dibatalkan dengan rollback ke sebelum_biaya_admin. Perintah COMMIT di akhir menutup transaksi dengan saldo yang tidak berubah sama sekali — seluruh tangga hanya berlangsung di dalam transaksi.

RELEASE SAVEPOINT: Membuang Penanda #

RELEASE SAVEPOINT nama adalah pelengkap yang sering terlewat: penandanya dibuang tetapi perubahan setelah penanda tetap dipertahankan. Gunakan saat tahap percobaan sudah selesai dan Anda yakin tidak akan menariknya lagi — daftar penanda transaksi jadi bersih. Nama penanda yang sudah dirilis juga bisa dipakai ulang untuk penanda baru. Contoh berikut menarik tunai Rp50.000 dari rekening Siti (dipertahankan lewat RELEASE), lalu memasang penanda bernama sama untuk biaya admin yang akhirnya dibatalkan:

BEGIN;

SAVEPOINT sebelum_tarik;

UPDATE rekening SET saldo = saldo - 50000 WHERE id = 2;

RELEASE SAVEPOINT sebelum_tarik;

SELECT id, nama, saldo FROM rekening WHERE id = 2;

SAVEPOINT sebelum_tarik;

UPDATE rekening SET saldo = saldo - 25000 WHERE id = 2;

ROLLBACK TO SAVEPOINT sebelum_tarik;

COMMIT;

SELECT id, nama, saldo FROM rekening ORDER BY id;

Output yang Dihasilkan:

text
BEGIN
SAVEPOINT
UPDATE 1
RELEASE
 id |    nama     |   saldo   
----+-------------+-----------
  2 | Siti Aminah | 400000.00
(1 row)

SAVEPOINT
UPDATE 1
ROLLBACK
COMMIT
 id |     nama     |   saldo   
----+--------------+-----------
  1 | Budi Santoso | 350000.00
  2 | Siti Aminah  | 400000.00
  3 | Dewi Lestari | 950000.00
(3 rows)

SELECT di tengah transaksi menunjukkan penarikan Rp50.000 sudah terpasang meski penandanya sudah dirilis — RELEASE membuang penanda, bukan pekerjaan. Penanda kedua bernama sama dipasang sebelum biaya admin Rp25.000, dan ROLLBACK TO membatalkan biaya admin itu saja. SELECT akhir merangkum hasil akhirnya: penarikan tunai tersimpan, biaya admin lenyap.

Menarik Kembali Penanda yang Sudah Tidak Ada #

Apa yang terjadi jika Anda melakukan ROLLBACK TO pada penanda yang sudah dirilis, atau pada nama yang memang tidak pernah dipasang? PostgreSQL menolaknya dengan error — dan karena error itu terjadi di dalam transaksi, status transaksi berubah menjadi aborted:

BEGIN;

SAVEPOINT titik_aman;

UPDATE rekening SET saldo = saldo - 10000 WHERE id = 1;

RELEASE SAVEPOINT titik_aman;

ROLLBACK TO SAVEPOINT titik_aman;

Output yang Dihasilkan:

text
psql:savepoint-tak-dikenal.sql:9: ERROR:  savepoint "titik_aman" does not exist

Penanda titik_aman sempat ada, tetapi sudah dirilis dua baris sebelumnya — nama penanda yang telah dirilis hilang dari daftar transaksi, sehingga ROLLBACK TO SAVEPOINT titik_aman ditolak dengan pesan savepoint "titik_aman" does not exist. Error yang sama juga muncul untuk nama penanda yang memang tidak pernah dipasang. Karena error ini terjadi di dalam transaksi, status transaksi berubah menjadi aborted dan saat psql keluar, seluruh transaksi dibatalkan secara implisit — pengurangan saldo Budi Rp10.000 pada baris sebelumnya ikut lenyap. Pegang aturan praktisnya: pasang penanda segar sebelum setiap tahap yang ingin bisa Anda batalkan, dan lakukan ROLLBACK TO hanya pada penanda yang Anda yakini masih hidup.

Dengan BEGIN, COMMIT, ROLLBACK, dan SAVEPOINT, Anda kini memiliki kendali bentuk apa pun atas unit kerja: dibatalkan seluruhnya, sebagian, bertingkat, atau diteruskan setelah pulih dari error. Semua mekanisme ini adalah alat; jaminan di baliknya — mengapa data tetap utuh saat server mati listrik, mengapa dua nasabah bisa menulis bersamaan tanpa saling merusak — dirumuskan dalam empat huruf: ACID. Chapter berikutnya membedah satu per satu Atomicity, Consistency, Isolation, dan Durability serta membuktikannya di tabel rekening yang sama.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

buat-tabel.sql

Unduh berkas
sql
CREATE TABLE rekening (
    id    integer PRIMARY KEY,
    nama  text NOT NULL,
    jenis text NOT NULL,
    saldo numeric(12, 2) NOT NULL CHECK (saldo >= 0),
    CONSTRAINT saldo_ewallet_maks
        CHECK (jenis <> 'e-wallet' OR saldo <= 1000000)
);

CREATE TABLE mutasi (
    id          integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    rekening_id integer NOT NULL REFERENCES rekening(id),
    arah        text NOT NULL CHECK (arah IN ('debit', 'kredit')),
    jumlah      numeric(12, 2) NOT NULL CHECK (jumlah > 0),
    keterangan  text NOT NULL
);

CREATE TABLE notifikasi (
    id          integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    rekening_id integer NOT NULL REFERENCES rekening(id),
    kanal       text NOT NULL CHECK (kanal IN ('sms', 'email')),
    pesan       text NOT NULL
);

INSERT INTO rekening (id, nama, jenis, saldo) VALUES
    (1, 'Budi Santoso', 'tabungan', 500000),
    (2, 'Siti Aminah', 'tabungan', 300000),
    (3, 'Dewi Lestari', 'e-wallet', 950000);

SELECT id, nama, jenis, saldo FROM rekening ORDER BY id;

pulih-dari-error.sh

Unduh berkas
bash
psql -X 2>&1 <<'SQL'
BEGIN;
UPDATE rekening SET saldo = saldo - 50000 WHERE id = 1;
UPDATE rekening SET saldo = saldo + 50000 WHERE id = 2;
SAVEPOINT sebelum_notifikasi;
INSERT INTO notifikasi (rekening_id, kanal, pesan) VALUES (1, 'whatsapp', 'Transfer Rp50000 ke Siti Aminah');
ROLLBACK TO SAVEPOINT sebelum_notifikasi;
INSERT INTO notifikasi (rekening_id, kanal, pesan) VALUES (1, 'sms', 'Transfer Rp50000 ke Siti Aminah');
COMMIT;
SELECT id, rekening_id, kanal, pesan FROM notifikasi ORDER BY id;
SELECT id, nama, saldo FROM rekening WHERE id IN (1, 2) ORDER BY id;
SQL

release-savepoint.sql

Unduh berkas
sql
BEGIN;

SAVEPOINT sebelum_tarik;

UPDATE rekening SET saldo = saldo - 50000 WHERE id = 2;

RELEASE SAVEPOINT sebelum_tarik;

SELECT id, nama, saldo FROM rekening WHERE id = 2;

SAVEPOINT sebelum_tarik;

UPDATE rekening SET saldo = saldo - 25000 WHERE id = 2;

ROLLBACK TO SAVEPOINT sebelum_tarik;

COMMIT;

SELECT id, nama, saldo FROM rekening ORDER BY id;

savepoint-berlapis.sql

Unduh berkas
sql
BEGIN;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

SAVEPOINT sebelum_biaya_admin;

UPDATE rekening SET saldo = saldo - 25000 WHERE id = 1;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

SAVEPOINT sebelum_pajak;

UPDATE rekening SET saldo = saldo - 50000 WHERE id = 1;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

ROLLBACK TO SAVEPOINT sebelum_pajak;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

ROLLBACK TO SAVEPOINT sebelum_biaya_admin;

SELECT id, nama, saldo FROM rekening WHERE id = 1;

COMMIT;

savepoint-dasar.sql

Unduh berkas
sql
BEGIN;

UPDATE rekening SET saldo = saldo - 100000 WHERE id = 1;

UPDATE rekening SET saldo = saldo + 100000 WHERE id = 2;

SAVEPOINT catat_mutasi;

INSERT INTO mutasi (rekening_id, arah, jumlah, keterangan) VALUES
    (1, 'debit', 10000, 'Transfer ke Siti Aminah');

SELECT id, rekening_id, arah, jumlah, keterangan FROM mutasi ORDER BY id;

ROLLBACK TO SAVEPOINT catat_mutasi;

INSERT INTO mutasi (rekening_id, arah, jumlah, keterangan) VALUES
    (1, 'debit', 100000, 'Transfer ke Siti Aminah'),
    (2, 'kredit', 100000, 'Transfer dari Budi Santoso');

COMMIT;

SELECT id, rekening_id, arah, jumlah, keterangan FROM mutasi ORDER BY id;

savepoint-tak-dikenal.sql

Unduh berkas
sql
BEGIN;

SAVEPOINT titik_aman;

UPDATE rekening SET saldo = saldo - 10000 WHERE id = 1;

RELEASE SAVEPOINT titik_aman;

ROLLBACK TO SAVEPOINT titik_aman;