【Oracle】デッドロック(ORA-00060)の原因と解決方法完全ガイド|検出・調査・予防策まで解説

【Oracle】デッドロック(ORA-00060)の原因と解決方法完全ガイド|検出・調査・予防策まで解説 Oracle

本番環境で突然 ORA-00060: deadlock detected while waiting for resource が発生すると、焦って対応しがちです。しかしOracleのデッドロックは自動で検出・解消される仕組みがあるため、冷静に原因を特定し再発防止策を打つことが重要です。本記事ではデッドロックの発生メカニズムから、トレースファイルの読み方、ロック調査SQL、予防策まで体系的に解説します。

この記事でわかること

  • デッドロックの発生メカニズム(なぜ2つのセッションが互いに待ち合うのか)
  • ORA-00060 発生時の Oracle の自動検出・ロールバック動作
  • トレースファイル(.trc)の場所と読み方
  • V$LOCK / V$SESSION / DBA_BLOCKERS を使ったロック調査SQL
  • 行ロックデッドロックと ITL デッドロックの違い
  • デッドロックを予防する設計パターン
  • SELECT FOR UPDATE と NOWAIT / WAIT / SKIP LOCKED の使い分け
スポンサーリンク

デッドロックの発生メカニズム

デッドロックは、2つ以上のセッションが互いに相手が保持しているロックを待つ状態です。どちらも進めず、どちらも引かないため、永遠に待ち続けることになります。

最も典型的な発生パターンを図示します。

セッションA                  セッションB
─────────                    ─────────
UPDATE orders SET ... WHERE order_id = 1;   (orders の行1 をロック)
                             UPDATE orders SET ... WHERE order_id = 2;   (orders の行2 をロック)
UPDATE orders SET ... WHERE order_id = 2;   → セッションBのロック解放を待機...
                             UPDATE orders SET ... WHERE order_id = 1;   → セッションAのロック解放を待機...

→ 両者が互いのロック解放を待つ → デッドロック発生

ORA-00060 発生時の Oracle の動作

Oracleにはデッドロック検出機構が内蔵されており、デッドロックを自動的に検知します。検出後の動作は次のとおりです。

  1. デッドロックを検出する(通常3秒以内)
  2. 一方のセッションの最後に実行したSQL文だけをロールバックする(トランザクション全体ではない)
  3. ロールバックされたセッションに ORA-00060 を返す
  4. もう一方のセッションはロック解放を受けて処理を続行する
  5. トレースファイル(.trc)にデッドロックグラフを出力する
重要:ORA-00060 はトランザクション全体をロールバックしません。最後のSQL文のみがロールバックされます。アプリケーション側でORA-00060を検知したら、そのトランザクションをどうするか(リトライ/ロールバック)を明示的に決める必要があります。

トレースファイルの場所と読み方

デッドロック発生時、Oracleはバックグラウンドで自動的にトレースファイルを生成します。このファイルにはデッドロックグラフが含まれており、原因の特定に不可欠です。

トレースファイルの場所を確認する

トレースファイルの出力先を確認する
-- ADR(Automatic Diagnostic Repository)のパスを確認
SELECT value FROM v$diag_info WHERE name = 'Diag Trace';
-- 例: /u01/app/oracle/diag/rdbms/orcl/orcl/trace/

