一意でないインデックスに重複したキー行を挿入できませんか?


14

8週間エラーがなかった後、過去数日間でこの奇妙なエラーに3回遭遇しましたが、困惑しています。

これはエラーメッセージです。

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

私のシナリオに関するいくつかのこと:

  • procはTransactionID(主キーの一部)を更新しています-これがエラーの原因であると思いますが、理由はわかりませんか?そのロジックを削除します。
  • テーブルで変更追跡が有効になっています
  • コミットされていないトランザクションの読み取りを行う

各テーブルには45のフィールドがあり、主にインデックスで使用されるものをリストしました。update文の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)2018年11月30日12:57:58 Copyright(C)2017 Microsoft Corporation Enterprise Edition(64-bit)on Windows Server 2016 Standard 10.0(Build 14393 :)

回答:


10

私の質問は、ボンネットの下で何が起こっているのですか?そして解決策は何ですか?

これはバグです。問題は、たまにしか発生せず、再現が難しいことです。それでも、あなたの最善のチャンスはマイクロソフトのサポートに従事することです。更新処理は非常に複雑であるため、非常に詳細な調査が必要になります。

複雑さの種類の例については、フィルターされたインデックスを使用したMERGEバグとインデックス付きビューを使用した誤った結果に関する私の投稿を参照してください。どちらもあなたの問題に直接関係していませんが、風味を与えています。

確定的な更新を書く

もちろん、これはかなり一般的なものです。おそらくもっと便利なのは、現在のUPDATEステートメントを書き直すことを検討すべきだと言うことです。ドキュメントは言います:

FROM句を指定して更新操作の基準を指定する場合は注意してください。UPDATEステートメントの結果は、更新される各列オカレンスに対して1つの値のみが使用できるように指定されていないFROM句がステートメントに含まれている場合、つまりUPDATEステートメントが決定論的でない場合、未定義です。

あなたUPDATE決定論的ではないので、結果は未定義です。各ターゲット行に対して最大で1つのソース行が識別されるように変更する必要があります。その変更がないと、更新の結果に個々のソース行が反映されない場合があります。

質問で与えられたテーブルを大まかにモデル化したテーブルを使用した例を示します。

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

物事をシンプルに保つために、ターゲットテーブルに1行、ソースに4行を配置します。

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

4つのソース行すべてがのターゲットと一致する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、ターゲットテーブルがすべての列でnullを許可していないにもかかわらず、エラーなしで完了することです(すべての候補行にnullが含まれています)。

重要な点は、結果が未定義であり、この場合、ソース行のいずれにも一致しない結果が生成されることです。

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デモ

詳細:ANY Aggregate is Broken

更新は、同等のMERGEステートメントとして記述された場合に成功するように記述される必要があります。このステートメントは、同じターゲット行を複数回更新する試みをチェックします。MERGE多くの実装バグの影響を受けており、通常はパフォーマンスが低下するため、一般に直接使用することはお勧めしません。

おまけとして、現在の更新を決定論的に書き換えると、バグの問題がなくなることもあります。製品のバグは、もちろん非決定的な更新を書く人々のためにまだ存在します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.