OUTPUT ile bir MERGE, koşullu bir INSERT ve SELECT'den daha iyi bir uygulama mıdır?


12

Sık sık "Varsa, ekle" durumu ile karşılaşırız. Dan Guzman'ın blogunda bu işlemin nasıl güvenli hale getirileceği konusunda mükemmel bir araştırma var.

Sadece bir tamsayı bir dize kataloglar temel bir tablo var SEQUENCE. Saklı bir yordamda, ben varsa değer için tamsayı anahtarını almak ya INSERTda sonra sonuç değerini almak gerekir. dbo.NameLookup.ItemNameVeri bütünlüğü risk altında değil, ancak istisnalarla karşılaşmak istemiyorum sütununda bir benzersizlik kısıtlaması var .

Bu IDENTITYalamadım SCOPE_IDENTITYve değeri NULLbazı durumlarda olabilir.

Benim durumumda sadece INSERTmasadaki güvenlikle uğraşmak zorundayım, bu yüzden böyle kullanmak için daha iyi bir uygulama olup olmadığına karar vermeye çalışıyorum MERGE:

SET NOCOUNT, XACT_ABORT ON;

DECLARE @vValueId INT 
DECLARE @inserted AS TABLE (Id INT NOT NULL)

MERGE 
    dbo.NameLookup WITH (HOLDLOCK) AS f 
USING 
    (SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
        ON f.ItemName= new_item.val
WHEN MATCHED THEN
    UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
    INSERT
      (ItemName)
    VALUES
      (@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s

Ben kullanarak witout bu yapabileceğini MERGEsadece şartlı ile INSERTbir takip SELECT bu ikinci yaklaşım okuyucuya net olduğunu düşünüyorum, ama 's "daha iyi" pratik ikna olmadım

SET NOCOUNT, XACT_ABORT ON;

INSERT INTO 
    dbo.NameLookup (ItemName)
SELECT
    @vName
WHERE
    NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)

DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName

Ya da düşünmediğim daha iyi bir yol daha var

Diğer soruları araştırdım ve referans verdim. Bu: /programming/5288283/sql-server-insert-if-not-exists-best-practice bulabildiğim en uygun şey ama kullanım durumum için çok uygun görünmüyor. Düşünmediğim IF NOT EXISTS() THENyaklaşımla ilgili diğer sorular da kabul edilebilir.


Tamponuzdan daha büyük tablolarla deneme yapmayı denediniz mi, tablo belirli bir boyuta ulaştığında birleştirme performansının düştüğü deneyimler yaşadım.
barışsever

Yanıtlar:


8

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.IdIdSCOPE_IDENTITYOUTPUTSELECTINSERTSET 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...CATCHbelirli bir hatayı etkin bir şekilde yakalayabilir (bu durumda: "benzersiz kısıtlama ihlali", Msg 2601) ve değeri SELECTalmak için yeniden yürütebilirsiniz, Idçünkü CATCHo özel ile blokta olduğu için artık var olduğunu biliyoruz hata. Diğer hatalar tipik RAISERROR/ RETURNveya 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 TRYkullanarak 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 HOLDLOCKvb. Benzersiz bir Sınır ihlali elde edemezsiniz . Çalışması neredeyse garanti edilir.

Bu yaklaşımın ardındaki gerekçe:

  1. Bu yordamda, çarpışmalar hakkında endişelenmeniz gereken kadar yürütme varsa, şunları yapmak istemezsiniz:
    1. gerekenden daha fazla adım atın
    2. kaynakların gereğinden fazla kilitlenmesi
  2. Ç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 serializableağı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, OUTPUTyan tümce (mevcut kullanımda) verileri sonuç kümesi olarak geri göndermesidir. Bir sonuç kümesi, basit bir OUTPUTparametreden 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 OUTPUTbir OUTPUTparametreyi 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 OUTPUTparametreye 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:

  1. Bir insanın en azından bir dereceye kadar önyargılı olmaması imkansızdır ve onu minimumda tutmaya çalışıyorum,
  2. 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.
  3. 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.
  4. INSERTBenzersiz 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 SELECTalmak için açık bir gereksinim vardır ID. Bu SELECT IF EXISTSkontrol 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 EXISTSilk ö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 CATCHbloka sık sık vurmaz. Sadece iki seansın olduğu durumlar olabilir,ItemNameINSERT...SELECTher iki oturum da aynı anda "doğru" alacak WHERE NOT EXISTSve 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 ItemNameveya 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:

  • AArdından (gelen 1. aralığı ^için Ckilitli): oturum 56 değerini ekleyemezsiniz B, bile eşsiz ve geçerli (henüz). Ama oturumu 56 değerlerini ekleyebilir D, Gve M.
  • DArdından (# 2 aralığı Ciçin Fkilitli): oturum 56 bir değer eklemek olamaz E(henüz). Ama oturumu 56 değerlerini ekleyebilir A, Gve M.
  • MArdından (# 4 aralığı Jiçin $kilitli): oturum 56 bir değer eklemek olamaz X(henüz). Ama oturumu 56 değerlerini ekleyebilir A, Dve 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:

  1. 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).

  2. 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 CATCHblok 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 updlockipucu eklendi ve artık çıkmazlar olmayacak.)AMA, "serileştirilebilir" yaklaşımda (güncellenmiş, optimize edilmiş versiyonda bile) işlem kilitlenecektir. Neden? Çünkü serializabledavranış sadece INSERTokunan ve dolayısıyla kilitlenen aralıktaki işlemleri önler ; bu SELECTaralıktaki işlemleri engellemez .

    serializableYaklaşı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 :).


