web-dev-qa-db-ja.com

SQL Server:巨大なテーブルのフィールドを小さなチャンクで更新:進行状況/ステータスを取得する方法?

非常に大きな(1億行)テーブルがあり、その上のいくつかのフィールドを更新する必要があります。

ログシッピングなどの場合も、一口サイズのトランザクションを維持する必要があります。

  • 以下はトリックを行いますか?
  • そして、どのようにして出力を印刷して、進行状況を確認できますか? (そこにPRINTステートメントを追加しようとしましたが、whileループ中に何も出力されませんでした)

コードは次のとおりです。

DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000

UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null

WHILE @@ROWCOUNT > 0
BEGIN
    UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
    where deleted is null or deletedDate is null
END

関連する質問( このwhileループでは明示的なトランザクションが必要ですか? )に答えたとき、この質問に気づいていませんでしたが、完全を期すために、この問題は一部ではなかったので、ここで取り上げますそのリンクされた答えで私の提案の。

私はこれをSQLエージェントジョブ(結局1億行)を介してスケジュールすることを提案しているので、ステータスメッセージをクライアント(つまりSSMS)に送信する形式は理想的ではないと思います(ただし、他のプロジェクトが必要になったときは、RAISERROR('', 10, 1) WITH NOWAIT;を使用するのがよいとVladimirに同意します).

この特定のケースでは、これまでに更新された行数でループごとに更新できるステータステーブルを作成します。また、プロセスにハートビートを設定するために現在の時刻を投入しても問題はありません。

プロセスをキャンセルして再開できるようにしたい場合、 メインテーブルのUPDATEとステータステーブルのUPDATEを明示的なトランザクションでラップすることにうんざりしています。ただし、キャンセルが原因でステータステーブルが同期されていない場合は、COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULLを使用して手動で更新するだけで、現在の値で簡単に更新できます。 UPDATEする2つのテーブル(メインテーブルとステータステーブル)がある場合すべき明示的なトランザクションを使用して、これら2つのテーブルの同期を維持しますが、孤立したトランザクションが発生する危険を冒したくありません。トランザクションを開始した後でコミットしていない時点でプロセスをキャンセルした場合。これは、SQLエージェントジョブを停止しない限り安全です。

ええと、それを停止せずにプロセスを停止するにはどうすればよいでしょうか。 :-)を停止するように要求します。うん。プロセスに「シグナル」(Unixの_kill -3_と同様)を送信することにより、次の都合の良い瞬間(つまり、アクティブなトランザクションがない場合)で停止するように要求し、すべてを正常にクリーンアップすることができます。きちんとした。

別のセッションで実行中のプロセスとどのように通信できますか?そのために作成したメカニズムと同じメカニズムを使用して、現在のステータスを通知します。ステータステーブルです。処理を続行するか中止するかがわかるように、各ループの開始時にプロセスがチェックする列を追加するだけです。また、これはSQLエージェントジョブとしてスケジュールすることを目的としているため(10分または20分ごとに実行)、プロセスがちょうど進行している場合は、一時テーブルに100万行を入力しても意味がないため、最初から確認する必要もあります。しばらくして終了し、そのデータを使用しません。

_DECLARE @BatchRows INT = 1000000,
        @UpdateRows INT = 4995;

IF (OBJECT_ID(N'dbo.HugeTable_TempStatus') IS NULL)
BEGIN
  CREATE TABLE dbo.HugeTable_TempStatus
  (
    RowsUpdated INT NOT NULL, -- updated by the process
    LastUpdatedOn DATETIME NOT NULL, -- updated by the process
    PauseProcess BIT NOT NULL -- read by the process
  );

  INSERT INTO dbo.HugeTable_TempStatus (RowsUpdated, LastUpdatedOn, PauseProcess)
  VALUES (0, GETDATE(), 0);
END;

-- First check to see if we should run. If no, don't waste time filling temp table
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
  PRINT 'Process is paused. No need to start.';
  RETURN;
END;

