はい、それは非常に一般的な問題のように聞こえますが、私はまだそれをあまり絞り込むことができませんでした。
したがって、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以外の接続はありませんが、実際にはハングしていません。
私は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に変更されることがあります。これは、ログをフラッシュしているときに発生すると思います
その他の役立つ情報:
私はまだそれが完了するのを待っています(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分かかる理由です。どちらにしても、同じくらいの時間がかかりませんか?
編集:元の投稿にはコメントできないため、ここでは編集4からの質問に回答します。A.Xには7つのインデックスがあります。インデックスは B-tree であり、そのフィールドを更新するたびにBツリーが再調整されます。インデックスを毎回再調整するよりも、これらのインデックスを最初から再構築する方が高速です。
更新シナリオは、手順を使用するよりも常に高速です。
テーブルAのすべての行の列Xを更新しているので、最初にその行のインデックスを削除してください。また、その列でアクティブなトリガーや制約などがないことを確認してください。
インデックスの更新は、制約を検証し、他のデータで検索を行う行レベルのトリガーを実行するのと同様に、コストのかかるビジネスです。
注目すべき点の1つは、このプロセス中のシステムリソース(メモリ、ディスク、CPU)です。 700万の個別の行を1つの大きなジョブで単一のテーブルに挿入しようとすると、サーバーはあなたと同じようにハングしました。
この大量挿入ジョブを実行するのに十分なメモリがサーバーにないことがわかりました。このような状況では、SQLはメモリを保持し、それを手放さないようにしています。..挿入コマンドが完了した後でも完了していない場合もあります。大きなジョブで処理されるコマンドが多いほど、より多くのメモリが消費されます。すばやく再起動すると、このメモリが解放されました。
タスクマネージャーを実行して、このプロセスを最初から開始します。メモリ使用量が75%を超えると、システム/プロセスが天文学的に急上昇する可能性があります。
上記のように実際にメモリ/リソースが制限されている場合、1つの大きなジョブではなく、プロセスをより小さな部分に分割する(メモリ使用率が高い場合は再起動する)か、大量のメモリを搭載した64ビットサーバーにアップグレードするかを選択できます。