Foto oleh panumas nikhomkhai via Pexels

Isolation level menentukan seberapa ketat SQL Server mengisolasi satu transaksi dari transaksi lain yang berjalan bersamaan, dan pilihan level ini secara langsung memengaruhi jenis locking yang terjadi di belakang layar serta anomali data apa saja yang mungkin muncul.

SQL Server menyediakan beberapa isolation level standar: READ UNCOMMITTED (paling longgar, bisa membaca data yang belum di-commit), READ COMMITTED (default, hanya membaca data yang sudah commit), REPEATABLE READ (mencegah data berubah selama transaksi berjalan), dan SERIALIZABLE (paling ketat, mencegah baris baru muncul di tengah transaksi).

Memilih isolation level yang tepat adalah soal menyeimbangkan antara konsistensi data yang dibutuhkan aplikasi dan dampaknya terhadap concurrency, karena semakin ketat isolasinya, semakin besar pula kemungkinan transaksi lain harus menunggu.

READ UNCOMMITTED dan Masalah Dirty Read

READ UNCOMMITTED berisiko dirty read

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT * FROM Orders WHERE Status = 'Processing'; -- Risiko: bisa membaca baris yang sedang diubah -- transaksi lain dan BELUM di-commit (dirty read), -- yang bisa saja akhirnya di-rollback
READ UNCOMMITTED memungkinkan satu transaksi membaca data yang sedang diubah oleh transaksi lain meskipun transaksi tersebut belum di-commit, sebuah kondisi yang disebut dirty read. Ini memberi performa terbaik karena tidak perlu menunggu lock, namun berisiko membaca data yang ternyata akan dibatalkan (rollback) oleh transaksi asalnya, sehingga jarang dipakai untuk data finansial.

READ COMMITTED sebagai Default yang Seimbang

READ COMMITTED adalah default SQL Server

SET TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT * FROM Orders WHERE Status = 'Processing'; -- Hanya membaca data yang sudah di-commit, -- namun data bisa saja berubah lagi segera -- setelah SELECT ini selesai dibaca
READ COMMITTED mencegah dirty read dengan hanya mengizinkan pembacaan data yang sudah di-commit, namun tidak menjamin data tersebut tidak berubah jika dibaca ulang dalam transaksi yang sama (non-repeatable read). Level ini menjadi default karena memberi keseimbangan yang cukup baik antara konsistensi dasar dan performa untuk sebagian besar aplikasi.

REPEATABLE READ Mencegah Perubahan Saat Transaksi Berjalan

REPEATABLE READ menjamin baris yang sudah dibaca tidak berubah

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN TRANSACTION; SELECT Saldo FROM Rekening WHERE RekeningID = 1; -- ...proses lain berjalan... SELECT Saldo FROM Rekening WHERE RekeningID = 1; -- Expected: nilai Saldo pada kedua SELECT dijamin SAMA COMMIT TRANSACTION;
REPEATABLE READ mengunci baris yang sudah dibaca sehingga transaksi lain tidak bisa mengubahnya sampai transaksi ini selesai, menjamin jika baris yang sama dibaca dua kali dalam transaksi, hasilnya akan identik. Konsekuensinya, lock yang dipegang lebih lama dan lebih luas dibanding READ COMMITTED, yang bisa menurunkan concurrency pada sistem dengan banyak transaksi bersamaan.

SERIALIZABLE sebagai Level Paling Ketat

SERIALIZABLE mencegah phantom read

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION; SELECT COUNT(*) FROM Orders WHERE Status = 'Pending'; -- Expected: tidak ada baris BARU dengan Status = 'Pending' -- yang bisa muncul dari transaksi lain sampai COMMIT COMMIT TRANSACTION;
SERIALIZABLE adalah level paling ketat yang mencegah phantom read, yaitu munculnya baris baru yang memenuhi kriteria WHERE di antara dua pembacaan dalam transaksi yang sama. Level ini pada dasarnya membuat transaksi berjalan seolah-olah satu per satu secara berurutan, yang paling aman namun juga paling besar dampaknya terhadap concurrency dan paling rawan menyebabkan deadlock.

Perbedaan READ COMMITTED SNAPSHOT dengan Isolation Level Tradisional

RCSI menghindari blocking dengan membaca versi baris dari tempdb

