web-dev-qa-db-ja.com

SQL更新ステートメントに非常に長い時間がかかる/ディスク使用率が数時間高い

はい、それは非常に一般的な問題のように聞こえますが、私はまだそれをあまり絞り込むことができませんでした。

したがって、SQLバッチファイルにUPDATEステートメントがあります。

UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID

Bには40kレコード、Aには4Mレコードがあり、A.B_IDを介して1対nに関連付けられていますが、2つの間にFKはありません。

したがって、基本的にはデータマイニングの目的でフィールドを事前に計算しています。この質問のためにテーブルの名前を変更しましたが、ステートメントは変更しませんでした。それは本当に簡単です。

実行には数時間かかるため、すべてをキャンセルすることにしました。 DBが破損したので、それを削除し、ステートメントを実行する直前に行ったバックアップを復元し、カーソルで詳細に進むことにしました。

DECLARE CursorB CURSOR FOR SELECT ID FROM B ORDER BY ID DESC -- Descending order
OPEN CursorB 
DECLARE @Id INT
FETCH NEXT FROM CursorB INTO @Id

WHILE @@FETCH_STATUS = 0
BEGIN
    DECLARE @Msg VARCHAR(50) = 'Updating A for B_ID=' + CONVERT(VARCHAR(10), @Id)
    RAISERROR(@Msg, 10, 1) WITH NOWAIT

    UPDATE A
    SET A.X = B.X
    FROM A JOIN B ON A.B_ID = B.ID
    WHERE B.ID = @Id

    FETCH NEXT FROM CursorB INTO @Id
END

これで、IDが降順のメッセージで実行されていることがわかります。何が起こるかは、id = 40kからid = 13になるまで約5分かかります

そして、id 13では、何らかの理由で、ハングしているようです。 DBにはSSMS以外の接続はありませんが、実際にはハングしていません。

  • ハードドライブは継続的に実行されているため、確実に何かを実行しています(Process Explorerで、実際にそれを使用しているsqlserver.exeプロセスであることを確認しました)
  • 私はsp_who2を実行し、SUSPENDEDセッションのSPID(70)を見つけて、次のスクリプトを実行しました。

    select * from sys.dm_exec_requests r join sys.dm_os_tasks t on r.session_id = t.session_id where r.session_id = 70

これにより、ほとんどの場合、PAGEIOLATCH_SHであるwait_typeが得られますが、実際にはWRITE_COMPLETIONに変更されることがあります。これは、ログをフラッシュしているときに発生すると思います

  • dBを復元したとき(およびIDが13になったとき)のログファイルは1.6GBでしたが、現在は3.5GBです。

その他の役立つ情報:

  • b_ID 13のテーブルAのレコード数は大きくありません(14)
  • 私の同僚は彼女のマシンで同じ問題を抱えていません。同じ構造を持つこのDBのコピー(数か月前から)があります。
  • テーブルAは、DBの中で最大のテーブルです。
  • いくつかのインデックスがあり、いくつかのインデックス付きビューがそれを使用します。
  • DBには他のユーザーはいません。ローカルであり、アプリケーションはそれを使用していません。
  • LDFファイルのサイズに制限はありません。
  • 復旧モデルはシンプル、互換性レベルは100
  • Procmonは多くの情報を提供しません:sqlserver.exeは、MDFおよびLDFファイルから多くの読み書きを行っています。

私はまだそれが完了するのを待っています(1時間30分前です)が、誰かが私にこれをトラブルシューティングできる他のアクションを提供することを望んでいました。

編集:procmonログから抽出を追加

15:24:02.0506105    sqlservr.exe    1760    ReadFile    C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf  SUCCESS Offset: 5,498,732,544, Length: 8,192, I/O Flags: Non-cached, Priority: Normal
15:24:02.0874427    sqlservr.exe    1760    WriteFile   C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf  SUCCESS Offset: 6,225,805,312, Length: 16,384, I/O Flags: Non-cached, Write Through, Priority: Normal
15:24:02.0884897    sqlservr.exe    1760    WriteFile   C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA_1.LDF    SUCCESS Offset: 4,589,289,472, Length: 8,388,608, I/O Flags: Non-cached, Write Through, Priority: Normal

DBCC PAGEを使用すると、テーブルA(またはそのインデックスの1つ)のように見えるフィールドからの読み取りと書き込みのように見えますが、異なるB_IDの場合は13。

編集2:実行計画

そのため、クエリをキャンセルし(実際にはDBとそのファイルを削除してから復元しました)、実行プランを確認しました。

UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = 13

(推定)実行計画は、どのB.IDの場合も同じであり、かなり簡単に見えます。 WHERE句はBの非クラスター化インデックスでインデックスシークを使用し、JOINはテーブルの両方のPKでクラスター化インデックスシークを使用します。 Aのクラスター化インデックスシークは並列処理(x7)を使用し、CPU時間の90%を表します。

さらに重要なのは、ID 13のクエリを実際に実行するとすぐに実行できることです。

編集3:インデックスの断片化

インデックスの構造は次のとおりです。

Bには1つのクラスター化PK(IDフィールドではない)と1つの非クラスター化一意インデックスがあり、最初のフィールドはB.IDです。この2番目のインデックスは常に使用されるようです。

