web-dev-qa-db-fra.com

Impossible d'insérer une ligne de clé en double sur un index non unique?

Nous avons rencontré cette étrange erreur à trois reprises au cours des derniers jours, après avoir été sans erreur pendant 8 semaines, et je suis perplexe.

Voici le message d'erreur:

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 que nous avons est pas unique. Si vous remarquez, la valeur de clé en double dans le message d'erreur ne correspond même pas à l'index. Chose étrange, si je relance le proc, il réussit.

C'est le lien le plus récent que j'ai pu trouver qui a mes problèmes mais je ne vois pas de solution.

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

Quelques choses sur mon scénario:

  • Le proc met à jour le TransactionID (une partie de la clé primaire) - Je pense que c'est ce qui cause l'erreur, mais je ne sais pas pourquoi? Nous allons supprimer cette logique.
  • Le suivi des modifications est activé sur la table
  • Faire la transaction en lecture non validée

Il y a 45 champs pour chaque table, j'ai surtout répertorié ceux utilisés dans les index. Je mets à jour TransactionID (clé en cluster) dans l'instruction de mise à jour (inutilement). Étrange que nous n'ayons eu aucun problème pendant des mois jusqu'à la semaine dernière. Et cela ne se produit que sporadiquement via SSIS.

Tableau

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

table temporaire

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

Clé primaire

 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]

Index non clusterisé

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)

exemple d'instruction de mise à jour

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

Ma question est, que se passe-t-il sous le capot? Et quelle est la solution? Pour référence, le lien ci-dessus mentionne ceci:

À ce stade, j'ai quelques théories:

  • Bug lié à la pression de la mémoire ou à un grand plan de mise à jour parallèle, mais je m'attendrais à un type d'erreur différent et jusqu'à présent, je ne peux pas corréler les faibles ressources temporelles de ces erreurs isolées et sporadiques.
  • Un bogue dans l'instruction ou les données UPDATE provoque une violation en double réelle sur la clé primaire, mais un bogue SQL Server obscur entraîne un message d'erreur qui cite le mauvais nom d'index.
  • Lectures sales résultant d'une isolation non validée en lecture provoquant une double mise à jour parallèle à double insertion. Mais les développeurs ETL prétendent que la lecture par défaut validée est utilisée, et il est difficile de déterminer exactement quel niveau d'isolement le processus est réellement utilisé lors de l'exécution.

Je soupçonne que si je modifie le plan d'exécution comme solution de contournement, peut-être un indice MAXDOP (1) ou l'utilisation d'un indicateur de trace de session pour désactiver le fonctionnement du spouleur, l'erreur disparaîtra simplement, mais on ne sait pas comment cela affecterait les performances

Version

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

14
Gabe

Ma question est, que se passe-t-il sous le capot? Et quelle est la solution?

C'est un bug. Le problème est que cela ne se produit qu'occasionnellement et sera difficile à reproduire. Pourtant, votre meilleure chance est d'engager le support Microsoft. Le traitement des mises à jour est extrêmement complexe, ce qui nécessitera une enquête très détaillée.

Pour un exemple du genre de complexités impliquées, jetez un œil à mes publications Bug MERGE avec index filtrés et Résultats incorrects avec des vues indexées . Aucun de ces éléments n'est directement lié à votre problème, mais ils donnent une saveur.

Rédiger une mise à jour déterministe

C'est tout à fait générique bien sûr. Peut-être plus utilement, je peux dire que vous devriez chercher à réécrire votre instruction UPDATE actuelle. Comme le dit documentation :

Soyez prudent lorsque vous spécifiez la clause FROM pour fournir les critères de l'opération de mise à jour. Les résultats d'une instruction UPDATE ne sont pas définis si l'instruction inclut une clause FROM qui n'est pas spécifiée de telle sorte qu'une seule valeur est disponible pour chaque occurrence de colonne qui est mise à jour, c'est-à-dire si l'instruction UPDATE n'est pas déterministe.

Votre UPDATE n'est pas non déterministe , et les résultats sont donc indéfinis . Vous devez le modifier pour qu'au plus une ligne source soit identifiée pour chaque ligne cible. Sans cette modification, le résultat de la mise à jour peut ne pas refléter aucune ligne source individuelle .

Exemple

Permettez-moi de vous montrer un exemple, en utilisant des tableaux calqués sur ceux donnés dans la question:

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

Pour simplifier les choses, placez une ligne dans la table cible et quatre lignes dans la source:

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

Les quatre lignes source correspondent à la cible sur TransactionID, alors laquelle sera utilisée si nous exécutons une mise à jour (comme celle de la question) qui se joint à TransactionID seule?

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;

(La mise à jour de la colonne TransactionID n'est pas importante pour la démo, vous pouvez la commenter si vous le souhaitez.)

La première surprise est que le UPDATE se termine sans erreur, malgré le fait que la table cible n'autorise pas les null dans aucune colonne (toutes les lignes candidates contiennent un null).

Le point important est que le résultat est non défini , et dans ce cas produit un résultat qui ne correspond à aucune des lignes source:

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

démo db <> violon

Plus de détails: N'IMPORTE QUEL agrégat est cassé

La mise à jour doit être écrite de manière à réussir si elle est écrite sous la forme de l'instruction MERGE équivalente, qui vérifie les tentatives de mise à jour de la même ligne cible plusieurs fois. Je ne recommande généralement pas d'utiliser directement MERGE, car il a été soumis à de nombreux bogues d'implémentation et a normalement de moins bonnes performances.

En prime, vous pouvez constater que la réécriture de votre mise à jour actuelle pour être déterministe entraînera la disparition de votre problème de bogue occasionnel. Le bug du produit existera toujours pour les personnes qui écrivent des mises à jour non déterministes bien sûr.

10
Paul White 9