-- 最近のトレースファイルをOSコマンドで検索
-- ls -lt /u01/app/oracle/diag/rdbms/orcl/orcl/trace/*.trc | head

デッドロックグラフの読み方

トレースファイルを開くと次のような「Deadlock Graph」セクションがあります。

DEADLOCK DETECTED ( ORA-00060 )

[Transaction Deadlock]

The following deadlock is not an ORACLE error. It is a
temporary situation that will automatically resolve itself.

Deadlock graph:
                       ---------Blocker(s)--------  ---------Waiter(s)---------
Resource Name          process session holds waits  process session holds waits
TX-00070012-0000042f        22     145     X             25     203           X
TX-000a0008-00000531        25     203     X             22     145           X

session 145: DID 0001-0016-00000003
session 203: DID 0001-0019-00000005

Rows waited on:
  Session 145: obj - rowid = 000124AB - AAABSrAAFAAAACjAAA
    (dictionary objn - 74923, file - 5, block - 169, slot - 0)
  Session 203: obj - rowid = 000124AB - AAABSrAAFAAAACjAAB
    (dictionary objn - 74923, file - 5, block - 169, slot - 1)
項目 意味
Resource Name (TX-…) トランザクションロックのリソースID
session セッションID(V$SESSIONのSID)
holds X 排他ロック(X)を保持している
waits X 排他ロック(X)を待機している
Rows waited on 待機している行のROWIDとオブジェクトID
ROWIDから対象テーブルと行を特定する
-- オブジェクトIDからテーブル名を特定する
SELECT owner, object_name, object_type
FROM dba_objects
WHERE object_id = 74923;
-- 例: HR.ORDERS

-- ROWIDからレコードの内容を確認する
SELECT * FROM hr.orders
WHERE ROWID = 'AAABSrAAFAAAACjAAA';

ロック調査SQL

デッドロック発生中、またはロック待ちが発生している状況でリアルタイムに調査するためのSQLです。

ロック待ちが発生しているセッションを一覧する
SELECT
    w.sid           AS waiting_sid,
    w.serial#       AS waiting_serial,
    w.username      AS waiting_user,
    w.sql_id        AS waiting_sql_id,
    w.event         AS wait_event,
    b.sid           AS blocking_sid,
    b.serial#       AS blocking_serial,
    b.username      AS blocking_user,
    b.sql_id        AS blocking_sql_id
FROM v$session w
JOIN v$session b ON w.blocking_session = b.sid
WHERE w.blocking_session IS NOT NULL
ORDER BY w.sid;
V$LOCK でロックの種類と状態を確認する
SELECT
    s.sid,
    s.serial#,
    s.username,
    l.type,         -- TX=トランザクションロック, TM=テーブルロック
    l.id1,
    l.id2,
    DECODE(l.lmode, 0, 'None', 1, 'Null', 2, 'Row-S', 3, 'Row-X',
                    4, 'Share', 5, 'S/Row-X', 6, 'Exclusive') AS lock_mode,
    DECODE(l.request, 0, 'None', 1, 'Null', 2, 'Row-S', 3, 'Row-X',
                      4, 'Share', 5, 'S/Row-X', 6, 'Exclusive') AS request_mode,
    l.block         -- 1=他セッションをブロック中, 0=ブロックしていない
FROM v$lock l
JOIN v$session s ON l.sid = s.sid
WHERE l.type IN ('TX', 'TM')
ORDER BY l.type, s.sid;
ロック中のオブジェクト(テーブル)を特定する
SELECT
    s.sid,
    s.username,
    o.object_name,
    o.object_type,
    DECODE(l.locked_mode, 2, 'Row-S', 3, 'Row-X', 4, 'Share',
                          5, 'S/Row-X', 6, 'Exclusive') AS locked_mode
FROM v$locked_object l
JOIN dba_objects o ON l.object_id = o.object_id
JOIN v$session s ON l.session_id = s.sid
ORDER BY o.object_name, s.sid;

セッション確認・強制切断の詳しい方法は「【Oracle】セッションの確認・強制切断方法完全ガイド」を参照してください。

デッドロックの種類

行ロックデッドロック(TX-TX)

最も一般的なデッドロックです。2つのセッションが異なる行を異なる順序で更新しようとして発生します。上述の発生メカニズムの例がこれに該当します。

自己参照デッドロック

外部キー制約のあるテーブルで、親テーブルと子テーブルを異なる順序で更新した場合に発生することがあります。OracleはFKの参照先(親テーブル)のロックも暗黙的に取得するため、明示的なUPDATEでは見えにくいデッドロックが起きます。

ITLデッドロック

ITL(Interested Transaction List)はブロックヘッダ内のトランザクション管理領域です。同一ブロック内で多数のトランザクションが同時にDMLを実行すると、ITLスロットが不足して待ちが発生し、まれにデッドロックに至ります。INITRANS の値を増やすことで回避できます。

ITLデッドロックの対策 ── INITRANS を増やす
-- テーブルの INITRANS を増やす(デフォルトは 1 〜 2)
ALTER TABLE orders INITRANS 10;

-- インデックスの INITRANS を増やす
ALTER INDEX idx_orders_date INITRANS 10;

ビットマップインデックスデッドロック

ビットマップインデックスはインデックスエントリが行範囲単位でロックされるため、通常のB*Treeインデックスよりもデッドロックが発生しやすくなります。OLTP(頻繁な更新がある)テーブルではビットマップインデックスの使用は推奨されません。

デッドロックを予防する設計パターン

テーブル・行の更新順序をルール化する

デッドロックの最も効果的な予防策は、全てのトランザクションでテーブルと行の更新順序を統一することです。

更新順序の統一 ── デッドロック予防の基本
-- ルール: orders → order_items の順で更新する(全プログラムで統一)
-- BAD: プログラムAは orders→items、プログラムBは items→orders の順序(デッドロックの原因)
-- GOOD: 全プログラムで orders→items の順序に統一

-- 同一テーブル内でも主キー昇順で更新する
UPDATE orders SET status = 'shipped' WHERE order_id = 1;
UPDATE orders SET status = 'shipped' WHERE order_id = 2;
-- 全セッションが order_id 昇順で更新すればデッドロックは起きない

トランザクションを短く保つ

ロック保持時間が長いほどデッドロックの発生確率は上がります。トランザクション内で不要な処理(ファイルI/O・外部API呼び出し等)を挟まないようにしましょう。

SELECT FOR UPDATE で明示ロックする

更新対象の行を事前にロックしてから処理することで、ロック取得順序を明確にできます。

SELECT FOR UPDATE ── 事前ロックでデッドロックを回避する
-- 更新対象を事前にロックしてから UPDATE する
SELECT * FROM orders
WHERE order_id IN (1, 2)
ORDER BY order_id       -- ロック順序を主キー昇順に統一
FOR UPDATE NOWAIT;       -- ロックが取れなければ即座にエラー

-- ロック取得後に安全に UPDATE
UPDATE orders SET status = 'shipped' WHERE order_id = 1;
UPDATE orders SET status = 'shipped' WHERE order_id = 2;
COMMIT;

NOWAIT / WAIT / SKIP LOCKED の使い分け

オプション 動作 用途
FOR UPDATE NOWAIT ロックが取れなければ即座に ORA-00054 を返す 対話的アプリケーション(ユーザーに即通知)
FOR UPDATE WAIT 5 最大5秒間ロック取得を待つ。超えたら ORA-30006 短時間なら待ってよい場合
FOR UPDATE SKIP LOCKED ロック中の行をスキップして残りの行だけロックする キュー処理・並列ワーカー(ロック中の行は他ワーカーに任せる)

アプリケーション側のORA-00060対処パターン

PL/SQL でのデッドロックリトライ実装
DECLARE
    deadlock_detected EXCEPTION;
    PRAGMA EXCEPTION_INIT(deadlock_detected, -60);  -- ORA-00060
    v_max_retry CONSTANT PLS_INTEGER := 3;
BEGIN
    FOR i IN 1..v_max_retry LOOP
        BEGIN
            -- デッドロックが起きやすい処理
            UPDATE orders SET status = 'processing' WHERE order_id = :id;
            COMMIT;
            EXIT;  -- 成功したらループを抜ける
        EXCEPTION
            WHEN deadlock_detected THEN
                ROLLBACK;
                IF i = v_max_retry THEN
                    RAISE;  -- 最終リトライでも失敗したら呼び出し元にエラーを返す
                END IF;
                DBMS_SESSION.SLEEP(i);  -- 待機時間を段階的に増やす(1秒, 2秒, 3秒)
        END;
    END LOOP;
END;
/
リトライ時の注意
ORA-00060は最後のSQL文だけロールバックされますが、リトライ時はトランザクション全体を ROLLBACK してからやり直すのが安全です。部分的に成功した状態から再開すると、データの整合性が保証されません。

まとめ

ORA-00060(デッドロック)はOracleが自動検出して一方のセッションの最後のSQL文をロールバックすることで解消されます。致命的なエラーではありませんが、再発防止には根本原因の特定と設計の見直しが不可欠です。

  • デッドロックは2つ以上のセッションが互いのロック解放を待つ状態で発生する
  • Oracleは自動検出し、一方のセッションの最後のSQL文のみをロールバックする(トランザクション全体ではない)
  • トレースファイル(.trc)の「Deadlock Graph」セクションで、どのセッションがどのリソースを待っていたかを特定できる
  • V$LOCK / V$SESSION / V$LOCKED_OBJECT でリアルタイムのロック状況を調査する
  • 最も効果的な予防策はテーブル・行の更新順序を全プログラムで統一すること
  • SELECT FOR UPDATE + NOWAIT / WAIT / SKIP LOCKED で明示的にロックを取得し、競合を制御する
  • ITLデッドロックは INITRANS の値を増やすことで対策する
  • アプリケーション側ではORA-00060を検知してリトライする実装が必要