web-dev-qa-db-ja.com

SQL Serverで、選択した行のグループがロックされているかどうかを確認する方法はありますか?

数十億行のテーブルにある多数のレコードを更新/削除しようとしています。これは人気のあるテーブルなので、このテーブルのさまざまなセクションで多くのアクティビティがあります。大規模な更新/削除アクティビティが長時間ブロックされているため(すべての行のロックまたはページロックまたはテーブルロックの取得を待機しているため)、タイムアウトになるか、タスクの完了に数日かかります。

したがって、行の小さなバッチを一度に削除する方法を変更しています。ただし、選択した行(100行、1000行、2000行など)が現在別のプロセスによってロックされているかどうかを確認したいとします。

  • そうでない場合は、削除/更新を続行します。
  • ロックされている場合は、次のレコードグループに移動します。
  • 最後に、最初に戻って、残っているものを更新/削除してみます。

これは可能ですか?

ありがとう、ToC

21
ToC

リクエストを正しく理解していれば、目的は行のバッチを削除することですが、同時にDML操作はテーブル全体の行で発生しています。目的はバッチを削除することです。ただし、そのバッチで定義された範囲内に含まれる基になる行がロックされている場合は、そのバッチをスキップして次のバッチに移動する必要があります。次に、以前に削除されていないバッチに戻り、元の削除ロジックを再試行する必要があります。必要な行のバッチがすべて削除されるまで、このサイクルを繰り返す必要があります。

前述のように、ブロックされた行を含む可能性のある過去の範囲をスキップするには、READPASTヒントとREAD COMMITTED(デフォルト)分離レベルを使用するのが妥当です。さらに一歩進んで、SERIALIZABLE分離レベルとニブル削除を使用することをお勧めします。

SQL Serverは、キー範囲ロックを使用して、シリアル化可能なトランザクション分離レベルを使用しながら、Transact-SQLステートメントによって読み取られるレコードセットに暗黙的に含まれる行の範囲を保護します...詳細はこちらをご覧ください https:// technet .Microsoft.com/en-US/library/ms191272(v = SQL.105).aspx

ニブル削除の目的は、行の範囲を分離し、それらの行が削除されている間、それらの行が変更されないようにすることです。つまり、幻像の読み取りや挿入は必要ありません。シリアライズ可能な分離レベルは、この問題を解決するためのものです。

私のソリューションを説明する前に、データベースのデフォルトの分離レベルをSERIALIZABLEに切り替えることをお勧めしません。また、私のソリューションが最適であることをお勧めしません。私はそれを提示して、ここからどこに行くことができるかを見たいだけです。

ハウスキーピングに関する注意事項:

  1. 使用しているSQL Serverのバージョンは、Microsoft SQL Server 2012-11.0.5343.0(X64)です。
  2. テストデータベースは完全復旧モデルを使用しています

実験を開始するために、テストデータベースとサンプルテーブルをセットアップし、テーブルに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;

enter image description here

つまり、データベース、テーブル、インデックス、行があります。それでは、削除をニブルする実験をセットアップしましょう。まず、典型的なニブリング削除メカニズムを作成するための最良の方法を決定する必要があります。


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;

はい、削除されました。

Proof of Deleted Row

次に、ロックを確認するために、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;

挿入から始まり、削除に続く両方の操作を開始しましょう。キー範囲ロックと排他ロックを確認できます。

Range and eXclusive Locks

挿入はこれらのロックを生成しました:

Insert's Locks

ニブル削除/選択はこれらのロックを保持しています:

enter image description here

挿入は期待どおりに削除をブロックしています:

Insert Blocks Delete

次に、挿入トランザクションをコミットして、何が起きているかを見てみましょう。

Commit the Delete

そして予想通り、すべてのトランザクションが完了します。ここで、挿入がファントムであったかどうか、または削除操作によって挿入が削除されたかどうかを確認する必要があります。


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20015;

実際、挿入は削除されました。そのため、ファントム挿入は許可されませんでした。

No Phantom Insert

したがって、結論として、この演習の真の意図は、すべての行、ページ、またはテーブルレベルのロックを追跡して追跡することではなく、バッチの要素がロックされているかどうかを判断しようとすることです。待つ。それが質問者の意図だったのかもしれません。ただし、その作業は非常に困難であり、不可能ではないにしても基本的に非現実的です。本当の目標は、自分のロックでバッチの範囲を分離し、次にバッチを削除する前に、不要な現象が発生しないようにすることです。 SERIALIZABLE分離レベルはこの目的を達成します。重要なのは、ニブルを小さく保ち、トランザクションログを管理し、不要な現象を排除することです。

速度が必要な場合は、パーティション分割できない巨大なテーブルを作成しないでください。そのため、パーティションスイッチングを使用して最速の結果を得ることができません。速度の鍵は、パーティショニングと並列処理です。苦しみの鍵はニブルとライブロックです。

ご意見をお聞かせください。

実際のSERIALIZABLE分離レベルの例をいくつか作成しました。以下のリンクから入手できます。

削除操作

操作を挿入

等価操作-次のキー値のキー範囲ロック

等価操作-既存のデータのシングルトンフェッチ

等価操作-存在しないデータのシングルトンフェッチ

不等式演算-範囲と次のキー値のキー範囲ロック

10
ooutwire

したがって、行の小さなバッチを一度に削除する方法を変更しています。

これは 小さな慎重なバッチ または 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-GanPushting 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_を使用すると、スループットが本当に良いことがわかりました。

9
Kin Shah

comments で最初に提供された他のアプローチを要約して質問に示します


  1. 互換性のないロックに遭遇するとすぐにチャンク全体を失敗させることが目的の動作である場合は、NOWAITを使用します。

    NOWAIT documentation から:

    テーブルでロックが発生するとすぐにメッセージを返すようにデータベースエンジンに指示します。 NOWAITは、特定のテーブルに対してSET LOCK_TIMEOUT 0を指定することと同じです。 NOWAITヒントも含まれている場合、TABLOCKヒントは機能しません。 TABLOCKヒントの使用時に待機せずにクエリを終了するには、代わりにSETLOCK_TIMEOUT 0;を使用してクエリを開始します。

  2. SET LOCK_TIMEOUTを使用して同様の結果を達成しますが、タイムアウトは構成可能です。

    SET LOCK_TIMEOUTのドキュメント から

    ステートメントがロックが解放されるのを待つミリ秒数を指定します。

    ロックの待機がタイムアウト値を超えると、エラーが返されます。値0は、まったく待機せず、ロックが発生するとすぐにメッセージを返すことを意味します。

2
Paul White 9

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;
0
Lebnik