Не удается вставить строку с повторяющимся ключом в неуникальный индекс?

14

Мы сталкивались с этой странной ошибкой три раза за последние несколько дней, после того, как в течение 8 недель не допускали ошибок, и я в замешательстве.

Это сообщение об ошибке:

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

Индекс у нас не уникальный. Если вы заметили, значение дублированного ключа в сообщении об ошибке даже не совпадает с индексом. Странная вещь, если я перезапущу процесс, это будет успешно.

Это самая свежая ссылка, по которой я смог найти свои проблемы, но я не вижу решения.

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

Несколько вещей о моем сценарии:

  • Процесс обновляет TransactionID (часть первичного ключа) - я думаю, что это является причиной ошибки, но не знаете почему? Мы удалим эту логику.
  • Отслеживание изменений включено на столе
  • Делать транзакцию читать незафиксированным

Для каждой таблицы есть 45 полей, я в основном перечислил те, которые используются в индексах. Я обновляю TransactionID (кластеризованный ключ) в операторе обновления (без необходимости). Странно, что у нас не было проблем в течение нескольких месяцев до прошлой недели. И это происходит спорадически только через SSIS.

Стол

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

временный стол

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

Первичный ключ

 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]

Некластеризованный индекс

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)

пример обновления заявления

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

У меня вопрос, что происходит под капотом? И каково решение? Для справки, ссылка выше упоминает это:

На данный момент у меня есть несколько теорий:

  • Ошибка связана с нехваткой памяти или большим планом параллельного обновления, но я ожидал бы другого типа ошибки, и пока я не могу соотнести малые ресурсы с временными рамками этих изолированных и спорадических ошибок.
  • Ошибка в операторе UPDATE или данных приводит к фактическому дублированию нарушения в первичном ключе, но в результате возникает неясная ошибка SQL Server и сообщение об ошибке, содержащее неверное имя индекса.
  • Грязные операции чтения в результате незафиксированной изоляции, приводящей к двойному обновлению при большом параллельном обновлении. Но разработчики ETL утверждают, что используется фиксация чтения по умолчанию, и трудно точно определить, какой уровень изоляции фактически используется процессом во время выполнения.

Я подозреваю, что если я настрою план выполнения в качестве обходного пути, возможно, подсказку MAXDOP (1) или с помощью флага трассировки сеанса для отключения операции буферизации, ошибка просто исчезнет, ​​но неясно, как это повлияет на производительность

Версия

Microsoft SQL Server 2017 (RTM-CU13) (KB4466404) - 14.0.3048.4 (X64) 30 ноября 2018 г. 12:57:58 Авторское право (C) 2017 Выпуск Microsoft Corporation Enterprise (64-разрядный) на Windows Server 2016 Standard 10.0 (сборка 14393) :)

Гейб
источник

Ответы:

10

У меня вопрос, что происходит под капотом? И каково решение?

Это ошибка. Проблема в том, что это случается только изредка, и будет трудно воспроизвести. Тем не менее, ваш лучший шанс - обратиться в службу поддержки Microsoft. Обработка обновлений невероятно сложна, так что это потребует очень подробного изучения.

Для примера таких сложностей, посмотрите на мои сообщения Ошибка MERGE с отфильтрованными индексами и неверные результаты с индексированными представлениями . Ни один из них не имеет прямого отношения к вашей проблеме, но они действительно дают вкус.

Написать детерминистическое обновление

Это все, конечно, довольно общее. Возможно, более полезно, я могу сказать, что вы должны переписать свое текущее UPDATEзаявление. Как сказано в документации :

Будьте внимательны при указании предложения FROM, чтобы указать критерии для операции обновления. Результаты оператора UPDATE не определены, если он содержит предложение FROM, которое не указано таким образом, чтобы для каждого обновляемого вхождения столбца было доступно только одно значение, то есть если оператор UPDATE не является детерминированным.

Ваш неUPDATE является детерминированным , и поэтому результаты не определены . Вы должны изменить его так, чтобы для каждой целевой строки была идентифицирована не более одной строки источника. Без этого изменения результат обновления может не отражать какую-либо отдельную строку источника.

пример

Позвольте мне показать вам пример, используя таблицы, смоделированные по таблицам, приведенным в вопросе:

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

Для простоты поместите одну строку в таблицу назначения и четыре строки в источник:

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

Все четыре исходные строки соответствуют цели TransactionID, поэтому какая из них будет использоваться, если мы запустим обновление (как в вопросе), которое включается в TransactionIDодиночку?

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;

(Обновление TransactionIDстолбца не важно для демонстрации, вы можете закомментировать его, если хотите.)

Первый сюрприз состоит в том, что UPDATEзавершается без ошибок, несмотря на то, что таблица назначения не допускает пустых значений в любом столбце (все строки-кандидаты содержат нулевые значения).

Важным моментом является то, что результат не определен , и в этом случае получается результат, который не соответствует ни одной из исходных строк:

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 <> Fiddle demo

Более подробная информация: ЛЮБОЙ Агрегат сломан

Обновление должно быть написано так, чтобы оно было успешным, если оно записано как эквивалентный MERGEоператор, который проверяет попытки обновления одной и той же целевой строки более одного раза. Я обычно не рекомендую использовать MERGEнапрямую, потому что он был подвержен очень многим ошибкам реализации и обычно имеет худшую производительность.

В качестве бонуса вы можете обнаружить, что перезапись вашего текущего обновления на детерминированную приведет к тому, что ваша проблема с ошибками также исчезнет. Ошибка продукта все еще будет существовать для людей, которые, конечно, пишут неопределенные обновления.

Пол Уайт 9
источник