web-dev-qa-db-ja.com

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

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回の反復に制限)

enter image description here

VendorId[〜#〜] pk [〜#〜]およびnon-clustered、ここでclustered indexはこのスクリプトでは使用されていません。他に5つの非一意の非クラスター化インデックスがあります。

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

9
cilerler

実行プランは、非クラスター化インデックスから行をある順序で読み取り、読み取った各外部行に対してシークを実行して_NOT EXISTS_を評価していることを示しています

enter image description here

テーブルの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シークを実行するだけで済みます。

8
Martin Smith

実行計画は、連続する各ループが前のループよりも多くの作業を実行することを示唆しています。削除する行がテーブル全体に均等に分散されていると仮定すると、最初のループは、削除する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変数は各ループの後に進みますので、ソーステーブルから同じ行を複数回読み取ることを回避できます。

4
Joe Obbish

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

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

または.

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

2
Jon