数十億行のテーブルにある多数のレコードを更新/削除しようとしています。これは人気のあるテーブルなので、このテーブルのさまざまなセクションで多くのアクティビティがあります。大規模な更新/削除アクティビティが長時間ブロックされているため(すべての行のロックまたはページロックまたはテーブルロックの取得を待機しているため)、タイムアウトになるか、タスクの完了に数日かかります。
したがって、行の小さなバッチを一度に削除する方法を変更しています。ただし、選択した行(100行、1000行、2000行など)が現在別のプロセスによってロックされているかどうかを確認したいとします。
これは可能ですか?
ありがとう、ToC
リクエストを正しく理解していれば、目的は行のバッチを削除することですが、同時にDML操作はテーブル全体の行で発生しています。目的はバッチを削除することです。ただし、そのバッチで定義された範囲内に含まれる基になる行がロックされている場合は、そのバッチをスキップして次のバッチに移動する必要があります。次に、以前に削除されていないバッチに戻り、元の削除ロジックを再試行する必要があります。必要な行のバッチがすべて削除されるまで、このサイクルを繰り返す必要があります。
前述のように、ブロックされた行を含む可能性のある過去の範囲をスキップするには、READPASTヒントとREAD COMMITTED(デフォルト)分離レベルを使用するのが妥当です。さらに一歩進んで、SERIALIZABLE分離レベルとニブル削除を使用することをお勧めします。
SQL Serverは、キー範囲ロックを使用して、シリアル化可能なトランザクション分離レベルを使用しながら、Transact-SQLステートメントによって読み取られるレコードセットに暗黙的に含まれる行の範囲を保護します...詳細はこちらをご覧ください https:// technet .Microsoft.com/en-US/library/ms191272(v = SQL.105).aspx
ニブル削除の目的は、行の範囲を分離し、それらの行が削除されている間、それらの行が変更されないようにすることです。つまり、幻像の読み取りや挿入は必要ありません。シリアライズ可能な分離レベルは、この問題を解決するためのものです。
私のソリューションを説明する前に、データベースのデフォルトの分離レベルをSERIALIZABLEに切り替えることをお勧めしません。また、私のソリューションが最適であることをお勧めしません。私はそれを提示して、ここからどこに行くことができるかを見たいだけです。
ハウスキーピングに関する注意事項:
実験を開始するために、テストデータベースとサンプルテーブルをセットアップし、テーブルに2,000,000行を入力します。
USE [master];
GO
SET NOCOUNT ON;
IF DATABASEPROPERTYEX (N'test', N'Version') > 0
BEGIN
ALTER DATABASE [test] SET SINGLE_USER
WITH ROLLBACK IMMEDIATE;
DROP DATABASE [test];
END
GO
-- Create the test database
CREATE DATABASE [test];
GO
-- Set the recovery model to FULL
ALTER DATABASE [test] SET RECOVERY FULL;
-- Create a FULL database backup
-- in order to ensure we are in fact using
-- the FULL recovery model
-- I pipe it to dev null for simplicity
BACKUP DATABASE [test]
TO DISK = N'nul';
GO
USE [test];
GO
-- Create our table
IF OBJECT_ID('dbo.tbl','U') IS NOT NULL
BEGIN
DROP TABLE dbo.tbl;
END;
CREATE TABLE dbo.tbl
(
c1 BIGINT IDENTITY (1,1) NOT NULL
, c2 INT NOT NULL
) ON [PRIMARY];
GO
-- Insert 2,000,000 rows
INSERT INTO dbo.tbl
SELECT TOP 2000
number
FROM
master..spt_values
ORDER BY
number
GO 1000
この時点で、SERIALIZABLE分離レベルのロックメカニズムが作用できる1つ以上のインデックスが必要になります。
-- Add a clustered index
CREATE UNIQUE CLUSTERED INDEX CIX_tbl_c1
ON dbo.tbl (c1);
GO
-- Add a non-clustered index
CREATE NONCLUSTERED INDEX IX_tbl_c2
ON dbo.tbl (c2);
GO
次に、2,000,000行が作成されたことを確認します
SELECT
COUNT(*)
FROM
tbl;
つまり、データベース、テーブル、インデックス、行があります。それでは、削除をニブルする実験をセットアップしましょう。まず、典型的なニブリング削除メカニズムを作成するための最良の方法を決定する必要があります。
DECLARE
@BatchSize INT = 100
, @LowestValue BIGINT = 20000
, @HighestValue BIGINT = 20010
, @DeletedRowsCount BIGINT = 0
, @RowCount BIGINT = 1;
SET NOCOUNT ON;
GO
WHILE @DeletedRowsCount < ( @HighestValue - @LowestValue )
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION
DELETE
FROM
dbo.tbl
WHERE
c1 IN (
SELECT TOP (@BatchSize)
c1
FROM
dbo.tbl
WHERE
c1 BETWEEN @LowestValue AND @HighestValue
ORDER BY
c1
);
SET @RowCount = ROWCOUNT_BIG();
COMMIT TRANSACTION;
SET @DeletedRowsCount += @RowCount;
WAITFOR DELAY '000:00:00.025';
CHECKPOINT;
END;
ご覧のとおり、明示的なトランザクションをwhileループ内に配置しました。ログのフラッシュを制限したい場合は、ループの外に配置してください。さらに、完全復旧モデルであるため、ニブル削除操作の実行中にトランザクションログのバックアップをより頻繁に作成して、トランザクションログが極端に大きくなるのを防ぐことができます。
したがって、この設定にはいくつかの目標があります。まず、キーレンジロックが必要です。だから、私はバッチをできるだけ小さく保つようにしています。また、「巨大な」テーブルの同時実行性に悪影響を与えたくありません。だから、私は自分のロックを取り、できる限り速くそれらを残したいと思います。したがって、バッチサイズを小さくすることをお勧めします。
次に、この削除ルーチンの実際の非常に短い例を示します。 SSMS内で新しいウィンドウを開き、テーブルから1行を削除する必要があります。デフォルトのREAD COMMITTED分離レベルを使用して、暗黙的なトランザクション内でこれを行います。
DELETE FROM
dbo.tbl
WHERE
c1 = 20005;
この行は実際に削除されましたか?
SELECT
c1
FROM
dbo.tbl
WHERE
c1 BETWEEN 20000 AND 20010;
はい、削除されました。
次に、ロックを確認するために、SSMS内で新しいウィンドウを開き、コードスニペットを1つまたは2つ追加します。私はAdam Mechanicのsp_whoisactiveを使用しています。これは次の場所にあります: sp_whoisactive
SELECT
DB_NAME(resource_database_id) AS DatabaseName
, resource_type
, request_mode
FROM
sys.dm_tran_locks
WHERE
DB_NAME(resource_database_id) = 'test'
AND resource_type = 'KEY'
ORDER BY
request_mode;
-- Our insert
sp_lock 55;
-- Our deletions
sp_lock 52;
-- Our active sessions
sp_whoisactive;
これで準備が整いました。新しいSSMSウィンドウで、削除した1つの行を再挿入しようとする明示的なトランザクションを開始します。同時に、ニブル削除操作を開始します。
挿入コード:
BEGIN TRANSACTION
SET IDENTITY_INSERT dbo.tbl ON;
INSERT INTO dbo.tbl
( c1 , c2 )
VALUES
( 20005 , 1 );
SET IDENTITY_INSERT dbo.tbl OFF;
--COMMIT TRANSACTION;
挿入から始まり、削除に続く両方の操作を開始しましょう。キー範囲ロックと排他ロックを確認できます。
挿入はこれらのロックを生成しました:
ニブル削除/選択はこれらのロックを保持しています:
挿入は期待どおりに削除をブロックしています:
次に、挿入トランザクションをコミットして、何が起きているかを見てみましょう。
そして予想通り、すべてのトランザクションが完了します。ここで、挿入がファントムであったかどうか、または削除操作によって挿入が削除されたかどうかを確認する必要があります。
SELECT
c1
FROM
dbo.tbl
WHERE
c1 BETWEEN 20000 AND 20015;
実際、挿入は削除されました。そのため、ファントム挿入は許可されませんでした。
したがって、結論として、この演習の真の意図は、すべての行、ページ、またはテーブルレベルのロックを追跡して追跡することではなく、バッチの要素がロックされているかどうかを判断しようとすることです。待つ。それが質問者の意図だったのかもしれません。ただし、その作業は非常に困難であり、不可能ではないにしても基本的に非現実的です。本当の目標は、自分のロックでバッチの範囲を分離し、次にバッチを削除する前に、不要な現象が発生しないようにすることです。 SERIALIZABLE分離レベルはこの目的を達成します。重要なのは、ニブルを小さく保ち、トランザクションログを管理し、不要な現象を排除することです。
速度が必要な場合は、パーティション分割できない巨大なテーブルを作成しないでください。そのため、パーティションスイッチングを使用して最速の結果を得ることができません。速度の鍵は、パーティショニングと並列処理です。苦しみの鍵はニブルとライブロックです。
ご意見をお聞かせください。
実際のSERIALIZABLE分離レベルの例をいくつか作成しました。以下のリンクから入手できます。
したがって、行の小さなバッチを一度に削除する方法を変更しています。
これは 小さな慎重なバッチ または chunks で削除するのは本当に良い考えです。私はadd小さな_waitfor delay '00:00:05'
_をデータベースの復旧モデルに応じて-FULL
の場合、_log backup
_およびSIMPLE
の場合、_manual CHECKPOINT
_を実行して、バッチ間でトランザクションログの肥大化を回避します。
ただし、選択した行(たとえば、100行、1000行、または2000行)が現在別のプロセスによってロックされているかどうかを確認します。
あなたが言っていることは、箱から出して完全に可能ではありません(3つの箇条書きを覚えておいてください)。上記の提案-_small batches + waitfor delay
_が機能しない場合(適切なテストを行った場合)、_query HINT
_を使用できます。
NOLOCK
を使用しないでください- kb/308886 を参照してください SQL Serverの読み取り一貫性の問題Itzik Ben-Gan 、 Pushting NOLOCK everywhere-By Aaron Bertrand および SQL Server NOLOCKヒント&その他の悪いアイデア 。
READPAST
ヒントはシナリオに役立ちます。 READPAST
ヒントの要点は-行レベルのロックがある場合、SQLサーバーはそれを読み取りません。
データベースエンジンが他のトランザクションによってロックされている行を読み取らないことを指定します。
READPAST
を指定すると、行レベルのロックがスキップされます。つまり、データベースエンジンは、ロックが解放されるまで現在のトランザクションをブロックする代わりに、行をスキップします。
限られたテストで、DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)
を使用し、クエリセッション分離レベルを_READ COMMITTED
_に設定し、デフォルトの分離レベルである_SET TRANSACTION ISOLATION LEVEL READ COMMITTED
_を使用すると、スループットが本当に良いことがわかりました。
comments で最初に提供された他のアプローチを要約して質問に示します
互換性のないロックに遭遇するとすぐにチャンク全体を失敗させることが目的の動作である場合は、NOWAIT
を使用します。
テーブルでロックが発生するとすぐにメッセージを返すようにデータベースエンジンに指示します。
NOWAIT
は、特定のテーブルに対してSET LOCK_TIMEOUT 0
を指定することと同じです。NOWAIT
ヒントも含まれている場合、TABLOCK
ヒントは機能しません。TABLOCK
ヒントの使用時に待機せずにクエリを終了するには、代わりにSETLOCK_TIMEOUT 0;
を使用してクエリを開始します。
SET LOCK_TIMEOUT
を使用して同様の結果を達成しますが、タイムアウトは構成可能です。
ステートメントがロックが解放されるのを待つミリ秒数を指定します。
ロックの待機がタイムアウト値を超えると、エラーが返されます。値0は、まったく待機せず、ロックが発生するとすぐにメッセージを返すことを意味します。
2つの並列クエリがあると仮定します。
接続/セッション1:行をロックする= 777
SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777
接続/セッション2:ロックされた行を無視します= 777
SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777
または接続/セッション2:例外をスローします
DECLARE @id integer;
SELECT @id = id FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777;
IF @id is NULL
THROW 51000, 'Hi, a record is locked or does not exist.', 1;