Aには1つのクラスター化PKがあります(フィールドは関連していません)。

Aには7つのビューもあり(すべてにA.Xフィールドが含まれます)、それぞれに独自のクラスター化されたPKがあり、他のインデックスこれにはA.Xフィールドも含まれます

ビューは(この方程式にないフィールドで)フィルターされているので、UPDATE Aがビュー自体をuseする方法はないと思います。ただし、A.Xを含むインデックスがあるため、A.Xを変更すると、フィールドを含む7つのビューと7つのインデックスが書き込まれます。

このため、UPDATEは遅くなることが予想されますが、特定のIDが他のIDよりもはるかに長くなる理由はありません。

私はすべてのインデックスの断片化をチェックしました。すべて<0.1%でしたビューのセカンダリインデックスを除く、すべて25%から50%の間でした。すべてのインデックスのFILL FACTORは、90%から95%の間で問題ないようです。

すべてのセカンダリインデックスを再編成し、スクリプトを再実行しました。

それはまだ絞首刑にされていますが、別の時点で:

...
(0 row(s) affected)

        Updating A for B_ID=14

(4 row(s) affected)

以前は、メッセージログは次のようになりました。

...
(0 row(s) affected)

        Updating A for B_ID=14

(4 row(s) affected)

        Updating A for B_ID=13

これは、WHILEループの同じポイントでハングしていないことを意味するため、奇妙です。残りは同じように見えます。sp_who2で待機している同じUPDATE行、同じPAGEIOLATCH_EX待機タイプ、およびsqlserver.exeからの同じ重いHDの使用。

次のステップは、すべてのインデックスとビューを削除して、それらを再作成することです。

編集4:インデックスの削除と再構築

そのため、テーブルにあるすべてのインデックス付きビューを削除しました(そのうちの7つ、クラスター化されたビューを含め、ビューごとに2つのインデックス)。最初のスクリプトを(カーソルなしで)実行しましたが、実際には5分で実行されました。

したがって、私の問題はこれらのインデックスの存在に起因します。

更新を実行した後、インデックスを再作成しましたが、16分かかりました。

インデックスの再構築に時間がかかることを理解しました。実際、タスク全体で20分かかります。

それでもわからないのは、インデックスを削除せずに更新を実行すると数時間かかるのに、最初に削除してから再作成すると20分かかる理由です。どちらにしても、同じくらいの時間がかかりませんか?

8
GFK
  1. UPDATEコマンドを使います。 CURSORは、実行しようとしている処理が遅くなります。
  2. インデックス付きビューのインデックスを含むすべてのインデックスを削除/無効化します。 A.Xに外部キーがある場合は、それをドロップします。
  3. A.B_IDのみを含むインデックスを作成し、B.IDには別のインデックスを作成します。
  4. 単純復旧モデルを使用している場合でも、最後のトランザクションは、ディスクにフラッシュされる前に常にトランザクションログに記録されます。そのため、トランザクションログを事前に拡張し、それよりも大きなサイズ(100 MBなど)まで拡張するように設定する必要があります。
  5. また、データファイルの増加量をある程度大きく設定します。
  6. ログファイルとデータファイルをさらに拡張するのに十分なディスク領域があることを確認してください。
  7. 更新が完了すると、手順2で削除/無効にしたインデックスが再作成/有効になります。
  8. 不要になった場合は、手順3で作成したインデックスを削除します。

編集:元の投稿にはコメントできないため、ここでは編集4からの質問に回答します。A.Xには7つのインデックスがあります。インデックスは B-tree であり、そのフィールドを更新するたびにBツリーが再調整されます。インデックスを毎回再調整するよりも、これらのインデックスを最初から再構築する方が高速です。

0
bojan

更新シナリオは、手順を使用するよりも常に高速です。

テーブルAのすべての行の列Xを更新しているので、最初にその行のインデックスを削除してください。また、その列でアクティブなトリガーや制約などがないことを確認してください。

インデックスの更新は、制約を検証し、他のデータで検索を行う行レベルのトリガーを実行するのと同様に、コストのかかるビジネスです。

0
ik_zelf

注目すべき点の1つは、このプロセス中のシステムリソース(メモリ、ディスク、CPU)です。 700万の個別の行を1つの大きなジョブで単一のテーブルに挿入しようとすると、サーバーはあなたと同じようにハングしました。

この大量挿入ジョブを実行するのに十分なメモリがサーバーにないことがわかりました。このような状況では、SQLはメモリを保持し、それを手放さないようにしています。..挿入コマンドが完了した後でも完了していない場合もあります。大きなジョブで処理されるコマンドが多いほど、より多くのメモリが消費されます。すばやく再起動すると、このメモリが解放されました。

タスクマネージャーを実行して、このプロセスを最初から開始します。メモリ使用量が75%を超えると、システム/プロセスが天文学的に急上昇する可能性があります。

上記のように実際にメモリ/リソースが制限されている場合、1つの大きなジョブではなく、プロセスをより小さな部分に分割する(メモリ使用率が高い場合は再起動する)か、大量のメモリを搭載した64ビットサーバーにアップグレードするかを選択できます。

0
Techie Joe