Episode ini membahas prinsip ACID, perintah transaksi BEGIN COMMIT ROLLBACK dan SAVEPOINT, isolation level di PostgreSQL, serta locking eksplisit dengan SELECT FOR UPDATE dan FOR SHARE untuk mencegah race condition dan double spending.

Selamat datang di episode 11 series Belajar SQL PostgreSQL! Sejauh ini kita belajar mengolah data — tapi ada satu pertanyaan besar yang belum kita jawab: apa yang terjadi kalau dua user mengubah data yang sama secara bersamaan? Bayangkan dua orang membeli produk yang stoknya tinggal satu, dua transaksi menarik uang dari saldo yang sama, atau dua order memakai voucher yang sama. Tanpa perlindungan yang tepat, semua skenario ini bisa menghasilkan data yang salah dan hilangnya uang sungguhan.
Jawaban atas semua masalah ini adalah transaction — mekanisme yang menjamin sekelompok operasi database berjalan sebagai satu kesatuan yang tak terpisahkan. Dan di balik transaction, ada empat pilar yang disebut ACID. Memahami keduanya adalah batas antara aplikasi yang "kelihatan jalan" dengan aplikasi yang benar-benar aman.
Di episode ini, kita akan membahas prinsip ACID, perintah transaksi BEGIN, COMMIT, ROLLBACK, dan SAVEPOINT, fenomena concurrency dan isolation level di PostgreSQL, serta locking eksplisit dengan SELECT ... FOR UPDATE untuk mencegah race condition.
ACID adalah akronim empat properti yang menjamin keandalan transaksi database:
| Properti | Arti | Analogi |
|---|---|---|
| Atomicity | Transaksi all-or-nothing: semua statement sukses, atau semuanya batal | Transfer uang: uang keluar dan masuk, atau tidak sama sekali |
| Consistency | Database berpindah dari satu state valid ke state valid lain | Constraint selalu terjaga setelah transaksi |
| Isolation | Transaksi berjalan seolah-olah sendirian | Dua kasir tidak saling melihat setengah pekerjaan |
| Durability | Perubahan yang sudah commit tetap ada walau server crash | Bukti transaksi tidak hilang setelah mati listrik |
Ketiga perintah ini adalah siklus hidup transaksi:
BEGIN: memulai transaksi.COMMIT: menyimpan semua perubahan secara permanen.ROLLBACK: membatalkan semua perubahan sejak BEGIN.BEGIN;
INSERT INTO orders (customer_id, total)
VALUES ('1c2d3e4f-0000-0000-0000-000000000001', 150000);
UPDATE inventory
SET stock = stock - 1
WHERE product_id = '9a8b7c6d-0000-0000-0000-000000000001';
COMMIT;Jika salah satu statement di atas gagal — katakanlah stok tidak cukup dan CHECK (stock >= 0) menolak — kalian bisa memilih ROLLBACK untuk mengembalikan semuanya, termasuk insert order yang sudah jalan. Atomicity dijamin.
Tip
Satu rahasia yang jarang diketahui pemula: setiap statement di psql sudah berjalan dalam transaksi otomatis yang langsung di-commit (autocommit). Jadi BEGIN hanyalah cara menyatakan "mulai sekarang, saya yang pegang kendali sampai saya bilang COMMIT atau ROLLBACK". Kebiasaan menulis transaksi eksplisit untuk operasi multi-step adalah penanda developer berpengalaman.
Kadang kita tidak ingin membatalkan seluruh transaksi — hanya sebagian. SAVEPOINT membuat titik pemulihan parsial:
BEGIN;
INSERT INTO orders (customer_id, total) VALUES (1, 50000);
SAVEPOINT order_created;
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (10, 20, 1);
ROLLBACK TO order_created;
UPDATE order_items SET quantity = 2 WHERE order_id = 10;
COMMIT;Setelah ROLLBACK TO order_created, hanya perubahan sejak savepoint tersebut yang dibatalkan — insert order tetap dipertahankan. Ini berguna untuk alur bisnis yang bercabang dengan fallback.
Isolation membahas: seberapa "terisolasi" transaksi satu dari yang lain. Tanpa isolasi yang cukup, muncul fenomena concurrency yang mendistorsi hasil.
| Fenomena | Deskripsi | Contoh |
|---|---|---|
| Dirty Read | Membaca data transaksi lain yang belum commit | Membaca saldo yang belum final |
| Non-Repeatable Read | Data yang sama berubah di antara dua pembacaan | Baca harga, harga berubah sebelum dibaca ulang |
| Phantom Read | Kumpulan baris hasil query berubah di antara dua eksekusi | Baca daftar produk, ada produk baru muncul |
| Serialization Anomaly | Hasil transaksi bersamaan tidak seperti eksekusi berurutan | Overbooking tiket karena dua transaksi "menang" |
PostgreSQL punya tiga isolation level (PostgreSQL tidak pernah mengalami dirty read karena MVCC):
| Level | Mencegah | Masih memungkinkan |
|---|---|---|
| Read Committed (default) | Dirty Read | Non-Repeatable Read, Phantom Read |
| Repeatable Read | Dirty, Non-Repeatable Read | Phantom Read (terkadang), Serialization Anomaly |
| Serializable | Semua fenomena | — (gagal dengan error serialization bila konflik) |
Read Committed adalah default PostgreSQL: setiap statement melihat snapshot terbaru yang sudah commit. Untuk transaksi yang panjang dan butuh pandangan konsisten, gunakan Repeatable Read. Serializable dipakai saat akurasi ketat lebih penting daripada throughput.
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT balance FROM accounts WHERE id = 1;
COMMIT;Note
MVCC (Multi-Version Concurrency Control) adalah alasan PostgreSQL bisa punya isolasi kuat tanpa mengorbankan concurrency: setiap transaksi melihat "snapshot" versi data pada saat ia mulai, sehingga pembaca tidak pernah memblokir penulis dan penulis tidak pernah memblokir pembaca. Inilah warisan yang disebut-sebut di episode 1.
Isolation level melindungi dari fenomena baca, tapi tidak mencegah write-write race. Bayangkan dua transaksi sama-sama membaca stok 5, keduanya mengurangi 1, dan keduanya menulis 4 — padahal seharusnya jadi 3. Ini disebut race condition, dan inilah asal dari bencana double spending dan overbooking.
Solusinya adalah locking eksplisit: kunci baris agar transaksi lain menunggu.
SELECT ... FOR UPDATE mengunci baris yang dibaca sampai transaksi selesai. Transaksi lain yang mencoba mengunci atau mengubah baris yang sama akan menunggu:
BEGIN;
SELECT stock
FROM inventory
WHERE product_id = '9a8b7c6d-0000-0000-0000-000000000001'
FOR UPDATE;
UPDATE inventory
SET stock = stock - 1
WHERE product_id = '9a8b7c6d-0000-0000-0000-000000000001';
COMMIT;Dengan FOR UPDATE, transaksi kedua yang memproses order untuk produk yang sama akan menunggu sampai transaksi pertama commit. Race condition double spending berhasil dicegah.
SELECT ... FOR SHARE mengunci baris agar tidak bisa diubah oleh transaksi lain, tapi masih bisa dibaca (bahkan di-lock share oleh banyak transaksi). Cocok saat beberapa proses ingin memastikan data tidak berubah selama memproses:
BEGIN;
SELECT id, price FROM products WHERE id = 5 FOR SHARE;
COMMIT;Warning
FOR UPDATE adalah keputusan desain — bukan default. Terlalu banyak lock menurunkan concurrency; terlalu sedikit menyebabkan race condition. Kunci transaksi harus sesingkat mungkin: lakukan di dalam transaction kecil yang langsung COMMIT, jangan memegang lock sambil menunggu input user atau panggilan API lambat — kalau tidak, barisan antrian menunggu akan menggunung.
Perlu ditegaskan: dengan MVCC, baris yang di-lock FOR UPDATE masih bisa dibaca oleh transaksi lain (mereka melihat versi snapshot lama). Yang diblokir hanya pembaruan — transaksi lain harus menunggu sebelum mengunci/mengubah baris tersebut. Ini kombinasi terbaik: konsistensi untuk penulis, concurrency untuk pembaca.
| # | Kesalahan | Gejala | Solusi |
|---|---|---|---|
| 1 | Update stok tanpa FOR UPDATE | Double spending / stok negatif | Kunci baris dengan FOR UPDATE |
| 2 | Memegang lock terlalu lama | Query lain menumpuk menunggu | Transaksi pendek, langsung COMMIT |
| 3 | Lupa COMMIT | Perubahan "hilang" dari transaksi lain | Selalu akhiri dengan COMMIT/ROLLBACK |
| 4 | Mengharapkan psql tanpa BEGIN berperilaku transaksional | Perubahan langsung terlihat | Gunakan BEGIN eksplisit untuk multi-step |
Di episode 11 ini kita sudah memahami fondasi keandalan database: prinsip ACID (Atomicity, Consistency, Isolation, Durability), perintah transaksi BEGIN, COMMIT, ROLLBACK, dan SAVEPOINT, fenomena concurrency dan isolation level, serta locking eksplisit dengan SELECT ... FOR UPDATE dan FOR SHARE untuk mencegah race condition.
Inti yang harus dibawa pulang:
BEGIN → statement → COMMIT atau ROLLBACK adalah pola transaksi yang benar.SAVEPOINT memungkinkan rollback parsial tanpa membatalkan seluruh transaksi.SELECT ... FOR UPDATE adalah senjata mencegah double spending dan overbooking.Di episode 12 selanjutnya, kita masuk ke dunia database objects tingkat lanjut: Views, Materialized Views & Generated Columns — mulai dari standard view untuk menyederhanakan akses data, materialized view untuk query analitik berkecepatan tinggi dengan REFRESH ... CONCURRENTLY, hingga generated columns yang menghitung nilai secara otomatis.