私たちのデータベースの1つに、複数のスレッドによって集中的に同時にアクセスされるテーブルがあります。スレッドはを介して行を更新または挿入しますMERGE
。行を時々削除するスレッドもあるので、テーブルデータは非常に不安定です。アップサートを実行するスレッドは時々デッドロックに悩まされます。この問題は、この質問で説明されている問題に似ています。ただし、違いは、私たちの場合、各スレッドが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
はここにあります、それは同時性のためです(ここでアドバイスされます)。
簡単な調査を行ったところ、次のことがわかりました。
ほとんどの場合、クエリ実行プランは
次のロックパターン
つまりIX
、オブジェクトをロックしてから、より詳細なロックを実行します。
ただし、クエリ実行プランが異なる場合があります
(この平面形状はINDEX(0)
ヒントを追加することで強制できます)そしてそのロックパターンは
既に配置されたX
後にオブジェクトに配置されたロックに注意しIX
てください。
2つIX
は互換性がありますが、2つは互換性X
がないため、同時実行で発生するのは
デッドロック!
そして、ここで質問の最初の部分が発生します。適格X
後にオブジェクトをロックしていますIX
か?バグじゃないですか?
ドキュメントの状態:
意図的ロックは、より低いレベルでのロックの前に取得されるため、意図的ロックと呼ばれ、より低いレベルでロックを配置する意図を示します。
そしてまた
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
インデックスがクラスタ化され、予想されるとして、働いていました。これにより、クラスター化インデックスシークと詳細なロックを使用した計画が可能になりました。さらに、詳細なロックも示すクラスター化インデックススキャンを強制的に実行しました。
しかしながら。追加の観察。元のセットアップでFORCESEEK(IX_Cache(ItemKey)))
、配置されていても、@itemKey
変数宣言をvarchar(200)からnvarchar(200)に変更すると、実行プランは次のようになります。
シークが使用されていることを確認してください。ただし、この場合のロックパターンはX
、オブジェクトのロックがの後であることを示していますIX
。
したがって、強制シークでは必ずしも細かいロック(およびデッドロックの不在)が保証されるわけではないようです。クラスタ化されたインデックスを使用すると、きめ細かなロックが保証されるとは確信していません。それともそうですか?
私が理解していること(間違っている場合は訂正してください)は、ロックは状況によって大きく異なり、特定の実行プランの形状は特定のロックパターンを意味するものではないということです。
まだ開いたX
後にオブジェクトをロックする適格性に関する質問IX
。そして、それが適格である場合、オブジェクトのロックを防ぐためにできることはありますか?