Menggabungkan pernyataan menemui jalan buntu

22

Saya memiliki prosedur berikut (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey membentuk kunci komposit untuk tabel target. CompanyId adalah kunci asing ke tabel induk. Juga, ada indeks non-clustered di CompanyId asc, UserId asc.

Ini dipanggil dari berbagai utas, dan saya secara konsisten mendapatkan kebuntuan antara berbagai proses yang menyebut pernyataan yang sama. Pemahaman saya adalah bahwa "with (holdlock)" diperlukan untuk mencegah kesalahan kondisi sisipan / perbarui.

Saya berasumsi bahwa dua utas berbeda mengunci baris (atau halaman) dalam urutan yang berbeda ketika mereka memvalidasi kendala, dan dengan demikian menemui jalan buntu.

Apakah ini asumsi yang tepat?

Apa cara terbaik untuk mengatasi situasi ini (yaitu tidak ada deadlock, dampak minimum pada kinerja multi-threaded)?

Gambar Rencana Kueri (Jika Anda melihat gambar di tab baru, itu dapat dibaca. Maaf untuk ukurannya yang kecil.)

  • Paling banyak ada 28 baris dalam @datatable.
  • Saya telah menelusuri kembali kode tersebut, dan saya tidak dapat melihat di mana pun kami memulai transaksi di sini.
  • Kunci asing diatur untuk kaskade hanya pada penghapusan, dan tidak ada penghapusan dari tabel induk.
Sako73
sumber

Jawaban:

12

OK, setelah melihat semuanya beberapa kali, saya pikir asumsi dasar Anda benar. Apa yang mungkin terjadi di sini adalah:

  1. Bagian MATCH dari MERGE memeriksa indeks untuk menemukan kecocokan, mengunci-baca baris / halaman tersebut saat berjalan.

  2. Ketika memiliki baris tanpa kecocokan, itu akan mencoba untuk memasukkan Baris Indeks baru terlebih dahulu sehingga akan meminta kunci tulis baris / halaman ...

Tetapi jika pengguna lain juga mendapatkan langkah 1 pada baris / halaman yang sama, maka pengguna pertama akan diblokir dari Pembaruan, dan ...

Jika pengguna kedua juga perlu memasukkan pada halaman yang sama, maka mereka menemui jalan buntu.

AFAIK, hanya ada satu cara (sederhana) untuk menjadi 100% yakin bahwa Anda tidak bisa mendapatkan jalan buntu dengan prosedur ini dan itu adalah menambahkan petunjuk TABLOCKX ke MERGE, tetapi itu mungkin akan memiliki dampak yang sangat buruk pada kinerja.

Ada kemungkinan bahwa menambahkan petunjuk TABLOCK sebagai gantinya akan cukup untuk menyelesaikan masalah tanpa harus berpengaruh besar pada kinerja Anda.

Terakhir, Anda juga dapat mencoba menambahkan PAGLOCK, XLOCK atau keduanya PAGLOCK dan XLOCK. Sekali lagi itu mungkin bekerja dan kinerja mungkin tidak terlalu buruk. Anda harus mencobanya untuk melihatnya.

RBarryYoung
sumber
Apakah menurut Anda tingkat isolasi snapshot (versi baris) dapat membantu di sini?
Mikael Eriksson
Mungkin. Atau mungkin mengubah pengecualian kebuntuan menjadi pengecualian konkurensi.
RBarryYoung
2
Menentukan petunjuk TABLOCK pada tabel yang merupakan target pernyataan INSERT memiliki efek yang sama dengan menentukan petunjuk TABLOCKX. (Sumber: msdn.microsoft.com/en-us/library/bb510625.aspx )
tuespetre
31

Tidak akan ada masalah jika variabel tabel hanya memiliki satu nilai. Dengan beberapa baris, ada kemungkinan baru untuk kebuntuan. Misalkan dua proses bersamaan (A & B) dijalankan dengan variabel tabel yang berisi (1, 2) dan (2, 1) untuk perusahaan yang sama.

Proses A membaca tujuan, tidak menemukan baris, dan memasukkan nilai '1'. Ini memegang kunci baris eksklusif pada nilai '1'. Proses B membaca tujuan, tidak menemukan baris, dan memasukkan nilai '2'. Itu memegang kunci baris eksklusif pada nilai '2'.

Sekarang proses A perlu memproses baris 2, dan proses B perlu memproses baris 1. Tidak ada proses yang dapat membuat kemajuan karena membutuhkan kunci yang tidak kompatibel dengan kunci eksklusif yang dipegang oleh proses lain.

Untuk menghindari kebuntuan dengan beberapa baris, baris harus diproses (dan tabel diakses) dalam urutan yang sama setiap waktu . Variabel tabel dalam rencana eksekusi yang ditunjukkan dalam pertanyaan adalah heap, jadi baris tidak memiliki urutan intrinsik (mereka cenderung dibaca dalam urutan penyisipan, meskipun ini tidak dijamin):

Rencana yang ada

Kurangnya urutan pemrosesan baris yang konsisten mengarah langsung ke peluang kebuntuan. Pertimbangan kedua adalah bahwa kurangnya jaminan keunikan kunci berarti bahwa Spool Tabel diperlukan untuk memberikan Perlindungan Halloween yang benar. Spool adalah spool bersemangat, artinya semua baris ditulis ke meja kerja tempdb sebelum dibaca kembali dan diputar ulang untuk operator Insert.

Mendefinisikan ulang TYPEvariabel tabel untuk menyertakan sebuah cluster PRIMARY KEY:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

Rencana pelaksanaan sekarang menunjukkan pemindaian indeks berkerumun dan jaminan keunikan berarti pengoptimal dapat menghapus Spool Tabel dengan aman:

Dengan kunci utama

Dalam tes dengan 5000 iterasi MERGEpernyataan di 128 utas, tidak ada deadlock terjadi dengan variabel tabel berkerumun. Saya harus menekankan bahwa ini hanya berdasarkan pengamatan; variabel tabel berkerumun juga bisa (secara teknis ) menghasilkan baris dalam berbagai pesanan, tetapi kemungkinan pesanan yang konsisten sangat sangat ditingkatkan. Perilaku yang diamati perlu diuji ulang untuk setiap pembaruan kumulatif baru, paket layanan, atau versi baru dari SQL Server, tentu saja.

Dalam hal definisi variabel tabel tidak dapat diubah, ada alternatif lain:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Ini juga mencapai penghapusan spool (dan konsistensi baris-urutan) dengan biaya memperkenalkan semacam eksplisit:

Sortir rencana

Rencana ini juga tidak menghasilkan kebuntuan menggunakan tes yang sama. Skrip reproduksi di bawah ini:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;
Paul White mengatakan GoFundMonica
sumber
8

Saya pikir SQL_Kiwi memberikan analisis yang sangat bagus. Jika Anda perlu menyelesaikan masalah dalam database, Anda harus mengikuti sarannya. Tentu saja Anda perlu menguji ulang bahwa itu masih berfungsi untuk Anda setiap kali Anda meningkatkan, menerapkan paket layanan, atau menambah / mengubah indeks atau tampilan yang diindeks.

Ada tiga alternatif lain:

  1. Anda dapat membuat cerita berseri seri sehingga tidak bertabrakan: Anda dapat menggunakan sp_getapplock pada awal transaksi Anda dan mendapatkan kunci eksklusif sebelum mengeksekusi MERGE Anda. Tentu Anda masih perlu stress untuk mengujinya.

  2. Anda dapat memiliki satu utas menangani semua sisipan Anda, sehingga server aplikasi Anda menangani konkurensi.

  3. Anda dapat secara otomatis mencoba lagi setelah kebuntuan - ini mungkin pendekatan yang paling lambat jika konkurensi tinggi.

Either way, hanya Anda yang dapat menentukan dampak dari solusi Anda pada kinerja.

Biasanya kita tidak memiliki kebuntuan dalam sistem kita sama sekali, meskipun kita memiliki banyak potensi untuk memilikinya. Pada 2011 kami melakukan kesalahan dalam satu penyebaran dan memiliki setengah lusin kebuntuan terjadi dalam beberapa jam, semuanya mengikuti skenario yang sama. Saya memperbaikinya segera dan itu semua adalah jalan buntu untuk tahun ini.

Kami sebagian besar menggunakan pendekatan 1 dalam sistem kami. Ini bekerja sangat baik untuk kita.

AK
sumber
-1

Satu pendekatan lain yang mungkin - Saya telah menemukan Gabung untuk terkadang menghadirkan masalah penguncian dan kinerja - mungkin ada baiknya bermain dengan opsi kueri Opsi (MaxDop x)

Dalam SQL Server masa lalu redup dan jauh memiliki opsi Penguncian Baris Baris - tetapi ini tampaknya telah mati, namun PK berkerumun dengan identitas harus membuat sisipan berjalan bersih.

Ed Green
sumber