Bir Sekans kullandığınızdan, yeni bir değer oluşturmak için Birincil Anahtar alanında zaten bir Varsayılan Kısıtlamaya sahip olduğunuz aynı NEXT VALUE FOR işlevini kullanabilirsiniz . Önce değeri üretmek, sahip olmama konusunda endişelenmenize gerek olmadığı anlamına gelir; bu , yeni maddeyi elde etmek için ya cümleye ya da bir ek yapmaya ihtiyacınız olmadığı anlamına gelir ; Bunu yapmadan önce değere sahip olacaksınız ve :-) ile uğraşmanıza bile gerek yok.Id
Id
SCOPE_IDENTITY
OUTPUT
SELECT
INSERT
SET IDENTITY INSERT ON / OFF
Böylece genel durumun bir kısmını halleder. Diğer kısım, aynı anda iki işlemin eşzamanlılık sorununu ele alıyor, tam olarak aynı dize için mevcut bir satır bulamıyor ve ile devam ediyor INSERT
. Endişe, oluşacak Benzersiz Kısıtlama ihlallerinden kaçınmakla ilgilidir.
Bu tür eşzamanlılık sorunlarını ele almanın bir yolu, bu belirli işlemi tek iş parçacıklı olmaya zorlamaktır. Bunu yapmanın yolu uygulama kilitlerini kullanmaktır (oturumlar boyunca çalışır). Etkili olsalar da, çarpışma sıklığının muhtemelen oldukça düşük olduğu bir durum için biraz ağır olabilirler.
Çarpışmalarla başa çıkmanın diğer yolu, bazen meydana geleceğini kabul etmek ve onlardan kaçınmak yerine onları ele almaktır. Yapıyı kullanarak, TRY...CATCH
belirli bir hatayı etkin bir şekilde yakalayabilir (bu durumda: "benzersiz kısıtlama ihlali", Msg 2601) ve değeri SELECT
almak için yeniden yürütebilirsiniz, Id
çünkü CATCH
o özel ile blokta olduğu için artık var olduğunu biliyoruz hata. Diğer hatalar tipik RAISERROR
/ RETURN
veya THROW
şekilde ele alınabilir .
Test Kurulumu: Sıra, Tablo ve Benzersiz Dizin
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
Test Kurulumu: Saklı Yordam
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
Test
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
OP'den soru
Neden bu daha iyi MERGE
? Maddeyi TRY
kullanarak aynı işlevi almayacak mıyım WHERE NOT EXISTS
?
MERGE
çeşitli "sorunları" vardır (birkaç referans @ SqlZim'in cevabında bağlantılıdır, bu yüzden bu bilgiyi burada kopyalamaya gerek yoktur). Ve bu yaklaşımda ek bir kilitleme yoktur (daha az çekişme), bu yüzden eşzamanlılıkta daha iyi olmalıdır. Bu yaklaşımda, hiçbiri olmadan HOLDLOCK
vb. Benzersiz bir Sınır ihlali elde edemezsiniz . Çalışması neredeyse garanti edilir.
Bu yaklaşımın ardındaki gerekçe:
- Bu yordamda, çarpışmalar hakkında endişelenmeniz gereken kadar yürütme varsa, şunları yapmak istemezsiniz:
- gerekenden daha fazla adım atın
- kaynakların gereğinden fazla kilitlenmesi
- Çarpışmalar yalnızca yeni girişlerde ( aynı anda gönderilen yeni girişlerde) meydana gelebileceğinden
CATCH
, ilk etapta bloğa düşme sıklığı oldukça düşük olacaktır. Zamanın% 1'ini çalıştıracak kod yerine zamanın% 99'unu çalıştıracak kodu optimize etmek daha mantıklıdır (her ikisini de optimize etmek için bir maliyet yoksa, ancak durum böyle değildir).
@ SqlZim'in cevabından yorum (vurgu eklendi)
Ben şahsen bunu mümkün olduğunca önlemek için bir çözüm denemeyi ve uyarlamayı tercih ediyorum . Bu durumda, kilitleri kullanmanın serializable
ağır bir yaklaşım olduğunu düşünmüyorum ve yüksek eşzamanlılığı iyi idare edeceğinden emin olurum.
"Ve _huddi ihtiyatlı" olarak değiştirilmiş olsaydı, bu ilk cümleyi kabul ediyorum. Bir şeyin teknik olarak mümkün olması, durumun (yani amaçlanan kullanım durumu) bundan faydalanacağı anlamına gelmez.
Bu yaklaşımla gördüğüm sorun, önerilenden daha fazla kilitlenmesi. "Serileştirilebilir" hakkındaki alıntılanmış dokümanları yeniden okumak önemlidir, özellikle aşağıdakiler (vurgu eklenmiştir):
- Diğer işlemler , geçerli işlem tamamlanana kadar geçerli işlemdeki herhangi bir deyim tarafından okunan anahtar aralığına girecek anahtar değerlere sahip yeni satırlar ekleyemez .
Şimdi, örnek koddaki yorum:
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
Operatif kelime "aralık" tır. Atılıyor kilit sadece değerine değil @vName
, ama daha doğru bir dizi başlayanbu yeni değerin gitmesi gereken konum (yani, yeni değerin uyduğu her iki taraftaki mevcut anahtar değerler arasında), ancak değerin kendisi değil. Yani, şu anda aranan değerlere bağlı olarak diğer işlemlerin yeni değerler girmesi engellenecektir. Arama, aralığın en üstünde yapılıyorsa, aynı konumu işgal edebilecek herhangi bir şey eklemek engellenir. Örneğin, "a", "b" ve "d" değerleri varsa, bir işlem "f" üzerinde SELECT yapıyorsa, "g" ve hatta "e" değerlerini eklemek mümkün olmayacaktır ( çünkü bunlardan herhangi biri "d" den hemen sonra gelecek). Ancak, "ayrılmış" aralığa yerleştirilmeyeceğinden "c" değeri eklemek mümkün olacaktır.
Aşağıdaki örnekte bu davranış gösterilmelidir:
(Sorgu sekmesinde (ör. Oturum) # 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;
(Sorgu sekmesinde (ör. Oturum) # 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
Benzer şekilde, "C" değeri varsa ve "A" değeri seçiliyse (ve dolayısıyla kilitliyse), "D" değerini girebilirsiniz, ancak "B" değerini giremezsiniz:
(Sorgu sekmesinde (ör. Oturum) # 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;
(Sorgu sekmesinde (ör. Oturum) # 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
Adil olmak gerekirse, önerilen yaklaşımımda, bir istisna olduğunda, İşlem Günlüğünde bu "serileştirilebilir işlem" yaklaşımında gerçekleşmeyecek 4 giriş olacaktır. AMA, yukarıda söylediğim gibi, istisna zamanın% 1'i (hatta% 5'i) gerçekleşirse, bu, ilk SELECT işlemlerini geçici olarak engelleyen çok daha olası ilk SELECT durumundan çok daha az etkilidir.
Bu "serileştirilebilir işlem + ÇIKTI yan tümcesi" yaklaşımı ile ilgili küçük de olsa bir başka sorun, OUTPUT
yan tümce (mevcut kullanımda) verileri sonuç kümesi olarak geri göndermesidir. Bir sonuç kümesi, basit bir OUTPUT
parametreden daha fazla ek yük (muhtemelen her iki tarafta: SQL Server'da iç imleci yönetmek için ve uygulama katmanında DataReader nesnesini yönetmek için) gerektirir . Sadece tek bir skaler değerle uğraştığımız ve varsayımın yüksek bir yürütme frekansı olduğu göz önüne alındığında, sonuç kümesinin ekstra ek yükü muhtemelen artıyor.
Cümle OUTPUT
bir OUTPUT
parametreyi döndürecek şekilde kullanılabilse de , geçici bir tablo veya tablo değişkeni oluşturmak ve daha sonra bu geçici tablo / tablo değişkeninden değeri OUTPUT
parametreye seçmek için ek adımlar gerekir .
Ek Açıklama: @ SqlZim'in Yanıtı ve performans ;-) hakkındaki açıklamama @ SqlZim'in Yanıtıma (orijinal yanıtta) Yanıtım'a yanıtı (güncellenmiş yanıt) yanıtı
Üzgünüm, bu kısım çok uzunsa, ama bu noktada iki yaklaşımın nüanslarına düştük.
Bilginin sunulma şeklinin serializable
, orijinal soruda gösterildiği gibi senaryoda kullanıldığında karşılaşılabilecek kilitleme miktarı hakkında yanlış varsayımlara yol açabileceğine inanıyorum .
Evet, adil olsa da önyargılı olduğumu itiraf edeceğim:
- Bir insanın en azından bir dereceye kadar önyargılı olmaması imkansızdır ve onu minimumda tutmaya çalışıyorum,
- Verilen örnek basitti, ama bu açıklayıcı amaçlar için davranışı aşırı karmaşıklaştırmadan aktarmaktı. Aşırı frekansı ima etmek amaçlanmamıştı, ancak açıkça başka şekilde belirtmediğimi anlıyorum ve gerçekte var olandan daha büyük bir sorun olduğu ima edilebilir. Bunu aşağıda açıklığa kavuşturmaya çalışacağım.
- Ayrıca, mevcut iki anahtar ("Sorgu sekmesi 1" ve "Sorgu sekmesi 2" bloklarının ikinci kümesi) arasındaki bir aralığı kilitleme örneği de ekledim.
INSERT
Benzersiz Kısıtlama ihlali nedeniyle her seferinde başarısız olduğunda dört ekstra Tran Log girişi olan yaklaşımımın "gizli maliyetini" buldum (ve gönüllü olarak) . Diğer cevapların / gönderilerin hiçbirinde bahsedileni görmedim.
@ Gbn'in "JFDI" yaklaşımı, Michael J. Swart'ın "Çirkin Pragmatizm For The Win" yazısı ve Aaron Bertrand'ın Michael'ın gönderisine yaptığı yorum (hangi senaryoların performansı düşürdüğünü gösteren testlerine ilişkin) ve "Michael J'nin uyarlanması" . Stewart'ın @ gbn's Try Catch JFDI prosedürüne uyarlaması "şunu belirtiyor:
Mevcut değerleri seçmekten daha sık yeni değerler ekliyorsanız, bu @ srutzky'nin sürümüne göre daha yüksek performans gösterebilir. Aksi takdirde @ srutzky'nin versiyonunu buna tercih ederim.
"JFDI" yaklaşımı ile ilgili bu gbn / Michael / Aaron tartışması ile ilgili olarak, önerimi gbn'in "JFDI" yaklaşımına eşitlemek yanlış olur. "Al veya Ekle" işleminin doğası gereği, varolan kayıtların değerini SELECT
almak için açık bir gereksinim vardır ID
. Bu SELECT IF EXISTS
kontrol görevi görür , bu da bu yaklaşımı Aaron testlerinin "CheckTryCatch" varyasyonuna eşit hale getirir. Michael'ın yeniden yazılan kodu (ve Michael'ın adaptasyonuna son adaptasyonunuz) WHERE NOT EXISTS
ilk önce aynı kontrolü yapmak için bir de içerir . Bu nedenle, önerim (Michael'ın nihai koduyla ve nihai koduna uyarlamanızla birlikte) aslında CATCH
bloka sık sık vurmaz. Sadece iki seansın olduğu durumlar olabilir,ItemName
INSERT...SELECT
her iki oturum da aynı anda "doğru" alacak WHERE NOT EXISTS
ve böylece her ikisi de tam olarak aynı anda yapmaya çalışacaktır INSERT
. Bu çok özel senaryo , aynı anda başka bir işlem yapmaya çalışmadığında mevcut olanı seçmekten ItemName
veya yeni bir ekleme yapmaktan çok daha az gerçekleşir .ItemName
ZİHİNDEKİ TÜM YUKARIDA: Neden yaklaşımımı tercih ederim?
İlk olarak, "serileştirilebilir" yaklaşımda hangi kilitlemenin gerçekleştiğine bakalım. Yukarıda belirtildiği gibi, kilitlenen "aralık", yeni anahtar değerinin sığacağı her iki taraftaki mevcut anahtar değerlerine bağlıdır. Aralığın başlangıcı veya bitişi, bu yönde mevcut bir anahtar değeri yoksa sırasıyla dizinin başlangıcı veya bitişi de olabilir. Aşağıdaki dizine ve anahtarlara sahip olduğumuzu varsayalım ( ^
dizinin başlangıcını $
temsil ederken dizinin başlangıcını temsil eder):
Range #: |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value: ^ C F J $
Oturum 55 aşağıdakilerin anahtar değerini girmeye çalışırsa:
A
Ardından (gelen 1. aralığı ^
için C
kilitli): oturum 56 değerini ekleyemezsiniz B
, bile eşsiz ve geçerli (henüz). Ama oturumu 56 değerlerini ekleyebilir D
, G
ve M
.
D
Ardından (# 2 aralığı C
için F
kilitli): oturum 56 bir değer eklemek olamaz E
(henüz). Ama oturumu 56 değerlerini ekleyebilir A
, G
ve M
.
M
Ardından (# 4 aralığı J
için $
kilitli): oturum 56 bir değer eklemek olamaz X
(henüz). Ama oturumu 56 değerlerini ekleyebilir A
, D
ve G
.
Daha fazla anahtar değer eklendikçe, anahtar değerler arasındaki aralıklar daralır, böylece aynı anda aynı aralıkta çarpışırken aynı anda eklenen birden fazla değerin olasılığı / sıklığı azalır. Kuşkusuz, bu büyük bir sorun değil ve neyse ki zamanla azalan bir sorun gibi görünüyor.
Yaklaşımımla ilgili sorun yukarıda açıklandı: yalnızca iki oturum aynı anahtar değerini aynı anda girmeye çalıştığında olur . Bu bağlamda, daha yüksek olma olasılığına sahip olana iner: aynı anda iki farklı, ancak yakın, anahtar değer denenir mi, yoksa aynı anahtar değer denenir mi? Cevabın, eklentileri yapan uygulamanın yapısında yattığını düşünüyorum, ancak genel olarak konuşursak, aynı aralığı paylaşan iki farklı değerin eklenmesi olasılığının daha yüksek olduğunu varsayıyorum. Ama gerçekten bilmenin tek yolu her iki OP sistemini de test etmektir.
Şimdi, iki senaryoyu ve her bir yaklaşımın bunları nasıl ele aldığını ele alalım:
Tüm istekler benzersiz anahtar değerler içindir:
Bu durumda, CATCH
önerimdeki blok asla girilmez, bu nedenle "sorun" (yani 4 tran günlük girişi ve bunu yapmak için gereken süre). Ancak, "serileştirilebilir" yaklaşımda, tüm kesici uçlar benzersiz olsa bile, aynı aralıktaki diğer kesici uçları bloke etmek için her zaman bazı potansiyeller olacaktır (çok uzun olmasa da).
Aynı anda aynı anahtar değere sahip isteklerin yüksek sıklığı:
Bu durumda - mevcut olmayan anahtar değerler için gelen talepler açısından çok düşük bir benzersizlik derecesi - önerimdeki CATCH
blok düzenli olarak girilecektir. Bunun etkisi, her başarısız ekin otomatik olarak geri alınması ve 4 girişi her seferinde hafif bir performans isabeti olan İşlem Günlüğüne yazması gerekecektir. Ancak genel operasyon asla başarısız olmamalıdır (en azından bundan dolayı değil).
("Güncellenmiş" yaklaşımın önceki sürümünde çıkmazlardan muzdarip olmasına neden olan bir sorun vardı. Bunu ele almak için bir updlock
ipucu eklendi ve artık çıkmazlar olmayacak.)AMA, "serileştirilebilir" yaklaşımda (güncellenmiş, optimize edilmiş versiyonda bile) işlem kilitlenecektir. Neden? Çünkü serializable
davranış sadece INSERT
okunan ve dolayısıyla kilitlenen aralıktaki işlemleri önler ; bu SELECT
aralıktaki işlemleri engellemez .
serializable
Yaklaşım, bu durumda, hiçbir ek yükü var gibi olurdu, ve düşündüren ben ne göre biraz daha iyi performans gösterebilir.
Performansla ilgili birçok / en fazla tartışmada olduğu gibi, sonucu etkileyebilecek çok fazla faktör olduğu için, bir şeyin nasıl performans göstereceğine dair gerçekten bir fikre sahip olmanın tek yolu, onu çalışacağı hedef ortamda denemektir. Bu noktada bir görüş meselesi olmayacak :).