7

Güncellenmiş Yanıt


@Srutzky tarafından verilen yanıt

Bu "serileştirilebilir işlem + ÇIKTI yan tümcesi" yaklaşımı ile ilgili küçük de olsa bir başka sorun, ÇIKTI yan tümcesi (mevcut kullanımında) verileri sonuç kümesi olarak geri göndermesidir. Sonuç kümesi, basit bir OUTPUT parametresinden 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.

Kabul ediyorum ve aynı nedenlerden dolayı ihtiyatlı olduğunda çıkış parametrelerini kullanıyorum . İlk cevabımda çıkış parametresi kullanmamak benim hatamdı, tembel oluyordum.

Burada birlikte bir çıkış parametresi, ek optimizasyonlar kullanılarak gözden geçirilmiş bir prosedürdür next value foro @srutzky onun cevabını açıklar :

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50), @vValueId int output) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                                        /* if @vName is empty, return early */
  select  @vValueId = Id                                              /* go get the Id */
    from  dbo.NameLookup
    where ItemName = @vName;
  if @vValueId is not null                                 /* if we got the id, return */
    return;
  begin try;                                  /* if it is not there, then get the lock */
    begin tran;
      select  @vValueId = Id
        from  dbo.NameLookup with (updlock, serializable) /* hold key range for @vName */
        where ItemName = @vName;
      if @@rowcount = 0                    /* if we still do not have an Id for @vName */
      begin;                                         /* get a new Id and insert @vName */
        set @vValueId = next value for dbo.IdSequence;      /* get next sequence value */
        insert into dbo.NameLookup (ItemName, Id)
          values (@vName, @vValueId);
      end;
    commit tran;
  end try
  begin catch;
    if @@trancount > 0 
      begin;
        rollback transaction;
        throw;
      end;
  end catch;
end;

güncelleme notu : updlockselect ile birlikte bu senaryoda uygun kilitleri kapmak olacaktır. Srutzky sayesinde, bunun sadece serializableüzerinde kullanıldığında kilitlenmelere neden olabileceğini belirtti select.

Not: Bu kudreti durum, ama eğer mümkünse prosedür için bir değerle çağrılır değil @vValueId, dahil set @vValueId = null;sonra set xact_abort on;aksi takdirde kaldırılabilir.


@ Srutzky'nin anahtar menzil kilitleme davranışı örnekleri ile ilgili olarak:

@srutzky, tablosunda yalnızca bir değer kullanır ve testlerinde anahtar aralığı kilitlemeyi göstermek için "sonraki" / "sonsuzluk" anahtarını kilitler. Testleri bu durumlarda ne olduğunu gösterirken, bilgilerin sunulma şeklinin serializable, orijinal soruda gösterildiği gibi senaryoda kullanıldığında karşılaşabileceği kilitleme miktarı hakkında yanlış varsayımlara yol açabileceğine inanıyorum .

Anahtar aralığı kilitlemenin açıklamasını ve örneklerini sunma biçiminde (belki de yanlış) bir önyargı algılasam da, hala doğrudurlar.


Daha fazla araştırmadan sonra, 2011'den Michael J. Swart: Mythbusting: Eşzamanlı Güncelleme / Insert Çözümleri ile ilgili bir blog makalesi buldum . İçinde, doğruluk ve eşzamanlılık için çeşitli yöntemleri test eder. Yöntem 4: Artırılmış İzolasyon + İnce Ayar Kilitleri , Sam Saffron'un SQL Server için Ekleme veya Güncelleme Deseni'ne ve beklentilerini karşılamak için orijinal testteki tek yönteme (daha sonra katılacak ) dayanmaktadır merge with (holdlock).

Şubat 2016'da Michael J. Swart Çirkin Pragmatizmi Kazanmak için yayınladı . Bu yazıda, kilitlemeyi azaltmak için Safffert üst prosedürlerine yaptığı bazı ek ayarlamaları kapsar (yukarıdaki prosedüre dahil ettim).

