Apakah MERGE dengan OUTPUT praktik yang lebih baik daripada INSERT dan SELECT bersyarat?


12

Kita sering menghadapi situasi "Jika tidak ada, masukkan". Blog Dan Guzman memiliki penyelidikan yang sangat baik tentang bagaimana membuat proses ini threadsafe.

Saya punya tabel dasar yang hanya katalog string ke integer dari a SEQUENCE. Dalam prosedur tersimpan saya harus mendapatkan kunci integer untuk nilai jika ada, atau INSERTkemudian mendapatkan nilai yang dihasilkan. Ada batasan keunikan pada dbo.NameLookup.ItemNamekolom sehingga integritas data tidak berisiko tetapi saya tidak ingin menemukan pengecualian.

Ini bukan IDENTITYjadi saya tidak bisa mendapatkan SCOPE_IDENTITYdan nilainya bisa NULLdalam kasus tertentu.

Dalam situasi saya, saya hanya harus berurusan dengan INSERTkeselamatan di atas meja, jadi saya mencoba untuk memutuskan apakah itu praktik yang lebih baik untuk digunakan MERGEseperti ini:

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

Saya bisa melakukan ini tanpa menggunakan MERGEhanya dengan persyaratan INSERTdiikuti oleh SELECT saya pikir pendekatan kedua ini lebih jelas bagi pembaca, tapi saya tidak yakin itu "lebih baik" latihan

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

Atau mungkin ada cara lain yang lebih baik yang belum saya pertimbangkan

Saya melakukan pencarian dan referensi pertanyaan lain. Yang ini: /programming/5288283/sql-server-insert-if-not-exists-best-practice adalah yang paling tepat yang bisa saya temukan tetapi tampaknya tidak terlalu berlaku untuk kasus penggunaan saya. Pertanyaan lain untuk IF NOT EXISTS() THENpendekatan yang menurut saya tidak bisa diterima.


Sudahkah Anda mencoba bereksperimen dengan tabel yang lebih besar dari buffer Anda, saya memiliki pengalaman di mana kinerja gabungan turun setelah tabel mencapai ukuran tertentu.
pacreely

Jawaban:


8

Karena Anda menggunakan Urutan, Anda dapat menggunakan fungsi NEXT VALUE FOR yang sama - yang sudah Anda miliki di Batasan Default pada Idbidang Kunci Utama - untuk menghasilkan Idnilai baru sebelumnya. Menghasilkan nilai terlebih dahulu berarti Anda tidak perlu khawatir tidak memiliki SCOPE_IDENTITY, yang berarti Anda tidak perlu OUTPUTklausa atau melakukan tambahan SELECTuntuk 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...CATCHkonstruk, Anda dapat secara efektif menjebak kesalahan spesifik (dalam kasus ini: "pelanggaran kendala unik", Msg 2601) dan menjalankan kembali SELECTuntuk mendapatkan Idnilai karena kami tahu bahwa sekarang ada karena berada di CATCHblok dengan tertentu kesalahan. Kesalahan lainnya dapat ditangani dengan cara yang khas RAISERROR/ RETURNatau 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 TRYmenggunakan WHERE NOT EXISTSklausa?

