Saya kemarin berdiskusi dengan Kendal Van Dyke (@SQLDBA) tentang IDENT_CURRENT(). Pada dasarnya, Kendal memiliki kode ini, yang telah ia uji dan percayai sendiri, dan ingin tahu apakah ia dapat mengandalkan IDENT_CURRENT() yang akurat dalam lingkungan serentak skala tinggi:
BEGIN TRANSACTION; INSERT dbo.TableName(ColumnName) VALUES('Value'); SELECT IDENT_CURRENT('dbo.TableName'); COMMIT TRANSACTION;
Alasan dia harus melakukan ini adalah karena dia perlu mengembalikan nilai IDENTITY yang dihasilkan ke klien. Cara umum yang kami lakukan adalah:
- SCOPE_IDENTITY()
- klausa OUTPUT
- @@IDENTITY
- IDENT_CURRENT()
Beberapa di antaranya lebih baik daripada yang lain, tetapi itu telah dilakukan sampai mati, dan saya tidak akan membahasnya di sini. Dalam kasus Kendal, IDENT_CURRENT adalah pilihan terakhir dan satu-satunya, karena:
- TableName memiliki pemicu INSTEAD OF INSERT, membuat klausa SCOPE_IDENTITY() dan OUTPUT tidak berguna dari pemanggil, karena:
- SCOPE_IDENTITY() mengembalikan NULL, karena penyisipan sebenarnya terjadi dalam lingkup yang berbeda
- klausa OUTPUT menghasilkan pesan kesalahan 334 karena pemicunya
- Dia menghilangkan @@IDENTITY; pertimbangkan bahwa pemicu INSTEAD OF INSERT sekarang dapat (atau mungkin nanti diubah menjadi) menyisipkan ke tabel lain yang memiliki kolom IDENTITAS mereka sendiri, yang akan mengacaukan nilai yang dikembalikan. Ini juga akan menggagalkan SCOPE_IDENTITY(), jika memungkinkan.
- Dan akhirnya, dia tidak bisa menggunakan klausa OUTPUT (atau hasil dari kueri kedua dari tabel semu yang dimasukkan setelah penyisipan akhirnya) di dalam pemicu, karena kemampuan ini memerlukan pengaturan global, dan tidak digunakan lagi sejak SQL Server 2005. Dapat dimengerti bahwa kode Kendal harus kompatibel ke depan dan, jika memungkinkan, tidak sepenuhnya bergantung pada database atau pengaturan server tertentu.
Jadi, kembali ke realita Kendal. Kodenya tampaknya cukup aman – bagaimanapun juga, dalam sebuah transaksi; apa yang bisa salah? Baiklah, mari kita lihat beberapa kalimat penting dari dokumentasi IDENT_CURRENT (penekanan dari saya, karena peringatan ini ada untuk alasan yang baik):
Mengembalikan nilai identitas terakhir yang dihasilkan untuk tabel atau tampilan tertentu. Nilai identitas terakhir yang dihasilkan dapat untuk setiap sesi dan cakupan apa pun .…
Berhati-hatilah dalam menggunakan IDENT_CURRENT untuk memprediksi nilai identitas yang dihasilkan berikutnya. nilai yang dihasilkan sebenarnya mungkin berbeda dari IDENT_CURRENT plus IDENT_INCR karena penyisipan dilakukan oleh sesi lain .
Transaksi hampir tidak disebutkan di badan dokumen (hanya dalam konteks kegagalan, bukan konkurensi), dan tidak ada transaksi yang digunakan dalam sampel mana pun. Jadi, mari kita uji apa yang dilakukan Kendal, dan lihat apakah kita bisa membuatnya gagal saat beberapa sesi dijalankan secara bersamaan. Saya akan membuat tabel log untuk melacak nilai yang dihasilkan oleh setiap sesi – baik nilai identitas yang benar-benar dihasilkan (menggunakan pemicu setelah), dan nilai yang diklaim dihasilkan menurut IDENT_CURRENT().
Pertama, tabel dan pemicu:
-- the destination table: CREATE TABLE dbo.TableName ( ID INT IDENTITY(1,1), seq INT ); -- the log table: CREATE TABLE dbo.IdentityLog ( SPID INT, seq INT, src VARCHAR(20), -- trigger or ident_current id INT ); GO -- the trigger, adding my logging: CREATE TRIGGER dbo.InsteadOf_TableName ON dbo.TableName INSTEAD OF INSERT AS BEGIN INSERT dbo.TableName(seq) SELECT seq FROM inserted; -- this is just for our logging purposes here: INSERT dbo.IdentityLog(SPID,seq,src,id) SELECT @@SPID, seq, 'trigger', SCOPE_IDENTITY() FROM inserted; END GO
Sekarang, buka beberapa jendela kueri, dan rekatkan kode ini, jalankan sedekat mungkin untuk memastikan tumpang tindih:
SET NOCOUNT ON; DECLARE @seq INT = 0; WHILE @seq <= 100000 BEGIN BEGIN TRANSACTION; INSERT dbo.TableName(seq) SELECT @seq; INSERT dbo.IdentityLog(SPID,seq,src,id) SELECT @@SPID,@seq,'ident_current',IDENT_CURRENT('dbo.TableName'); COMMIT TRANSACTION; SET @seq += 1; END
Setelah semua jendela kueri selesai, jalankan kueri ini untuk melihat beberapa baris acak di mana IDENT_CURRENT mengembalikan nilai yang salah, dan jumlah total baris yang terpengaruh oleh nomor yang salah dilaporkan ini:
SELECT TOP (10) id_cur.SPID, [ident_current] = id_cur.id, [actual id] = tr.id, total_bad_results = COUNT(*) OVER() FROM dbo.IdentityLog AS id_cur INNER JOIN dbo.IdentityLog AS tr ON id_cur.SPID = tr.SPID AND id_cur.seq = tr.seq AND id_cur.id <> tr.id WHERE id_cur.src = 'ident_current' AND tr.src = 'trigger' ORDER BY NEWID();
Berikut adalah 10 baris saya untuk satu tes:
Saya terkejut bahwa hampir sepertiga dari baris itu mati. Hasil Anda pasti akan bervariasi, dan mungkin bergantung pada kecepatan drive Anda, model pemulihan, pengaturan file log, atau faktor lainnya. Pada dua mesin yang berbeda, saya memiliki tingkat kegagalan yang sangat berbeda – dengan faktor 10 (mesin yang lebih lambat hanya memiliki sekitar 10.000 kegagalan, atau kira-kira 3%).
Segera jelas bahwa transaksi tidak cukup untuk mencegah IDENT_CURRENT menarik nilai IDENTITY yang dihasilkan oleh sesi lain. Bagaimana dengan transaksi SERIALIZABLE? Pertama, bersihkan dua tabel:
TRUNCATE TABLE dbo.TableName; TRUNCATE TABLE dbo.IdentityLog;
Kemudian, tambahkan kode ini ke awal skrip di beberapa jendela kueri, dan jalankan lagi secara bersamaan:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Kali ini, ketika saya menjalankan kueri terhadap tabel IdentityLog, ini menunjukkan bahwa SERIALIZABLE mungkin sedikit membantu, tetapi tidak menyelesaikan masalah:
Dan walaupun salah tetap salah, terlihat dari hasil sample saya bahwa nilai IDENT_CURRENT biasanya hanya turun satu atau dua. Namun, kueri ini harus menghasilkan bahwa itu bisa * jauh * tidak aktif. Dalam pengujian saya, hasil ini setinggi 236:
SELECT MAX(ABS(id_cur.id - tr.id)) FROM dbo.IdentityLog AS id_cur INNER JOIN dbo.IdentityLog AS tr ON id_cur.SPID = tr.SPID AND id_cur.seq = tr.seq AND id_cur.id <> tr.id WHERE id_cur.src = 'ident_current' AND tr.src = 'trigger';
Melalui bukti ini kita dapat menyimpulkan bahwa IDENT_CURRENT tidak aman untuk transaksi. Tampaknya mengingatkan pada masalah serupa tetapi hampir berlawanan, di mana fungsi metadata seperti OBJECT_NAME() diblokir – bahkan ketika tingkat isolasi READ UNCOMMITTED – karena mereka tidak mematuhi semantik isolasi di sekitarnya. (Lihat Hubungkan Item #432497 untuk detail lebih lanjut.)
Di permukaan, dan tanpa mengetahui lebih banyak tentang arsitektur dan aplikasi, saya tidak memiliki saran yang bagus untuk Kendal; Saya baru tahu bahwa IDENT_CURRENT *bukan* jawabannya. :-) Jangan menggunakannya. Untuk apapun. Pernah. Pada saat Anda membaca nilainya, itu bisa saja salah.