Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Untuk terakhir kalinya, TIDAK, Anda tidak dapat mempercayai IDENT_CURRENT()

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.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. T-SQL vs SQL

  2. Memformat Data dalam Visualisasi Desktop Power BI

  3. Cara Mengganti Nama Kolom di SQL

  4. 19 Sumber Daya Online untuk Mempelajari Kesalahan Desain Basis Data

  5. T-SQL Selasa #33 :Trick Shots :Skema Switch-A-Roo