SQLテーブルから数百万行を削除する


9

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

実行計画(2回の反復に限定)

ここに画像の説明を入力してください

VendorIdPKおよび非クラスター化であり、クラスター化インデックスはこのスクリプトでは使用されません。他に5つの非一意の非クラスター化インデックスがあります。

タスクは、「別のテーブルに存在しないベンダーを削除して」、それらを別のテーブルにバックアップすることです。3つのテーブルがありvendors, SpecialVendors, SpecialVendorBackupsます。テーブルにSpecialVendors存在しないVendorsものを削除し、私がやっていることが間違っていて、1〜2週間でそれらを戻す必要がある場合に備えて、削除されたレコードのバックアップを作成しようとします。


私はそのクエリの最適化に取り組み、nullである左結合を試します
パパラッツォ

回答:


8

実行プランは、非クラスター化インデックスから行を何らかの順序で読み取り、読み取った外側の行ごとにシークを実行して、 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)あなたはまだ含める必要があるかもしれませんNOT EXISTSDELETE一時テーブルが読み込まれてからの更新に対応するために、クエリ自体が、それが唯一の4500は、バッチごとに求めて実行する必要がありますので、これははるかに効率的でなければなりません。


「最初に削除する行を一時テーブルに具体化する」と言ったとき、すべての列を持つすべてのレコードを一時テーブルに配置することを提案していますか?またはPK列のみ? (私はそれらを一時テーブルに完全に移動することを提案していると思いますが、再確認したいと
思います

@cilerler-キー列のみ
Martin Smith

私があなたが正しく言ったことを理解できたかどうか、あなたはすぐにこれをレビューできますか?
cilerler 2017年

@cilerler - DELETE TOP (@BATCHSIZE) FROM MySourceTableちょうどべきであるDELETE FROM MySourceTable にも一時テーブルのインデックスCREATE TABLE #MyTempTable ( Id BIGINT, BatchNumber BIGINT, PRIMARY KEY(BatchNumber, Id) );とでVendorId、間違いなく自分自身でPK?2億2,100万を超えるベンダーがありますか?
マーティン・スミス

マーティンに感謝します、午後6時にそれをテストします。そして、あなたの答えは、それは間違いなくそのテーブルに存在する唯一のPKです
cilerler

4

実行計画は、連続する各ループが前のループよりも多くの作業を行うことを示唆しています。削除する行がテーブル全体に均等に分散されていると仮定すると、最初のループでは、削除する4500行を見つけるために約4500 * 221000000/16000000 = 62156行をスキャンする必要があります。また、vendorテーブルに対して同じ数のクラスター化インデックスシークを実行します。ただし、2番目のループでは、最初に削除しなかった同じ62156-4500 = 57656行を超えて読み取る必要があります。2番目のループMySourceTableは、vendorテーブルから120000行をスキャンし、テーブルに対して120000シークを実行すると予想されます。ループごとに必要な作業量は線形速度で増加します。概算として、平均ループはfromから102516868行を読み取る必要がありMySourceTablevendorテーブル。バッチサイズ4500で1600万行を削除するには、コードで16000000/4500 = 3556ループを実行する必要があるため、コードが完了する作業の合計量は、約3,645億行が読み取られMySourceTable、3,645億インデックスがシークされます。

小さな問題は、ローカル変数@BATCHSIZEをTOP式で使用した場合に、RECOMPILEヒントやその他のヒントがないことです。クエリオプティマイザーは、プランの作成時にそのローカル変数の値を認識しません。それは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
)   

各ループはから60000行のみを読み取りますMySourceTable。その結果、トランザクションあたりの平均削除サイズは4500行、トランザクションあたりの最大削除サイズは60000行になります。バッチサイズを小さくしてより保守的にしたい場合も問題ありません。@STARTIDあなたはより多くのソース表から何度も同じ行を読んで避けることができるので、変数は各ループの後に移行します。


詳しい情報ありがとうございます。テーブルをロックしないように4500の制限を設定しました。誤解しない限り、SQLには、削除カウントが5000を超えるとテーブル全体をロックするハード制限があります。これは長いプロセスになるため、そのテーブルを長期間ロックすることはできません。60000から4500に設定した場合、同じパフォーマンスが得られると思いますか?
cilerler 2017年

@cilerlerロックのエスカレーションが心配な場合は、テーブルレベルで無効にすることができます。バッチサイズ4500を使用しても何も問題はありません。重要なのは、各ループがほぼ同じ量の作業を実行することです。
ジョー・オブビッシュ

速度の違いで他の答えを受け入れざるを得ません。私はあなたのソリューションと@ Martin-Smithのソリューションをテストしました、そして彼のバージョンは10分のテストでより多くのデータを約2%取得しています。あなたの解決策は私のものよりはるかに優れており、私はあなたの時間に本当に感謝しています... –
cilerler

2

2つの考えが思い浮かびます。

遅延はおそらく、そのボリュームのデータでのインデックス付けが原因です。インデックスの削除、削除、およびインデックスの再構築を試みてください。

または

保持したい行を一時テーブルにコピーし、1600万行のテーブルを削除し、一時テーブルの名前を変更する(またはソーステーブルの新しいインスタンスにコピーする)方が速い場合があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.