Bayangkan Anda mentransfer Rp100.000 dari rekening Budi ke dompet digital Dewi. Proses ini sebenarnya terdiri dari dua perubahan yang terpisah: saldo Budi berkurang Rp100.000 dan saldo Dewi bertambah Rp100.000. Kedua perubahan itu harus terjadi bersamaan — jika hanya satu yang berhasil, uang lenyap dari sistem atau justru tercipta dari udara tipis. Database dunia nyata menghadapi masalah serupa setiap detik: koneksi bisa terputus di tengah proses, query bisa ditolak constraint, dan listrik server bisa padam. Transaksi adalah mekanisme PostgreSQL untuk mengelola kelompok perubahan sebagai satu unit kerja yang berlaku semua-atau-tidak-sama-sekali (all-or-nothing).

Menyiapkan Tabel Latihan #

Kita pakai satu tabel rekening untuk seluruh demo di chapter ini. Selain CHECK (saldo >= 0) yang biasa, tabel ini punya aturan bisnis kedua yang disengaja: saldo e-wallet tidak boleh melebihi Rp1.000.000 — aturan umum untuk akun dompet digital yang belum terverifikasi:

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

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

Tiga baris data siap dipakai. Perhatikan saldo Dewi yang memang diatur tepat di bawah batas atas e-wallet: Rp950.000. Konsekuensinya, transfer Rp100.000 ke rekening Dewi akan ditolak constraint karena hasil penjumlahannya Rp1.050.000 melebihi batas — kegagalan inilah yang akan kita manfaatkan untuk mensimulasikan error di tengah proses transfer.

Autocommit: Perilaku Default PostgreSQL #

Sebelum berkenalan dengan transaksi eksplisit, kita perlu memahami bagaimana PostgreSQL memperlakukan perintah secara default. Di mode autocommit, setiap perintah INSERT, UPDATE, atau DELETE yang berhasil otomatis menjadi transaksinya sendiri dan langsung bersifat permanen — tidak ada momen “menunggu konfirmasi”. Contoh berikut mensetor Rp25.000 ke rekening Siti dengan satu UPDATE polos:

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

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

Output yang Dihasilkan:

text
UPDATE 1
 id |    nama     |   saldo   
----+-------------+-----------
  2 | Siti Aminah | 325000.00
(1 row)

Saldo Siti berubah dari Rp300.000 menjadi Rp325.000 seketika. Tidak ada COMMIT yang kita tulis — perubahan itu sudah tersimpan permanen begitu tag UPDATE 1 muncul. Perilaku ini praktis untuk pekerjaan satu langkah, tetapi menjadi bumerang ketika satu operasi bisnis terdiri dari beberapa perintah yang harus sukses bersama-sama.

Tanpa Transaksi: Uang yang Menguap #

Sekarang kita jalankan transfer Budi ke Dewi secara apa adanya — dua perintah UPDATE berurutan tanpa pembungkus apa pun. Pengurangan saldo Budi berhasil, tetapi penambahan saldo Dewi ditolak constraint batas e-wallet:

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

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

Output yang Dihasilkan:

text
psql:transfer-tanpa-transaksi.sql:3: ERROR:  new row for relation "rekening" violates check constraint "saldo_ewallet_maks"
DETAIL:  Failing row contains (3, Dewi Lestari, e-wallet, 1050000.00).

Perintah pertama sukses dan — karena autocommit — langsung permanen. Perintah kedua gagal total, dan psql menghentikan eksekusi file di titik itu. Pertanyaannya: kondisi apa yang ditinggalkan kedua perintah ini di dalam tabel? Mari kita periksa:

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

SELECT sum(saldo) AS total_saldo FROM rekening;

Output yang Dihasilkan:

text
 id |     nama     |  jenis   |   saldo   
----+--------------+----------+-----------
  1 | Budi Santoso | tabungan | 400000.00
  2 | Siti Aminah  | tabungan | 325000.00
  3 | Dewi Lestari | e-wallet | 950000.00
(3 rows)

 total_saldo 
-------------
  1675000.00
(1 row)

Inilah bencananya: saldo Budi sudah terpotong menjadi Rp400.000, tetapi saldo Dewi tetap Rp950.000. Total saldo sistem tinggal Rp1.675.000 — padahal sebelum transfer totalnya Rp1.725.000. Rp100.000 benar-benar menguap, tidak ada di rekening siapa pun, dan tidak ada perintah SQL yang bisa mengembalikannya tanpa intervensi manual. Aplikasi yang gagal di tengah jalan seperti ini meninggalkan state setengah jadi (partial state) yang merusak kepercayaan terhadap seluruh sistem.

Membungkus Transfer dengan Transaksi #

Solusinya adalah menandai batas awal dan batas akhir dari unit kerja: BEGIN membuka transaksi, dan hasilnya diselesaikan dengan COMMIT (terapkan semua) atau ROLLBACK (batalkan semua). Skrip berikut menjalankan transfer yang sama seperti sebelumnya, kali ini di dalam satu transaksi. Karena kita ingin melihat kelanjutannya meski ada error, skrip ini dijalankan sebagai sesi psql interaktif — persis seperti yang akan Anda ketik sendiri di terminal:

