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


10

非常に大きな(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

回答:


12

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

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

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

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

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

別のセッションで実行中のプロセスとどのように通信できますか?そのために作成したメカニズムと同じメカニズムを使用して、現在のステータスを通知します。ステータステーブルです。処理を続行するか中止するかがわかるように、各ループの開始時にプロセスがチェックする列を追加するだけです。また、これは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;

更新:

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

  1. TOP (@UpdateRows)UPDATEクエリに追加して、一番上の行が次のように
    UPDATE TOP (@UpdateRows) ht
    なるようにします。最大数が影響を受ける行数をオプティマイザが知るのに役立つことがあり、それ以上の検索に時間を浪費しません。
  2. #CurrentSet一時テーブルに主キーを追加します。ここでのアイデアは、オプティマイザが1億行のテーブルにJOINするのを助けることです。

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

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

1
これは素晴らしいです。私は今それを走らせており、日中オンラインでそれを走らせることができるのは煙草です。ありがとうございました!
Jonesome Reinstate Monica 2015

@samsmithプロセスをさらに高速化するためのアイデアがあるので、先ほど追加したUPDATEセクションを参照してください。
ソロモンルツキー

UPDATEの拡張機能がないと、@ BatchRowsが10000000(1000万)に設定された状態で、毎時約800万回の更新が行われます
Jonesome Reinstate Monica

@samsmithそれは素晴らしいです:)そうですか?次の2つの点に注意してください。1) WHERE句に一致する行が次第に少なくなるため、プロセス遅くなります。そのため、フィルターされたインデックスを追加するのに良いタイミングですが、フィルターされていないインデックスを始めるので、それが役立つか傷つくかはわかりませんが、それでも、処理が完了するとスループットが低下することを期待します。2)を0.5秒程度に減らすことでスループットを増やすことができますWAITFOR DELAY。ただし、これは同時実行性とのトレードオフであり、ログシッピングを介して送信されるデータ量も異なります。
ソロモン・ルツキー

1時間あたり800万行に満足しています。はい、減速しています。これ以上のインデックスの作成はためらっています(ビルド全体でテーブルがロックされているため)。数回行ったのは、既存のインデックスの再編成です(これはオンラインであるため)。
Jonesome Reinstate Monica、2015

4

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

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

私はそれらを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

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

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

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


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

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

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


SQL 2008/2014では、raiseerrorの結果が表示されません。
Jonesome Reinstate Monica、2015

@samsmith、私は完全な例を追加しました。それを試してみてください。この単純な例では、どのような振る舞いをしますか?
Vladimir Baranov

2

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

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

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

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

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