私たちのデータベースの1つには、複数のスレッドによって集中的に同時にアクセスされるテーブルがあります。スレッドはMERGE
を介して行を更新または挿入します。行を時々削除するスレッドもあるので、テーブルデータは非常に不安定です。アップサートを実行するスレッドは時々デッドロックに悩まされます。この問題は this 質問で説明されている問題に似ています。ただし、違いは、私たちの場合、各スレッドはを正確に1行に更新または挿入することです。
簡略化されたセットアップは次のとおりです。テーブルは、2つの一意の非クラスター化インデックスを持つヒープです。
_CREATE TABLE [Cache]
(
[UID] uniqueidentifier NOT NULL CONSTRAINT DF_Cache_UID DEFAULT (newid()),
[ItemKey] varchar(200) NOT NULL,
[FileName] nvarchar(255) NOT NULL,
[Expires] datetime2(2) NOT NULL,
CONSTRAINT [PK_Cache] PRIMARY KEY NONCLUSTERED ([UID])
)
GO
CREATE UNIQUE INDEX IX_Cache ON [Cache] ([ItemKey]);
GO
_
典型的なクエリは
_DECLARE
@itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
@fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';
MERGE INTO [Cache] WITH (HOLDLOCK) T
USING (
VALUES (@itemKey, @fileName, dateadd(minute, 10, sysdatetime()))
) S(ItemKey, FileName, Expires)
ON T.ItemKey = S.ItemKey
WHEN MATCHED THEN
UPDATE
SET
T.FileName = S.FileName,
T.Expires = S.Expires
WHEN NOT MATCHED THEN
INSERT (ItemKey, FileName, Expires)
VALUES (S.ItemKey, S.FileName, S.Expires)
OUTPUT deleted.FileName;
_
つまり、マッチングは一意のインデックスキーによって行われます。ヒントHOLDLOCK
は、同時実行のためにここにあります(アドバイス here )。
簡単な調査を行ったところ、次のことがわかりました。
ほとんどの場合、クエリ実行プランは
次のロックパターン
つまり、オブジェクトのIX
ロックの後に、より詳細なロックが続きます。
ただし、クエリ実行プランが異なる場合があります
(このプラン形状はINDEX(0)
ヒントを追加することで強制できます)そしてそのロックパターンは
X
がすでに配置された後にオブジェクトに配置されたIX
ロックに注意してください。
2つのIX
は互換性がありますが、2つのX
は互換性がないため、同時実行で発生することは
デッドロック!
そしてここで質問の最初の部分が発生します。 X
の後にオブジェクトにIX
ロックをかけることはできますか?バグじゃないですか?
Documentation の状態:
インテントロックは、低レベルのロックの前に取得されるため、インテントロックと呼ばれます。したがって、低レベルにロックを配置するシグナルインテント。
および または
IXは、すべてではなく一部の行のみを更新する意図を意味します
そのため、X
の後にオブジェクトにIX
ロックを設定すると、非常に疑わしく見えます。
最初に、テーブルロックのヒントを追加してデッドロックを防止しようとしました
_MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCK) T
_
そして
_MERGE INTO [Cache] WITH (HOLDLOCK, TABLOCKX) T
_
TABLOCK
を配置すると、ロックパターンが次のようになります。
TABLOCKX
のロックパターンでは
2つのSIX
(および2つのX
)には互換性がないため、デッドロックは効果的に防止されますが、残念ながら同時実行も防止されます(これは望ましくありません)。
次の試みは、PAGLOCK
とROWLOCK
を追加して、ロックをよりきめ細かくし、競合を減らしました。どちらも効果がありません(X
の直後にオブジェクトに対するIX
がまだ観察されていました)。
私の最後の試みは、FORCESEEK
ヒントを追加することにより、適切な細かいロックを使用して「適切な」実行プランの形状を強制することでした
_MERGE INTO [Cache] WITH (HOLDLOCK, FORCESEEK(IX_Cache(ItemKey))) T
_
そしてそれは働いた。
そして、ここで質問の2番目の部分が発生します。 FORCESEEK
が無視され、不正なロックパターンが使用される可能性がありますか? (私が述べたように、PAGLOCK
とROWLOCK
は一見無視されました)。
UPDLOCK
を追加しても効果はありません(X
を実行してもオブジェクトで引き続きIX
を監視できます)。
予想どおり、_IX_Cache
_インデックスをクラスター化することは機能しました。 Clustered Index Seekと詳細なロックを使用した計画につながりました。さらにクラスター化インデックススキャンを強制的に実行してみました。
しかしながら。追加の観察。 FORCESEEK(IX_Cache(ItemKey)))
が設定されている元のセットアップで、_@itemKey
_変数宣言をvarchar(200)からnvarchar(200)に変更した場合、実行計画は
シークが使用されていることを確認してください。ただし、この場合のロックパターンは、X
の後にオブジェクトに配置されたIX
ロックを再度示しています。
したがって、強制シークでは必ずしも細かいロック(およびデッドロックの不在)は保証されないようです。クラスタ化されたインデックスを使用すると、きめ細かいロックが保証されるとは確信していません。それとも?
私の理解(私が間違っている場合は私を訂正してください)は、ロックは状況によって大きく異なり、特定の実行プランの形状は特定のロックパターンを意味するものではないということです。
X
を開いたままオブジェクトにIX
ロックを配置する資格についての質問。それが適格である場合、オブジェクトのロックを防ぐためにできることはありますか?
オブジェクトに
IX
の後にX
を付けて配置できますか?それはバグですか?
少し奇妙に見えますが、有効です。 IX
が取得された時点で、より低いレベルでX
ロックを取得することが意図されている場合があります。そのようなロックが実際に行われなければならないということは言うまでもありません。結局、下位レベルでロックするものは何もないかもしれません。エンジンは事前にそれを知ることができません。さらに、より低いレベルのロックをスキップできるように最適化される場合があります(IS
およびS
ロックの例を見ることができます ここ )。
より具体的には、現在のシナリオでは、シリアライズ可能なキー範囲ロックがヒープで使用できないことは事実です。そのため、唯一の代替策はオブジェクトレベルのX
ロックです。その意味で、エンジンは、アクセス方法がヒープスキャンである場合にX
ロックが必然的に必要になることを早期に検出できるため、IX
ロックの取得を避けます。
一方、ロックは複雑であり、意図的なロックは、必ずしもより低いレベルのロックを取得する意図とは関係のない内部的な理由で取得される場合があります。 IX
を取ることは、一部のあいまいなEdgeケースに必要な保護を提供するための最も侵襲性の低い方法です。同様の考慮事項については、 IsolationLevel.ReadUncommitted で発行された共有ロックを参照してください。
したがって、現在の状況はデッドロックシナリオにとって不幸であり、原則として回避できる可能性がありますが、これは必ずしも「バグ」と同じではありません。この問題について明確な回答が必要な場合は、通常のサポートチャネルまたはMicrosoft Connectで問題を報告できます。
FORCESEEK
が無視され、不正なロックパターンが使用される可能性がありますか?
いいえ。FORCESEEK
は、ヒントではなく、ディレクティブです。オプティマイザが「ヒント」を尊重するプランを見つけられない場合、エラーが発生します。
インデックスの強制は、キー範囲ロックを確実に取得できるようにする方法です。行を変更するためのアクセス方法を処理するときに自然に取られる更新ロックと合わせて、これはシナリオでの同時実行の問題を回避するための十分な保証を提供します。
テーブルのスキーマが変更されない場合(新しいインデックスの追加など)、ヒントは、このクエリ自体によるデッドロックを回避するのにも十分です。非クラスタ化インデックスの前にヒープにアクセスする可能性のある他のクエリ(非クラスタ化インデックスのキーの更新など)で循環デッドロックが発生する可能性があります。
...
varchar(200)
からnvarchar(200)
...への変数宣言.
これは、単一の行が影響を受けるという保証を破るので、ハロウィーン保護のためにイーガーテーブルスプールが導入されています。これのさらなる回避策として、MERGE TOP (1) INTO [Cache]...
を使用して保証を明示的にします。
私の理解[...]は、ロックは状況によって大きく異なり、特定の実行プランの形状は特定のロックパターンを意味するものではないということです。
実行計画に表示されていることは確かに多くあります。たとえば、特定の平面形状を強制できます。計画ガイドですが、エンジンは実行時に異なるロックを取得することを決定する場合があります。上記のTOP (1)
要素を組み込んだ場合、可能性はかなり低くなります。
ヒープテーブルがこのように使用されているのを見るのは珍しいことです。おそらくコメントでDan Guzmanが提案したインデックスを使用して、それをクラスター化されたテーブルに変換するメリットを考慮する必要があります。
_CREATE UNIQUE CLUSTERED INDEX IX_Cache ON [Cache] ([ItemKey]);
_
これには、スペースの再利用に関する重要な利点があるだけでなく、現在のデッドロックの問題に対する適切な回避策が提供されます。
MERGE
も、同時実行性の高い環境で見られるのは少し珍しいです。直感にとらわれず、個別のINSERT
ステートメントとUPDATE
ステートメントを実行する方が効率的であることがよくあります。次に例を示します。
_DECLARE
@itemKey varchar(200) = 'Item_0F3C43A6A6A14255B2EA977EA730EDF2',
@fileName nvarchar(255) = 'File_0F3C43A6A6A14255B2EA977EA730EDF2.dat';
BEGIN TRANSACTION;
DECLARE @expires datetime2(2) = DATEADD(MINUTE, 10, SYSDATETIME());
UPDATE TOP (1) dbo.Cache WITH (SERIALIZABLE, UPDLOCK)
SET [FileName] = @fileName,
Expires = @expires
OUTPUT Deleted.[FileName]
WHERE
ItemKey = @itemKey;
IF @@ROWCOUNT = 0
INSERT dbo.Cache
(ItemKey, [FileName], Expires)
VALUES
(@itemKey, @fileName, @expires);
COMMIT TRANSACTION;
_
RIDルックアップが不要になったことに注意してください。
(質問のように)ItemKey
の一意のインデックスの存在を保証できる場合、UPDATE
の冗長なTOP (1)
を削除して、より単純な計画を与えることができます。
INSERT
とUPDATE
の両方のプランは、どちらの場合でも簡単なプランの対象になります。 MERGE
は常に完全なコストベースの最適化を必要とします。
使用する正しいパターンとMERGE
の詳細については、関連するQ&A SQL Server 2014の同時入力の問題 を参照してください。
デッドロックは常に防止できるわけではありません。コーディングと設計を注意深く行うことで最小限に抑えることができますが、アプリケーションは常に奇妙なデッドロックを適切に処理できるように準備する必要があります(条件を再確認してから再試行するなど)。
問題のオブジェクトにアクセスするプロセスを完全に制御できる場合は、アプリケーションロックを使用して個々の要素へのアクセスをシリアル化することも検討できます( SQL Serverの同時挿入と削除 。