-- Mengaktifkan READ_COMMITTED_SNAPSHOT di level database ALTER DATABASE SalesDB SET READ_COMMITTED_SNAPSHOT ON; -- Setelah ini, SELECT di bawah READ COMMITTED default membaca versi baris -- dari row versioning store, bukan menunggu shared lock dilepas -- Sesi A (belum commit) BEGIN TRANSACTION; UPDATE Products SET Price = 150000 WHERE ProductID = 1; -- belum COMMIT -- Sesi B, dengan RCSI aktif: tidak diblokir, membaca versi SEBELUM update SELECT Price FROM Products WHERE ProductID = 1; -- Hasil: harga lama, tanpa menunggu lock dari Sesi A

Pada isolation level READ COMMITTED tradisional, pembacaan data akan memakai shared lock sesaat dan harus menunggu jika baris yang sama sedang dikunci exclusive lock oleh transaksi lain yang belum commit, sehingga sering menimbulkan blocking pada sistem dengan trafik baca-tulis yang padat. READ_COMMITTED_SNAPSHOT (RCSI) mengubah mekanisme ini menjadi berbasis row versioning.

Dengan RCSI aktif, setiap kali baris diubah, SQL Server menyimpan versi baris sebelumnya di tempdb, sehingga transaksi lain yang membaca baris tersebut pada waktu bersamaan akan diarahkan membaca versi lama itu alih-alih menunggu lock dilepas; pembaca tidak lagi memblokir atau diblokir oleh penulis, meskipun data yang dibaca mungkin sedikit lebih lama dibanding versi yang sedang diproses transaksi lain.

Konsekuensi mengaktifkan RCSI adalah peningkatan beban pada tempdb karena setiap versi baris harus disimpan sementara, sehingga kapasitas dan performa tempdb perlu dipastikan memadai sebelum fitur ini diaktifkan di database production, dan perilaku ini berbeda dari SNAPSHOT isolation level yang harus diaktifkan secara eksplisit per sesi melalui SET TRANSACTION ISOLATION LEVEL SNAPSHOT.

Fenomena Phantom Read pada Isolation Level REPEATABLE READ

REPEATABLE READ tidak mencegah baris baru muncul di antara dua pembacaan rentang

-- Sesi A: REPEATABLE READ mengunci baris yang SUDAH dibaca, tapi tidak mencegah baris BARU SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN TRANSACTION; SELECT COUNT(*) FROM Orders WHERE Status = 'Pending'; -- hasil: 10 baris -- ...sesi lain menyisipkan baris baru dengan Status='Pending' dan COMMIT... SELECT COUNT(*) FROM Orders WHERE Status = 'Pending'; -- hasil: 11 baris (PHANTOM READ) COMMIT TRANSACTION; -- SERIALIZABLE mencegah phantom read dengan mengunci rentang (range lock) SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION; SELECT COUNT(*) FROM Orders WHERE Status = 'Pending'; -- hasil: 10 baris -- sesi lain yang mencoba INSERT baris Status='Pending' akan DIBLOKIR SELECT COUNT(*) FROM Orders WHERE Status = 'Pending'; -- tetap 10 baris COMMIT TRANSACTION;

Phantom read terjadi ketika sebuah transaksi menjalankan query dengan kondisi filter yang sama dua kali, namun mendapatkan jumlah baris yang berbeda karena transaksi lain berhasil menyisipkan atau menghapus baris yang memenuhi kondisi filter tersebut di antara kedua pembacaan. Isolation level REPEATABLE READ mencegah non-repeatable read dengan mengunci baris-baris yang sudah dibaca agar tidak bisa diubah transaksi lain, tetapi kunci tersebut hanya berlaku pada baris yang sudah ada, bukan pada rentang nilai secara keseluruhan.

Akibatnya, REPEATABLE READ tidak mencegah transaksi lain menyisipkan baris baru yang memenuhi kondisi WHERE yang sama, sehingga query agregasi seperti COUNT(*) atau SUM() yang dijalankan ulang di dalam transaksi yang sama bisa menghasilkan angka yang berbeda meskipun seluruh baris yang dibaca sebelumnya tidak berubah nilainya sama sekali.

SERIALIZABLE adalah isolation level paling ketat yang mengatasi masalah ini dengan menerapkan range lock pada rentang nilai yang dicakup kondisi WHERE, bukan hanya pada baris yang sudah ada, sehingga transaksi lain yang mencoba menyisipkan baris baru ke dalam rentang yang dikunci akan diblokir sampai transaksi pertama selesai. Konsekuensinya, SERIALIZABLE memiliki risiko blocking dan deadlock yang jauh lebih tinggi dibanding isolation level lain, sehingga pemakaiannya dibatasi pada operasi yang benar-benar membutuhkan konsistensi rentang yang ketat.