CREATE TABLE #FullSet (KeyField1 DataType1, KeyField2 DataType2);
CREATE TABLE #CurrentSet (KeyField1 DataType1, KeyField2 DataType2);

INSERT INTO #FullSet (KeyField1, KeyField2)
  SELECT TOP (@BatchRows) ht.KeyField1, ht.KeyField2
  FROM   dbo.HugeTable ht
  WHERE  ht.deleted IS NULL
  OR     ht.deletedDate IS NULL

WHILE (1 = 1)
BEGIN
  -- Check if process is paused. If yes, just exit cleanly.
  IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
  BEGIN
    PRINT 'Process is paused. Exiting.';
    BREAK;
  END;

  -- grab a set of rows to update
  DELETE TOP (@UpdateRows)
  FROM   #FullSet
  OUTPUT Deleted.KeyField1, Deleted.KeyField2
  INTO   #CurrentSet (KeyField1, KeyField2);

  IF (@@ROWCOUNT = 0)
  BEGIN
    RAISERROR(N'All rows have been updated!!', 16, 1);
    BREAK;
  END;

  BEGIN TRY
    BEGIN TRAN;

    -- do the update of the main table
    UPDATE ht
    SET    ht.deleted = 0,
           ht.deletedDate = '2000-01-01'
    FROM   dbo.HugeTable ht
    INNER JOIN #CurrentSet cs
            ON cs.KeyField1 = ht.KeyField1
           AND cs.KeyField2 = ht.KeyField2;

    -- update the current status
    UPDATE ts
    SET    ts.RowsUpdated += @@ROWCOUNT,
           ts.LastUpdatedOn = GETDATE()
    FROM   dbo.HugeTable_TempStatus ts;

    COMMIT TRAN;
  END TRY
  BEGIN CATCH
    IF (@@TRANCOUNT > 0)
    BEGIN
      ROLLBACK TRAN;
    END;

    THROW; -- raise the error and terminate the process
  END CATCH;

  -- clear out rows to update for next iteration
  TRUNCATE TABLE #CurrentSet;

  WAITFOR DELAY '00:00:01'; -- 1 second delay for some breathing room
END;

-- clean up temp tables when testing
-- DROP TABLE #FullSet; 
-- DROP TABLE #CurrentSet; 
_

その後、次のクエリを使用していつでもステータスを確認できます。

_SELECT sp.[rows] AS [TotalRowsInTable],
       ts.RowsUpdated,
       (sp.[rows] - ts.RowsUpdated) AS [RowsRemaining],
       ts.LastUpdatedOn
FROM sys.partitions sp
CROSS JOIN dbo.HugeTable_TempStatus ts
WHERE  sp.[object_id] = OBJECT_ID(N'ResizeTest')
AND    sp.[index_id] < 2;
_

SQLエージェントジョブで実行されている場合でも、他のユーザーのコンピューターのSSMSで実行されている場合でも、プロセスを一時停止したいですか?とにかく走れ:

_UPDATE ht
SET    ht.PauseProcess = 1
FROM   dbo.HugeTable_TempStatus ts;
_

プロセスが再びバックアップを開始できるようにしたいですか?とにかく走れ:

_UPDATE ht
SET    ht.PauseProcess = 0
FROM   dbo.HugeTable_TempStatus ts;
_

UPDATE:

