2億2000万以上の行テーブルから1600万以上のレコードを削除する必要がありますが、非常に遅いです。
以下のコードをより速くするための提案を共有していただければ幸いです。
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
DECLARE @BATCHSIZE INT,
@ITERATION INT,
@TOTALROWS INT,
@MSG VARCHAR(500);
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;
BEGIN TRY
BEGIN TRANSACTION;
WHILE @BATCHSIZE > 0
BEGIN
DELETE TOP (@BATCHSIZE) FROM MySourceTable
OUTPUT DELETED.*
INTO MyBackupTable
WHERE NOT EXISTS (
SELECT NULL AS Empty
FROM dbo.vendor AS v
WHERE VendorId = v.Id
);
SET @BATCHSIZE = @@ROWCOUNT;
SET @ITERATION = @ITERATION + 1;
SET @TOTALROWS = @TOTALROWS + @BATCHSIZE;
SET @MSG = CAST(GETDATE() AS VARCHAR) + ' Iteration: ' + CAST(@ITERATION AS VARCHAR) + ' Total deletes:' + CAST(@TOTALROWS AS VARCHAR) + ' Next Batch size:' + CAST(@BATCHSIZE AS VARCHAR);
PRINT @MSG;
COMMIT TRANSACTION;
CHECKPOINT;
END;
END TRY
BEGIN CATCH
IF @@ERROR <> 0
AND @@TRANCOUNT > 0
BEGIN
PRINT 'There is an error occured. The database update failed.';
ROLLBACK TRANSACTION;
END;
END CATCH;
GO
VendorId
は[〜#〜] pk [〜#〜]およびnon-clustered、ここでclustered indexはこのスクリプトでは使用されていません。他に5つの非一意の非クラスター化インデックスがあります。
タスクは、「別のテーブルに存在しないベンダーを削除する」ことと、それらを別のテーブルにバックアップすることです。 3つのテーブルがあります。vendors, SpecialVendors, SpecialVendorBackups
。 SpecialVendors
テーブルに存在しないVendors
を削除して、削除したレコードのバックアップを取り、私が間違っている場合に備えて、1週間以内に戻す必要があります。二。
実行プランは、非クラスター化インデックスから行をある順序で読み取り、読み取った各外部行に対してシークを実行して_NOT EXISTS
_を評価していることを示しています
テーブルの7.2%を削除します。 4,500の3,556バッチで16,000,000行
条件を満たす行がインデックス全体に最終的に分散すると仮定すると、13.8行ごとに約1行が削除されます。
したがって、反復1は62,156行を読み取り、削除する4,500行を見つける前にその数のインデックスシークを実行します。
反復2では57,656(62,156-4,500)行が読み込まれ、同時更新(既に処理されているため)を無視しても間違いなく適格とされ、さらに62,156行が削除されて4,500が削除されます。
反復3は(2 * 57,656)+ 62,156行を読み取り、最終的に反復3,556は(3,555 * 57,656)+ 62,156行を読み取り、その数のシークを実行します。
したがって、すべてのバッチで実行されるインデックスシークの数はSUM(1, 2, ..., 3554, 3555) * 57,656 + (3556 * 62156)
です。
_((3555 * 3556 / 2) * 57656) + (3556 * 62156)
_-または_364,652,494,976
_はどちらですか
削除する行を最初に一時テーブルに具体化することをお勧めします
_INSERT INTO #MyTempTable
SELECT MySourceTable.PK,
1 + ( ROW_NUMBER() OVER (ORDER BY MySourceTable.PK) / 4500 ) AS BatchNumber
FROM MySourceTable
WHERE NOT EXISTS (SELECT *
FROM dbo.vendor AS v
WHERE VendorId = v.Id)
_
また、DELETE
を削除してWHERE PK IN (SELECT PK FROM #MyTempTable WHERE BatchNumber = @BatchNumber)
を削除します。一時テーブルが入力されているため、更新に対応するためにDELETE
クエリ自体に_NOT EXISTS
_を含める必要がある場合がありますが、これははるかに効率的です。バッチごとに4,500シークを実行するだけで済みます。
実行計画は、連続する各ループが前のループよりも多くの作業を実行することを示唆しています。削除する行がテーブル全体に均等に分散されていると仮定すると、最初のループは、削除する4500行を見つけるために約4500 * 221000000/16000000 = 62156行をスキャンする必要があります。また、vendor
テーブルに対して同じ数のクラスター化インデックスシークを実行します。ただし、2番目のループでは、最初に削除していない同じ62156-4500 = 57656行を超えて読み取る必要があります。 2番目のループがMySourceTable
から120000行をスキャンし、vendor
テーブルに対して120000シークを実行すると予想される場合があります。ループごとに必要な作業量は線形速度で増加します。概算として、平均ループはMySourceTable
から102516868行を読み取り、vendor
テーブルに対して102516868シークを実行する必要があると言えます。バッチサイズ4500で1600万行を削除するには、コードで16000000/4500 = 3556ループを実行する必要があるため、コードが完了する作業の合計量は、MySourceTable
から読み取った約3,645億行と3,645億行になります。インデックスシーク。
小さな問題は、RECOMPILE
またはその他のヒントなしで、TOP式でローカル変数@BATCHSIZE
を使用することです。クエリオプティマイザーは、プランの作成時にそのローカル変数の値を認識しません。それは100に等しいと仮定します。実際には、100ではなく4500行を削除しており、その不一致により、効率の悪い計画になる可能性があります。テーブルに挿入するときにカーディナリティの推定値が低いと、パフォーマンスが低下する可能性があります。 SQL Serverは、4500行ではなく100行を挿入する必要があると考える場合、挿入を行うために別の内部APIを選択する場合があります。
1つの代替方法は、削除する行の主キー/クラスター化キーを一時テーブルに挿入することです。キー列のサイズによっては、これはtempdbに簡単に適合します。その場合、 最小限のロギング を取得できます。これは、トランザクションログが爆発しないことを意味します。 SIMPLE
の復旧モデルを使用して、任意のデータベースに対して最小限のログを取得することもできます。要件の詳細については、リンクを参照してください。
それが選択肢でない場合は、MySourceTable
のクラスター化インデックスを利用できるようにコードを変更する必要があります。重要なことは、ループごとにほぼ同じ量の作業を実行できるようにコードを記述することです。毎回最初からテーブルをスキャンするのではなく、インデックスを利用することでそれを行うことができます。私は ブログ投稿 を書いて、ループのいくつかの異なる方法を説明しています。その投稿の例では、削除ではなくテーブルに挿入を行っていますが、コードを適合させることができるはずです。
以下のサンプルコードでは、MySourceTable
の主キーとクラスターキーを想定しています。私はこのコードをかなり早く書き、それをテストすることはできません:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
DECLARE @BATCHSIZE INT,
@ITERATION INT,
@TOTALROWS INT,
@MSG VARCHAR(500)
@STARTID BIGINT,
@NEXTID BIGINT;
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;
SELECT @STARTID = ID
FROM MySourceTable
ORDER BY ID
OFFSET 0 ROWS
FETCH FIRST 1 ROW ONLY;
SELECT @NEXTID = ID
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
OFFSET (60000) ROWS
FETCH FIRST 1 ROW ONLY;
BEGIN TRY
BEGIN TRANSACTION;
WHILE @STARTID IS NOT NULL
BEGIN
WITH MySourceTable_DELCTE AS (
SELECT TOP (60000) *
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
)
DELETE FROM MySourceTable_DELCTE
OUTPUT DELETED.*
INTO MyBackupTable
WHERE NOT EXISTS (
SELECT NULL AS Empty
FROM dbo.vendor AS v
WHERE VendorId = v.Id
);
SET @BATCHSIZE = @@ROWCOUNT;
SET @ITERATION = @ITERATION + 1;
SET @TOTALROWS = @TOTALROWS + @BATCHSIZE;
SET @MSG = CAST(GETDATE() AS VARCHAR) + ' Iteration: ' + CAST(@ITERATION AS VARCHAR) + ' Total deletes:' + CAST(@TOTALROWS AS VARCHAR) + ' Next Batch size:' + CAST(@BATCHSIZE AS VARCHAR);
PRINT @MSG;
COMMIT TRANSACTION;
CHECKPOINT;
SET @STARTID = @NEXTID;
SET @NEXTID = NULL;
SELECT @NEXTID = ID
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
OFFSET (60000) ROWS
FETCH FIRST 1 ROW ONLY;
END;
END TRY
BEGIN CATCH
IF @@ERROR <> 0
AND @@TRANCOUNT > 0
BEGIN
PRINT 'There is an error occured. The database update failed.';
ROLLBACK TRANSACTION;
END;
END CATCH;
GO
重要な部分はここにあります:
WITH MySourceTable_DELCTE AS (
SELECT TOP (60000) *
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
)
各ループは、MySourceTable
から60000行のみを読み取ります。その結果、トランザクションあたりの平均削除サイズは4500行、トランザクションあたりの最大削除サイズは60000行になります。バッチサイズを小さくしてより保守的にしたい場合も問題ありません。 @STARTID
変数は各ループの後に進みますので、ソーステーブルから同じ行を複数回読み取ることを回避できます。
2つの考えが思い浮かびます。
遅延はおそらく、そのボリュームのデータのインデックス作成が原因です。インデックスの削除、削除、およびインデックスの再構築を試みてください。
または.
保持したい行を一時テーブルにコピーし、1600万行のテーブルを削除して、一時テーブルの名前を変更する(またはソーステーブルの新しいインスタンスにコピーする)方が速い場合があります。