Bu değişiklikleri yaptıktan sonra Michael, prosedürünün daha karmaşık görünmeye başladığından memnun değildi ve Chris adlı bir meslektaşa danıştı. Chris tüm orijinal Mythbusters yayınını okudu ve tüm yorumları okudu ve @ gbn'nin TRY CATCH JFDI modelini sordu . Bu model @ srutzky'nin cevabına benzer ve Michael'ın bu örnekte kullandığı çözümdür.

Michael J Swart:

Dün eşzamanlılık yapmanın en iyi yolu hakkında fikrimi değiştirdim. Mythbusting: Eşzamanlı Güncelleme / Ekleme Çözümlerinde birkaç yöntemi açıklarım. Tercih ettiğim yöntem, izolasyon seviyesini artırmak ve kilitleri hassas bir şekilde ayarlamaktır.

En azından benim tercihimdi. Yakın zamanda gbn'nin yorumlarda önerilen bir yöntemi kullanma yaklaşımımı değiştirdim. Yöntemini “TRY CATCH JFDI modeli” olarak tanımlıyor. Normalde böyle çözümlerden kaçınırım. Geliştiricilerin hata yakalamaya veya kontrol akışı istisnalarına güvenmemesi gerektiğini söyleyen bir kural var. Ama dün bu kuralý çiğnedim.

Bu arada, gbn'nin “JFDI” deseni için açıklamasını seviyorum. Bana Shia Labeouf'un motivasyon videosunu hatırlatıyor.


Bence her iki çözüm de uygulanabilir. Yine de yalıtım düzeyini artırmayı ve ince ayar kilitlerini tercih etsem de , @ srutzky'nin yanıtı da geçerlidir ve özel durumunuzda daha fazla performans gösterebilir veya etmeyebilir.

Belki gelecekte ben de Michael J. Swart'ın yaptığı aynı sonuca varacağım, ama henüz orada değilim.


Bu benim tercihim değil, ama Michael J. Stewart'ın @ gbn'in Try Catch JFDI prosedürüne uyarlamasının uyarlaması şöyle görünecekti:

create procedure dbo.NameLookup_JFDI (
    @vName nvarchar(50)
  , @vValueId int output
  ) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                     /* if @vName is empty, return early */
  begin try                                                 /* JFDI */
    insert into dbo.NameLookup (ItemName)
      select @vName
      where not exists (
        select 1
          from dbo.NameLookup
          where ItemName = @vName);
  end try
  begin catch        /* ignore duplicate key errors, throw the rest */
    if error_number() not in (2601, 2627) throw;
  end catch
  select  @vValueId = Id                              /* get the Id */
    from  dbo.NameLookup
    where ItemName = @vName
  end;

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 .

Aaron Bertrand'ın Michael J Swart'ın gönderdiği ilgili testlere bağlantılarını gönderdiği ve bu değişime yol açtığı yorumları. Ugly Pragmatism for Win bölümünden yorum bölümünden alıntı :

Yine de, JFDI, çağrıların% 'sinin başarısız olmasına bağlı olarak genel olarak daha kötü performansa neden olur. İstisnaları arttırmanın önemli bir yükü vardır. Bunu birkaç yazıda gösterdim:

http://sqlperformance.com/2012/08/t-sql-queries/error-handling

https://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-before-entering-sql-server-try-and-catch-logic/

Yorum: Aaron Bertrand - 11 Şubat 2016 @ 11:49

ve yanıtı:

Haklısın Aaron ve test ettik.

Bizim durumumuzda başarısız olan çağrıların yüzdesinin 0 olduğu ortaya çıktı (en yakın yüzdeye yuvarlandığında).

Sanırım, mümkün olan her şeyi, aşağıdaki kurallara göre her durum için ayrı ayrı değerlendirdiğinizi açıklıyorsunuz.

Ayrıca kesinlikle gerekli olmayan WHERE NOT EXISTS yan tümcesini de ekledik.

Yorum yapan: Michael J. Swart - 11 Şubat 2016 @ 11:57


Yeni bağlantılar:


Orijinal cevap


Özellikle tek sıra ile uğraşırken Sam Saffron upert yaklaşımını kullanmayı tercih ediyorum merge.

Bu upert yöntemini şu şekilde bu duruma uyarlayacağım:

declare @vName nvarchar(50) = 'Invader';
declare @vValueId int       = null;

if nullif(@vName,'') is not null /* this gets your where condition taken care of before we start doing anything */
begin tran;
  select @vValueId = Id
    from dbo.NameLookup with (serializable) 
    where ItemName = @vName;
  if @@rowcount > 0 
    begin;
      select @vValueId as id;
    end;
    else
    begin;
      insert into dbo.NameLookup (ItemName)
        output inserted.id
          values (@vName);
      end;
commit tran;

