web-dev-qa-db-ja.com

テーブルをロックせずに多数のレコードを挿入する

1,500,0レコードをテーブルに挿入しようとしています。挿入中にテーブルロックの問題に直面しています。だから私は以下のバッチ挿入を思いつきました。

DECLARE @BatchSize INT = 50000

WHILE 1 = 1
  BEGIN
      INSERT INTO [dbo].[Destination] 
                  (proj_details_sid,
                   period_sid,
                   sales,
                   units)
      SELECT TOP(@BatchSize) s.proj_details_sid,
                             s.period_sid,
                             s.sales,
                             s.units
      FROM   [dbo].[SOURCE] s
      WHERE  NOT EXISTS (SELECT 1
                         FROM   dbo.Destination d
                         WHERE  d.proj_details_sid = s.proj_details_sid
                                AND d.period_sid = s.period_sid)

      IF @@ROWCOUNT < @BatchSize
        BREAK
  END 

Destinationテーブルにクラスター化インデックスがあります(proj_details_sid ,period_sid )NOT EXISTS部分は、挿入されたレコードがテーブルに再度挿入されるのを制限するだけです

私はそれを正しくやっていますか?これはテーブルロックを回避しますか?またはより良い方法があります。

注:バッチを使用した場合とバッチを挿入しない場合の所要時間はほぼ同じです

6

ロックのエスカレーションは、ステートメントのSELECTの部分とはまったく関係がないようです。

多数の行を挿入することによる自然な結果

ロックのエスカレーションは、ALTER TABLE SET LOCK_ESCALATIONオプションを使用してテーブルでロックのエスカレーションが無効になっていない場合、および次のいずれかの条件が存在する場合にトリガーされます。

  • 単一のTransact-SQLステートメントは、単一の非パーティションテーブルまたはインデックスに対して少なくとも5,000のロックを取得します。
  • 単一のTransact-SQLステートメントは、パーティションテーブルの単一パーティションで少なくとも5,000ロックを取得し、ALTER TABLE SET LOCK_ESCALATIONオプションはAUTOに設定されています。
  • データベースエンジンのインスタンスのロックの数がメモリまたは構成のしきい値を超えています。

ロックの競合が原因でロックをエスカレートできない場合、データベースエンジンは、1,250の新しいロックを取得するたびに定期的にロックのエスカレーションをトリガーします。

プロファイラーでロックエスカレーションイベントをトレースするか、以下のように異なるバッチサイズで試してみると、これを簡単に確認できます。私にとってTOP (6228)は6250個のロックが保持されていることを示していますが、TOP (6229)はロックのエスカレーションが始まると突然1に落ちます。正確な数値は異なる場合があります(データベースの設定と現在利用可能なリソースによって異なります)。試行錯誤して、ロックのエスカレーションが表示されるしきい値を見つけます。

_CREATE TABLE [dbo].[Destination]
  (
     proj_details_sid INT,
     period_sid       INT,
     sales            INT,
     units            INT
  )

BEGIN TRAN --So locks are held for us to count in the next statement
INSERT INTO [dbo].[Destination]
SELECT TOP (6229) 1,
                  1,
                  1,
                  1
FROM   master..spt_values v1,
       master..spt_values v2

SELECT COUNT(*)
FROM   sys.dm_tran_locks
WHERE  request_session_id = @@SPID;

COMMIT

DROP TABLE [dbo].[Destination] 
_

50,000行を挿入しているため、ほぼ確実にロックのエスカレーションが試行されます。

記事 SQL Serverでのロックのエスカレーションによって引き起こされるブロッキングの問題を解決する方法 はかなり古くなっていますが、提案の多くはまだ有効です。

  1. 大きなバッチ操作をいくつかの小さな操作に分割します(つまり、より小さなバッチサイズを使用します)
  2. 別のSPIDが現在互換性のないテーブルロックを保持している場合、ロックのエスカレーションは発生しません-それらが与える例は、実行中の別のセッションです。

_BEGIN TRAN
SELECT * FROM mytable (UPDLOCK, HOLDLOCK) WHERE 1=0
WAITFOR DELAY '1:00:00'
COMMIT TRAN 
_
  1. トレースフラグ1211を有効にして、ロックのエスカレーションを無効にします。ただし、これはグローバル設定であり、深刻な問題を引き起こす可能性があります。問題が少ない新しいオプション 1224 がありますが、これはまだグローバルです。

別のオプションはALTER TABLE blah SET (LOCK_ESCALATION = DISABLE)にすることですが、ここでの単一のシナリオだけでなく、テーブルに対するすべてのクエリに影響を与えるため、これはまだあまり対象としていません。

したがって、私はオプション1またはおそらくオプション2を選択し、その他を割り引きます。

6
Martin Smith

データがDestinationに存在することを確認する代わりに、最初にすべてのデータを一時テーブルに格納し、Destinationにバッチ挿入することをお勧めします

参照: INSERTステートメントでのROWLOCKの使用(SQL Server)

DECLARE @batch int = 100
DECLARE @curRecord int = 1
DECLARE @maxRecord int

-- remove (nolock) if you don't want to have dirty read
SELECT row_number over (order by s.proj_details_sid, s.period_sid) as rownum,
       s.proj_details_sid,
       s.period_sid,
       s.sales,
       s.units
INTO #Temp
FROM   [dbo].[SOURCE] s WITH (NOLOCK)
WHERE  NOT EXISTS (SELECT 1
                   FROM   dbo.Destination d WITH (NOLOCK)
                   WHERE  d.proj_details_sid = s.proj_details_sid
                          AND d.period_sid = s.period_sid)

-- change this maxRecord if you want to limit the records to insert
SELECT @maxRecord = count(1) from #Temp

WHILE @maxRecord >= @curRecord
   BEGIN
       INSERT INTO [dbo].[Destination] 
              (proj_details_sid,
               period_sid,
               sales,
               units)
       SELECT proj_details_sid, period_sid, sales, units
       FROM #Temp
       WHERE rownum >= @curRecord and rownum < @curRecord + @batch

       SET @curRecord = @curRecord + @batch
   END

DROP TABLE #Temp
4
Prisoner

宛先テーブル(NOLOCK)を追加しました-> dbo.Destination(NOLOCK)。今、あなたはあなたのテーブルをロックしません。

WHILE 1 = 1
  BEGIN
      INSERT INTO [dbo].[Destination] 
                  (proj_details_sid,
                   period_sid,
                   sales,
                   units)
      SELECT TOP(@BatchSize) s.proj_details_sid,
                             s.period_sid,
                             s.sales,
                             s.units
      FROM   [dbo].[SOURCE] s
      WHERE  NOT EXISTS (SELECT 1
                         FROM   dbo.Destination(NOLOCK) d
                         WHERE  d.proj_details_sid = s.proj_details_sid
                                AND d.period_sid = s.period_sid)

      IF @@ROWCOUNT < @BatchSize
        BREAK
  END 
0
Huseyin Durmus

これを行うには、選択ステートメントでWITH(NOLOCK)を使用できます。 BUT NOLOCKは、OLTPデータベースでは推奨されません。

0
Mohit Dagar