Tidak dapat memasukkan baris kunci duplikat pada indeks yang tidak unik?

14

Kami telah menemukan kesalahan aneh ini tiga kali selama beberapa hari terakhir, setelah bebas dari kesalahan selama 8 minggu, dan saya bingung.

Ini adalah pesan kesalahan:

Executing the query "EXEC dbo.MergeTransactions" failed with the following error:
"Cannot insert duplicate key row in object 'sales.Transactions' with unique index
'NCI_Transactions_ClientID_TransactionDate'.
The duplicate key value is (1001, 2018-12-14 19:16:29.00, 304050920).".

Indeks yang kami miliki tidak unik. Jika Anda perhatikan, nilai kunci duplikat dalam pesan kesalahan bahkan tidak sejalan dengan indeks. Hal yang aneh adalah jika saya menjalankan kembali proc, itu berhasil.

Ini adalah tautan terbaru yang bisa saya temukan yang memiliki masalah, tetapi saya tidak melihat solusinya.

https://www.sqlservercentral.com/forums/topic/error-cannot-insert-duplicate-key-row-in-a-non-unique-index

Beberapa hal tentang skenario saya:

  • Proc sedang memperbarui TransactionID (bagian dari kunci utama) - Saya pikir ini yang menyebabkan kesalahan tetapi tidak tahu mengapa? Kami akan menghapus logika itu.
  • Ubah pelacakan diaktifkan di atas meja
  • Melakukan transaksi tanpa komitmen

Ada 45 bidang untuk setiap tabel, saya terutama mencantumkan yang digunakan dalam indeks. Saya memperbarui TransactionID (kunci berkerumun) dalam pernyataan pembaruan (tidak perlu). Aneh bahwa kami belum memiliki masalah selama berbulan-bulan sampai minggu lalu. Dan itu hanya terjadi secara sporadis melalui SSIS.

Meja

USE [DB]
GO

/****** Object:  Table [sales].[Transactions]    Script Date: 5/29/2019 1:37:49 PM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[sales].[Transactions]') AND type in (N'U'))
BEGIN
CREATE TABLE [sales].[Transactions]
(
    [TransactionID] [bigint] NOT NULL,
    [ClientID] [int] NOT NULL,
    [TransactionDate] [datetime2](2) NOT NULL,
    /* snip*/
    [BusinessUserID] [varchar](150) NOT NULL,
    [BusinessTransactionID] [varchar](150) NOT NULL,
    [InsertDate] [datetime2](2) NOT NULL,
    [UpdateDate] [datetime2](2) NOT NULL,
 CONSTRAINT [PK_Transactions_TransactionID] PRIMARY KEY CLUSTERED 
(
    [TransactionID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, DATA_COMPRESSION=PAGE) ON [DB_Data]
) ON [DB_Data]
END
GO
USE [DB]

IF NOT EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[sales].[Transactions]') AND name = N'NCI_Transactions_ClientID_TransactionDate')
begin
CREATE NONCLUSTERED INDEX [NCI_Transactions_ClientID_TransactionDate] ON [sales].[Transactions]
(
    [ClientID] ASC,
    [TransactionDate] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, DATA_COMPRESSION = PAGE) ON [DB_Data]
END

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[sales].[DF_Transactions_Units]') AND type = 'D')
BEGIN
ALTER TABLE [sales].[Transactions] ADD  CONSTRAINT [DF_Transactions_Units]  DEFAULT ((0)) FOR [Units]
END
GO

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[sales].[DF_Transactions_ISOCurrencyCode]') AND type = 'D')
BEGIN
ALTER TABLE [sales].[Transactions] ADD  CONSTRAINT [DF_Transactions_ISOCurrencyCode]  DEFAULT ('USD') FOR [ISOCurrencyCode]
END
GO

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[sales].[DF_Transactions_InsertDate]') AND type = 'D')
BEGIN
ALTER TABLE [sales].[Transactions] ADD  CONSTRAINT [DF_Transactions_InsertDate]  DEFAULT (sysdatetime()) FOR [InsertDate]
END
GO

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[sales].[DF_Transactions_UpdateDate]') AND type = 'D')
BEGIN
ALTER TABLE [sales].[Transactions] ADD  CONSTRAINT [DF_Transactions_UpdateDate]  DEFAULT (sysdatetime()) FOR [UpdateDate]
END
GO

meja sementara