Snapshot Isolation dan Deteksi Update Conflict Dibanding Serializable

SNAPSHOT isolation mendeteksi update conflict dan membatalkan transaksi, bukan memblokir

ALTER DATABASE SalesDB SET ALLOW_SNAPSHOT_ISOLATION ON; -- Sesi A SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; SELECT Balance FROM Accounts WHERE AccountID = 1; -- membaca versi saat transaksi mulai -- Sesi B mengubah dan commit baris yang sama di antara waktu itu UPDATE Accounts SET Balance = Balance - 100000 WHERE AccountID = 1; -- Sesi A mencoba UPDATE baris yang sama berdasarkan versi lama UPDATE Accounts SET Balance = Balance + 50000 WHERE AccountID = 1; COMMIT TRANSACTION; -- Msg 3960: Snapshot isolation transaction aborted due to update conflict

SNAPSHOT isolation level harus diaktifkan terlebih dahulu di level database melalui ALLOW_SNAPSHOT_ISOLATION ON, dan serupa dengan RCSI karena sama-sama memakai row versioning dari tempdb, namun bedanya SNAPSHOT diaktifkan eksplisit per sesi dengan SET TRANSACTION ISOLATION LEVEL SNAPSHOT, bukan menjadi default otomatis seperti RCSI.

Perbedaan penting dengan SERIALIZABLE adalah cara menangani konflik tulis: SERIALIZABLE mencegah konflik dengan memblokir transaksi lain lewat range lock, sedangkan SNAPSHOT membiarkan kedua transaksi berjalan tanpa blocking namun akan membatalkan transaksi yang belakangan commit jika baris yang sama ternyata sudah diubah transaksi lain sejak baris itu dibaca, ditandai error 3960 yang mengharuskan aplikasi melakukan retry transaksi. Model optimistik semacam ini cocok untuk beban kerja dengan konflik tulis yang jarang terjadi, karena menghindari blocking pada kondisi normal namun tetap membutuhkan logika retry di sisi aplikasi untuk menangani kegagalan yang sesekali muncul.

Kesalahan Umum yang Sering Terjadi

Memakai READ UNCOMMITTED untuk Data Finansial

Menggunakan READ UNCOMMITTED pada laporan keuangan atau saldo bisa menghasilkan angka yang ternyata berasal dari transaksi yang akhirnya dibatalkan, menyebabkan laporan yang tidak akurat tanpa indikasi error apa pun.

Mengabaikan Dampak Isolation Level Tinggi pada Concurrency

Mengganti isolation level ke SERIALIZABLE di seluruh aplikasi tanpa analisis bisa membuat sistem yang sebelumnya cepat menjadi sangat lambat karena banyak transaksi saling menunggu lock yang dipegang lebih lama.

Tidak Mempertimbangkan Snapshot Isolation sebagai Alternatif

SQL Server juga menyediakan SNAPSHOT isolation yang memakai row versioning alih-alih locking untuk mencegah banyak anomali sekaligus tanpa blocking berlebihan, sebuah opsi yang sering terlewat karena harus diaktifkan eksplisit di level database.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Isolation level yang lebih longgar seperti READ COMMITTED memberikan performa dan concurrency lebih baik, cocok untuk kebanyakan operasi CRUD sehari-hari yang tidak terlalu sensitif terhadap anomali kecil.

Isolation level yang lebih ketat seperti REPEATABLE READ atau SERIALIZABLE memberikan jaminan konsistensi lebih kuat yang dibutuhkan untuk operasi finansial atau laporan kritikal, namun dengan konsekuensi performa yang harus diuji dan dipertimbangkan matang-matang pada sistem dengan trafik tinggi.

Ringkasan

Setiap isolation level menawarkan titik keseimbangan berbeda antara konsistensi data dan performa, mulai dari READ UNCOMMITTED yang paling longgar sampai SERIALIZABLE yang paling ketat. Pilih level berdasarkan seberapa kritikal konsistensi data untuk operasi tertentu, bukan sekadar memakai default atau level paling ketat secara membabi buta di semua tempat.

Seri Belajar SQL Server:

← Sebelumnya: Transaction dan ACID pada SQL Server

→ Selanjutnya: Memahami Deadlock dan Cara Menghindarinya (segera terbit)