web-dev-qa-db-ja.com

ロックを引き起こさない大きな更新/削除スクリプトを書く

大量の削除を実行しているT-SQLスクリプトを実行しているときに、他の接続の一部のテーブルにデータを挿入できないようなロックの競合が発生するという不愉快な経験がありました。私は、包括的なトランザクションとロックを回避することを期待して、リクエストをより小さなトランザクションに分割することでこれを修正しようとしました:

set implicit_transactions off

declare @idsToDelete table (id bigint)
declare @currentId bigint
declare @deleteCursor cursor

begin transaction
    insert into @idsToDelete
    select someId from someTable with (nolock) where someCondition = 'some value'
commit

set @deleteCursor = cursor for select id from @idsToDelete
open @deleteCursor
fetch next from @deleteCursor into @currentId

while @@fetch_status = 0 begin

    begin transaction

        delete from relatedTable
        where id = @currentId

        delete from someTable
        where id = @currentId

    commit

    fetch next from @deleteCursor into @currentId

end

close @deleteCursor

私の理解はset implicit_transactions offnotがトップレベルのトランザクションを持つことを可能にする必要があります。したがって、それぞれの削除が実行されて完了し、これで完了です。行のセットが少しずつ削除されていく様子を見ることができたので、すべて良かったと思いました。ただし、実際には、このテーブルへの挿入はブロックされていました。そしてさらに悪いことに、実行に気づいてキャンセルしたとき、全体がロールバックし、ブロックしたようにブロックし続けました!

これを独立したトランザクションでロックなしで実行する方法はありますか(行ロックはOKですが)。注意しないと、同様の問題が発生する可能性がある大きな更新スクリプトを作成する必要があります。

2
Jacob

@ジェイコブ-私たちは私たちの人生のある時点で私たち全員が不愉快な経験をしたと思いますので、打ちのめさないでください。あなたが指導を求め、そこから学んでいるのは立派です。だからあなたに嫌い。

ターゲットにするいくつかの重要なことは、影響を受ける行数かもしれません。

  • @idsToDeleteに挿入する場合は、レコードセットが約1000行以下であることが理想的です。行数が多い(数千以上)と、クエリの速度が大幅に低下します。 #temptableと@temptableを1000行以上で並べて挿入するか、CTEを使用して並べて実行したところ、#temptableまたはより高いレコードセットを使用したCTEが@temptableを大幅に上回ることが何度も見られます。
  • (IDなどで)複数の行を確実に削除するテーブルから削除するには、WITH(ROWLOCK)ヒントを含める必要があります。つまり、指定されたレコードセットを削除するためにテーブルをスキャンしても、テーブル全体がロックされず、このような大規模なブロックが発生します。

重要な行数を持つテーブルパラメーターに対するパフォーマンスの低下と、そのロックヒントなしに各関連テーブルから数千行の行を削除する可能性の組み合わせにより、クロールして永続的なブロックパターンが表示されると思います。

トランザクションについては、トランザクションを削除ステートメントのみを削除するようにするか、tsqlをストアドプロシージャに利用/ラップすることを検討します。使用方法にもよりますが、パフォーマンスとベストプラクティスのために、通常はカーソルから離れることをお勧めします。

参考になれば、私も例をあげることができます。 :)

そして、それでも確信が持てない場合、2つ目の目を見つけることができれば、それもいくつかの節約になります!

乾杯!

-skibunnysqldiva

3
skibunnysqldiva

いくつかのこと:

  1. _set implicit_transactions off_を削除します 実際には、あなたがそれらを望まないので、これはおそらく最良のものです。この設定は、「トップレベルのトランザクション」とは関係ありません。 _implicit_transactions_がONの場合、INSERTおよびDELETE(および others )はトランザクションを自動的に開始します。

  2. テーブル変数の周りにBEGIN TRAN/COMMITは必要ありません。作業/スクラッチテーブルにデータを入力するためだけにトランザクションを要求することは意味がないだけでなく、テーブル変数はそもそもトランザクションにバインドされていません;-)。

  3. DELETEステートメントにWITH (ROWLOCK)ヒントを追加できます。ヒントがないと、低レベルのロックがテーブルロックにアップグレードされ、5000回の変更で発生するロックエスカレーションに遭遇する可能性があります。

  4. 一連の削除を実行するように切り替える方がよい場合があります。この場合、_@idsToDelete_テーブル変数のすべてを同時に削除するわけではないため、IDのバッチを保持する別のテーブル変数または一時テーブルが必要になります。テーブル変数は通常、1行しかないように見えるため、JOINingには適していませんが、通常はTOP (n)を使用することで回避できます。ただし、ローカル一時テーブルの方が簡単な場合があります。

たとえば、CURSORを介して行ごとに移動するのではなく、完了するまでWHILEループを実行します。

_CREATE #TempIDs (ID BIGINT NOT NULL);

declare @idsToDelete table (id bigint);

insert into @idsToDelete
  select someId
  from someTable with (nolock)
  where someCondition = 'some value';

WHILE (1 = 1)
BEGIN
  BEGIN TRAN

  DELETE TOP (500) tmp
  OUTPUT DELETED.ID
  INTO   #TempIDs (ID)
  FROM   @idsToDelete;

  IF (@@ROWCOUNT = 0)
  BEGIN
    ROLLBACK TRAN;
    BREAK; -- exit the loop cuz we done
  END;

  delete tab
  from relatedTable tab WITH (ROWLOCK)
  INNER JOIN #TempIDs tmp
          ON tmp.ID = tab.ID;

  delete tab
  from someTable tab WITH (ROWLOCK)
  INNER JOIN #TempIDs tmp
          ON tmp.ID = tab.ID;

  COMMIT;

  TRUNCATE TABLE #TempIDs; -- clear out for the next run
END;
_
3
Solomon Rutzky