same columns as the mgdata. including the relevant fields. Also has a non-unique clustered index
(
    [BusinessTransactionID] [varchar](150) NULL,
    [BusinessUserID] [varchar](150) NULL,
    [PostalCode] [varchar](25) NULL,
    [TransactionDate] [datetime2](2) NULL,

    [Units] [int] NOT NULL,
    [StartDate] [datetime2](2) NULL,
    [EndDate] [datetime2](2) NULL,
    [TransactionID] [bigint] NULL,
    [ClientID] [int] NULL,

) 

CREATE CLUSTERED INDEX ##workingTransactionsMG_idx ON #workingTransactions (TransactionID)

It is populated in batches (500k rows at a time), something like this
IF OBJECT_ID(N'tempdb.dbo.#workingTransactions') IS NOT NULL DROP TABLE #workingTransactions;
select fields 
into #workingTransactions
from import.Transactions
where importrowid between two number ranges -- pseudocode

Kunci utama

 CONSTRAINT [PK_Transactions_TransactionID] PRIMARY KEY CLUSTERED 
(
    [TransactionID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, DATA_COMPRESSION=PAGE) ON [Data]
) ON [Data]

Indeks tidak berkerumun

CREATE NONCLUSTERED INDEX [NCI_Transactions_ClientID_TransactionDate] ON [sales].[Transactions]
(
    [ClientID] ASC,
    [TransactionDate] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, DATA_COMPRESSION = PAGE)

contoh pernyataan pembaruan

