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:
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:
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:
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:
- Dua
UPDATEmenjalankan transfer Budi ke Siti — langkah wajib yang harus tetap aman apa pun yang terjadi pada pencatatan. SAVEPOINT catat_mutasimemasang penanda bernamacatat_mutasipada titik ini. Nama savepoint bebas; PostgreSQL mencatatnya dalam daftar penanda transaksi berjalan.INSERTpertama mencatat jurnal dengan jumlah salah: Rp10.000, seharusnya Rp100.000.SELECTdi tengah transaksi memperlihatkan baris keliru itu denganid = 1.ROLLBACK TO SAVEPOINT catat_mutasimemundurkan transaksi ke penanda — semua efek setelah penanda dibatalkan, termasuk baris jurnal yang salah. Tag yang muncul tetapROLLBACK, jadi andalkan konteks perintahnya untuk membedakannya dariROLLBACKpenuh.INSERTkedua mencatat ulang jurnal dengan jumlah benar, laluCOMMITmengunci 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.
| Aspek | ROLLBACK | ROLLBACK TO SAVEPOINT nama |
|---|---|---|
| Cakupan pembatalan | Seluruh transaksi | Hanya perintah setelah penanda nama |
| Status transaksi setelahnya | Berakhir — sesi kembali ke autocommit | Tetap berjalan — lanjutkan atau akhiri manual |
| Perintah wajib setelahnya | Tidak ada | COMMIT atau ROLLBACK di akhir |
| Kasus pakai | Membatalkan unit kerja gagal total | Membuang 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;
SQLOutput yang Dihasilkan:
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:
- Transfer dua
UPDATEdieksekusi lebih dulu — ini langkah wajib. SAVEPOINT sebelum_notifikasidipasang sebelum langkah yang berisiko gagal.INSERTnotifikasi WhatsApp ditolak constraintnotifikasi_kanal_check. Error ini tidak membuat transaksi aborted permanen, karenaROLLBACK TO SAVEPOINTsegera memundurkan keadaan ke titik sebelum kegagalan — transaksi kembali sehat dan bisa dilanjutkan.- Notifikasi dicoba ulang lewat kanal
smsyang valid, laluCOMMITmenutup transaksi. SELECTakhir membuktikan hasil akhirnya: transfer tersimpan (saldo Budi berkurang Rp50.000, saldo Siti bertambah Rp50.000) dan satu notifikasismstercatat.
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:
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)
COMMITUrutan 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:
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:
psql:savepoint-tak-dikenal.sql:9: ERROR: savepoint "titik_aman" does not existPenanda 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 berkasCREATE 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 berkaspsql -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 berkasBEGIN;
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 berkasBEGIN;
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 berkasBEGIN;
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 berkasBEGIN;
SAVEPOINT titik_aman;
UPDATE rekening SET saldo = saldo - 10000 WHERE id = 1;
RELEASE SAVEPOINT titik_aman;
ROLLBACK TO SAVEPOINT titik_aman;