データベースの専門家は、不適切なインデックス作成や本番SQLインスタンスでのコードの記述が不十分であるなど、データベースのパフォーマンスの問題に日常的に直面しています。トランザクションを更新し、SQLServerが次のデッドロックメッセージを報告したとします。始めたばかりのDBAにとって、これはショックになるかもしれません。
この記事では、SQLServerのデッドロックとそれを回避するための最良の方法について説明します。
SQL Serverのデッドロックとは何ですか?
SQL Serverは、トランザクション性の高いデータベースです。たとえば、24時間体制で顧客から新しい注文を受け取るオンラインショッピングポータルのデータベースをサポートしているとします。複数のユーザーが同時に同じアクティビティを実行している可能性があります。この場合、データベースは、一貫性と信頼性を確保し、データの整合性を保護するために、Atomicity、Consistency、Isolation、Durability(ACID)のプロパティに従う必要があります。
次の画像は、リレーショナルデータベースのACIDプロパティを示しています。
ACIDプロパティに従うために、SQL Serverはロックメカニズム、制約、および先行書き込みログを使用します。さまざまなロックタイプには、排他ロック(X)、共有ロック(S)、更新ロック(U)、インテントロック(I)、スキーマロック(SCH)、および一括更新ロック(BU)が含まれます。これらのロックは、キー、テーブル、行、ページ、およびデータベースレベルで取得できます。
顧客データベースに接続しているJohnとPeterの2人のユーザーがいるとします。
- ジョンは、[customerid]1の顧客のレコードを更新したいと考えています。
- 同時に、Peterは[customerid]1を持っている顧客の値を取得したいと考えています。
この場合、SQLServerはJohnとPeterの両方に次のロックを使用します。
ジョンのロック
- レコードを含む顧客テーブルとページに対して、インテントエクスクルーシブ(IX)ロックが必要です。
- さらに、ジョンが更新したい行に排他的(X)ロックをかけます。プロセスAがロックを解除するまで、他のユーザーが行データを変更するのを防ぎます。
ピーターのロック
- where句に従って、顧客テーブルとレコードを含むページのインテント共有(IS)ロックを取得します。
- 行を読み取るために共有ロックを取得しようとします。この行には、すでにJohn専用のロックがあります。
この場合、ピーターはジョンが作業を終了して排他ロックを解放するまで待つ必要があります。この状況はブロッキングと呼ばれます。
ここで、別のシナリオで、ジョンとピーターが次のロックを持っているとします。
- ジョンは、顧客ID1の顧客テーブルを排他的にロックしています。
- Peterは、顧客ID1の注文テーブルを排他的にロックしています。
- ジョンは、トランザクションを完了するために注文テーブルを排他的にロックする必要があります。ピーターはすでに注文テーブルを排他的にロックしています。
- Peterは、トランザクションを完了するために顧客テーブルの排他ロックを必要とします。ジョンはすでに顧客テーブルに排他ロックを持っています。
この場合、各トランザクションは他のトランザクションによって保持されているリソースを必要とするため、どちらのトランザクションも続行できません。この状況は、SQLServerのデッドロックと呼ばれます。
SQLServerデッドロック監視メカニズム
SQL Serverは、デッドロック監視スレッドを使用してデッドロック状況を定期的に監視します。これにより、デッドロックに関係するプロセスがチェックされ、セッションがデッドロックの犠牲になったかどうかが識別されます。内部メカニズムを使用して、デッドロックの犠牲者のプロセスを識別します。デフォルトでは、ロールバックに必要なリソースが最も少ないトランザクションが犠牲者と見なされます。
SQL Serverは、別のセッションがトランザクションを完了するために必要なロックを取得できるように、犠牲セッションを強制終了します。デフォルトでは、SQLServerはデッドロックモニターを使用して5秒ごとにデッドロック状況をチェックします。デッドロックを検出した場合、デッドロックの発生に応じて、頻度が5秒から100ミリ秒に短縮される可能性があります。頻繁なデッドロックが発生しない場合は、監視スレッドが再び5秒にリセットされます。
SQL Serverがデッドロックの犠牲者としてプロセスを強制終了すると、次のメッセージが表示されます。このセッションでは、プロセスID69がデッドロックの犠牲者でした。
SQLServerデッドロック優先度ステートメントの使用による影響
デフォルトでは、SQL Serverは、最も安価なロールバックを使用してトランザクションをデッドロックの犠牲者としてマークします。ユーザーは、DEADLOCK_PRIORITYステートメントを使用してトランザクションのデッドロック優先度を設定できます。
SET DEADLOCK_PRIORITY
次の引数を使用します:
- 低:デッドロック優先度-5と同等です
- 通常:デフォルトのデッドロック優先度0です
- 高:デッドロックの優先度が最も高い5です。
デッドロック優先度の数値を-10から10(合計21の値)に設定することもできます。
デッドロック優先度ステートメントの例をいくつか見てみましょう。
例1:
デッドロック優先度のあるセッション1:通常(0)>デッドロック優先度のあるセッション2:低(-5)
デッドロックの犠牲者: セッション2
例2:
デッドロック優先度のあるセッション1:通常(0)<デッドロック優先度のあるセッション2:高(+5)
デッドロックの犠牲者: セッション1
例3
デッドロック優先度のセッション1:-3>デッドロック優先度のセッション2:-7
例4:
デッドロック優先度のセッション1:-5 <デッドロック優先度のセッション2:5
デッドロックの犠牲者: セッション1
デッドロックグラフを使用したSQLServerのデッドロック
デッドロックグラフは、デッドロックプロセス、それらのロック、およびデッドロックの犠牲者を視覚的に表したものです。トレースフラグ1204および1222を有効にして、デッドロックの詳細情報をXMLおよびグラフィック形式でキャプチャできます。デフォルトのsystem_health拡張イベントを使用して、デッドロックの詳細を取得できます。デッドロックをすばやく簡単に解釈する方法は、デッドロックグラフを使用することです。デッドロック状態をシミュレートし、対応するデッドロックグラフを表示してみましょう。
このデモンストレーションでは、CustomerテーブルとOrdersテーブルを作成し、いくつかのサンプルレコードを挿入しました。
CREATE TABLE Customer
(ID INT IDENTITY(1,1), CustomerName VARCHAR(20))
GO
CREATE TABLE Orders
(OrderID INT IDENTITY(1,1), ProductName VARCHAR(50))
GO
INSERT INTO Customer(CustomerName) VALUES ('Rajendra')
Go 100
S INSERT INTO Orders(ProductName) VALUES ('Laptop')
Go 100
次に、新しいクエリウィンドウを開き、トレースフラグをグローバルに有効にしました。
DBCC traceon(1222、-1)
デッドロックトレースフラグを有効にしたら、2つのセッションを開始し、次の順序でクエリを実行しました。
- 最初のセッションは、顧客ID1の顧客テーブルを更新するトランザクションを開始します。
- 2番目のセッションは、注文ID10の注文テーブルを更新するトランザクションを開始します。
- 最初のセッションは、同じ注文ID 10の注文テーブルを更新しようとします。2番目のセッションは、すでにこの行をロックしています。セッション2がロックしているため、セッション1はブロックされています。
- 次に、セッション2で、顧客ID 1の顧客テーブルを更新します。これにより、セッションID63とID65の両方が進行できないデッドロック状態が発生します。
この例では、SQL Serverはデッドロックの犠牲者(セッションID 65)を選択し、トランザクションを強制終了します。 system_health拡張イベントセッションからデッドロックグラフを取得しましょう。
SELECT XEvent.query('(event/data/value/deadlock)[1]') AS DeadlockGraph
FROM (
SELECT XEvent.query('.') AS XEvent
FROM (
SELECT CAST(target_data AS XML) AS TargetData
FROM sys.dm_xe_session_targets st
INNER JOIN sys.dm_xe_sessions s
ON s.address = st.event_session_address
WHERE s.NAME = ‘system_health’
AND st.target_name = ‘ring_buffer’
) AS Data
CROSS APPLY TargetData.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]
') AS XEventData(XEvent)
) AS source;
このクエリは、情報を解釈するために経験豊富なDBAを必要とするデッドロックXMLを提供します。
.XDL拡張子を使用してこのデッドロックXMLを保存し、SSMSでXDLファイルを開くと、以下に示すデッドロックグラフが表示されます。
このデッドロックグラフは、次の情報を提供します。
- プロセスノード: 楕円形では、プロセス関連の情報を取得します。
- リソースノード: リソースノード(四角いボックス)は、ロックとともにトランザクションに関係するオブジェクトに関する情報を提供します。この例では、両方のテーブルのインデックスがないため、RIDロックが表示されます。
- エッジ: エッジは、プロセスノードとリソースノードを接続します。リソース所有者とリクエストロックモードが表示されます。
デッドロックグラフの楕円を消すことで、デッドロックの犠牲者を表しています。
次の方法でSQLServerのデッドロック情報を取得できます。
- SQLServerプロファイラー
- SQLServer拡張イベント
- SQLServerエラーログ
- SQLServerのデフォルトのトレース
SQLServerの5種類のデッドロック
1)ブックマークルックアップのデッドロック
ブックマークルックアップは、SQLServerでよく見られるデッドロックです。これは、selectステートメントとDML(挿入、更新、および削除)ステートメントの間の競合が原因で発生します。通常、SQL Serverはselectステートメントをデッドロックの犠牲者として選択します。これは、データの変更を引き起こさず、ロールバックが迅速であるためです。ブックマークの検索を回避するために、カバーするインデックスを使用できます。 selectステートメントでNOLOCKクエリヒントを使用することもできますが、コミットされていないデータを読み取ります。
2)範囲スキャンのデッドロック
サーバーレベルまたはセッションレベルでSERIALIZABLE分離レベルを使用する場合があります。これは同時実行制御の制限的な分離レベルであり、ページまたは行レベルのロックの代わりに範囲スキャンロックを作成できます。 SERIALIZABLE分離レベルでは、データが変更されているがトランザクションでコミットされるのを待っている場合、ユーザーはデータを読み取ることができません。同様に、トランザクションがデータを読み取る場合、別のトランザクションはそれを変更できません。同時実行性が最も低いため、特定のアプリケーション要件でこの分離レベルを使用する必要があります。
3)カスケード制約のデッドロック
SQL Serverは、外部キー制約を使用してテーブル間の親子関係を使用します。このシナリオでは、親テーブルからレコードを更新または削除する場合、孤立したレコードを防ぐために子テーブルに必要なロックが必要です。これらのデッドロックを解消するには、常に最初に子テーブルのデータを変更し、次に親データを変更する必要があります。 DELETECASCADEまたはUPDATECASCADEオプションを使用して、親テーブルを直接操作することもできます。また、外部キー列に適切なインデックスを作成する必要があります。
4)クエリ内の並列処理のデッドロック
ユーザーがSQLクエリエンジンにクエリを送信すると、クエリオプティマイザは最適化された実行プランを作成します。クエリのコスト、最大並列度(MAXDOP)、および並列処理のコストしきい値に応じて、シリアルまたは並列の順序でクエリを実行できます。
並列処理モードでは、SQLServerは複数のスレッドを割り当てます。並列処理モードの大規模なクエリの場合、これらのスレッドは互いにブロックし始めることがあります。最終的には、デッドロックに変換されます。この場合、実行プランと、並列処理構成のMAXDOPおよびコストのしきい値を確認する必要があります。デッドロックシナリオのトラブルシューティングを行うために、セッションレベルでMAXDOPを指定することもできます。
5)オブジェクトの順序のデッドロックを逆にする
このタイプのデッドロックでは、複数のトランザクションがT-SQLで異なる順序でオブジェクトにアクセスします。これにより、各セッションのリソースがブロックされ、デッドロックに変換されます。デッドロック状態を引き起こさないように、常に論理的な順序でオブジェクトにアクセスする必要があります。
SQLServerのデッドロックを回避および最小化するための便利な方法
- トランザクションを短くするようにしてください。これにより、トランザクションで長期間ロックを保持することを回避できます。
- 複数のトランザクションで同様の論理的な方法でオブジェクトにアクセスします。
- デッドロックの可能性を減らすために、カバーインデックスを作成します。
- 外部キー列に一致するインデックスを作成します。このようにして、カスケード参照整合性によるデッドロックを排除できます。
- SETDEADLOCK_PRIORITYセッション変数を使用してデッドロックの優先順位を設定します。デッドロックの優先度を設定すると、SQLServerはデッドロックの優先度が最も低いセッションを強制終了します。
- try-catchブロックを使用してエラー処理を利用します。デッドロックエラーをトラップし、デッドロックの犠牲者が発生した場合にトランザクションを再実行できます。
- 分離レベルをREADCOMMITTEDSNAPSHOTISOLATIONまたはSNAPSHOTISOLATIONに変更します。これにより、SQLServerのロックメカニズムが変更されます。ただし、他のクエリに悪影響を与える可能性があるため、分離レベルの変更には注意が必要です。
SQLServerのデッドロックに関する考慮事項
デッドロックは、セッションがロックを保持して他のリソースを待機することを回避するためのSQLServerの自然なメカニズムです。デッドロッククエリをキャプチャし、それらが互いに競合しないように最適化する必要があります。他のクエリが効果的に使用できるように、ロックを短期間キャプチャして解放することが重要です。
SQL Serverのデッドロックが発生します。SQLServerはデッドロックの状況を内部的に処理しますが、可能な限りデッドロックを最小限に抑えるようにしてください。デッドロックを解消するための最良の方法のいくつかは、インデックスを作成するか、アプリケーションコードの変更を適用するか、デッドロックグラフのリソースを注意深く調べることです。 SQLデッドロックを回避するためのその他のヒントについては、投稿「クエリチューニングによるSQLデッドロックの回避」をご覧ください。