-- updates every field
update t 
set 
    t.transactionid = s.transactionid,
    t.[CityCode]=s.[CityCode],
      t.TransactionDate=s.[TransactionDate],
     t.[ClientID]=s.[ClientID],
                t.[PackageMonths] = s.[PackageMonths],
                t.UpdateDate = @UpdateDate
              FROM #workingTransactions s
              JOIN [DB].[sales].[Transactions] t 
              ON s.[TransactionID] = t.[TransactionID]
             WHERE CAST(HASHBYTES('SHA2_256 ',CONCAT( S.[BusinessTransactionID],'|',S.[BusinessUserID],'|', etc)
                <> CAST(HASHBYTES('SHA2_256 ',CONCAT( T.[BusinessTransactionID],'|',T.[BusinessUserID],'|', etc)

Pertanyaan saya adalah, apa yang terjadi di bawah tenda? Dan apa solusinya? Untuk referensi, tautan di atas menyebutkan ini:

Pada titik ini, saya punya beberapa teori:

  • Bug yang berkaitan dengan tekanan memori atau rencana pembaruan paralel besar, tetapi saya akan mengharapkan jenis kesalahan yang berbeda dan sejauh ini saya tidak dapat menghubungkan sumber daya yang rendah akan kerangka waktu dari kesalahan yang terisolasi dan sporadis ini.
  • Bug dalam pernyataan atau data UPDATE menyebabkan pelanggaran duplikat yang sebenarnya pada kunci utama, tetapi beberapa bug SQL Server tidak jelas mengakibatkan dan pesan kesalahan yang mengutip nama indeks yang salah.
  • Bacaan kotor yang dihasilkan dari isolasi yang tidak terikat baca menyebabkan pembaruan paralel besar untuk memasukkan ganda. Tetapi pengembang ETL mengklaim default read berkomitmen digunakan, dan sulit untuk menentukan dengan tepat tingkat isolasi apa yang sebenarnya digunakan saat runtime.

Saya menduga bahwa jika saya mengubah rencana eksekusi sebagai solusi, mungkin MAXDOP (1) mengisyaratkan atau menggunakan flag jejak sesi untuk menonaktifkan operasi spool, kesalahan hanya akan hilang, tetapi tidak jelas bagaimana ini akan mempengaruhi kinerja

Versi: kapan

Microsoft SQL Server 2017 (RTM-CU13) (KB4466404) - 14.0.3048.4 (X64) 30 Nov 2018 12:57:58 Hak cipta (C) 2017 Microsoft Corporation Enterprise Edition (64-bit) pada Windows Server 2016 Standard 10.0 (Build 14393 :)

Gabe
sumber

Jawaban:

10

Pertanyaan saya adalah, apa yang terjadi di bawah tenda? Dan apa solusinya?

Itu adalah bug. Masalahnya adalah itu hanya terjadi sesekali, dan akan sulit untuk mereproduksi. Namun, peluang terbaik Anda adalah menggunakan dukungan Microsoft. Pemutakhiran pemrosesan sangat rumit, jadi ini membutuhkan penyelidikan yang sangat terperinci.

Untuk contoh jenis kompleksitas yang terlibat, lihat posting saya MERGE Bug dengan Indeks yang Difilter dan Hasil yang Salah dengan Tampilan yang Diindeks . Tak satu pun dari mereka yang berhubungan langsung dengan masalah Anda, tetapi mereka memberikan rasa.

Tulis pembaruan deterministik

Itu tentu saja agak generik. Mungkin lebih bermanfaat, saya dapat mengatakan bahwa Anda harus melihat untuk menulis ulang UPDATEpernyataan Anda saat ini . Seperti yang dikatakan dalam dokumentasi :

Berhati-hatilah saat menentukan klausa FROM untuk memberikan kriteria untuk operasi pembaruan. Hasil pernyataan UPDATE tidak terdefinisi jika pernyataan menyertakan klausa FROM yang tidak ditentukan sedemikian rupa sehingga hanya satu nilai yang tersedia untuk setiap kejadian kolom yang diperbarui, yaitu jika pernyataan UPDATE tidak deterministik.

Anda UPDATEadalah tidak deterministik , dan oleh karena itu hasilnya tidak terdefinisi . Anda harus mengubahnya sehingga paling banyak satu baris sumber diidentifikasi untuk setiap baris target. Tanpa perubahan itu, hasil pembaruan mungkin tidak mencerminkan apa pun baris sumber individu.

Contoh

Izinkan saya menunjukkan kepada Anda sebuah contoh, dengan menggunakan tabel-tabel yang secara longgar dimodelkan pada tabel-tabel yang diberikan dalam pertanyaan:

CREATE TABLE dbo.Transactions
(
    TransactionID bigint NOT NULL,
    ClientID integer NOT NULL,
    TransactionDate datetime2(2) NOT NULL,

    CONSTRAINT PK_dbo_Transactions
        PRIMARY KEY CLUSTERED (TransactionID),

    INDEX dbo_Transactions_ClientID_TranDate
        (ClientID, TransactionDate)
);

CREATE TABLE #Working
(
    TransactionID bigint NULL,
    ClientID integer NULL,
    TransactionDate datetime2(2) NULL,

    INDEX cx CLUSTERED (TransactionID)
);

Untuk mempermudah, letakkan satu baris di tabel target, dan empat baris di sumber:

INSERT dbo.Transactions 
    (TransactionID, ClientID, TransactionDate)
VALUES 
    (1, 1, '2019-01-01');

INSERT #Working 
    (TransactionID, ClientID, TransactionDate)
VALUES 
    (1, 2, NULL),
    (1, NULL, '2019-03-03'),
    (1, 3, NULL),
    (1, NULL, '2019-02-02');

Keempat baris sumber cocok dengan target TransactionID, jadi yang mana yang akan digunakan jika kita menjalankan pembaruan (seperti yang ada di pertanyaan) yang bergabung TransactionIDsendirian?

UPDATE T
SET T.TransactionID = W.TransactionID,
    T.ClientID = W.ClientID,
    T.TransactionDate = W.TransactionDate
FROM #Working AS W
JOIN dbo.Transactions AS T
    ON T.TransactionID = W.TransactionID;

(Memperbarui TransactionIDkolom tidak penting untuk demo, Anda dapat berkomentar jika Anda suka.)

Kejutan pertama adalah bahwa UPDATE melengkapi tanpa kesalahan, meskipun tabel target tidak mengizinkan nol di kolom mana pun (semua baris kandidat berisi nol).

Poin penting adalah bahwa hasilnya tidak terdefinisi , dan dalam hal ini menghasilkan hasil yang tidak cocok dengan baris sumber:

SELECT
    T.TransactionID,
    T.ClientID,
    T.TransactionDate
FROM dbo.Transactions AS T;
╔═══════════════╦══════════╦════════════════════════╗
║ TransactionID ║ ClientID ║    TransactionDate     ║
╠═══════════════╬══════════╬════════════════════════╣
║             1 ║        2 ║ 2019-03-03 00:00:00.00 ║
╚═══════════════╩══════════╩════════════════════════╝

db <> demo biola

Lebih detail: Agregat APAPUN Patah

Pembaruan harus ditulis sedemikian rupa sehingga akan berhasil jika ditulis sebagai MERGEpernyataan yang setara , yang memeriksa upaya untuk memperbarui baris target yang sama lebih dari satu kali. Saya biasanya tidak merekomendasikan penggunaan MERGElangsung, karena telah mengalami banyak bug implementasi, dan biasanya memiliki kinerja yang lebih buruk.

Sebagai bonus, Anda mungkin menemukan bahwa menulis pembaruan Anda saat ini menjadi deterministik akan mengakibatkan masalah bug Anda sesekali juga hilang. Bug produk masih akan ada untuk orang yang menulis pembaruan non-determinstik, tentu saja.

Paul White 9
sumber