psql -X 2>&1 <<'SQL'
BEGIN;
UPDATE rekening SET saldo = saldo - 100000 WHERE id = 1;
SELECT id, nama, saldo FROM rekening WHERE id = 1;
UPDATE rekening SET saldo = saldo + 100000 WHERE id = 3;
ROLLBACK;
SELECT id, nama, saldo FROM rekening WHERE id = 1;
SQL

Output yang Dihasilkan:

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

ERROR:  new row for relation "rekening" violates check constraint "saldo_ewallet_maks"
DETAIL:  Failing row contains (3, Dewi Lestari, e-wallet, 1050000.00).
ROLLBACK
 id |     nama     |   saldo   
----+--------------+-----------
  1 | Budi Santoso | 400000.00
(1 row)

Mari kita bedah urutan peristiwanya satu per satu:

  1. BEGIN membuka blok transaksi. Mulai titik ini, semua perubahan berikutnya berstatus belum final — milik sementara sesi kita sendiri.
  2. UPDATE pertama memotong saldo Budi menjadi Rp300.000. SELECT di tengah transaksi memperlihatkan angka Rp300.000 — perubahan yang belum di-commit memang langsung terlihat oleh sesi pembuatnya, tetapi tidak oleh sesi lain.
  3. UPDATE kedua ditolak constraint saldo_ewallet_maks — error yang sama seperti sebelumnya. Karena berada di dalam transaksi, PostgreSQL menandai transaksi ini sebagai gagal (aborted).
  4. ROLLBACK membatalkan seluruh transaksi, termasuk pengurangan saldo Budi yang tadi “sudah berhasil”.
  5. SELECT terakhir membuktikan saldo Budi kembali ke Rp400.000 — nilai sebelum transaksi dimulai. Tidak ada satu rupiah pun yang hilang.

Diagram ini merangkum prinsip all-or-nothing: jalur ke COMMIT hanya tersedia bila seluruh langkah unit kerja sukses. Satu langkah gagal berarti satu-satunya jalan keluar yang aman adalah ROLLBACK, dan PostgreSQL menjamin hasil akhirnya seolah-olah tidak ada satu pun perintah yang pernah dijalankan.

Apa yang Sebenarnya Terjadi di Balik Layar #

Ketika BEGIN dijalankan, PostgreSQL mulai mengumpulkan semua efek perintah ke dalam satu wadah kerja milik sesi tersebut. Perubahan yang terjadi di dalam transaksi terlihat oleh sesi itu sendiri (seperti SELECT tengah yang menunjukkan Rp300.000), sementara sesi lain tetap melihat data lama sampai COMMIT terjadi. COMMIT mengunci semua perubahan menjadi permanen dan langsung terlihat bagi semua orang; ROLLBACK membuang semuanya.

Ada dua jalan keluar tambahan yang tidak memerlukan perintah dari Anda. Pertama, jika sebuah perintah di dalam transaksi gagal, PostgreSQL menandai transaksi sebagai gagal dan menolak perintah-perintah berikutnya sampai transaksi diakhiri. Kedua, jika koneksi terputus di tengah transaksi — aplikasi crash, jaringan mati, psql ditutup — server otomatis menjalankan ROLLBACK implisit. Kombinasi kedua jalan ini yang menyelamatkan data pada kasus-kasus nyata.

AspekAutocommit (default)Transaksi eksplisit
Satuan kerjaSatu perintahKelompok perintah antara BEGIN dan COMMIT/ROLLBACK
Perubahan bersifat permanenSegera setelah perintah suksesHanya setelah COMMIT
Kegagalan di tengah jalanPerintah yang sudah sukses tetap tersimpanSeluruh kelompok dibatalkan
Koneksi terputusTidak ada yang hangus (semua sudah tersimpan)Seluruh kelompok di-rollback otomatis

Anda sekarang memegang konsepnya: transaksi adalah pembatas unit kerja yang menjamin hasil akhir bersih. Namun BEGIN, COMMIT, dan ROLLBACK baru kita lihat sekilas lewat satu skrip — chapter berikutnya membedah ketiga perintah ini secara penuh, mulai dari siklus hidup lengkapnya sampai perilaku PostgreSQL ketika error terjadi di tengah transaksi.

Referensi #

Berkas Sumber

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

autocommit-default.sql

Unduh berkas
sql
UPDATE rekening SET saldo = saldo + 25000 WHERE id = 2;

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

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

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;

cek-kerusakan.sql

Unduh berkas
sql
SELECT id, nama, jenis, saldo FROM rekening ORDER BY id;

SELECT sum(saldo) AS total_saldo FROM rekening;

transfer-dengan-transaksi.sh

Unduh berkas
bash
psql -X 2>&1 <<'SQL'
BEGIN;
UPDATE rekening SET saldo = saldo - 100000 WHERE id = 1;
SELECT id, nama, saldo FROM rekening WHERE id = 1;
UPDATE rekening SET saldo = saldo + 100000 WHERE id = 3;
ROLLBACK;
SELECT id, nama, saldo FROM rekening WHERE id = 1;
SQL

transfer-tanpa-transaksi.sql

Unduh berkas
sql
UPDATE rekening SET saldo = saldo - 100000 WHERE id = 1;

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