¿No se puede insertar una fila de clave duplicada en un índice no exclusivo?

14

Hemos encontrado este extraño error tres veces en los últimos días, después de estar libre de errores durante 8 semanas, y estoy perplejo.

Este es el mensaje de error:

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

El índice que tenemos no es único. Si observa, el valor clave duplicado en el mensaje de error ni siquiera se alinea con el índice. Lo extraño es que si vuelvo a ejecutar el proceso, tiene éxito.

Este es el enlace más reciente que pude encontrar que tiene mis problemas, pero no veo una solución.

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

Un par de cosas sobre mi escenario:

  • El proceso está actualizando el TransactionID (parte de la clave principal). Creo que esto es lo que está causando el error, pero no sé por qué. Vamos a eliminar esa lógica.
  • El seguimiento de cambios está habilitado en la tabla
  • Hacer transacciones de lectura no confirmadas

Hay 45 campos para cada tabla, enumeré principalmente los que se usan en los índices. Estoy actualizando el TransactionID (clave agrupada) en la declaración de actualización (innecesariamente). Es extraño que no hayamos tenido problemas durante meses hasta la semana pasada. Y solo sucede esporádicamente a través de SSIS.

Mesa

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

mesa temporal

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

Clave 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]

Índice no agrupado

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)

declaración de actualización de muestra

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

Mi pregunta es, ¿qué está pasando debajo del capó? y cual es la solución? Como referencia, el enlace de arriba menciona esto:

En este punto, tengo algunas teorías:

  • Error relacionado con la presión de memoria o un gran plan de actualización en paralelo, pero esperaría un tipo diferente de error y hasta ahora no puedo correlacionar los bajos recursos con el tiempo de estos errores aislados y esporádicos.
  • Un error en la declaración o datos de ACTUALIZACIÓN está causando una violación duplicada real en la clave primaria, pero un error oscuro de SQL Server está dando como resultado un mensaje de error que cita el nombre de índice incorrecto.
  • Lecturas sucias resultantes del aislamiento de lectura no confirmada que causa una gran actualización paralela para doble inserción. Pero los desarrolladores de ETL afirman que se usa la lectura predeterminada comprometida, y es difícil determinar exactamente qué nivel de aislamiento se usa realmente el proceso en tiempo de ejecución.

Sospecho que si modifico el plan de ejecución como una solución alternativa, quizás una sugerencia de MAXDOP (1) o si uso el indicador de seguimiento de sesión para deshabilitar la operación de spool, el error simplemente desaparecerá, pero no está claro cómo esto afectaría el rendimiento

Versión

Microsoft SQL Server 2017 (RTM-CU13) (KB4466404) - 14.0.3048.4 (X64) 30 de noviembre de 2018 12:57:58 Copyright (C) 2017 Microsoft Corporation Enterprise Edition (64 bits) en Windows Server 2016 Standard 10.0 (compilación 14393 :)

Gabe
fuente

Respuestas:

10

Mi pregunta es, ¿qué está pasando debajo del capó? y cual es la solución?

Es un error El problema es que solo ocurre ocasionalmente y será difícil de reproducir. Aún así, su mejor oportunidad es contratar el soporte de Microsoft. El procesamiento de actualizaciones es increíblemente complejo, por lo que esto requerirá una investigación muy detallada.

Para ver un ejemplo del tipo de complejidades involucradas, eche un vistazo a mis publicaciones MERGE Bug con índices filtrados y resultados incorrectos con vistas indexadas . Ninguno de los dos se relaciona directamente con su problema, pero dan un sabor.

Escribe una actualización determinista

Todo eso es bastante genérico, por supuesto. Quizás más útilmente, puedo decir que debería buscar reescribir su UPDATEdeclaración actual . Como el dice documentación :

Tenga cuidado al especificar la cláusula FROM para proporcionar los criterios para la operación de actualización. Los resultados de una declaración UPDATE no están definidos si la declaración incluye una cláusula FROM que no se especifica de tal manera que solo un valor esté disponible para cada aparición de columna que se actualice, es decir, si la declaración UPDATE no es determinista.

Tu noUPDATE es determinista y, por lo tanto, los resultados no están definidos . Debe cambiarlo para que, como máximo, se identifique una fila de origen para cada fila de destino. Sin ese cambio, el resultado de la actualización puede no reflejar ninguna fila de origen individual.

Ejemplo

Permíteme mostrarte un ejemplo, usando tablas modeladas libremente sobre las dadas en la pregunta:

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

Para simplificar las cosas, coloque una fila en la tabla de destino y cuatro filas en la fuente:

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

Las cuatro filas de origen coinciden con el destino TransactionID, entonces, ¿cuál se usará si ejecutamos una actualización (como la de la pregunta) que se une 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;

(Actualizando el TransactionID columna no es importante para la demostración, puede comentarlo si lo desea).

La primera sorpresa es que el UPDATE completa sin un error, a pesar de que la tabla de destino no permite nulos en ninguna columna (todas las filas candidatas contienen un nulo).

El punto importante es que el resultado no está definido , y en este caso produce un resultado que no coincide con ninguna de las filas de origen:

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

demostración de violín db <>

Más detalles: CUALQUIER agregado está roto

La actualización debe escribirse de manera que tenga éxito si se escribe como una MERGEdeclaración equivalente , que comprueba si hay intentos de actualizar la misma fila de destino más de una vez. En general, no recomiendo usarlo MERGEdirectamente, porque ha estado sujeto a muchos errores de implementación y normalmente tiene un rendimiento peor.

Como beneficio adicional, puede encontrar que reescribir su actualización actual para que sea determinista dará como resultado que su problema de error ocasional también desaparezca. El error del producto seguirá existiendo para las personas que escriben actualizaciones no determinísticas, por supuesto.

Paul White 9
fuente