İsminizle tutarlı olacağım serializableve aynı şekilde holdlockbirini seçip kullanımında tutarlı olurum . Kullanmaya eğilimliyim serializableçünkü belirlerken kullanılanla aynı isim set transaction isolation level serializable.

Kullanarak serializableveya holdlockbir aralık kilidi, değerine dayalı olarak alınır ve @vNamebu, diğer tüm işlemleri dbo.NameLookup, wherecümledeki değeri içeren değerleri seçer veya eklerlerse bekler .

Aralık kilidinin düzgün çalışması için, ItemNamesütunda da kullanılırken geçerli olan bir dizin olması gerekir merge.


İşte prosedür nasıl görüneceğini olduğunu çoğunlukla aşağıdaki hata işleme için Erland Sommarskog en tanıtım broşürünü kullanarak throw. Eğer throwsiz hatalarını yükselterek nasıl değil, senin prosedürlerin kalanı ile tutarlı olması için değiştirmek:

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50) ) as
begin
  set nocount on;
  set xact_abort on;
  declare @vValueId int;
  if nullif(@vName,'') is null /* if @vName is null or empty, select Id as null */
    begin
      select Id = cast(null as int);
    end 
    else                       /* else go get the Id */
    begin try;
      begin tran;
        select @vValueId = Id
          from dbo.NameLookup with (serializable) /* hold key range for @vName */
          where ItemName = @vName;
        if @@rowcount > 0      /* if we have an Id for @vName select @vValueId */
          begin;
            select @vValueId as Id; 
          end;
          else                     /* else insert @vName and output the new Id */
          begin;
            insert into dbo.NameLookup (ItemName)
              output inserted.Id
                values (@vName);
            end;
      commit tran;
    end try
    begin catch;
      if @@trancount > 0 
        begin;
          rollback transaction;
          throw;
        end;
    end catch;
  end;
go

Yukarıdaki prosedürde neler olduğunu özetlemek için: set nocount on; set xact_abort on;her zaman yaptığınız gibi, sonuç olarak girdi değişkenimiz is nullveya boşsa select id = cast(null as int). Boş veya boş değilse, o zaman bu noktayı tutarkenId değişkenimizi al . Eğer orada, onu gönderir. Orada yoksa, takın ve yenisini gönderin .IdId

Bu arada, aynı değerin Kimliğini bulmaya çalışan bu yordama yapılan diğer çağrılar, ilk işlem tamamlanana kadar bekleyecek ve ardından seçip iade edecektir. Bu prosedüre yapılan diğer çağrılar veya başka değerler arayan diğer ifadeler devam edecektir, çünkü bu engel değildir.

Srutzky'ye, bu tür bir sorun için çarpışmalarla başa çıkabileceğiniz ve istisnaları yutabileceğiniz konusunda hemfikir olsam da, kişisel olarak mümkün olduğunda bunu yapmaktan kaçınmak için bir çözüm bulmayı ve uyarlamayı tercih ediyorum. Bu durumda, kilitleri kullanmanın serializableağır bir yaklaşım olduğunu düşünmüyorum ve yüksek eşzamanlılığı iyi idare edeceğinden emin olurum.

Tablo ipuçlarındaki sql server belgelerindenserializableholdlock alıntı / :

SERİLEŞTİRİLEBİLİR

HOLDLOCK ile eşdeğerdir. İşlem tamamlanmış olsun veya olmasın, gerekli tablo veya veri sayfasına artık ihtiyaç duyulmaz paylaşılmaz kilidi serbest bırakmak yerine, bir işlem tamamlanıncaya kadar paylaşılan kilitleri daha kısıtlayıcı hale getirir. Tarama, SERIALIZABLE yalıtım seviyesinde çalışan bir işlemle aynı semantikle gerçekleştirilir. Yalıtım düzeyleri hakkında daha fazla bilgi için bkz. İŞLEM İZOLASYON DÜZEYİNİ AYARLAMA (Transact-SQL).

İşlem yalıtım düzeyinde sql sunucusu belgelerinden alıntıserializable

SERIALIZABLE Aşağıdakileri belirtir:

  • İfadeler değiştirilmiş ancak henüz diğer işlemler tarafından işlenmemiş verileri okuyamaz.

  • Başka hiçbir işlem, geçerli işlem tamamlanıncaya kadar geçerli işlem tarafından okunan verileri değiştiremez.

  • 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.


Yukarıdaki çözümle ilgili bağlantılar:

MERGEsivilceli bir geçmişi var ve kodun tüm sözdizimi altında nasıl olmasını istediğinizden emin olmak için daha fazla alay etmek gibi görünüyor. İlgili mergemakaleler:

Son bir bağlantı, Kendra Küçük bir yaptığımız kaba karşılaştırma mergevsinsert with left join diye "Bu konuda ayrıntılı yük testi yapmadım" diyor uyarı ile, ama yine de iyi bir okuma olduğunu.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.