MERGEmemiliki 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:

  1. Jika Anda memiliki cukup banyak eksekusi prosedur ini sehingga Anda perlu khawatir tentang tabrakan, maka Anda tidak ingin:
    1. mengambil langkah lebih banyak dari yang diperlukan
    2. tahan kunci pada sumber daya apa pun lebih lama dari yang diperlukan
  2. Karena tabrakan hanya dapat terjadi pada entri baru (entri baru dikirimkan pada waktu yang sama persis ), frekuensi jatuh ke CATCHblok 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 serializableadalah 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 OUTPUTklausa (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 OUTPUTparameter 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 OUTPUTklausa dapat digunakan sedemikian rupa untuk mengembalikan OUTPUTparameter, 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 OUTPUTparameter.

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 serializabledalam skenario seperti yang disajikan dalam pertanyaan awal.

Ya, saya akan mengakui bahwa saya bias, meskipun harus adil:

  1. Tidak mungkin bagi manusia untuk tidak bias, setidaknya sampai tingkat tertentu, dan saya mencoba untuk mempertahankannya seminimal mungkin,
  2. 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.
  3. Saya juga menyertakan contoh mengunci rentang antara dua kunci yang ada (set kedua blok "Kueri tab 1" dan "Kueri tab 2").
  4. Saya menemukan (dan secara sukarela) "biaya tersembunyi" dari pendekatan saya, bahwa menjadi empat entri Tran Log tambahan setiap kali INSERTgagal 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 SELECTuntuk mendapatkan IDnilai catatan yang ada. SELECT ini bertindak sebagai IF EXISTScek, 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 EXISTSuntuk 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 CATCHblok terlalu sering. Itu hanya bisa situasi di mana dua sesi,ItemNameINSERT...SELECTpada saat yang sama persis sehingga kedua sesi menerima "benar" untuk WHERE NOT EXISTSpada saat yang sama dan dengan demikian keduanya berusaha melakukan INSERTpada saat yang sama persis. Skenario yang sangat spesifik itu terjadi jauh lebih jarang daripada memilih yang sudah ada ItemNameatau menyisipkan yang baru ItemNameketika 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, Gdan M.
  • D, maka rentang # 2 (dari Cke F) dikunci: sesi 56 tidak dapat memasukkan nilai E(belum). Tapi sesi 56 dapat memasukkan nilai-nilai A, Gdan M.
  • M, maka rentang # 4 (dari Jke $) dikunci: sesi 56 tidak dapat memasukkan nilai X(belum). Tapi sesi 56 dapat memasukkan nilai-nilai A, Ddan 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:

  1. Semua permintaan adalah untuk nilai kunci unik:

    Dalam hal ini, CATCHblok 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).

  2. 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 - CATCHblok 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 updlockpetunjuk 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 serializableperilaku hanya mencegah INSERToperasi dalam kisaran yang telah dibaca dan karenanya dikunci; itu tidak mencegah SELECToperasi pada kisaran itu.

    The serializablependekatan, 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 :).


7

Jawaban yang Diperbarui


Tanggapan untuk @srutzky

