Karena Anda menggunakan Urutan, Anda dapat menggunakan fungsi NEXT VALUE FOR yang sama - yang sudah Anda miliki di Batasan Default pada Id
bidang Kunci Utama - untuk menghasilkan Id
nilai baru sebelumnya. Menghasilkan nilai terlebih dahulu berarti Anda tidak perlu khawatir tidak memiliki SCOPE_IDENTITY
, yang berarti Anda tidak perlu OUTPUT
klausa atau melakukan tambahan SELECT
untuk mendapatkan nilai baru; Anda akan memiliki nilai sebelum melakukannya INSERT
, dan Anda bahkan tidak perlu dipusingkan dengan SET IDENTITY INSERT ON / OFF
:-)
Sehingga itu mengurus sebagian dari keseluruhan situasi. Bagian lainnya adalah menangani masalah konkurensi dari dua proses, pada saat yang bersamaan, tidak menemukan baris yang ada untuk string yang sama persis, dan melanjutkan dengan INSERT
. Kekhawatirannya adalah tentang menghindari pelanggaran Batasan Unik yang akan terjadi.
Salah satu cara untuk menangani masalah konkurensi ini adalah dengan memaksa operasi khusus ini menjadi utas tunggal. Cara untuk melakukannya adalah dengan menggunakan kunci aplikasi (yang bekerja lintas sesi). Meskipun efektif, mereka bisa agak sulit untuk situasi seperti ini di mana frekuensi tabrakan mungkin cukup rendah.
Cara lain untuk menangani tabrakan adalah dengan menerima bahwa mereka kadang-kadang akan terjadi dan menanganinya daripada mencoba menghindarinya. Dengan menggunakan TRY...CATCH
konstruk, Anda dapat secara efektif menjebak kesalahan spesifik (dalam kasus ini: "pelanggaran kendala unik", Msg 2601) dan menjalankan kembali SELECT
untuk mendapatkan Id
nilai karena kami tahu bahwa sekarang ada karena berada di CATCH
blok dengan tertentu kesalahan. Kesalahan lainnya dapat ditangani dengan cara yang khas RAISERROR
/ RETURN
atau THROW
.
Pengaturan Tes: Urutan, Tabel, dan Indeks Unik
USE [tempdb];
CREATE SEQUENCE dbo.MagicNumber
AS INT
START WITH 1
INCREMENT BY 1;
CREATE TABLE dbo.NameLookup
(
[Id] INT NOT NULL
CONSTRAINT [PK_NameLookup] PRIMARY KEY CLUSTERED
CONSTRAINT [DF_NameLookup_Id] DEFAULT (NEXT VALUE FOR dbo.MagicNumber),
[ItemName] NVARCHAR(50) NOT NULL
);
CREATE UNIQUE NONCLUSTERED INDEX [UIX_NameLookup_ItemName]
ON dbo.NameLookup ([ItemName]);
GO
Pengaturan Tes: Prosedur Tersimpan
CREATE PROCEDURE dbo.GetOrInsertName
(
@SomeName NVARCHAR(50),
@ID INT OUTPUT,
@TestRaceCondition BIT = 0
)
AS
SET NOCOUNT ON;
BEGIN TRY
SELECT @ID = nl.[Id]
FROM dbo.NameLookup nl
WHERE nl.[ItemName] = @SomeName
AND @TestRaceCondition = 0;
IF (@ID IS NULL)
BEGIN
SET @ID = NEXT VALUE FOR dbo.MagicNumber;
INSERT INTO dbo.NameLookup ([Id], [ItemName])
VALUES (@ID, @SomeName);
END;
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 2601) -- "Cannot insert duplicate key row in object"
BEGIN
SELECT @ID = nl.[Id]
FROM dbo.NameLookup nl
WHERE nl.[ItemName] = @SomeName;
END;
ELSE
BEGIN
;THROW; -- SQL Server 2012 or newer
/*
DECLARE @ErrorNumber INT = ERROR_NUMBER(),
@ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
RAISERROR(N'Msg %d: %s', 16, 1, @ErrorNumber, @ErrorMessage);
RETURN;
*/
END;
END CATCH;
GO
Ujian
DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
@SomeName = N'test1',
@ID = @ItemID OUTPUT;
SELECT @ItemID AS [ItemID];
GO
DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
@SomeName = N'test1',
@ID = @ItemID OUTPUT,
@TestRaceCondition = 1;
SELECT @ItemID AS [ItemID];
GO
Pertanyaan dari OP
Mengapa ini lebih baik daripada MERGE
? Tidak bisakah saya mendapatkan fungsi yang sama tanpa TRY
menggunakan WHERE NOT EXISTS
klausa?
MERGE
memiliki berbagai "masalah" (beberapa referensi ditautkan dalam jawaban @ SqlZim sehingga tidak perlu menggandakan info itu di sini). Dan, tidak ada penguncian tambahan dalam pendekatan ini (kurang pertikaian), jadi itu harus lebih baik pada konkurensi. Dalam pendekatan ini Anda tidak akan pernah mendapatkan pelanggaran Kendala Unik, semua tanpa ada HOLDLOCK
, dll. Ini cukup banyak dijamin untuk bekerja.
Alasan di balik pendekatan ini adalah:
- Jika Anda memiliki cukup banyak eksekusi prosedur ini sehingga Anda perlu khawatir tentang tabrakan, maka Anda tidak ingin:
- mengambil langkah lebih banyak dari yang diperlukan
- tahan kunci pada sumber daya apa pun lebih lama dari yang diperlukan
- Karena tabrakan hanya dapat terjadi pada entri baru (entri baru dikirimkan pada waktu yang sama persis ), frekuensi jatuh ke
CATCH
blok di tempat pertama akan sangat rendah. Lebih masuk akal untuk mengoptimalkan kode yang akan menjalankan 99% dari waktu daripada kode yang akan berjalan 1% dari waktu (kecuali tidak ada biaya untuk mengoptimalkan keduanya, tetapi itu tidak terjadi di sini).
Komentar dari jawaban @ SqlZim (penekanan ditambahkan)
Saya pribadi lebih suka mencoba dan menyesuaikan solusi untuk menghindari hal itu jika memungkinkan . Dalam hal ini, saya tidak merasa bahwa menggunakan kunci dari serializable
adalah pendekatan yang berat, dan saya yakin itu akan menangani konkurensi tinggi dengan baik.
Saya akan setuju dengan kalimat pertama ini jika diubah untuk menyatakan "dan _ ketika bijaksana". Hanya karena sesuatu mungkin secara teknis tidak berarti bahwa situasi (yaitu kasus penggunaan yang dimaksudkan) akan diuntungkan olehnya.
Masalah yang saya lihat dengan pendekatan ini adalah bahwa ia mengunci lebih dari apa yang disarankan. Penting untuk membaca kembali dokumentasi yang dikutip pada "serializable", khususnya yang berikut (penekanan ditambahkan):
- Transaksi lain tidak dapat menyisipkan baris baru dengan nilai-nilai kunci yang akan jatuh dalam kisaran kunci yang dibaca oleh setiap pernyataan dalam transaksi saat ini sampai transaksi saat ini selesai.
Sekarang, inilah komentar dalam kode contoh:
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
Kata operatif ada "rentang". Kunci yang diambil tidak hanya pada nilai in @vName
, tetapi lebih tepatnya rentang mulai darilokasi di mana nilai baru ini harus pergi (yaitu antara nilai-nilai kunci yang ada di kedua sisi di mana nilai baru cocok), tetapi bukan nilai itu sendiri. Artinya, proses lain akan diblokir dari memasukkan nilai baru, tergantung pada nilai yang sedang dicari. Jika pencarian dilakukan di bagian atas rentang, maka memasukkan apa pun yang bisa menempati posisi yang sama akan diblokir. Misalnya, jika nilai "a", "b", dan "d" ada, maka jika satu proses melakukan SELECT pada "f", maka tidak mungkin untuk memasukkan nilai "g" atau bahkan "e" ( karena salah satu dari mereka akan datang segera setelah "d"). Tetapi, memasukkan nilai "c" akan dimungkinkan karena tidak akan ditempatkan dalam rentang "dicadangkan".
Contoh berikut harus menggambarkan perilaku ini:
(Di tab permintaan (yaitu Sesi) # 1)
INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'test5');
BEGIN TRAN;
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE ItemName = N'test8';
--ROLLBACK;
(Di tab permintaan (yaitu Sesi) # 2)
EXEC dbo.NameLookup_getset_byName @vName = N'test4';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'test9';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
EXEC dbo.NameLookup_getset_byName @vName = N'test7';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
EXEC dbo.NameLookup_getset_byName @vName = N's';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'u';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
Demikian juga, jika nilai "C" ada, dan nilai "A" dipilih (dan karenanya dikunci), maka Anda dapat memasukkan nilai "D", tetapi bukan nilai "B":
(Di tab permintaan (yaitu Sesi) # 1)
INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'testC');
BEGIN TRAN
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE ItemName = N'testA';
--ROLLBACK;
(Di tab permintaan (yaitu Sesi) # 2)
EXEC dbo.NameLookup_getset_byName @vName = N'testD';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'testB';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
Agar adil, dalam pendekatan yang saya sarankan, ketika ada pengecualian, akan ada 4 entri dalam Log Transaksi yang tidak akan terjadi dalam pendekatan "transaksi yang dapat diubah serial" ini. TETAPI, seperti yang saya katakan di atas, jika pengecualian terjadi 1% (atau bahkan 5%) pada saat itu, itu jauh lebih sedikit berdampak daripada kasus SELECT awal yang jauh lebih mungkin untuk sementara waktu memblokir operasi INSERT.
Masalah lain, meskipun kecil, dengan pendekatan "transaksi serializable + klausa OUTPUT" ini adalah OUTPUT
klausa (dalam penggunaannya saat ini) mengirim data kembali sebagai hasil yang ditetapkan. Set hasil memerlukan lebih banyak overhead (mungkin di kedua sisi: di SQL Server untuk mengelola kursor internal, dan di lapisan aplikasi untuk mengelola objek DataReader) daripada OUTPUT
parameter sederhana . Mengingat bahwa kita hanya berurusan dengan nilai skalar tunggal, dan bahwa asumsi adalah frekuensi eksekusi yang tinggi, overhead tambahan dari set hasil mungkin bertambah.
Sementara OUTPUT
klausa dapat digunakan sedemikian rupa untuk mengembalikan OUTPUT
parameter, itu akan membutuhkan langkah-langkah tambahan untuk membuat tabel sementara atau variabel tabel, dan kemudian untuk memilih nilai dari variabel temp / tabel tabel ke dalam OUTPUT
parameter.
Klarifikasi Lebih Lanjut: Tanggapan atas Tanggapan @ SqlZim (jawaban yang diperbarui) terhadap Tanggapan saya terhadap Tanggapan @ SqlZim (dalam jawaban asli) terhadap pernyataan saya mengenai konkurensi dan kinerja ;-)
Maaf jika bagian ini agak lama, tetapi pada titik ini kita hanya sampai pada nuansa dari dua pendekatan.
Saya percaya cara informasi yang disajikan dapat mengarah pada asumsi yang salah tentang jumlah penguncian yang bisa diharapkan untuk dijumpai ketika menggunakan serializable
dalam skenario seperti yang disajikan dalam pertanyaan awal.
Ya, saya akan mengakui bahwa saya bias, meskipun harus adil:
- Tidak mungkin bagi manusia untuk tidak bias, setidaknya sampai tingkat tertentu, dan saya mencoba untuk mempertahankannya seminimal mungkin,
- Contoh yang diberikan sederhana, tetapi itu untuk tujuan ilustrasi untuk menyampaikan perilaku tanpa terlalu menyulitkannya. Menyiratkan frekuensi berlebihan tidak dimaksudkan, meskipun saya mengerti bahwa saya juga tidak secara eksplisit menyatakan sebaliknya dan bisa dibaca menyiratkan masalah yang lebih besar daripada yang sebenarnya ada. Saya akan mencoba menjelaskannya di bawah.
- Saya juga menyertakan contoh mengunci rentang antara dua kunci yang ada (set kedua blok "Kueri tab 1" dan "Kueri tab 2").
- Saya menemukan (dan secara sukarela) "biaya tersembunyi" dari pendekatan saya, bahwa menjadi empat entri Tran Log tambahan setiap kali
INSERT
gagal karena pelanggaran Kendala Unik. Saya belum melihat yang disebutkan di salah satu jawaban / posting lainnya.
Mengenai pendekatan @ gbn tentang "JFDI", posting Michael J. Swart "Ugly Pragmatism For The Win", dan komentar Aaron Bertrand pada posting Michael (mengenai tesnya menunjukkan skenario apa yang telah menurunkan kinerja), dan komentar Anda tentang "adaptasi Michael J" Anda Adaptasi Stewart tentang prosedur Try Catch JFDI @ gbn "yang menyatakan:
Jika Anda lebih sering memasukkan nilai baru daripada memilih nilai yang ada, ini mungkin lebih berkinerja daripada versi @ srutzky. Kalau tidak, saya lebih suka versi @ srutzky daripada yang ini.
Sehubungan dengan diskusi gbn / Michael / Aaron yang terkait dengan pendekatan "JFDI", akan salah untuk menyamakan saran saya dengan pendekatan "JFDI" gbn. Karena sifat operasi "Dapatkan atau Sisipkan", ada kebutuhan eksplisit untuk melakukan hal tersebut SELECT
untuk mendapatkan ID
nilai catatan yang ada. SELECT ini bertindak sebagai IF EXISTS
cek, yang membuat pendekatan ini lebih sesuai dengan variasi "CheckTryCatch" dari pengujian Aaron. Kode yang ditulis ulang Michael (dan adaptasi terakhir Anda dari adaptasi Michael) juga termasuk WHERE NOT EXISTS
untuk melakukan pemeriksaan yang sama terlebih dahulu. Karenanya, saran saya (bersama dengan kode akhir Michael dan adaptasi Anda terhadap kode terakhirnya) tidak akan benar-benar menghantam CATCH
blok terlalu sering. Itu hanya bisa situasi di mana dua sesi,ItemName
INSERT...SELECT
pada saat yang sama persis sehingga kedua sesi menerima "benar" untuk WHERE NOT EXISTS
pada saat yang sama dan dengan demikian keduanya berusaha melakukan INSERT
pada saat yang sama persis. Skenario yang sangat spesifik itu terjadi jauh lebih jarang daripada memilih yang sudah ada ItemName
atau menyisipkan yang baru ItemName
ketika tidak ada proses lain yang berusaha melakukannya pada saat yang sama persis .
DENGAN SEMUA YANG DI ATAS DALAM PIKIRAN: Mengapa saya lebih suka pendekatan saya?
Pertama, mari kita lihat apa yang terjadi dengan penguncian dalam pendekatan "serializable". Seperti disebutkan di atas, "rentang" yang dikunci tergantung pada nilai kunci yang ada di kedua sisi di mana nilai kunci baru cocok. Awal atau akhir rentang juga bisa menjadi awal atau akhir indeks, jika tidak ada nilai kunci yang ada di arah itu. Asumsikan kita memiliki indeks dan kunci berikut ( ^
mewakili awal indeks sementara $
mewakili akhir dari itu):
Range #: |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value: ^ C F J $
Jika sesi 55 mencoba memasukkan nilai kunci:
A
, maka rentang # 1 (dari ^
ke C
) dikunci: sesi 56 tidak dapat memasukkan nilai B
, meskipun unik dan valid (belum). Tapi sesi 56 dapat memasukkan nilai-nilai D
, G
dan M
.
D
, maka rentang # 2 (dari C
ke F
) dikunci: sesi 56 tidak dapat memasukkan nilai E
(belum). Tapi sesi 56 dapat memasukkan nilai-nilai A
, G
dan M
.
M
, maka rentang # 4 (dari J
ke $
) dikunci: sesi 56 tidak dapat memasukkan nilai X
(belum). Tapi sesi 56 dapat memasukkan nilai-nilai A
, D
dan G
.
Semakin banyak nilai-nilai kunci yang ditambahkan, rentang antara nilai-nilai kunci menjadi lebih sempit, sehingga mengurangi probabilitas / frekuensi dari beberapa nilai yang dimasukkan pada saat yang sama memperebutkan rentang yang sama. Memang, ini bukan masalah besar , dan untungnya itu tampaknya menjadi masalah yang benar-benar berkurang dari waktu ke waktu.
Masalah dengan pendekatan saya dijelaskan di atas: itu hanya terjadi ketika dua sesi mencoba untuk memasukkan nilai kunci yang sama pada saat yang sama. Dalam hal ini turun ke apa yang memiliki probabilitas lebih tinggi terjadi: dua nilai kunci berbeda, namun dekat, dicoba pada saat yang sama, atau nilai kunci yang sama dicoba pada waktu yang sama? Saya kira jawabannya terletak pada struktur aplikasi melakukan sisipan, tetapi secara umum saya akan menganggapnya lebih mungkin bahwa dua nilai berbeda yang kebetulan berbagi rentang yang sama sedang dimasukkan. Tetapi satu-satunya cara untuk benar-benar tahu adalah dengan menguji keduanya pada sistem OPs.
Selanjutnya, mari kita pertimbangkan dua skenario dan bagaimana masing-masing pendekatan menanganinya:
Semua permintaan adalah untuk nilai kunci unik:
Dalam hal ini, CATCH
blok di saran saya tidak pernah dimasukkan, maka tidak ada "masalah" (yaitu 4 entri log tran dan waktu yang diperlukan untuk melakukan itu). Tetapi, dalam pendekatan "serializable", bahkan dengan semua sisipan menjadi unik, akan selalu ada beberapa potensi untuk memblokir sisipan lain dalam rentang yang sama (meskipun tidak terlalu lama).
Frekuensi tinggi permintaan untuk nilai kunci yang sama pada saat yang sama:
Dalam hal ini - tingkat keunikan yang sangat rendah dalam hal permintaan masuk untuk nilai-nilai kunci yang tidak ada - CATCH
blok dalam saran saya akan dimasukkan secara teratur. Efek dari hal ini adalah bahwa setiap sisipan yang gagal akan perlu untuk memutar kembali secara otomatis dan menulis 4 entri ke Log Transaksi, yang merupakan sedikit performa yang memukul setiap kali. Tetapi operasi keseluruhan seharusnya tidak pernah gagal (setidaknya bukan karena ini).
(Ada masalah dengan versi sebelumnya dari pendekatan "diperbarui" yang memungkinkannya untuk mengalami kebuntuan. Sebuah updlock
petunjuk ditambahkan untuk mengatasi ini dan itu tidak lagi mendapatkan kebuntuan.)TETAPI, dalam pendekatan "serializable" (bahkan versi yang diperbarui dan dioptimalkan), operasi akan menemui jalan buntu. Mengapa? Karena serializable
perilaku hanya mencegah INSERT
operasi dalam kisaran yang telah dibaca dan karenanya dikunci; itu tidak mencegah SELECT
operasi pada kisaran itu.
The serializable
pendekatan, dalam hal ini, tampaknya akan memiliki overhead tambahan, dan mungkin melakukan sedikit lebih baik dari apa yang saya sarankan.
Seperti dengan banyak / sebagian besar diskusi mengenai kinerja, karena terdapat begitu banyak faktor yang dapat mempengaruhi hasil, satu-satunya cara untuk benar-benar memiliki perasaan tentang bagaimana sesuatu akan dilakukan adalah dengan mencobanya di lingkungan target di mana ia akan berjalan. Pada titik itu tidak akan menjadi masalah pendapat :).