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


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 Edition (64-розрядні) на Windows Server 2016 Standard 10.0 (Build 14393 :)

Відповіді:


10

Моє запитання: що відбувається під капотом? І яке рішення?

Це клоп. Проблема полягає в тому, що це відбувається лише зрідка, і їх буде важко відтворити. І все-таки ваш найкращий шанс - залучити підтримку Microsoft. Обробка оновлень є складним, тому це потребує дуже детального дослідження.

Для прикладу виду складності, подивіться на мої повідомлення MERGE Bug з відфільтрованими індексами та невірними результатами з індексованими переглядами . Жоден із цих питань не стосується безпосередньо вашої проблеми, але вони надають аромат.

Напишіть детерміновані оновлення

Це, звичайно, загальне. Можливо, більш корисно, я можу сказати, що ви повинні подивитись, щоб переписати своє поточне 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 <> скриптова демонстрація

Детальніше: БУДЬ-ЯК Агрегат розбитий

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.