Understanding Transactions and Locking
"In PostgreSQL, everything is a transaction"
Setiap perintah SQL yang kamu kirim ke PostgreSQL selalu dijalankan dalam sebuah transaksi.
Kalau kamu tidak eksplisit menulis
BEGIN ... COMMIT
, PostgreSQL akan otomatis membuat transaksi "sementara" di balik layar.Jadi satu query tunggal seperti
SELECT * FROM users;
itu sudah dianggap sebuah transaksi oleh PostgreSQL.
Contoh kasus SELECT now(), now();
SELECT now(), now();
SELECT now(), now();
PostgreSQL membuka transaksi → mengeksekusi query → menutup transaksi.
Fungsi
now()
mengembalikan waktu transaksi saat dimulai.Karena kedua
now()
dipanggil dalam transaksi yang sama, maka hasilnya identik (timestamp sama persis).
Outputnya:
now | now
----------------------------+----------------------------
2024-05-24 12:59:33.594603+02 | 2024-05-24 12:59:33.594603+02
Kedua kolom identik, karena waktu transaksi dibekukan saat query mulai.
Kalau query dieksekusi lagi
SELECT now();
PostgreSQL buat transaksi baru → maka nilai
now()
berbeda dari transaksi sebelumnya.Karena transaksi baru dimulai pada waktu yang berbeda.
Perbedaan now()
vs clock_timestamp()
now()
vs clock_timestamp()
now()
→ waktu transaksi, sama selama 1 transaksi berjalan.clock_timestamp()
→ waktu sistem saat ini, selalu berubah sesuai jam nyata.
Contoh:
SELECT now(), clock_timestamp(), pg_sleep(2), now(), clock_timestamp();
Hasil:
now | clock_timestamp
---------------------+---------------------
2024-05-24 13:00:00 | 2024-05-24 13:00:00
2024-05-24 13:00:00 | 2024-05-24 13:00:02
now()
tetap sama (karena masih dalam 1 transaksi).clock_timestamp()
berubah (karena baca langsung dari jam sistem).
"If more than one statement has to be a part of the same transaction, the BEGIN statement must be used"
Kalau kita ingin beberapa perintah SQL berjalan dalam satu transaksi yang sama, kita harus secara eksplisit membuka blok transaksi dengan BEGIN
(atau START TRANSACTION
).
Satu query tunggal = implicit transaction (otomatis dibuka & ditutup).
Banyak query yang harus konsisten = pakai
BEGIN ... COMMIT
supaya semuanya dianggap satu kesatuan atomik.
Contoh BEGIN ... COMMIT
BEGIN ... COMMIT
BEGIN; -- mulai transaksi
SELECT now(); -- eksekusi query pertama
SELECT now(); -- eksekusi query kedua
COMMIT; -- akhiri transaksi
Hasil:
2024-05-24 13:00:39.864604+02
2024-05-24 13:00:39.864604+02
Kenapa sama?
Karena kedua SELECT now()
dieksekusi dalam satu transaksi → maka now()
mengembalikan waktu transaksi (dibekukan sejak BEGIN
).
Perintah COMMIT
COMMIT
COMMIT
→ menyelesaikan transaksi dengan sukses → semua perubahan jadi permanen dan terlihat oleh transaksi lain.Variasi penulisan:
COMMIT
COMMIT WORK
COMMIT TRANSACTION
→ semuanya identik.
Alias:
END
→ sama artinya denganCOMMIT
.
Perintah ROLLBACK
ROLLBACK
ROLLBACK
→ membatalkan transaksi yang sedang berjalan.Semua perubahan dalam transaksi tersebut dibatalkan, tidak akan terlihat oleh transaksi lain.
Alias:
ABORT
→ sama denganROLLBACK
.
Contoh:
BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = 1;
ROLLBACK;
saldo user tidak berubah karena transaksi dibatalkan.
Transaction chaining (COMMIT AND CHAIN
)
COMMIT AND CHAIN
)Biasanya setelah
COMMIT
, transaksi berakhir → kalau mau mulai transaksi baru harus tulisBEGIN
lagi.Dengan
COMMIT AND CHAIN
, PostgreSQL otomatis membuka transaksi baru setelahCOMMIT
, dengan properti yang sama (misalnya mode read-only).Kebalikannya:
COMMIT AND NO CHAIN
→ transaksi berakhir normal, tidak lanjut otomatis.
Contoh penggunaan CHAIN
CHAIN
SHOW transaction_read_only; -- default: Off
BEGIN TRANSACTION READ ONLY; -- mulai transaksi read-only
SELECT 1;
COMMIT AND CHAIN; -- commit + otomatis mulai transaksi baru dengan mode yang sama
SHOW transaction_read_only; -- hasilnya masih On (read-only mode)
SELECT 1;
COMMIT AND NO CHAIN; -- commit + akhiri transaksi (tidak lanjut)
SHOW transaction_read_only; -- kembali Off (default)
Ringkasan perintah penting
BEGIN
/ START TRANSACTION
Memulai blok transaksi
COMMIT
/ END
Menyelesaikan transaksi, simpan perubahan
ROLLBACK
/ ABORT
Membatalkan transaksi, buang perubahan
COMMIT AND CHAIN
Commit + otomatis buka transaksi baru dengan properti yang sama
COMMIT AND NO CHAIN
Commit + selesai (transaksi tidak lanjut)
Gunakan
BEGIN ... COMMIT
kalau butuh beberapa statement satu kesatuan atomik.ROLLBACK
dipakai kalau transaksi gagal/ingin dibatalkan.CHAIN
berguna untuk aplikasi high-latency → mengurangi overhead round trip karena tidak perluBEGIN
ulang.
"Handling errors inside a transaction"
Dalam PostgreSQL, transaksi hanya bisa di-commit kalau benar-benar bebas dari error.
Kalau ada satu error saja, seluruh transaksi dianggap rusak (
aborted
).Setelah error, semua perintah berikutnya dalam transaksi itu akan diabaikan, sampai kita
ROLLBACK
.
Contoh transaksi gagal
BEGIN;
SELECT 1; -- berhasil
SELECT 1 / 0; -- ERROR: division by zero
SELECT 1; -- ERROR: transaksi sudah aborted
COMMIT; -- tidak commit, otomatis jadi ROLLBACK
Output:
ERROR: division by zero
ERROR: current transaction is aborted
ROLLBACK
Jadi begitu ada error (division by zero
), transaksi masuk mode aborted → semua query berikutnya ditolak.
COMMIT
tetap bisa diketik, tapi PostgreSQL akan menggantinya dengan ROLLBACK
.
Solusi: SAVEPOINT
SAVEPOINT
adalah "checkpoint" di dalam transaksi.Kalau ada error setelah
SAVEPOINT
, kita bisa balik lagi ke titik itu pakaiROLLBACK TO SAVEPOINT
.Dengan cara ini, kita tidak perlu membatalkan seluruh transaksi.
Contoh penggunaan SAVEPOINT
BEGIN;
SELECT 1; -- berhasil
SAVEPOINT a; -- bikin checkpoint bernama 'a'
SELECT 2 / 0; -- ERROR: division by zero
ROLLBACK TO SAVEPOINT a; -- balik lagi ke checkpoint 'a'
SELECT 3; -- berhasil
COMMIT; -- transaksi selesai sukses
Output:
1
ERROR: division by zero
ROLLBACK
3
COMMIT
Dengan ROLLBACK TO SAVEPOINT
, error "dibuang", dan transaksi bisa dilanjutkan.
Jumlah SAVEPOINT
PostgreSQL mendukung savepoint dalam jumlah sangat banyak (bahkan ratusan ribu).
Jadi aman dipakai untuk aplikasi besar dengan transaksi kompleks.
Menghapus SAVEPOINT
Kalau savepoint sudah tidak dibutuhkan, bisa dihapus:
RELEASE SAVEPOINT a;
Setelah di-
RELEASE
, savepoint tersebut hilang dan tidak bisa dipakai lagi.Semua savepoint otomatis hilang saat transaksi berakhir (
COMMIT
atauROLLBACK
).
Ringkasan perintah penting
SAVEPOINT name
Buat checkpoint di dalam transaksi
ROLLBACK TO SAVEPOINT name
Kembali ke kondisi transaksi saat savepoint dibuat
RELEASE SAVEPOINT name
Hapus savepoint dari transaksi
(otomatis)
Semua savepoint hilang saat transaksi selesai
Error sekecil apapun membuat transaksi PostgreSQL rusak total.
Kalau tidak pakai savepoint → harus
ROLLBACK
seluruh transaksi.Kalau pakai savepoint → kita bisa "lompat balik" ke titik aman, tanpa kehilangan semua progress.
"Transactional DDLs"
DDL (Data Definition Language) adalah perintah SQL yang mengubah struktur database, contoh:
CREATE TABLE
,ALTER TABLE
,DROP TABLE
,CREATE INDEX
, dll.
Di banyak database komersial (misalnya Oracle, MySQL versi lama), DDL langsung auto-commit → transaksi berjalan otomatis diakhiri.
PostgreSQL berbeda → hampir semua DDL bisa dijalankan dalam transaksi, sehingga bisa digabung dengan DML (insert/update/delete) dan tetap atomik.
Keuntungan: kalau ada error, bisa ROLLBACK
dan database balik ke kondisi semula, tanpa "setengah jalan" perubahan.
Contoh DDL di dalam transaksi
BEGIN;
CREATE TABLE t_test (id int);
ALTER TABLE t_test ALTER COLUMN id TYPE int8;
\d t_test; -- cek struktur tabel
ROLLBACK;
\d t_test; -- tabel hilang, tidak jadi dibuat
Output:
CREATE TABLE
ALTER TABLE
Column | Type
-------+-------
id | bigint
ROLLBACK
Did not find any relation named "t_test".
Jadi walaupun sudah CREATE
+ ALTER
, karena akhirnya ROLLBACK
, tabel tidak pernah benar-benar ada.
DDL yang tidak bisa dipakai dalam transaksi
Beberapa pengecualian (karena sifatnya low-level & global):
DROP DATABASE
CREATE TABLESPACE
DROP TABLESPACE
dll.
Selain itu, hampir semua DDL bisa transactional.
Kenapa ini penting?
Deploy software lebih aman → misalnya upgrade schema CMS (Content Management System).
Kalau upgrade dijalankan dalam satu transaksi:
Lama → tetap jalan.
Baru → bisa jalan.
Campuran setengah upgrade → tidak pernah terjadi (karena atomik).
Bisa gabungkan beberapa modul / migration script → jalankan dalam satu deployment transaction.
Kalau ada error di tengah, tinggal
ROLLBACK
→ database tetap bersih, tidak dalam kondisi "setengah berubah".
DDL dalam transaksi
Bisa (kecuali beberapa pengecualian)
Tidak bisa, auto-commit
Kalau ada error
Bisa ROLLBACK
, semua perubahan dibatalkan
Tidak bisa rollback, struktur sudah berubah
Keuntungan
Aman untuk deployment, atomic, konsisten
Risiko inkonsistensi saat upgrade
PostgreSQL memperlakukan hampir semua DDL seperti DML → bisa transactional. Hal ini bikin deployment, upgrade schema, dan perubahan besar jadi lebih aman, atomik, dan konsisten.
"Understanding basic locking"
PostgreSQL punya mekanisme locking supaya data tetap konsisten ketika ada banyak transaksi yang berjalan bersamaan.
Locking tidak selalu berarti "semua transaksi diblokir". PostgreSQL pakai konsep MVCC (Multi-Version Concurrency Control) → PostgreSQL tidak mengunci data yang sedang dibaca (
read
) oleh sebuah transaksi. Sebaliknya, saat sebuah transaksi melakukanread
, PostgreSQL akan membuat "snapshot" atau versi data yang berlaku pada saat transaksi dimulai. Ketika sebuah transaksi melakukanwrite
(misalnya,UPDATE
atauDELETE
), PostgreSQL tidak menimpa data yang lama. Ia menciptakan versi data yang baru. Versi data yang lama masih ada dan dapat dilihat oleh transaksi lain yang dimulai sebelumwrite
tersebut selesai. Hal inilah yang memungkinkan transaksiread
berjalan tanpa harus menunggu transaksiwrite
selesai.
Contoh awal: membuat tabel & data
CREATE TABLE t_test (id int);
INSERT INTO t_test VALUES (0);
Tabel
t_test
berisi satu baris denganid = 0
.PostgreSQL bisa melayani ribuan user membaca data tanpa saling menghalangi.
Kesimpulan awal:
Banyak pembaca (readers) bisa berjalan bersamaan tanpa blocking.
Kasus read vs write (MVCC)
Misalnya ada 2 transaksi:
BEGIN;
BEGIN;
UPDATE t_test SET id = id + 1 RETURNING *;
→ hasil = 1
SELECT * FROM t_test;
→ hasil = 0 (versi lama)
COMMIT;
COMMIT;
Kenapa hasilnya beda?
Transaction 1 update baris jadi
1
, tapi belum commit.Transaction 2 tetap melihat versi lama (0) karena MVCC → transaksi hanya bisa lihat data yang sudah committed saat dia mulai.
Catatan penting:
Write tidak akan terlihat oleh transaksi lain sampai di-commit.
Read tidak pernah terblokir oleh write.
Kasus write vs write (concurrent update)
Bagaimana kalau dua transaksi update baris yang sama?
BEGIN;
BEGIN;
UPDATE t_test SET id = id + 1 RETURNING *;
→ hasil = 2
UPDATE t_test SET id = id + 1 RETURNING *;
→ menunggu Transaction 1
COMMIT;
masih menunggu
selesai, baris = 2
reread baris, update jadi 3
COMMIT;
Jadi PostgreSQL mengantre update pada row yang sama → update dijalankan berurutan, bukan bersamaan.
Hasil akhir konsisten: id = 3
. Tidak ada update yang hilang (no lost update).
Efek pada pembaca (readers)
Sementara 2 transaksi tadi update baris, pembaca tetap bisa
SELECT
tanpa terblokir.PostgreSQL hanya mengunci row yang sedang diupdate, bukan seluruh tabel.
Artinya: kalau tabel punya 1.000 baris, bisa ada 1.000 transaksi update berjalan paralel, masing-masing mengupdate baris berbeda.
Ringkasan aturan locking dasar
Banyak pembaca (read-only)
Tidak saling blokir
Baca vs Tulis
Reader tetap jalan, lihat versi lama (MVCC)
Tulis vs Tulis (row sama)
Transaksi kedua harus menunggu yang pertama selesai
Tulis vs Tulis (row beda)
Bisa jalan bersamaan (hanya lock row terkait)
Semua write selesai + commit
Perubahan baru terlihat oleh transaksi berikutnya
Inti konsepnya:
PostgreSQL menggunakan MVCC → baca tidak menghalangi tulis, tulis tidak menghalangi baca.
Row-level lock: hanya baris yang diupdate/diubah yang terkunci, bukan seluruh tabel.
Update aman → PostgreSQL memastikan update berurutan, sehingga tidak ada data yang hilang walaupun banyak user update bersamaan.
Dalam praktik nyata, salah satu kesalahan yang sangat sering terjadi adalah saat developer membuat penomoran manual menggunakan SELECT max(id)
lalu menambahkan +1
untuk insert data baru.
Contoh:
Transaction 1
Transaction 2
BEGIN;
BEGIN;
SELECT max(id) FROM product;
→ hasil 17
SELECT max(id) FROM product;
→ hasil 17
Developer memutuskan untuk INSERT (18, ...)
Developer juga memutuskan untuk INSERT (18, ...)
COMMIT;
COMMIT;
Masalah:
Bisa terjadi duplicate key violation (dua row dengan ID sama).
Atau dua entry identik muncul.
Kedua hasil itu jelas tidak diinginkan.
Solusi Sederhana dengan Explicit Locking
PostgreSQL menyediakan LOCK untuk mencegah masalah concurrency seperti ini.
Syntax LOCK:
LOCK [ TABLE ] [ ONLY ] name [ * ] [, ...] [ IN lockmode MODE ] [ NOWAIT ];
Ada 8 jenis lock mode dengan tingkat kekuatan berbeda:
ACCESS SHARE
Lock teringan.
Diambil otomatis oleh
SELECT
.Hanya konflik dengan
ACCESS EXCLUSIVE
.Contoh: SELECT bisa tetap jalan kecuali ada
DROP TABLE
.
ROW SHARE
Diambil oleh
SELECT ... FOR SHARE
.Konflik dengan
EXCLUSIVE
danACCESS EXCLUSIVE
.
ROW EXCLUSIVE
Diambil oleh
INSERT
,UPDATE
,DELETE
.Konflik dengan
SHARE
,SHARE ROW EXCLUSIVE
,EXCLUSIVE
,ACCESS EXCLUSIVE
.
SHARE UPDATE EXCLUSIVE
Diambil oleh
CREATE INDEX CONCURRENTLY
,ANALYZE
,ALTER TABLE
,VACUUM
.Konflik dengan banyak lock lain, termasuk
EXCLUSIVE
danACCESS EXCLUSIVE
.
SHARE
Diambil saat membuat index biasa.
Konflik dengan lock tipe update/row exclusive/exclusive.
SHARE ROW EXCLUSIVE
Diambil oleh
CREATE TRIGGER
, beberapaALTER TABLE
.Konflik dengan semua lock kecuali
ACCESS SHARE
.
EXCLUSIVE
Sangat membatasi.
Menghalangi read maupun write.
Jika diambil, transaksi lain tidak bisa baca/tulis.
ACCESS EXCLUSIVE
Lock terberat.
Dipakai oleh
DROP TABLE
,TRUNCATE
,ALTER TABLE
.Menghalangi semua operasi lain (read & write).
Contoh Solusi dengan Lock
Untuk menghindari masalah duplikasi ID, salah satu cara:
BEGIN;
LOCK TABLE product IN ACCESS EXCLUSIVE MODE;
INSERT INTO product
SELECT max(id) + 1, 'nama_produk'
FROM product;
COMMIT;
Dengan ACCESS EXCLUSIVE MODE
, hanya 1 transaksi yang bisa akses table product
.
Namun, ini sangat berat karena semua read & write lain akan tertahan.
Catatan Penting
Cara ini bisa bekerja tapi tidak recommended.
Karena selama transaksi berjalan, tidak ada user lain yang bisa baca/tulis ke tabel.
Jadi, gunakan ini hanya sebagai solusi darurat.
Best Practice
Daripada manual max(id)+1
, gunakan:
Serial/Bigserial → otomatis sequence.
Identity column (
GENERATED ALWAYS AS IDENTITY
).UUID jika tidak mau numeric sequence.
Dengan begitu, kita tidak perlu explicit locking berat, dan PostgreSQL sudah menjamin keunikan tanpa blocking seluruh table.
Jadi ringkasnya:
Kesalahan umum = pakai
SELECT max(id)+1
untuk insert ID.Bisa pecah karena concurrency → duplicate key.
Solusi darurat = pakai
LOCK TABLE ... IN ACCESS EXCLUSIVE MODE
.Tapi lebih baik pakai sequence/identity supaya aman dan efisien.
Konteks Permasalahan
Ketika aplikasi atau query tiba-tiba lambat / hang, salah satu penyebab umum adalah adanya lock. Namun, mendeteksi lock tidak trivial. PostgreSQL menyediakan pg_stat_activity untuk memantau status session.
Contoh query:
SELECT pid, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE datname = 'test';
Output contoh:
-[ RECORD 3 ]-------------------------------
pid | 23068
wait_event_type | Client
wait_event | ClientRead
query | lock table t_test in access exclusive mode ;
-[ RECORD 4 ]-------------------------------
pid | 23071
wait_event_type | Lock
wait_event | relation
query | SELECT count(*) FROM t_test;
Penjelasan:
PID
23068
→ Session yang sedang menjalankan lock table.PID
23071
→ Session lain yang menunggu lock (wait_event =relation
).Dari sini kita bisa tahu siapa yang nge-hold lock dan siapa yang ke-block.
Masalah Awal
Kasus klasik: kita butuh membuat nomor unik (misalnya nomor invoice).
Cara naif → pakai table lock dengan
ACCESS EXCLUSIVE
.Tapi cara ini berbahaya → semua transaksi lain terblokir hanya demi generate 1 nomor invoice.
Solusi Alternatif dengan Watermark
Alih-alih lock seluruh tabel, kita bisa gunakan row lock dengan tabel watermark.
Langkah-langkah:
-- Tabel invoice utama
CREATE TABLE t_invoice (id int PRIMARY KEY);
-- Tabel watermark (hanya 1 baris untuk counter)
CREATE TABLE t_watermark (id int);
-- Isi awal counter
INSERT INTO t_watermark VALUES (0);
-- Generate nomor invoice baru
WITH x AS (
UPDATE t_watermark
SET id = id + 1
RETURNING *
)
INSERT INTO t_invoice
SELECT * FROM x RETURNING *;
Hasil:
id
----
1
(1 row)
Penjelasan:
UPDATE t_watermark SET id = id + 1 RETURNING *
Mengunci satu baris watermark.
Counter bertambah +1.
Nilai baru dikembalikan.
INSERT INTO t_invoice SELECT * FROM x
Masukkan nilai counter ke tabel invoice.
PostgreSQL menjamin bahwa hanya satu transaksi yang bisa update row watermark pada waktu yang sama → unik terjamin.
Keuntungan Pendekatan Watermark
Hanya row lock, bukan table lock → jauh lebih ringan.
Tidak ada blocking terhadap query
SELECT
di tabel invoice.Skala lebih baik karena hanya 1 baris yang dikunci, bukan seluruh tabel.
Cocok untuk use case: nomor invoice, nomor antrian, kode unik, dll.
Jadi ringkasnya:
Untuk cek lock → gunakan
pg_stat_activity
→ bisa lihat query yang menahan & yang menunggu lock.Untuk solusi penomoran unik → hindari table lock.
Gunakan teknik watermark table + row lock agar tetap konsisten tanpa mengorbankan scalability.
kita sudah bahas bahwa di PostgreSQL semua operasi adalah transaksi. Nah, transaksi itu tidak hanya soal BEGIN/COMMIT/ROLLBACK saja, tapi juga soal bagaimana satu transaksi melihat data yang sedang berubah oleh transaksi lain. Itulah yang disebut dengan transaction isolation level.
Kasus pada READ COMMITTED
(default di PostgreSQL)
READ COMMITTED
(default di PostgreSQL)PostgreSQL secara default menggunakan READ COMMITTED.
Artinya: setiap statement SQL dalam sebuah transaksi akan mengambil snapshot baru dari database.
Jadi kalau transaksi 1 menjalankan query, dia melihat "keadaan saat query itu mulai dieksekusi", meskipun sebelumnya ada query lain di dalam transaksi yang sama.
Hasilnya bisa berubah-ubah kalau query dijalankan ulang, karena ada transaksi lain yang sudah commit di tengah-tengah.
Contoh tabel t_account
punya total saldo = 300
.
Transaction 1 (READ COMMITTED):
BEGIN;
SELECT sum(balance) FROM t_account; -- hasil 300
-- (sementara transaksi 2 jalan)
SELECT sum(balance) FROM t_account; -- hasil 400
COMMIT;
Transaction 2:
BEGIN;
INSERT INTO t_account (balance) VALUES (100);
COMMIT;
Transaction 1 pertama kali lihat 300, tapi query kedua kali dia dapat 400, karena snapshot-nya diambil ulang.
Kasus pada REPEATABLE READ
REPEATABLE READ
Kalau kita set isolasi ke REPEATABLE READ, snapshot dibekukan di awal transaksi, bukan per query.
Jadi selama transaksi berjalan, semua query akan melihat data yang sama meskipun ada transaksi lain yang sudah commit.
Transaction 1 (REPEATABLE READ):
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT sum(balance) FROM t_account; -- hasil 300
-- (sementara transaksi 2 jalan)
SELECT sum(balance) FROM t_account; -- tetap 300
COMMIT;
Transaction 2:
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
INSERT INTO t_account (balance) VALUES (100);
COMMIT;
Transaction 1 tetap melihat 300 sampai akhir, karena snapshot "dibekukan". Baru transaksi berikutnya (atau transaksi lain yang mulai setelah commit transaksi 2) akan melihat 400.
Kapan pakai READ COMMITTED vs REPEATABLE READ?
READ COMMITTED → cocok untuk aplikasi OLTP (online transaksi harian).
Karena user biasanya butuh lihat data terbaru secepat mungkin.
Resiko inkonsistensi kecil, dan performanya oke.
REPEATABLE READ → cocok untuk laporan, analisis, atau batch processing.
Karena data harus konsisten dari awal sampai akhir.
Misalnya, bikin laporan keuangan bulanan: halaman pertama dan halaman terakhir harus punya dasar data yang sama.
Pentingnya
Isolation level ini sering diabaikan developer → akhirnya muncul anomali transaksi seperti:
Non-repeatable read (hasil berubah saat query diulang dalam 1 transaksi).
Phantom read (baris baru muncul di query berikutnya).
Kadang bug ini baru ketahuan bertahun-tahun setelah aplikasi dipakai, karena kasus tertentu jarang muncul tapi bisa bikin error fatal.
Oke, mari saya teruskan dan jabarkan biar makin jelas ya. Kita sudah sampai pada Serializable Snapshot Isolation (SSI) di PostgreSQL.
Serializable Snapshot Isolation (SSI)
PostgreSQL mendukung 3 level isolasi saja:
READ COMMITTED (default)
REPEATABLE READ
SERIALIZABLE
READ UNCOMMITTED
tidak didukung, kalau dicoba akan otomatis diubah jadi READ COMMITTED
.
Konsep dasar SERIALIZABLE
Tujuannya: kalau sebuah transaksi benar-benar aman saat dijalankan sendiri (single user), maka dia juga aman saat dijalankan paralel dengan transaksi lain.
PostgreSQL akan menjaga ilusi seolah-olah semua transaksi dijalankan satu per satu (serial execution), meskipun di realita berjalan paralel.
Ini artinya tidak ada anomali concurrency seperti:
Dirty read
Non-repeatable read
Phantom read
Write skew
Tapi ada konsekuensi:
Performance penalty → PostgreSQL perlu lebih banyak tracking dependency antar transaksi.
Kemungkinan transaksi gagal → PostgreSQL bisa menolak commit dengan error seperti:
ERROR: could not serialize access due to concurrent update
Sehingga aplikasi harus siap retry.
Contoh Deadlock (klasik)
Misalkan tabel:
CREATE TABLE t_deadlock (id int);
INSERT INTO t_deadlock VALUES (1), (2);
Transaction 1:
BEGIN;
UPDATE t_deadlock SET id = id * 10 WHERE id = 1;
UPDATE t_deadlock SET id = id * 10 WHERE id = 2;
Transaction 2:
BEGIN;
UPDATE t_deadlock SET id = id * 10 WHERE id = 2;
UPDATE t_deadlock SET id = id * 10 WHERE id = 1;
Terjadi deadlock karena T1 menunggu T2 dan T2 menunggu T1.
PostgreSQL akan mendeteksi ini, lalu setelah deadlock_timeout
(default 1 detik) akan memaksa salah satu transaksi rollback:
ERROR: deadlock detected
DETAIL: Process 91521 waits for ShareLock...
Contoh Konflik SERIALIZABLE
Kasus lain, misalkan:
Transaction 1:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT * FROM t_deadlock; -- lihat id=1,2
DELETE FROM t_deadlock; -- mencoba delete
Transaction 2:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
DELETE FROM t_deadlock; -- menghapus baris lebih dulu
COMMIT;
Transaction 1 akan gagal saat delete, karena barisnya sudah "mati" akibat transaksi lain. PostgreSQL akan memaksa rollback:
ERROR: could not serialize access due to concurrent update
Kesimpulan
READ COMMITTED → default, tiap statement pakai snapshot baru. Cocok OLTP harian.
REPEATABLE READ → snapshot dibekukan per transaksi, cocok untuk laporan / analisis konsisten.
SERIALIZABLE (SSI) → menjaga illusion eksekusi serial, paling aman, tapi:
bisa ada error "could not serialize access" → aplikasi harus retry.
lebih berat dari sisi performa.
PostgreSQL tidak mendukung READ UNCOMMITTED.
Jadi, kalau aplikasinya butuh konsistensi absolut (misalnya sistem keuangan bank), SERIALIZABLE bisa dipakai. Tapi harus diiringi dengan logic retry di level aplikasi.
Utilizing Advisory Locks
Konsep
PostgreSQL sudah punya sistem locking yang canggih dan efisien (row-level, table-level, MVCC, dsb). Namun, ada kebutuhan khusus: bagaimana kalau kita ingin sinkronisasi antar aplikasi (bukan sekadar antar query database)?
Dari sinilah lahir Advisory Locks.
Advisory lock tidak mengunci row/tabel, melainkan angka (integer atau bigint).
Jadi ini lebih ke mekanisme sinkronisasi aplikasi ketimbang pengendalian data.
Lock ini tidak otomatis dilepas saat COMMIT → developer wajib melakukan unlock secara eksplisit.
Contoh Skenario
Misalkan dua session mencoba mengunci angka 15:
Transaction 1:
BEGIN;
SELECT pg_advisory_lock(15);
-- berhasil dapat lock
Transaction 2:
BEGIN;
SELECT pg_advisory_lock(15);
-- akan menunggu sampai T1 melepas lock
Transaction 1 lanjut:
COMMIT;
SELECT pg_advisory_unlock(15);
Transaction 2 baru bisa lanjut setelah pg_advisory_unlock(15)
dijalankan.
Perhatikan: COMMIT saja tidak otomatis melepas advisory lock.
Fungsi Penting Advisory Locks
pg_advisory_lock(key)
→ ambil lock (akan menunggu kalau sudah dipakai orang lain).pg_advisory_try_lock(key)
→ coba ambil lock, langsung gagal kalau sedang dipakai orang lain.pg_advisory_unlock(key)
→ lepaskan lock spesifik.pg_advisory_unlock_all()
→ lepaskan semua lock yang dimiliki session.
Contoh Kasus Real
Misalnya aplikasi butuh generate nomor invoice unik (mirip watermark table tadi), tapi ingin lebih fleksibel:
-- Session 1
BEGIN;
SELECT pg_advisory_lock(1001); -- lock "nomor invoice generator"
-- jalankan logika buat generate invoice
INSERT INTO invoice VALUES (...);
SELECT pg_advisory_unlock(1001);
COMMIT;
Session lain yang mencoba pg_advisory_lock(1001)
harus menunggu sampai lock dilepas.
Dengan cara ini, kita bisa memastikan tidak ada race condition saat bikin nomor invoice, tanpa mengunci tabel invoice.
Catatan Penting
Advisory locks cocok untuk kontrol concurrency antar aplikasi (misalnya job scheduler, queue worker, generator ID unik).
Karena tidak otomatis dilepas saat COMMIT, kita harus hati-hati agar tidak lupa
unlock
.Jika aplikasi crash, PostgreSQL akan melepaskan lock saat koneksi database ditutup.
Jadi singkatnya:
Row/Table locks → untuk konsistensi data.
Advisory locks → untuk sinkronisasi aplikasi di atas database.
Berikut adalah penjelasan lengkap dan to-the-point dalam bahasa Indonesia mengenai "Optimizing Storage and Managing Cleanup" berdasarkan teks yang diberikan, mencakup semua aspek tanpa ada bagian yang terlewat, sesuai permintaan Anda:
Optimasi Penyimpanan dan Pengelolaan Pembersihan di PostgreSQL
Transaksi adalah bagian integral dari sistem PostgreSQL, namun memiliki konsekuensi tertentu. Salah satu konsekuensi adalah data yang dilihat oleh pengguna konkuren bisa berbeda, tergantung pada status transaksi. Selain itu, operasi seperti DELETE
dan UPDATE
tidak langsung menimpa data karena adanya kemungkinan ROLLBACK
. Hal ini menyebabkan kebutuhan untuk pembersihan data (cleanup) dilakukan secara asinkronus. Berikut adalah penjelasan mendalam tentang cara PostgreSQL mengelola penyimpanan dan pembersihan data.
Pengenalan VACUUM
Transaksi di PostgreSQL menghasilkan "dead rows" (baris mati) yang tidak lagi relevan tetapi tetap ada di penyimpanan sampai dibersihkan. Operasi seperti DELETE
atau UPDATE
menciptakan baris mati karena data lama harus dipertahankan sampai transaksi selesai, untuk mendukung ROLLBACK
atau visibilitas data bagi transaksi lain. Untuk menangani baris mati ini, PostgreSQL menyediakan perintah VACUUM
.
Sintaks VACUUM (berdasarkan dokumentasi PostgreSQL):
VACUUM [ ( option [, ...] ) ] [ table_and_columns [, ...] ]
Opsi yang tersedia:
FULL [boolean]
: Menulis ulang seluruh tabel, mengunci tabel (locking), dan mengembalikan ruang ke sistem file.FREEZE [boolean]
: Membekukan tuple untuk mencegah wraparound ID transaksi.VERBOSE [boolean]
: Menampilkan laporan detail proses VACUUM.ANALYZE [boolean]
: Memperbarui statistik optimizer untuk perencanaan kueri.DISABLE_PAGE_SKIPPING [boolean]
: Memaksa VACUUM memeriksa semua halaman, bahkan yang sudah terlihat oleh semua transaksi.SKIP_LOCKED [boolean]
: Melewatkan tabel yang tidak bisa langsung dikunci untuk menghindari konflik.INDEX_CLEANUP {AUTO | ON | OFF}
: Mengontrol pembersihan indeks.PROCESS_MAIN [boolean]
: Memproses tabel utama.PROCESS_TOAST [boolean]
: Memproses tabel TOAST (untuk data besar seperti teks panjang).TRUNCATE [boolean]
: Mengontrol apakah ruang kosong di akhir tabel dikembalikan ke sistem file.PARALLEL integer
: Menentukan jumlah CPU core untuk VACUUM paralel.SKIP_DATABASE_STATS [boolean]
: Melewatkan pembaruan statistik database.ONLY_DATABASE_STATS [boolean]
: Hanya memperbarui statistik database.BUFFER_USAGE_LIMIT size
: Membatasi penggunaan buffer selama VACUUM.
Fungsi VACUUM:
Mengunjungi halaman yang berpotensi memiliki baris mati.
Menandai ruang kosong di Free Space Map (FSM) untuk digunakan kembali.
Biasanya tidak mengecilkan ukuran tabel, kecuali jika hanya ada baris mati di akhir tabel.
Catatan Penting:
VACUUM tidak mengembalikan ruang ke sistem file dalam kebanyakan kasus, tetapi memungkinkan ruang dalam tabel digunakan kembali.
Penyusutan tabel hanya terjadi jika baris mati berada di akhir tabel, yang jarang terjadi.
Konfigurasi VACUUM dan Autovacuum
Dulu, VACUUM harus dijalankan secara manual, tetapi sekarang PostgreSQL memiliki autovacuum, sebuah proses latar belakang yang otomatis menangani pembersihan. Autovacuum berjalan setiap menit (dikonfigurasi melalui autovacuum_naptime = 1
di postgresql.conf
) dan memeriksa apakah ada pekerjaan pembersihan yang perlu dilakukan. Jika ada, autovacuum dapat memulai hingga tiga proses pekerja (autovacuum_max_workers
).
Kapan autovacuum berjalan? Autovacuum dipicu berdasarkan pengaturan berikut di postgresql.conf
:
autovacuum_vacuum_threshold = 50
: Minimal 50 baris mati diperlukan untuk memicu VACUUM.autovacuum_analyze_threshold = 50
: Minimal 50 baris untuk memperbarui statistik optimizer.autovacuum_vacuum_scale_factor = 0.2
: VACUUM berjalan jika 20% data tabel berubah.autovacuum_analyze_scale_factor = 0.1
: Statistik diperbarui jika 10% data berubah.autovacuum_vacuum_insert_threshold = 1000
: Mulai PostgreSQL 13, autovacuum juga dipicu olehINSERT
(jika lebih dari 1000 baris ditambahkan) untuk menangani beban kerja hanyaINSERT
.
Catatan:
Autovacuum tidak langsung membuat proses pekerja; ia meminta proses utama untuk melakukannya demi menghindari zombie processes dan meningkatkan keandalan.
Autovacuum biasanya juga memperbarui statistik optimizer selama proses VACUUM untuk menghemat waktu.
Masalah Wraparound Transaksi
PostgreSQL menggunakan Transaction ID (XID) untuk mengelola konkurensi. Setiap transaksi diberi ID unik, tetapi ID ini terbatas (bukan tak terbatas). Ketika ID transaksi mendekati batas maksimum, PostgreSQL harus mencegah transaction ID wraparound agar sistem tetap berfungsi dengan benar.
Bagaimana cara kerja wraparound?
PostgreSQL menggunakan watermark untuk melacak urutan transaksi.
VACUUM menyesuaikan watermark ini dengan "membekukan" tuple (mengatur
relfrozenxid
dipg_class
) sehingga transaksi lama tidak lagi relevan.Pengaturan penting di
postgresql.conf
:autovacuum_freeze_max_age = 200000000
: Batas usia transaksi sebelum VACUUM dipaksa untuk mencegah wraparound.autovacuum_multixact_freeze_max_age = 400000000
: Batas usia multixact ID sebelum VACUUM dipaksa.
Mengapa ini penting?
Jika watermark tidak disesuaikan, PostgreSQL bisa kehabisan ID transaksi, menyebabkan database berhenti berfungsi.
VACUUM memastikan ada cukup ID transaksi untuk masa depan.
Commit Log (clog), yang menyimpan status transaksi (running, aborted, committed), juga dibersihkan oleh VACUUM untuk mencegah penumpukan.
Catatan:
Transaksi yang hanya membaca tidak meningkatkan XID (menggunakan virtual transaction ID), sehingga menghemat ID transaksi.
Membekukan tuple (freezing) adalah proses penting untuk performa, yang akan dibahas lebih lanjut pada bab tentang optimasi kueri.
VACUUM FULL
Selain VACUUM biasa, ada VACUUM FULL, yang menulis ulang seluruh tabel dan mengembalikan ruang ke sistem file. Namun, ada kelemahan besar:
VACUUM FULL mengunci tabel, mencegah operasi tulis selama proses berlangsung.
Pada tabel besar, ini bisa menyebabkan downtime yang signifikan, sehingga pengguna mungkin mengira database mati.
Alternatif: Gunakan ekstensi seperti pg_squeeze (https://www.cybertec-postgresql.com/en/introducing-pg_squeeze-a-postgresql-extension-to-auto-rebuild-bloated-tables/), yang menulis ulang tabel tanpa mengunci operasi tulis.
Rekomendasi:
Hindari VACUUM FULL kecuali benar-benar diperlukan, terutama pada tabel besar.
Gunakan VACUUM biasa atau alat seperti pg_squeeze untuk performa lebih baik.
Melihat VACUUM Bekerja
Untuk memahami cara kerja VACUUM, berikut adalah contoh praktis:
Membuat dan mengisi tabel:
CREATE TABLE t_test (id int) WITH (autovacuum_enabled = off);
INSERT INTO t_test SELECT * FROM generate_series(1, 100000);
Tabel
t_test
berisi 100.000 baris.autovacuum_enabled = off
memastikan tidak ada pembersihan otomatis, sehingga kita bisa mengontrol proses VACUUM.
Memeriksa ukuran tabel:
SELECT pg_size_pretty(pg_relation_size('t_test'));
-- Hasil: 3544 kB
pg_relation_size
mengembalikan ukuran tabel dalam byte.pg_size_pretty
mengonversi ke format yang mudah dibaca (kB, MB, dll.).
Melakukan UPDATE:
UPDATE t_test SET id = id + 1;
Operasi
UPDATE
menyalin semua baris karena data lama harus dipertahankan untuk konkurensi dan kemungkinanROLLBACK
.Ukuran tabel bertambah:
SELECT pg_size_pretty(pg_relation_size('t_test'));
-- Hasil: 7080 kB
Menjalankan VACUUM:
VACUUM t_test;
Ukuran tabel tetap sama (7080 kB) karena VACUUM tidak mengembalikan ruang ke sistem file, tetapi menandai ruang kosong di FSM untuk digunakan kembali.
Update berikutnya tidak akan menambah ukuran tabel karena ruang kosong digunakan kembali:
UPDATE t_test SET id = id + 1;
SELECT pg_size_pretty(pg_relation_size('t_test'));
-- Hasil: 7080 kB
Namun, update ketiga akan meningkatkan ukuran tabel karena ruang kosong sudah habis:
UPDATE t_test SET id = id + 1;
SELECT pg_size_pretty(pg_relation_size('t_test'));
-- Hasil: 10 MB
Menghapus data:
DELETE FROM t_test WHERE id > 99000 OR id < 1000;
-- Menghapus 1999 baris
VACUUM t_test;
SELECT pg_size_pretty(pg_relation_size('t_test'));
-- Hasil: 3504 kB
Ukuran tabel menyusut karena baris mati berada di akhir tabel, memungkinkan VACUUM mengembalikan ruang ke sistem file. Ini adalah kasus langka.
Melihat posisi fisik data:
SELECT ctid, * FROM t_test ORDER BY ctid DESC;
ctid
menunjukkan posisi fisik baris di disk.Data di akhir tabel mungkin teracak karena blok terakhir tidak penuh saat tabel pertama kali diisi, dan
UPDATE
mengisi blok tersebut.
Pelajaran Penting:
Memahami penyimpanan adalah kunci untuk performa dan administrasi database.
VACUUM biasanya tidak mengecilkan tabel, tetapi memungkinkan penggunaan kembali ruang kosong.
Fitur Lain VACUUM
VACUUM telah ditingkatkan dengan berbagai fitur untuk mendukung konkurensi dan performa:
DISABLE_PAGE_SKIPPING: Memaksa VACUUM memeriksa semua halaman, bahkan yang sudah terlihat oleh semua transaksi. Berguna untuk pembersihan menyeluruh.
SKIP_LOCKED: Melewatkan tabel yang tidak bisa langsung dikunci, meningkatkan konkurensi pada sistem dengan banyak transaksi.
INDEX_CLEANUP: Mengontrol pembersihan indeks. Menonaktifkan pembersihan indeks bisa mempercepat VACUUM dalam situasi kritis (misalnya, untuk mencegah wraparound), tetapi jarang digunakan.
PROCESS_TOAST: Memungkinkan melewatkan pembersihan tabel TOAST (untuk data besar). Jarang diperlukan.
TRUNCATE: Mengontrol apakah ruang di akhir tabel dikembalikan ke sistem file (default: true).
PARALLEL: Memungkinkan VACUUM menggunakan beberapa CPU core untuk tabel besar, meningkatkan kecepatan.
Ringkasan
Optimasi penyimpanan dan pengelolaan pembersihan di PostgreSQL bergantung pada pemahaman transaksi, konkurensi, dan VACUUM. Berikut poin-poin utama:
Transaksi menciptakan baris mati yang perlu dibersihkan secara asinkronus oleh VACUUM.
Autovacuum otomatis menangani pembersihan dan pembaruan statistik, dengan konfigurasi seperti
autovacuum_vacuum_scale_factor
danautovacuum_freeze_max_age
.VACUUM tidak mengembalikan ruang ke sistem file kecuali baris mati berada di akhir tabel.
Wraparound transaksi dicegah dengan membekukan tuple dan menyesuaikan watermark.
VACUUM FULL harus dihindari pada tabel besar karena mengunci tabel; gunakan alternatif seperti pg_squeeze.
Fitur seperti
SKIP_LOCKED
,PARALLEL
, danINDEX_CLEANUP
meningkatkan fleksibilitas dan performa VACUUM.
Apa Itu Wraparound di PostgreSQL?
Wraparound (atau transaction ID wraparound) adalah masalah potensial di sistem database PostgreSQL yang terjadi ketika nomor identitas transaksi (Transaction ID atau XID) mencapai batas maksimum dan mulai "berputar" kembali dari awal. Ini seperti odometer mobil yang reset ke nol setelah mencapai 999.999 km—bukan berarti mobil mundur, tapi urutan waktu bisa menjadi ambigu. Wraparound bisa menyebabkan kegagalan serius pada database jika tidak ditangani dengan benar. Berikut penjelasan lengkap dan to-the-point:
1. Konteks Dasar: Bagaimana Transaction ID Bekerja di PostgreSQL
PostgreSQL menggunakan Transaction ID (XID) sebagai nomor unik untuk setiap transaksi yang memodifikasi data (seperti INSERT, UPDATE, DELETE). XID ini digunakan untuk mengelola konkurensi (multi-user access) dan visibilitas data.
Contoh: Jika transaksi Anda punya XID 4711 dan transaksi lain punya XID 4712, transaksi 4711 tidak bisa melihat data dari 4712 karena 4712 masih berjalan.
Jika XID Anda 4711 dan transaksi lain adalah 3900 (lebih lama), Anda bisa melihat data tersebut.
XID terbatas: PostgreSQL menggunakan 32-bit integer, sehingga maksimal sekitar 4 miliar XID (tepatnya 2^32 - 1). Ini cukup besar, tapi bisa habis dalam beban kerja tinggi (misalnya, ribuan transaksi per detik).
Tidak semua transaksi membakar XID: Transaksi baca-only (SELECT) hanya punya virtual XID, jadi XID hanya bertambah untuk transaksi yang menulis data.
2. Bagaimana Wraparound Terjadi?
XID diberikan secara berurutan dan monotonik (selalu naik), tapi ketika mencapai batas maksimum (sekitar 4 miliar), sistem mulai "wrap around" dan kembali ke 1.
Masalahnya: PostgreSQL membedakan urutan transaksi berdasarkan perbandingan XID (mana yang lebih baru). Setelah wraparound, XID baru (misalnya 5) bisa terlihat lebih tua daripada XID lama (misalnya 3.999.999.999).
Contoh sederhana:
Transaksi A: XID = 800.000.000 (lama, sudah committed).
Transaksi B: XID = 4.000.000.000 (baru, masih berjalan).
Setelah wraparound: Transaksi C: XID = 5 (seharusnya baru, tapi terlihat lebih tua dari A).
Tanpa mekanisme pelindung, PostgreSQL bisa salah mengasumsikan urutan waktu, menyebabkan data tidak konsisten atau transaksi gagal.
PostgreSQL mencegah kebingungan ini dengan watermark (penanda batas aman) di katalog sistem seperti
pg_class
:relfrozenxid
: Watermark untuk XID biasa.relminmxid
: Watermark untuk multixact ID (ID untuk operasi multi-transaksi, seperti locking bersama).
3. Dampak Wraparound Jika Tidak Ditangani
Pencegahan otomatis: PostgreSQL akan mengunci database (menolak transaksi baru) jika watermark mendekati batas wraparound, untuk menghindari kegagalan total.
Downtime: Database bisa "beku" selama berjam-jam atau hari, tergantung ukuran data. Pada tabel besar, ini berarti aplikasi tidak bisa menulis data.
Risiko lain:
Commit Log (CLOG) membengkak karena menyimpan status setiap XID (running, committed, aborted).
Statistik optimizer tidak akurat, memengaruhi performa kueri.
Dalam kasus ekstrem, database bisa crash atau memerlukan restorasi dari backup.
4. Cara Mencegah Wraparound: Peran VACUUM dan Autovacuum
VACUUM adalah "pembersih" utama yang menyesuaikan watermark dengan membekukan tuple (freezing):
VACUUM memindai baris mati (dead rows) dan mengubah tuple lama menjadi "beku" (tidak lagi bergantung pada XID lama).
Ini memindahkan watermark maju, memastikan XID baru tetap "lebih tua" dari yang lama secara logis.
Autovacuum (proses otomatis) menjalankan VACUUM secara berkala untuk mencegah wraparound:
Pengaturan kunci di
postgresql.conf
:autovacuum_freeze_max_age = 200000000
(default): Jika usia XID tabel melebihi ini, VACUUM dipaksa untuk membekukan tuple dan mencegah wraparound.autovacuum_multixact_freeze_max_age = 400000000
(default): Sama untuk multixact ID.
Autovacuum memantau usia XID setiap menit dan memulai pekerja jika diperlukan.
Tips pencegahan:
Pantau dengan query:
SELECT datname, age(datfrozenxid) FROM pg_database;
(usia XID database dalam transaksi).Jika mendekati batas (misalnya > 1 miliar), tingkatkan frekuensi autovacuum atau jalankan VACUUM manual.
Hindari menonaktifkan autovacuum kecuali untuk tabel sementara (seperti staging di data warehouse).
Pada versi PostgreSQL terbaru (seperti 17), fitur seperti
autovacuum_vacuum_insert_threshold
membantu menangani beban INSERT-only.
5. Contoh Praktis
Bayangkan tabel dengan 1 miliar transaksi:
XID awal: 1 hingga 1.000.000.000.
Setelah VACUUM rutin, watermark naik ke 900.000.000 → XID lama diabaikan.
Tanpa VACUUM: Saat XID mencapai 4 miliar, wraparound terjadi → database lock untuk VACUUM paksa (bisa memakan waktu lama pada tabel besar).
Ringkasan
Wraparound adalah risiko kegagalan database PostgreSQL akibat keterbatasan Transaction ID yang "berputar" setelah batas maksimum. Ini ditangani secara proaktif oleh VACUUM dan autovacuum, yang membekukan tuple lama dan menyesuaikan watermark untuk menjaga urutan waktu transaksi tetap aman. Pemantauan rutin dan konfigurasi yang tepat (seperti autovacuum_freeze_max_age
) mencegah downtime. Ini adalah konsep kunci untuk stabilitas jangka panjang, terutama pada database produksi dengan beban tinggi.
Read Phenomena
Dirty Read
Definisi: Ketika sebuah transaksi membaca data yang belum di-commit oleh transaksi lain.
Kapan terjadi: Pada isolation level READ UNCOMMITTED.
Contoh:
T1 update saldo user
A
dari 100 → 50 (belum commit).T2 membaca saldo
A
dan melihat 50.T1 kemudian rollback, saldo
A
kembali 100.T2 sudah membaca data "kotor" (dirty).
Non-Repeatable Read
Definisi: Ketika sebuah transaksi membaca baris yang sama lebih dari sekali, tapi hasilnya berbeda karena ada transaksi lain yang mengubah atau menghapus baris itu lalu commit.
Kapan terjadi: Pada isolation level READ COMMITTED.
Contoh:
T1 baca saldo user
A
, hasil = 100.T2 update saldo
A
jadi 200 lalu commit.T1 baca lagi saldo
A
, hasil = 200 (berbeda dari sebelumnya).Data yang dibaca tidak konsisten antar read dalam transaksi T1.
Phantom Read
Definisi: Ketika sebuah transaksi melakukan query dengan kondisi (WHERE clause) yang sama lebih dari sekali, tetapi hasil jumlah baris yang dikembalikan berbeda karena transaksi lain menambahkan atau menghapus baris baru yang memenuhi kondisi.
Kapan terjadi: Pada isolation level REPEATABLE READ.
Contoh:
T1:
SELECT * FROM orders WHERE amount > 100
→ dapat 5 baris.T2: Insert order baru dengan
amount = 150
, lalu commit.T1 jalankan query yang sama lagi → dapat 6 baris.
Muncul phantom row (baris hantu) yang sebelumnya tidak ada.
Lost Update
Definisi: Ketika dua transaksi meng-update data yang sama, dan salah satu update tertindih (hilang) karena transaksi lain overwrite hasilnya.
Kapan terjadi: Biasanya di isolation level rendah, tanpa locking eksplisit.
Contoh:
T1 baca saldo
A = 100
, lalu kurangi jadi 90.T2 juga baca saldo
A = 100
, lalu kurangi jadi 80.Keduanya commit, saldo
A
akhirnya 80 (update T1 hilang).
Ringkasan Isolation Level dan Anomalinya
Read Uncommitted
✅ Bisa
✅ Bisa
✅ Bisa
Read Committed
❌ Tidak
✅ Bisa
✅ Bisa
Repeatable Read
❌ Tidak
❌ Tidak
✅ Bisa
Serializable
❌ Tidak
❌ Tidak
❌ Tidak
Jadi intinya:
Dirty Read → baca data yang belum commit.
Non-Repeatable Read → baca baris yang sama, hasil berubah.
Phantom Read → baca query dengan kondisi sama, jumlah baris berubah.
Lost Update → hasil update hilang karena tabrakan.
1. "Row-level locks are acquired when rows are being updated (or deleted or marked for update)."
Artinya:
Begitu ada operasi UPDATE atau DELETE, PostgreSQL otomatis memasang row-level exclusive lock pada baris yang ditarget.
Bahkan pada kasus
SELECT ... FOR UPDATE
, row juga dianggap “marked for update” sehingga PostgreSQL pasang lock juga.
Contoh:
-- Session 1
BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- row id=1 terkunci
👉 Baris id=1
sekarang di-lock, tidak bisa diubah transaksi lain sampai COMMIT/ROLLBACK.
2. "Row-level locks don't affect data querying."
Maksudnya:
Lock ini hanya menghalangi transaksi lain yang ingin mengubah row (UPDATE, DELETE, SELECT ... FOR UPDATE/SHARE).
SELECT biasa (read-only) tidak terganggu, karena PostgreSQL menggunakan MVCC (Multi-Version Concurrency Control) → pembaca bisa tetap melihat versi row sesuai snapshot transaksinya.
Contoh:
-- Session 1
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- row id=1 terkunci
-- Session 2
SELECT * FROM users WHERE id = 1;
-- tetap bisa baca (tidak blocking), hanya membaca snapshot
3. "They block writers to the same row only."
Row lock hanya berlaku per baris, bukan seluruh tabel.
Jadi jika satu row terkunci, transaksi lain masih bisa menulis row lain.
Contoh:
-- Session 1
BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = 1;
-- lock hanya di row id=1
-- Session 2
UPDATE users SET balance = balance - 50 WHERE id = 2;
-- bisa jalan langsung, karena row berbeda
👉 Berbeda dengan table lock, di mana semua row di tabel ikut terdampak.
4. "PostgreSQL doesn't remember any information about modified rows in memory and so has no limit to the number of rows locked at one time."
Maksudnya:
PostgreSQL tidak punya in-memory lock table dengan ukuran terbatas (seperti database lama yang harus nyimpen daftar row lock di RAM).
Lock row di PostgreSQL di-embed langsung di row tuple (di heap page).
Dengan cara ini, PostgreSQL secara teori bisa mengunci jutaan row dalam satu transaksi tanpa ada limit “jumlah lock” di memori.
Tentu saja tetap ada overhead (WAL, MVCC cleanup, disk I/O), tapi tidak ada batas keras pada jumlah row lock.
Contoh:
-- Session 1
BEGIN;
UPDATE users SET balance = balance - 10;
-- misalnya ada 1 juta row, semuanya di-lock
-- PostgreSQL sanggup, karena lock tersimpan di row level (tidak ada cap di memory)
Jadi singkatnya:
Row lock otomatis saat UPDATE/DELETE/SELECT ... FOR UPDATE.
Tidak ganggu SELECT biasa, hanya memblokir transaksi yang mau menulis ke row sama.
Scoped ke row, bukan ke tabel.
Tidak ada batasan jumlah row lock, karena lock dicatat di row, bukan di memory global.
Last updated