この操作のパフォーマンスを向上させる可能性があるいくつかの追加事項を次に示します。役立つことは保証されていませんが、おそらくテストする価値があります。 1億行を更新するため、いくつかのバリエーションをテストする時間と機会がたくさんあります;-)。

  1. TOP (@UpdateRows)をUPDATEクエリに追加して、上の行が次のようになるようにします。
    UPDATE TOP (@UpdateRows) ht
    これは、オプティマイザが最大数の影響を受ける行を知るのに役立つ場合があります。これにより、追加の検索に時間を無駄にすることがなくなります。
  2. PRIMARY KEYを_#CurrentSet_一時テーブルに追加します。ここでのアイデアは、オプティマイザが1億行のテーブルにJOINするのを助けることです。

    また、あいまいにならないように説明するために、PKを_#FullSet_一時テーブルに追加する理由はありません。これは、順序が関係しない単純なキューテーブルであるためです。

  3. 場合によっては、フィルタされたインデックスを追加して、_#FullSet_一時テーブルにフィードするSELECTを支援することが役立ちます。このようなインデックスの追加に関する考慮事項は次のとおりです。
    1. WHERE条件はクエリのWHERE条件と一致する必要があるため、_WHERE deleted is null or deletedDate is null_
    2. プロセスの最初では、ほとんどの行がWHERE条件と一致するため、インデックスはそれほど役に立ちません。これを追加する前に、50%マークのあたりまで待つことをお勧めします。もちろん、それがどれだけ役立つか、およびインデックスを追加するのに最適な時期はいくつかの要因により異なりますので、これは試行錯誤です。
    3. 基本データは非常に頻繁に変更されるため、手動で統計を更新したり、インデックスを再構築して最新の状態に維持したりする必要がある場合があります
    4. インデックスは、SELECTを支援している間、UPDATEを傷つけることに注意してください。これは、その操作中に更新する必要がある別のオブジェクトであり、I/Oが増えるためです。これは、フィルターされたインデックス(フィルターに一致する行が少ないため、行を更新すると縮小する)を使用することと、インデックスを追加するのにしばらく待つこと(最初は非常に役に立たない場合、発生する理由がないこと)の両方に関係します。追加のI/O)。
12
Solomon Rutzky

2番目の部分への回答:ループ中に出力を出力する方法。

Sys管理者が時々実行しなければならないいくつかの長期にわたるメンテナンス手順があります。

私はそれらをSSMSから実行し、PRINTステートメントが手順全体が終了した後にのみSSMSに表示されることにも気付きました。

したがって、私は RAISERROR を重大度の低い値で使用しています:

DECLARE @VarTemp nvarchar(32);
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;

SQL Server 2008 StandardおよびSSMS 2012(11.0.3128.0)を使用しています。 SSMSで実行するための完全に機能する例を次に示します。

DECLARE @VarCount int = 0;
DECLARE @VarTemp nvarchar(32);

WHILE @VarCount < 3
BEGIN
    SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
    --RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
    --PRINT @VarTemp;

    WAITFOR DELAY '00:00:02';
    SET @VarCount = @VarCount + 1;
END

RAISERRORをコメントアウトしてPRINTのみを残すと、SSMSの[メッセージ]タブのメッセージは、バッチ全体が終了した後、6秒後に表示されます。

PRINTをコメントアウトしてRAISERRORを使用すると、SSMSの[メッセージ]タブのメッセージが6秒待たずに表示されますが、ループが進行します。

興味深いことに、RAISERRORPRINTの両方を使用すると、両方のメッセージが表示されます。最初に最初のRAISERRORからメッセージが送信され、次に2秒間遅延し、次に最初のPRINTと2番目のRAISERRORのように続きます。


他の場合では、別の専用のlogテーブルを使用して、長期実行プロセスの現在の状態とタイムスタンプを説明する情報を含む行をテーブルに挿入するだけです。

長いプロセスが実行されている間、私は定期的にSELECTテーブルからlogを実行して、何が起こっているのかを確認します。

これには明らかに一定のオーバーヘッドがありますが、後で自分のペースで調べることができるログ(またはログの履歴)が残ります。

4

次のような別の接続からそれを監視できます:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(*) FROM [huge-table] WHERE deleted IS NULL OR deletedDate IS NULL 

やることがどれだけ残っているかを確認します。これは、SSMSなどで手動で実行するのではなく、アプリケーションがプロセスを呼び出しており、進行状況を表示する必要がある場合に役立ちます。 "非同期呼び出し(またはスレッド)が完了するまで時々チェックします。

分離レベルを可能な限り緩やかに設定することは、ロックの問題が原因でメイントランザクションに遅れることなく、妥当な時間内に戻る必要があることを意味します。もちろん、戻り値が少し不正確であることを意味する可能性がありますが、単純な進行状況メーターとして、これはまったく問題になりません。

2
David Spillett