Masalah lain, meskipun kecil, dengan pendekatan "transaksi serial + klausa OUTPUT" ini adalah bahwa klausa OUTPUT (dalam penggunaan 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 parameter OUTPUT 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.

Saya setuju, dan untuk alasan yang sama saya menggunakan parameter output saat bijaksana . Adalah kesalahan saya untuk tidak menggunakan parameter output pada jawaban awal saya, saya malas.

Berikut adalah prosedur yang direvisi menggunakan parameter output, optimasi tambahan, bersama dengan next value foryang @srutzky jelaskan dalam jawabannya :

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;

catatan pembaruan : Termasuk updlockdengan pilih akan mengambil kunci yang tepat dalam skenario ini. Terima kasih kepada @srutzky, yang menunjukkan bahwa ini dapat menyebabkan deadlock ketika hanya digunakan serializablepada select.

Catatan: Ini mungkin bukan kasusnya, tetapi jika memungkinkan prosedur akan dipanggil dengan nilai untuk @vValueId, termasuk set @vValueId = null;setelahnya set xact_abort on;, jika tidak maka dapat dihapus.


Tentang contoh @ srutzky tentang perilaku penguncian rentang kunci:

@srutzky hanya menggunakan satu nilai di tabelnya, dan mengunci kunci "berikutnya" / "tak terbatas" untuk pengujiannya untuk menggambarkan penguncian rentang kunci. Sementara tesnya menggambarkan apa yang terjadi dalam situasi itu, saya percaya cara informasi disajikan dapat mengarah pada asumsi yang salah tentang jumlah penguncian yang bisa dihadapi seseorang ketika menggunakan serializabledalam skenario seperti yang disajikan dalam pertanyaan asli.

Meskipun saya merasakan bias (mungkin salah) dalam cara dia menyajikan penjelasan dan contoh penguncian rentang kunci, mereka masih benar.


Setelah penelitian lebih lanjut, saya menemukan artikel blog yang sangat relevan dari 2011 oleh Michael J. Swart: Mythbusting: Pembaruan Serentak / Solusi Penyisipan . Di dalamnya, ia menguji beberapa metode untuk akurasi dan konkurensi. Metode 4: Peningkatan Isolasi + Kunci Tuning Halus didasarkan pada posting Sam Saffron Masukkan atau Perbarui Pola Untuk SQL Server , dan satu-satunya metode dalam tes asli untuk memenuhi harapannya (bergabung kemudian dengan merge with (holdlock)).

Pada bulan Februari 2016, Michael J. Swart memposting Ugly Pragmatism For The Win . Dalam postingan itu, ia membahas beberapa penyetelan tambahan yang ia buat untuk prosedur peringatan Saffron untuk mengurangi penguncian (yang saya sertakan dalam prosedur di atas).

Setelah melakukan perubahan itu, Michael tidak senang prosedurnya mulai terlihat lebih rumit dan berkonsultasi dengan seorang rekan bernama Chris. Chris membaca semua posting Mythbusters asli dan membaca semua komentar dan bertanya tentang pola TRY CATCH JFDI @ gbn . Pola ini mirip dengan jawaban @ srutzky, dan merupakan solusi yang akhirnya Michael gunakan dalam contoh itu.

Michael J Swart:

Kemarin pikiran saya berubah tentang cara terbaik untuk melakukan konkurensi. Saya menjelaskan beberapa metode dalam Mythbusting: Pembaruan Serentak / Solusi Penyisipan. Metode pilihan saya adalah untuk meningkatkan tingkat isolasi dan kunci fine tune.

Paling tidak itu adalah pilihan saya. Saya baru-baru ini mengubah pendekatan saya untuk menggunakan metode yang disarankan GBN dalam komentar. Dia menggambarkan metodenya sebagai "TRY CATCH JFDI pattern". Biasanya saya menghindari solusi seperti itu. Ada aturan praktis yang mengatakan bahwa pengembang tidak harus bergantung pada menangkap kesalahan atau pengecualian untuk aliran kontrol. Tapi saya melanggar aturan itu kemarin.

Ngomong-ngomong, saya suka deskripsi gbn untuk pola “JFDI”. Ini mengingatkan saya pada video motivasi Shia Labeouf.


Menurut pendapat saya, kedua solusi itu layak. Walaupun saya masih lebih suka untuk meningkatkan level isolasi dan kunci fine tune, jawaban @ srutzky juga valid dan mungkin atau mungkin tidak lebih performan dalam situasi spesifik Anda.

Mungkin di masa depan saya juga akan sampai pada kesimpulan yang sama dengan Michael J. Swart, tapi saya belum sampai di sana.


Ini bukan pilihan saya, tapi inilah adaptasi saya terhadap adaptasi Michael J. Stewart dari @gbn's Try Catch. Prosedur JFDI akan terlihat seperti:

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;

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.

Komentar Aaron Bertrand tentang tautan posting Michael J Swart ke pengujian yang relevan yang telah dilakukannya dan mengarah ke pertukaran ini. Kutipan dari bagian komentar pada Pragmatisme Jelek Untuk Kemenangan :

Namun, kadang-kadang, JFDI mengarah ke kinerja yang lebih buruk secara keseluruhan, tergantung pada% panggilan yang gagal. Memunculkan pengecualian memiliki overhead yang substansial. Saya menunjukkan ini dalam beberapa posting:

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/

Komentar oleh Aaron Bertrand - 11 Februari 2016 @ 11:49 pagi

dan balasan dari:

Anda benar Aaron, dan kami memang mengujinya.

Ternyata dalam kasus kami, persentase panggilan yang gagal adalah 0 (bila dibulatkan ke persentase terdekat).

Saya pikir Anda menggambarkan poin bahwa sebanyak mungkin, mengevaluasi hal-hal berdasarkan kasus per kasus dengan mengikuti aturan praktis.

Itu juga mengapa kami menambahkan klausa WHERE NOT EXISTS yang tidak perlu.

Komentar oleh Michael J. Swart - 11 Februari 2016 @ 11:57 pagi


Tautan baru:


Jawaban asli


Saya masih lebih suka menggunakan pendekatan Sam Saffron vs menggunakan merge, terutama ketika berhadapan dengan satu baris.

Saya akan mengadaptasi metode yang bagus itu untuk situasi ini seperti ini:

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;

Saya akan konsisten dengan penamaan Anda, dan serializablesama seperti holdlock, pilih satu dan konsisten dalam penggunaannya. Saya cenderung menggunakan serializablekarena itu nama yang sama dengan yang digunakan saat menentukan set transaction isolation level serializable.

Dengan menggunakan serializableatau holdlockkunci rentang diambil berdasarkan nilai @vNameyang membuat operasi lain menunggu jika mereka memilih atau memasukkan nilai ke dalam dbo.NameLookupyang menyertakan nilai dalam whereklausa.

Agar kunci rentang berfungsi dengan baik, perlu ada indeks pada ItemNamekolom ini berlaku saat menggunakan mergejuga.


Berikut adalah prosedur yang akan terlihat kebanyakan mengikuti whitepaper Erland Sommarskog untuk penanganan kesalahan , menggunakan throw. Jika throwbukan bagaimana Anda meningkatkan kesalahan, ubahlah agar konsisten dengan prosedur lainnya:

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

Untuk meringkas apa yang terjadi dalam prosedur di atas: set nocount on; set xact_abort on;seperti yang selalu Anda lakukan , maka jika variabel input kami is nullatau kosong, select id = cast(null as int)sebagai hasilnya. Jika tidak nol atau kosong, dapatkan Idvariabel kami sambil memegang tempat itu kalau-kalau tidak ada. Jika Idada, kirimkan. Jika tidak ada, masukkan dan kirimkan yang baru Id.

Sementara itu, panggilan lain ke prosedur ini mencoba untuk menemukan Id untuk nilai yang sama akan menunggu sampai transaksi pertama selesai dan kemudian pilih & kembalikan. Panggilan lain ke prosedur ini atau pernyataan lain yang mencari nilai lain akan terus berlanjut karena ini tidak menghalangi.

Walaupun saya setuju dengan @srutzky bahwa Anda dapat menangani tabrakan dan menelan pengecualian untuk masalah semacam ini, saya pribadi lebih suka mencoba dan menyesuaikan solusi untuk menghindari hal itu jika memungkinkan. Dalam hal ini, saya tidak merasa bahwa menggunakan kunci dari serializableadalah pendekatan yang berat, dan saya yakin itu akan menangani konkurensi tinggi dengan baik.

Kutipan dari dokumentasi sql server pada tabel mengisyaratkan serializable/holdlock :

SERIALISASI

Setara dengan HOLDLOCK. Jadikan kunci bersama lebih ketat dengan menahannya hingga transaksi selesai, alih-alih melepaskan kunci bersama segera setelah tabel atau halaman data yang diperlukan tidak lagi diperlukan, apakah transaksi telah selesai atau belum. Pemindaian dilakukan dengan semantik yang sama dengan transaksi yang berjalan pada tingkat isolasi SERIALIZABLE. Untuk informasi lebih lanjut tentang tingkat isolasi, lihat SET TINGKAT ISOLASI TRANSAKSI (Transact-SQL).

Kutipan dari dokumentasi sql server pada tingkat isolasi transaksiserializable

SERIALIZABLE Menentukan hal berikut:

  • Pernyataan tidak dapat membaca data yang telah dimodifikasi tetapi belum dilakukan oleh transaksi lainnya.

  • Tidak ada transaksi lain yang dapat mengubah data yang telah dibaca oleh transaksi saat ini hingga transaksi saat ini selesai.

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


Tautan terkait dengan solusi di atas:

MERGEmemiliki sejarah jerawatan, dan tampaknya butuh lebih banyak waktu untuk memastikan bahwa kode tersebut berperilaku seperti yang Anda inginkan di bawah semua sintaks itu. mergeArtikel yang relevan :

Satu tautan terakhir, Kendra Little melakukan perbandingan kasar mergevsinsert with left join , dengan peringatan di mana ia berkata "Saya tidak melakukan pengujian beban menyeluruh tentang ini", tetapi masih merupakan bacaan yang baik.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.