Kann keine doppelte Schlüsselzeile in einen nicht eindeutigen Index einfügen?


13

Wir sind in den letzten Tagen dreimal auf diesen seltsamen Fehler gestoßen, nachdem wir 8 Wochen lang fehlerfrei waren, und ich bin ratlos.

Dies ist die Fehlermeldung:

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

Der Index, den wir haben, ist nicht eindeutig. Wenn Sie bemerken, stimmt der doppelte Schlüsselwert in der Fehlermeldung nicht einmal mit dem Index überein. Seltsamerweise gelingt es mir, den Vorgang erneut auszuführen.

Dies ist der neueste Link, den ich finden konnte, der meine Probleme hat, aber ich sehe keine Lösung.

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

Ein paar Dinge über mein Szenario:

  • Der Prozess aktualisiert die Transaktions-ID (Teil des Primärschlüssels). Ich denke, dies ist der Grund für den Fehler, weiß aber nicht warum? Wir werden diese Logik entfernen.
  • Die Änderungsverfolgung ist für die Tabelle aktiviert
  • Nicht festgeschriebene Transaktion wird gelesen

Es gibt 45 Felder für jede Tabelle, ich habe hauptsächlich diejenigen aufgelistet, die in Indizes verwendet werden. Ich aktualisiere die TransactionID (Clustered Key) in der Update-Anweisung (unnötigerweise). Seltsam, dass wir bis letzte Woche seit Monaten keine Probleme mehr hatten. Und es passiert nur sporadisch über SSIS.

Tabelle

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

temporäre Tabelle

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

Primärschlüssel

 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]

Nicht gruppierter Index

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)

Beispiel für eine Update-Anweisung

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

Meine Frage ist, was ist unter der Haube los? Und was ist die Lösung? Als Referenz erwähnt der obige Link Folgendes:

An dieser Stelle habe ich ein paar Theorien:

  • Fehler in Bezug auf Speicherdruck oder großen parallelen Aktualisierungsplan, aber ich würde eine andere Art von Fehler erwarten, und bis jetzt kann ich keine geringen Ressourcen korrelieren.
  • Ein Fehler in der UPDATE-Anweisung oder in den UPDATE-Daten verursacht eine tatsächliche doppelte Verletzung des Primärschlüssels, aber ein obskurer SQL Server-Fehler führt zu einer Fehlermeldung, in der der falsche Indexname angegeben ist.
  • Fehlerhafte Lesevorgänge aufgrund einer nicht festgeschriebenen Leseisolation, die ein großes paralleles Update mit doppeltem Insert zur Folge hat. ETL-Entwickler behaupten jedoch, dass standardmäßig ein festgeschriebener Lesezugriff verwendet wird und es schwierig ist, genau zu bestimmen, welche Isolationsstufe der Prozess zur Laufzeit tatsächlich verwendet wird.

Ich vermute, dass, wenn ich den Ausführungsplan als Workaround optimiere, vielleicht als MAXDOP (1) -Hinweis oder indem ich das Session-Trace-Flag verwende, um den Spool-Betrieb zu deaktivieren, der Fehler einfach verschwindet, aber es ist unklar, wie sich dies auf die Leistung auswirken würde

Ausführung

Microsoft SQL Server 2017 (RTM-CU13) (KB4466404) - 14.0.3048.4 (X64) 30. November 2018 12:57:58 Copyright (C) 2017 Microsoft Corporation Enterprise Edition (64-Bit) unter Windows Server 2016 Standard 10.0 (Build 14393) :)

Antworten:


10

Meine Frage ist, was ist unter der Haube los? Und was ist die Lösung?

Es ist ein Fehler. Das Problem ist, dass es nur gelegentlich vorkommt und sich nur schwer reproduzieren lässt. Ihre beste Chance besteht jedoch darin, den Microsoft-Support zu engagieren. Die Update-Verarbeitung ist unglaublich komplex, daher ist eine sehr detaillierte Untersuchung erforderlich.

Ein Beispiel für die Art der Komplexität finden Sie in meinen Beiträgen MERGE Bug mit gefilterten Indizes und Incorrect Results with Indexed Views . Keiner von beiden bezieht sich direkt auf Ihr Problem, aber sie geben einen Geschmack.

Schreiben Sie ein deterministisches Update

Das ist natürlich alles ziemlich allgemein. Vielleicht nützlicher kann ich sagen, dass Sie versuchen sollten, Ihre aktuelle UPDATEAussage umzuschreiben . Wie die Dokumentation sagt:

Seien Sie vorsichtig, wenn Sie die FROM-Klausel angeben, um die Kriterien für den Aktualisierungsvorgang anzugeben. Die Ergebnisse einer UPDATE-Anweisung sind undefiniert, wenn die Anweisung eine FROM-Klausel enthält, die nicht so angegeben ist, dass für jedes zu aktualisierende Spaltenvorkommen nur ein Wert verfügbar ist, dh wenn die UPDATE-Anweisung nicht deterministisch ist.

Ihr UPDATEist nicht deterministisch , und die Ergebnisse sind daher undefiniert . Sie sollten es so ändern, dass höchstens eine Quellzeile für jede Zielzeile identifiziert wird. Ohne diese Änderung spiegelt das Ergebnis der Aktualisierung möglicherweise keine einzelne Quellzeile wider .

Beispiel

Lassen Sie mich Ihnen ein Beispiel zeigen, in dem Tabellen verwendet werden, die den in der Frage angegebenen Tabellen lose nachempfunden sind:

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

Fügen Sie zur Vereinfachung eine Zeile in die Zieltabelle und vier Zeilen in die Quelle ein:

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

Alle vier Quellzeilen stimmen mit dem Ziel überein. Welche TransactionIDwird also verwendet, wenn wir ein Update (wie das in der Frage gezeigte) ausführen, das sich TransactionIDalleine anschließt?

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;

(Die Aktualisierung der TransactionIDSpalte ist für die Demo nicht wichtig. Sie können sie auskommentieren, wenn Sie möchten.)

Die erste Überraschung ist, dass der UPDATEVorgang fehlerfrei abgeschlossen wird, obwohl die Zieltabelle in keiner Spalte Nullen zulässt (alle Kandidatenzeilen enthalten eine Null).

Der wichtige Punkt ist, dass das Ergebnis undefiniert ist und in diesem Fall ein Ergebnis erzeugt, das keiner der Quellzeilen entspricht:

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 <> Geige Demo

Weitere Details: Das ANY-Aggregat ist defekt

Das Update sollte so geschrieben werden, dass es erfolgreich ist, wenn es als äquivalente MERGEAnweisung geschrieben wird, die mehr als einmal nach Versuchen sucht, dieselbe Zielzeile zu aktualisieren. Ich empfehle generell nicht, MERGEdirekt zu verwenden, da es so viele Implementierungsfehler gab und normalerweise eine schlechtere Leistung aufweist.

Als Bonus können Sie feststellen, dass das Umschreiben Ihres aktuellen Updates deterministisch ist und Ihr gelegentliches Bug-Problem ebenfalls verschwindet. Der Produktfehler wird natürlich weiterhin für Leute bestehen, die nicht deterministische Updates schreiben.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.