Impossibile inserire una riga chiave duplicata in un indice non univoco?


14

Abbiamo riscontrato questo strano errore tre volte negli ultimi giorni, dopo essere stato privo di errori per 8 settimane, e sono sconcertato.

Questo è il messaggio di errore:

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

L'indice che abbiamo non è unico. Se noti, il valore della chiave duplicata nel messaggio di errore non si allinea nemmeno con l'indice. La cosa strana è che se riesco a eseguire il proc, ci riesce.

Questo è il link più recente che ho trovato che ha i miei problemi ma non vedo una soluzione.

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

Un paio di cose sul mio scenario:

  • Il proc sta aggiornando TransactionID (parte della chiave primaria) - Penso che questo sia ciò che sta causando l'errore ma non so perché? Rimuoveremo questa logica.
  • Il rilevamento delle modifiche è abilitato nella tabella
  • Esecuzione della lettura non impegnata

Ci sono 45 campi per ogni tabella, ho elencato principalmente quelli usati negli indici. Sto aggiornando il TransactionID (chiave cluster) nell'istruzione update (inutilmente). Strano che non abbiamo avuto problemi da mesi fino alla scorsa settimana. E succede solo sporadicamente tramite SSIS.

tavolo

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

tabella temporanea

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

Chiave primaria

 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]

Indice non cluster

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)

dichiarazione di aggiornamento di esempio

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

La mia domanda è: cosa sta succedendo sotto il cofano? E qual è la soluzione? Per riferimento, il link sopra menziona questo:

A questo punto, ho alcune teorie:

  • Il bug relativo alla pressione della memoria o al piano di aggiornamento parallelo di grandi dimensioni, ma mi aspetterei un diverso tipo di errore e finora non riesco a correlare le risorse basse, faranno scadere questi errori isolati e sporadici.
  • Un bug nell'istruzione o nei dati UPDATE sta causando una reale violazione duplicata sulla chiave primaria, ma si stanno verificando alcuni oscuri bug di SQL Server e un messaggio di errore che cita il nome di indice errato.
  • Letture sporche risultanti dall'isolamento di lettura senza commit che causa un doppio aggiornamento parallelo al doppio inserimento. Ma gli sviluppatori ETL affermano che viene utilizzato il commit di lettura predefinito ed è difficile determinare esattamente quale livello di isolamento viene effettivamente utilizzato dal processo in fase di esecuzione.

Sospetto che se modifico il piano di esecuzione come soluzione alternativa, forse suggerimento MAXDOP (1) o utilizzo del flag di traccia della sessione per disabilitare l'operazione di spooling, l'errore scompare, ma non è chiaro come ciò influirebbe sulle prestazioni

Versione

Microsoft SQL Server 2017 (RTM-CU13) (KB4466404) - 14.0.3048.4 (X64) 30 nov 2018 12:57:58 Copyright (C) 2017 Microsoft Corporation Enterprise Edition (64-bit) su Windows Server 2016 Standard 10.0 (build 14393 :)

Risposte:


10

La mia domanda è: cosa sta succedendo sotto il cofano? E qual è la soluzione?

È un bug. Il problema è che succede solo occasionalmente e sarà difficile riprodurlo. Tuttavia, la migliore possibilità è quella di coinvolgere il supporto Microsoft. L'elaborazione degli aggiornamenti è incredibilmente complessa, quindi ciò richiederà un'indagine molto dettagliata.

Per un esempio del tipo di complessità coinvolta, dai un'occhiata ai miei post MERGE Bug con indici filtrati e risultati errati con visualizzazioni indicizzate . Nessuno di questi si riferisce direttamente al tuo problema, ma danno un sapore.

Scrivi un aggiornamento deterministico

Questo è tutto piuttosto generico, ovviamente. Forse più utilmente, posso dire che dovresti cercare di riscrivere la tua attuale UPDATEdichiarazione. Come dice la documentazione :

Prestare attenzione quando si specifica la clausola FROM per fornire i criteri per l'operazione di aggiornamento. I risultati di un'istruzione UPDATE non sono definiti se l'istruzione include una clausola FROM non specificata in modo tale che sia disponibile un solo valore per ogni occorrenza di colonna che viene aggiornata, ovvero se l'istruzione UPDATE non è deterministica.

Il tuo nonUPDATE è deterministico e quindi i risultati non sono definiti . È necessario modificarlo in modo tale che al massimo venga identificata una riga di origine per ciascuna riga di destinazione. Senza tale modifica, il risultato dell'aggiornamento potrebbe non riflettere alcuna riga di origine singola.

Esempio

Lascia che ti mostri un esempio, usando le tabelle liberamente modellate su quelle fornite nella domanda:

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)
);

Per semplificare le cose, inserisci una riga nella tabella di destinazione e quattro righe nell'origine:

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');

Tutte e quattro le righe di origine corrispondono alla destinazione TransactionID, quindi quale verrà utilizzata se eseguiamo un aggiornamento (come quello nella domanda) che si unisce da TransactionIDsolo?

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;

(L'aggiornamento della TransactionIDcolonna non è importante per la demo, puoi commentarlo se vuoi.)

La prima sorpresa è che si UPDATEcompleta senza errori, nonostante la tabella di destinazione non consenta null in nessuna colonna (tutte le righe candidate contengono un null).

Il punto importante è che il risultato non è definito e in questo caso produce un risultato che non corrisponde a nessuna delle righe di origine:

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 violino

Maggiori dettagli: ANY Aggregate è rotto

L'aggiornamento deve essere scritto in modo tale da riuscire se scritto come MERGEistruzione equivalente , che verifica la presenza di tentativi di aggiornamento della stessa riga di destinazione più di una volta. In genere non consiglio di usarlo MERGEdirettamente, perché è stato soggetto a così tanti bug di implementazione e normalmente ha prestazioni peggiori.

Come bonus, potresti scoprire che riscrivere il tuo attuale aggiornamento come deterministico comporterà la scomparsa del tuo problema occasionale di bug. Il bug del prodotto continuerà a esistere per le persone che ovviamente scrivono aggiornamenti non deterministici.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.