私は答えを見つけました:ループした削除がゴースト クリーンアップ プロシージャと競合しています。
ニコラスの提案を使用して、 BEGIN TRANSACTION
を追加しました と COMMIT
. BEGIN TRY
で削除ループをラップしました / BEGIN CATCH
. BEGIN CATCH
で 、 ROLLBACK
の直前 、 sp_lock
を実行しました と sp_who2
. (上記の質問にコードの変更を追加しました。)
プロセスがブロックされたとき、次の出力が表示されました:
spid dbid ObjId IndId Type Resource Mode Status
------ ------ ----------- ------ ---- -------------------------------- -------- ------
20 2 1401108082 0 TAB IX GRANT
20 2 1401108082 1 PAG 1:102368 X GRANT
SPID Status Login HostName BlkBy DBName Command CPUTime DiskIO
---- ---------- ----- -------- ----- ------ ------------- ------- ------
20 BACKGROUND sa . . tempdb GHOST CLEANUP 31 0
今後の参考のために、SQL Server がレコードを削除するとき、レコードにビットを設定して、単に "ゴースト レコード" としてマークします。数分ごとに、ゴースト クリーンアップと呼ばれる内部プロセスが実行され、完全に削除されたレコードのページが再利用されます (つまり、すべてのレコードはゴースト レコードです)。
ゴースト クリーンアップ プロセスについては、この質問の ServerFault で説明されています。
ポールですS. Randal によるゴースト クリーンアップ プロセスの説明
トレース フラグを使用してゴースト クリーンアップ プロセスを無効にすることができます。 しかし、この場合はそうする必要はありませんでした.
最終的に 100 ミリ秒のロック待機タイムアウトを追加しました。これにより、ゴースト レコードのクリーンアップ プロセスでロック待機タイムアウトが発生することがありますが、許容範囲内です。また、ロックのタイムアウトを最大 5 回再試行するループも追加しました。これら 2 つの変更により、私のプロセスは通常完了します。現在は、大量のデータをプッシュする非常に長いプロセスがあり、プロセスがクリーンアップする必要があるデータのテーブルまたはページ ロックを取得する場合にのみ、タイムアウトが発生します。
2016 年 7 月 20 日編集
最終的なコードは次のようになります:
-- Do not block long if records are locked.
SET LOCK_TIMEOUT 100
-- This process volunteers to be a deadlock victim in the case of a deadlock.
SET DEADLOCK_PRIORITY LOW
DECLARE @Error BIT
SET @Error = 0
DECLARE @ErrMsg VARCHAR(1000)
DECLARE @DeletedCount INT
SELECT @DeletedCount = 0
DECLARE @LockTimeoutCount INT
SET @LockTimeoutCount = 0
DECLARE @ContinueDeleting BIT,
@LastDeleteSuccessful BIT
SET @ContinueDeleting = 1
SET @LastDeleteSuccessful = 1
WHILE @ContinueDeleting = 1
BEGIN
DECLARE @RowCount INT
SET @RowCount = 0
BEGIN TRY
BEGIN TRANSACTION
-- The READPAST below attempts to skip over locked records.
-- However, it might still cause a lock wait error (1222) if a page or index is locked, because the delete has to modify indexes.
-- The threshold for row lock escalation to table locks is around 5,000 records,
-- so keep the deleted number smaller than this limit in case we are deleting a large chunk of data.
-- Table name, field, and value are all set dynamically in the actual script.
SET @SQL = N'DELETE TOP (1000) MyTable WITH(ROWLOCK, READPAST) WHERE MyField = SomeValue'
EXEC sp_executesql @SQL, N'@ProcGuid uniqueidentifier', @ProcGUID
SET @RowCount = @@ROWCOUNT
COMMIT
SET @LastDeleteSuccessful = 1
SET @DeletedCount = @DeletedCount + @RowCount
IF @RowCount = 0
BEGIN
SET @ContinueDeleting = 0
END
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK
IF Error_Number() = 1222 -- Lock timeout
BEGIN
IF @LastDeleteSuccessful = 1
BEGIN
-- If we hit a lock timeout, and we had already deleted something successfully, try again.
SET @LastDeleteSuccessful = 0
END
ELSE
BEGIN
-- The last delete failed, too. Give up for now. The job will run again shortly.
SET @ContinueDeleting = 0
END
END
ELSE -- On anything other than a lock timeout, report an error.
BEGIN
SET @ErrMsg = 'An error occurred cleaning up data. Table: MyTable Column: MyColumn Value: SomeValue. Message: ' + ERROR_MESSAGE() + ' Error Number: ' + CONVERT(VARCHAR(20), ERROR_NUMBER()) + ' Line: ' + CONVERT(VARCHAR(20), ERROR_LINE())
PRINT @ErrMsg -- this error message will be included in the SQL Server job history
SET @Error = 1
SET @ContinueDeleting = 0
END
END CATCH
END
IF @Error <> 0
RAISERROR('Not all data could be cleaned up. See previous messages.', 16, 1)