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
部分は、挿入されたレコードがテーブルに再度挿入されるのを制限するだけです
私はそれを正しくやっていますか?これはテーブルロックを回避しますか?またはより良い方法があります。
注:バッチを使用した場合とバッチを挿入しない場合の所要時間はほぼ同じです
ロックのエスカレーションは、ステートメントの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でのロックのエスカレーションによって引き起こされるブロッキングの問題を解決する方法 はかなり古くなっていますが、提案の多くはまだ有効です。
_BEGIN TRAN
SELECT * FROM mytable (UPDLOCK, HOLDLOCK) WHERE 1=0
WAITFOR DELAY '1:00:00'
COMMIT TRAN
_
別のオプションはALTER TABLE blah SET (LOCK_ESCALATION = DISABLE)
にすることですが、ここでの単一のシナリオだけでなく、テーブルに対するすべてのクエリに影響を与えるため、これはまだあまり対象としていません。
したがって、私はオプション1またはおそらくオプション2を選択し、その他を割り引きます。
データが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
宛先テーブル(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
これを行うには、選択ステートメントでWITH(NOLOCK)を使用できます。 BUT NOLOCKは、OLTPデータベースでは推奨されません。