web-dev-qa-db-ja.com

このwhileループで明示的なトランザクションが必要ですか?

SQL Server 2014:

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

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

以下を少し実行してからクエリをキャンセル/終了すると、これまでに行われた作業はすべてコミットされますか、それとも明示的にBEGIN TRANSACTION/END TRANSACTIONステートメントを追加していつでもキャンセルできるようにする必要がありますか?

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

個々のステートメント(DML、DDLなど)は、それ自体がトランザクションです。つまり、ループの各反復の後(技術的には各ステートメントの後)、UPDATEステートメントが変更したものはすべて自動コミットされます。

もちろん例外はありますよね? SET IMPLICIT_TRANSACTIONS を介して暗黙のトランザクションを有効にすることができます。その場合、最初のUPDATEステートメントがトランザクションを開始し、最後にCOMMITまたはROLLBACKを実行する必要があります。これは、ほとんどの場合デフォルトでオフになっているセッションレベルの設定です。

いつでもキャンセルできるように、明示的なBEGIN TRANSACTION/END TRANSACTIONステートメントを追加する必要がありますか?

いいえ。実際、プロセスを停止して再起動できるようにしたい場合、プロセスを停止するとCOMMITを実行する前にプロセスをキャッチできるため、明示的なトランザクションを追加する(または暗黙的なトランザクションを有効にする)ことはお勧めできません。その場合、COMMITを手動で発行する必要があります(SSMSを使用している場合)、またはこれをSQLエージェントジョブから実行している場合は、その機会がなく、孤立したトランザクションが発生する可能性があります。


また、_@CHUNK_SIZE_を小さい数値に設定することもできます。ロックのエスカレーションは通常、単一のオブジェクトで取得された5000個のロックで発生します。行のサイズに応じて、行ロックとページロックを実行している場合は、その制限を超えている可能性があります。行のサイズが各ページに1行または2行しか収まらない場合、ページロックを実行している場合でも、常にこれにぶつかることがあります。

テーブルがパーティション化されている場合、エスカレーション時にテーブル全体ではなくパーティションのみをロックするように、テーブルの_LOCK_ESCALATION_オプション(SQL Server 2008で導入)をAUTOに設定するオプションがあります。または、どのテーブルでも同じオプションをDISABLEに設定できますが、その場合は十分に注意する必要があります。詳細については、 ALTER TABLE を参照してください。

ロックのエスカレーションとしきい値について説明しているドキュメントは次のとおりです: ロックのエスカレーション (「SQL Server 2008 R2以降のバージョン」に適用されるとあります)。そして、これがロックのエスカレーションの検出と修正を扱うブログ投稿です: Microsoft SQL Serverのロック(パート12-ロックのエスカレーション)


正確な質問とは無関係ですが、質問のクエリに関連して、ここで行うことができるいくつかの改善点があります(または少なくともそれを見るだけでそのように見えます)。

  1. ループの場合、WHILE (@@ROWCOUNT = @CHUNK_SIZE)を実行すると、最後の反復で更新された行数がUPDATEに要求された数よりも少ない場合、実行する作業がなくなるため、少し優れています。

  2. deletedフィールドがBITデータ型である場合、その値はdeletedDateが_2000-01-01_であるかどうかによって決定されませんか?なぜ両方が必要なのですか?

  3. これらの2つのフィールドが新しく、NULLとして追加したため、オンライン/非ブロック操作である可能性があり、それらを「デフォルト」値に更新したい場合、それは不要でした。 SQL Server 2012(Enterprise Editionのみ)以降、DEFAULT制約を持つ_NOT NULL_列の追加は、DEFAULTの値が定数である限り、非ブロッキング操作です。そのため、まだフィールドを使用していない場合は、ドロップして_NOT NULL_としてドロップし、DEFAULT制約を追加します。

  4. このUPDATEの実行中に他のプロセスがこれらのフィールドを更新していない場合は、更新するレコードをキューに入れて、そのキューで作業するほうが高速です。変更が必要なセットを取得するために毎回テーブルを再クエリする必要があるため、現在のメソッドではパフォーマンスが低下します。代わりに、次のようにして、これらの2つのフィールドでテーブルを1回だけスキャンしてから、対象を絞ったUPDATEステートメントのみを発行することができます。また、キューの初期設定では、更新されていないレコードが見つかるだけなので、いつでもプロセスを停止して後で開始してもペナルティはありません。

    1. クラスター化インデックスのキーフィールドのみが含まれる一時テーブル(#FullSet)を作成します。
    2. 同じ構造の2番目の一時テーブル(#CurrentSet)を作成します。
    3. SELECT TOP(n) KeyField1, KeyField2 FROM [huge-table] where deleted is null or deletedDate is null;を使用して#FullSetに挿入します

      TOP(n)は、テーブルのサイズが原因でそこにあります。テーブルに1億行あるため、特にプロセスを頻繁に停止して後で再起動する場合は、キーのセット全体をキューテーブルに入力する必要はありません。したがって、nを100万に設定し、それを最後まで実行することができます。これは、100万セット(またはそれ以下)を実行するSQLエージェントジョブでいつでもスケジュールでき、次にスケジュールされた時間を待って再びピックアップします。次に、20分ごとに実行するようにスケジュールすることができます。これにより、nのセットの間に強制的な余地ができますが、プロセス全体が無人で終了します。次に、何もする必要がないときに、ジョブにそれ自体を削除させます:-)。

    4. ループで、次のことを行います:
      1. DELETE TOP (4995) FROM #FullSet OUTPUT Deleted.KeyField INTO #CurrentSet (KeyField);のようなものを介して現在のバッチを入力します
      2. IF (@@ROWCOUNT = 0) BREAK;
      3. 次のようなものを使用してUPDATEを実行します:_UPDATE ht SET ht.deleted = 0, ht.deletedDate='2000-01-01' FROM [huge-table] ht INNER JOIN #CurrentSet cs ON cs.KeyField = ht.KeyField;_
      4. 現在のセットを消去:_TRUNCATE TABLE #CurrentSet;_
  5. 場合によっては、フィルタされたインデックスを追加して、_#FullSet_一時テーブルにフィードするSELECTを支援することが役立ちます。このようなインデックスの追加に関する考慮事項は次のとおりです。
    1. WHERE条件はクエリのWHERE条件と一致する必要があるため、_WHERE deleted is null or deletedDate is null_
    2. プロセスの最初では、ほとんどの行がWHERE条件と一致するため、インデックスはそれほど役に立ちません。これを追加する前に、50%マークのあたりまで待つことをお勧めします。もちろん、それがどれだけ役立つか、およびインデックスを追加するのに最適な時期はいくつかの要因により異なりますので、これは試行錯誤です。
    3. 基本データは非常に頻繁に変更されるため、手動で統計を更新したり、インデックスを再構築して最新の状態に維持したりする必要がある場合があります
    4. インデックスは、SELECTを支援している間、UPDATEに悪影響を与えることに注意してください。これは、その操作中に更新する必要がある別のオブジェクトであり、I/Oが増えるためです。これは、フィルターされたインデックス(フィルターに一致する行が少ないため、行を更新すると縮小する)を使用することと、インデックスを追加するのにしばらく待つこと(最初は非常に役に立たない場合、発生する理由がないこと)の両方に関係します。追加のI/O)。

UPDATE:ステータスを追跡するメカニズムを含む、上記の提案の完全な実装については、この質問に関連する質問への私の回答を参照してください。きれいにキャンセル: SQLサーバー:巨大なテーブルのフィールドを小さなチャンクで更新:進行状況/ステータスを取得する方法

13
Solomon Rutzky