ORA-06512 の意味と読み方|PL/SQL エラースタックから本当の原因と発生行を特定する

ストアドプロシージャを呼び出したバッチが異常終了し、ログに ORA-06512 が何行も並んでいる。
先頭には別のエラー番号もあり、どの行を見て調査を始めればよいのか分からない…。

ORA-06512 は、それ自体が原因を持つエラーではありません。
本当のエラーが PL/SQL のどの行で起きたかを示す、位置情報の行です。

今回は、Oracle Database 19c の実機で ORA-06512 が積み上がる様子を再現し、読み方の型を整理します。
具体的には、①複数行のうちどの行が発生点か ②「行n」はどこから数えた行番号か ③位置情報が消える条件と DBMS_UTILITY.FORMAT_ERROR_BACKTRACE による対策、の3点を実機ログで確認します。


1. 症状

PL/SQL の実行中にエラーが起きると、次のような複数行の出力が返ります。
これは後述の検証環境で実際に再現した出力から、エラースタックの部分を抜粋したものです。

ORA-01476: 除数がゼロです
ORA-06512: "TS06512.PROC_INNER", 行4
ORA-06512: "TS06512.PROC_MIDDLE", 行3
ORA-06512: "TS06512.PROC_OUTER", 行3
ORA-06512: 行1

ORA-06512 が4行も並び、それぞれ別のオブジェクト名と行番号が付いています。
さらに、最後の行のようにオブジェクト名のない ORA-06512: 行1 という形もあります。

この積み上がった出力全体を、エラースタック(エラーの発生位置と呼び出し経路を積み上げた複数行の出力)と呼びます。
どの行が本当の原因で、どの行番号を見ればよいのかが分からないと、調査の入口で止まってしまいます。
※先頭のゼロを省いて ORA-6512 と表記されることもありますが、同じエラーを指します。


2. 原因の仕組み

ORA-06512 でエラーの対処法を検索しても、答えは見つかりません。
なぜなら、ORA-06512 は「何が起きたか」ではなく「どこで起きたか」を示す行だからです。

PL/SQL の実行中にエラーが発生すると、Oracle はまず本当のエラー(冒頭の例では ORA-01476)を出力します。
それに続けて、「そのエラーが発生した行」と「そこへ至る呼び出し経路」を、ORA-06512 の行として1段ずつ積み上げます。

冒頭のエラースタックを分解すると、各行の意味は次のとおりです。

ORA-01476: 除数がゼロです                  ← 本当のエラー(原因はこれ)
ORA-06512: "TS06512.PROC_INNER", 行4    ← 発生点(PROC_INNER の4行目)
ORA-06512: "TS06512.PROC_MIDDLE", 行3   ← PROC_INNER を呼び出した場所
ORA-06512: "TS06512.PROC_OUTER", 行3    ← PROC_MIDDLE を呼び出した場所
ORA-06512: 行1                           ← 入口(名前のない PL/SQL ブロック)

つまり、調べるべきは ORA-06512 の行ではなく、スタックの先頭にあるエラー番号(この例では ORA-01476)です
ORA-06512 の各行は、そのエラーの発生位置を特定するための情報として使います。

読み方の型は次の3つに集約されます。

#読み方の型見る場所
1原因は ORA-06512 ではなく、スタック先頭のエラー番号1行目(ORA-01476 等)
2発生点は ORA-06512 の最上段。下へ行くほど呼び出し元ORA-06512 の並び順
3オブジェクト名のない行は無名ブロック(名前を付けずにその場で実行する PL/SQL ブロック)ORA-06512: 行n の形

次の章から、この型を実機の再現で1つずつ確認します。


3. エラースタックの読み方を実機で確認する

3-1. 検証環境

検証は次の環境で行いました。

  • Oracle Database 19c Enterprise Edition・非CDB構成(バージョンは 19.28。四半期ごとの更新パッチ=RU の適用後番号です)
  • Oracle Linux 8・SQL*Plus 19c(エラーメッセージは日本語表示の設定)

検証用に、CREATE SESSION と CREATE PROCEDURE のみを付与したユーザー TS06512 を作成し、すべての実験をこのスキーマ内で完結させています。
本当のエラーは全実験で ORA-01476(ゼロ除算)に統一しました。
v_num := 1 / 0; という1行で確実に発生させられるため、スタックの形の違いだけを比較できます。

3-2. スタックは上から読む(プロシージャの3階層呼び出しで再現)

まず、ORA-06512 が複数行積まれる状況を最小構成で作ります。
プロシージャを3つ用意するのは、複数行のスタックを再現するためです。
呼び出しの階層が1段では、スタックが積み重ならず、読み順に迷う状況になりません。

発生点となる PROC_INNER(4行目でゼロ除算)と、それを呼ぶ PROC_MIDDLE、さらにそれを呼ぶ PROC_OUTER を作成します。

CREATE OR REPLACE PROCEDURE proc_inner IS
  v_num NUMBER;
BEGIN
  v_num := 1 / 0;
END;
/

CREATE OR REPLACE PROCEDURE proc_middle IS
BEGIN
  proc_inner;
END;
/

CREATE OR REPLACE PROCEDURE proc_outer IS
BEGIN
  proc_middle;
END;
/

入口の PROC_OUTER を実行すると、呼び出しは PROC_OUTER → PROC_MIDDLE → PROC_INNER の順に進み、最も深い PROC_INNER の4行目でエラーが発生します。

SQL> EXEC proc_outer
BEGIN proc_outer; END;

*
行1でエラーが発生しました。:
ORA-01476: 除数がゼロです
ORA-06512: "TS06512.PROC_INNER", 行4
ORA-06512: "TS06512.PROC_MIDDLE", 行3
ORA-06512: "TS06512.PROC_OUTER", 行3
ORA-06512: 行1

出力を上から読むと、発生点(PROC_INNER の4行目)→ その呼び出し元(PROC_MIDDLE の3行目)→ さらにその呼び出し元(PROC_OUTER の3行目)→ 入口、という並びです。
実行された順序(PROC_OUTER が最初)とは逆に、最も深い場所が最上段に来ます。
つまり、ORA-06512 の最上段が実際の発生点で、下へ行くほど呼び出し元です

なお、作成したプロシージャは3つなのに、ORA-06512 は4行出ています。
最後の ORA-06512: 行1 は、SQL*Plus の EXEC コマンドが内部で生成した無名ブロック BEGIN proc_outer; END; の分です。
出力の1行目にこの無名ブロックがそのまま表示されていることからも確認できます。
オブジェクト名のない行の意味は、後述の「オブジェクト名のない ORA-06512 は無名ブロック」の節で改めて整理します。

3-3. 「行n」はどこから数えるか(ソースとの突き合わせ)

次に、スタックに出た「行4」が PROC_INNER のソースのどこを指すのかを突き合わせます。
ストアドプロシージャのソースは、データディクショナリの USER_SOURCE ビュー(自分のスキーマのオブジェクトのソースを行番号付きで保持するビュー)で行番号ごと確認できます。

表示が縦に折り返されないよう、COLUMN コマンドで列幅を設定してから照会します。

SQL> COLUMN line FORMAT 99999
SQL> COLUMN text FORMAT a40
SQL> SELECT line, text FROM user_source WHERE name = 'PROC_INNER' ORDER BY line;

  LINE TEXT
------ ----------------------------------------
     1 PROCEDURE proc_inner IS
     2   v_num NUMBER;
     3 BEGIN
     4   v_num := 1 / 0;
     5 END;

LINE 4 が v_num := 1 / 0;、つまりエラースタックの「行4」と一致しました。
ORA-06512 の行番号の正体は、この USER_SOURCE(他スキーマなら DBA_SOURCE)の LINE 値です。

ここで注意したいのは、LINE 1 が PROCEDURE proc_inner IS から始まっている点です。
作成時に打った CREATE OR REPLACE は、行数に数えられていません。
つまり、行番号は CREATE OR REPLACE を数えず、PROCEDURE(または FUNCTION 等)の行を1行目として数えます
手元のスクリプトファイルを開いて CREATE 文の行から数えると1行ずれるため、ソースとの突き合わせは USER_SOURCE / DBA_SOURCE で行うのが確実です。

3-4. オブジェクト名のない ORA-06512 は無名ブロック

一方、無名ブロックでエラーが起きた場合はどうなるでしょうか。
DECLARE から始まる無名ブロックで、同じゼロ除算を発生させます。

DECLARE
  v_num NUMBER;
BEGIN
  v_num := 1 / 0;
END;
/
DECLARE
*
行1でエラーが発生しました。:
ORA-01476: 除数がゼロです
ORA-06512: 行4

ストアドプロシージャと異なり、オブジェクト名が付かず ORA-06512: 行4 だけの形になります。
行番号は、ブロックの先頭(この例では DECLARE の行)を1行目として数えた値です。
DECLARE が1行目・v_num := 1 / 0; が4行目なので、「行4」と一致しています。

この違いから、次の切り分けができます。

ORA-06512 の形発生場所ソースの確認先
"スキーマ.オブジェクト名", 行nストアドプロシージャ等のオブジェクト内USER_SOURCE / DBA_SOURCE の LINE 値
行n のみ(名前なし)無名ブロック内ブロックを送った側(アプリケーション等)のソース

アプリケーションが JDBC 等から実行する PL/SQL も、多くは無名ブロックの形で送られます。
オブジェクト名のない ORA-06512 を見たら、データベース内のオブジェクトではなく、アプリケーション側が発行した PL/SQL を確認します。


4. 落とし穴:例外処理部(EXCEPTION)を通ると位置情報はどうなるか

ここまでの実験のコードには、例外処理部(キーワード EXCEPTION で始まる、例外を処理する部分)がありませんでした。
例外処理部がない場合、発生したエラーは呼び出し元へ自動的に伝わっていきます。
プロシージャを3階層で呼び出した実験で ORA-06512 が1段ずつ積まれたのは、この伝わっていく通り道がそのまま記録されたためです。

一方、例外処理部を書くと、エラーはそこでいったん受け止められ、その時点で「処理済み」の扱いになります。
実務のコードでは、こうして受けた例外のログを書いてから投げ直したり、独自のエラー番号に置き換えたりする書き方が一般的です。
そして、そのまま投げ直す(RAISE)か、別のエラーに置き換えて投げる(RAISE_APPLICATION_ERROR)かの違いで、エラースタックに残る位置情報が変わります。

この章では、この2つの書き方を実機で比較します。
結論を先に言うと、19c 実機での線引きは「RAISE では元の発生行がスタックに残る・RAISE_APPLICATION_ERROR では元のエラー番号も発生行も消える」でした。

なお、例外を受けたまま何も投げ直さない書き方(WHEN OTHERS THEN NULL など。俗にいう例外の「握りつぶし」)では、エラーそのものが呼び出し元へ伝わらなくなります。
その場合はエラースタックの読み方以前の問題になるため、この章では扱いません。

4-1. RAISE(同じエラーの投げ直し)では発生行は消えない

まず、発生した例外をそのまま投げ直す RAISE の場合です。
4行目でゼロ除算が発生し、例外処理部の中の7行目で投げ直すプロシージャを作ります。

CREATE OR REPLACE PROCEDURE proc_swallow_before IS
  v_num NUMBER;
BEGIN
  v_num := 1 / 0;
EXCEPTION
  WHEN OTHERS THEN
    RAISE;
END;
/
SQL> EXEC proc_swallow_before
BEGIN proc_swallow_before; END;

*
行1でエラーが発生しました。:
ORA-01476: 除数がゼロです
ORA-06512: "TS06512.PROC_SWALLOW_BEFORE", 行7
ORA-06512: "TS06512.PROC_SWALLOW_BEFORE", 行4
ORA-06512: 行1

最上段は、発生点ではなく投げ直した RAISE; の行7 を指しています。
「最上段=発生点」とだけ覚えていると、ここで行7 のソース(RAISE と書いてあるだけの行)に誘導されてしまいます。

ただし、その1つ下を見ると、本当の発生行である行4 がスタックに残っています。

実行の流れを追うと、行4 でゼロ除算が発生 → 例外処理部がその ORA-01476 を受け取る → 行7 の RAISE; が同じエラーを呼び出し元へ投げ直す、という順です。
RAISE; が行4 の計算をやり直すわけではありません。
スタックに並んだ同名の2行は、この「同じ1つのエラーが通った2つの地点」を示しています。
同じプロシージャ名が2行並んだら、上が投げ直した行・下が元の発生行です。
例外処理部を通っていても、スタックを下まで読めば発生行にたどり着けます。

4-2. RAISE_APPLICATION_ERROR(別エラーへの置き換え)では番号も行も消える

一方、発生した例外を独自のエラー番号(ORA-20000〜ORA-20999)に置き換える RAISE_APPLICATION_ERROR の場合です。
発生行は同じ4行目、例外処理部の中の7行目で ORA-20001 に置き換えます。

CREATE OR REPLACE PROCEDURE proc_raise_app IS
  v_num NUMBER;
BEGIN
  v_num := 1 / 0;
EXCEPTION
  WHEN OTHERS THEN
    RAISE_APPLICATION_ERROR(-20001, 'ゼロ除算を検出しました');
END;
/
SQL> EXEC proc_raise_app
BEGIN proc_raise_app; END;

*
行1でエラーが発生しました。:
ORA-20001: ゼロ除算を検出しました ORA-06512:
"TS06512.PROC_RAISE_APP", 行7
ORA-06512: 行1

※出力は端末の折り返しをそのまま載せています。

今度は、元のエラー番号(ORA-01476)も発生行(行4)も、出力のどこにも残っていません。
スタックに残るのは、RAISE_APPLICATION_ERROR を書いた行7 だけです。

RAISE との違いは、「同じエラーを投げ直す」か「別のエラーに置き換える」かです。
別のエラーに置き換えた時点で、元のエラーの番号と位置情報はエラースタックから失われます。
スタックに発生行が見当たらないときは、途中の例外処理部で別のエラーに置き換えられていることを疑います

この検証のプロシージャは5行程度なので、発生行が消えても目視で追えます。
しかし、実務の数百行のプロシージャで発生行が消えると、「この中のどこかでエラーが起きた」以上のことが分からず、調査の起点がなくなります。
また、WHEN OTHERS はどの種類のエラーでも受けるため、仮に行4 で起きたのがゼロ除算以外のエラーだったとしても、出力されるメッセージは「ゼロ除算を検出しました」のままです。
元のエラー番号が消えるということは、置き換え後のメッセージが実際の原因と一致している保証もなくなる、ということです。

4-3. 対策:DBMS_UTILITY.FORMAT_ERROR_BACKTRACE で消える前に記録する

それでは、RAISE_APPLICATION_ERROR で置き換えつつ、元の情報も失わないようにするにはどうすればよいでしょうか。
消えるのは「最終的なエラースタック」であって、例外処理部の中ではまだ元の情報を参照できます。
そこで使うのが、DBMS_UTILITY パッケージの2つのファンクションです。

ファンクション取得できる情報
DBMS_UTILITY.FORMAT_ERROR_STACK元のエラー番号とメッセージ(ORA-01476: 除数がゼロです)
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE元の発生行までのスタック(ORA-06512: … 行4)

先ほどの proc_raise_app と同じ構造の例外処理部に、この2つを仕込みます。
検証では出力先を DBMS_OUTPUT にしていますが、実務ではログテーブルへの INSERT 等に置き換えます。

CREATE OR REPLACE PROCEDURE proc_raise_app_after IS
  v_num NUMBER;
BEGIN
  v_num := 1 / 0;
EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_STACK);
    DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
    RAISE_APPLICATION_ERROR(-20001, 'ゼロ除算を検出しました');
END;
/
SQL> SET SERVEROUTPUT ON
SQL> EXEC proc_raise_app_after
ORA-01476: 除数がゼロです

ORA-06512: "TS06512.PROC_RAISE_APP_AFTER", 行4

BEGIN proc_raise_app_after; END;

*
行1でエラーが発生しました。:
ORA-20001: ゼロ除算を検出しました ORA-06512:
"TS06512.PROC_RAISE_APP_AFTER", 行9
ORA-06512: 行1

最終的なエラースタック(後半)は、先ほどと同様に ORA-20001 と行9 だけで、元の情報は消えたままです。
しかし、その前の DBMS_OUTPUT には、FORMAT_ERROR_STACK で取得した元のエラー番号(ORA-01476)と、FORMAT_ERROR_BACKTRACE で取得した元の発生行(行4)が出力されています。

最終スタックでは消えるからこそ、例外処理部の中で拾って記録に残す、というのがこの対策の位置づけです。
RAISE_APPLICATION_ERROR で置き換えるコードには、FORMAT_ERROR_BACKTRACE の記録をセットで入れておくと、後からの調査で発生行に直行できます。

この章で取り上げた3つの書き方と、残る情報の対応は次のとおりです。

例外処理部の書き方エラーが起きた事実元のエラー番号元の発生行
何も投げ直さない(WHEN OTHERS THEN NULL)呼び出し元へ伝わらない(正常終了に見える)残らない残らない(スタック自体が出ない)
RAISE で投げ直す伝わる(ORA-01476 のまま)残る残る
RAISE_APPLICATION_ERROR で置き換える伝わる(ORA-20001 として)消える消える(対策=FORMAT_ERROR_BACKTRACE)

5. よくある Q&A

Q1. ORA-06512 そのものを解消する対処法はありますか?

ありません。
ORA-06512 は位置情報の行であり、修正の対象はスタック先頭のエラー番号(ORA-01476 等)です。
先頭のエラーを解消すれば、位置情報である ORA-06512 も出力されなくなります。

Q2. 本番環境のコードは変更できません。FORMAT_ERROR_BACKTRACE を仕込まずに発生行を特定できますか?

エラースタックに発生行が残っていれば、コードを変更しなくても特定できます。
スタックの行番号を、DBA_SOURCE への SELECT で照合します(SELECT できる権限があれば実行できます)。

SELECT line, text FROM dba_source
WHERE owner = 'スキーマ名' AND name = 'オブジェクト名'
ORDER BY line;

行数の多いオブジェクトでは、WHERE 句に行番号の条件を足して、発生行の前後だけに絞ると確認しやすくなります。
例えば ORA-06512 に「行214」と出ていれば、上のクエリに AND line BETWEEN 209 AND 219 を追加して、前後5行ずつだけを表示します。

一方、RAISE_APPLICATION_ERROR で置き換えられて発生行自体が消えている場合、後追いでの特定はできません。
この場合は、同じエラーを開発環境で再現して FORMAT_ERROR_BACKTRACE を仕込むのが基本です。
なお、コードを変更せずにサーバー側でエラー情報を取得する仕組みとしては、AFTER SERVERERROR トリガー(データベースでエラーが発生したときに発動するトリガー)があります。

Q3. 古い解説記事には「RAISE で投げ直すと発生行が失われる」とありますが、この記事の結果と異なります。どちらが正しいですか?

バージョンによる挙動の違いです。
古いバージョンでは、投げ直すとエラースタックが RAISE の行だけを指し、元の発生行が失われる挙動が知られていました。
DBMS_UTILITY.FORMAT_ERROR_BACKTRACE が 10g で追加された背景も、この「投げ直すと発生行が失われる」問題への対処です。

一方、本記事の 19c での検証では、「RAISE(同じエラーの投げ直し)では発生行は消えない」の節で見たとおり、RAISE で投げ直した後も元の発生行はスタックに残りました。
発生行が消えたのは、RAISE_APPLICATION_ERROR で別のエラーに置き換えた場合だけです。

古い解説を前提に「例外処理部を通ったらもう発生行は分からない」と諦める必要はありません。
本記事の実験はコピー&ペーストで数分で再現できるので、お使いのバージョンでの挙動は実機での確認をおすすめします。

Q4. RAISE_APPLICATION_ERROR は使わないほうがよいのでしょうか?

そうではありません。
RAISE_APPLICATION_ERROR の本来の主な用途は、実行部の業務チェックから業務上のエラー(在庫が足りない等)を作って、呼び出し元へ伝えることです。
ORA-20000〜ORA-20999 は、そのために利用者へ予約された番号帯です。
この使い方では、置き換える「元のエラー」がそもそも存在しないため、位置情報の消失も起きません。

注意が必要なのは、例外処理部で受けた技術的なエラーを別のエラーに置き換える使い方(本記事の実験の形)です。
この場合は、「対策:DBMS_UTILITY.FORMAT_ERROR_BACKTRACE で消える前に記録する」の節のとおり、FORMAT_ERROR_BACKTRACE の記録をセットで入れます。

Q5. アプリケーション(Java 等)から実行した場合も、このエラースタックは見えますか?

見えます。
SQL*Plus は届いたエラー情報を画面に表示しているだけで、データベースがクライアントへ返す中身(エラー番号・メッセージ・ORA-06512 のスタック)は、どのクライアントでも同じです。
アプリケーションはこれをデータベースエラーの例外として受け取るため、エラースタックがログに残るかどうかは、アプリケーション側がそれをログに出力しているかどうかで決まります。

本記事冒頭の「バッチのログに ORA-06512 が並んでいる」という状況は、アプリケーションが受け取ったエラーメッセージをそのままログに書き出している形です。
逆に、アプリケーション側で独自の文言に置き換えてログに出していると、データベース側の例外処理部で起きるのと同じ形で、スタックの情報が失われます。

Q6. エラーメッセージが英語で表示されます。読み方は同じですか?

同じです。
メッセージの表示言語はクライアントの NLS_LANG 設定で決まり、本記事の環境では日本語で表示されています。
エラー番号・オブジェクト名・行番号の構造は言語によらず共通なので、読み方の型はそのまま使えます。


6. まとめ

ORA-06512 の読み方の型を整理します。

  1. ORA-06512 自体は修正の対象ではない。 原因はスタック先頭のエラー番号(ORA-01476 等)
  2. スタックは上から読む。 ORA-06512 の最上段が発生点、下へ行くほど呼び出し元
  3. 行番号の正体は USER_SOURCE / DBA_SOURCE の LINE 値。 CREATE OR REPLACE は数えない。無名ブロックはブロック先頭から数え、オブジェクト名が付かない
  4. RAISE の投げ直しでは発生行は消えない(19c 実機で確認。同じ名前が2行並んだら下が元の発生行)。消えるのは RAISE_APPLICATION_ERROR で別エラーに置き換えたとき
  5. 置き換えるなら FORMAT_ERROR_BACKTRACE をセットで。 最終スタックで消える情報は、例外処理部の中で記録して残す

調査の流れをフローにすると次のとおりです。

ORA-06512 を含むエラースタックを見たら
 │
 ├─ 原因は? → スタック先頭のエラー番号を見る(ORA-06512 ではない)
 │
 ├─ 場所は? → ORA-06512 の最上段が発生点
 │    ├─ オブジェクト名あり → USER_SOURCE / DBA_SOURCE の LINE 値と照合
 │    └─ オブジェクト名なし → 無名ブロック(アプリ側が発行した PL/SQL を確認)
 │
 └─ 発生行が見当たらない?
      → 例外処理部での置き換え(RAISE_APPLICATION_ERROR)を疑う
      → 対策は例外処理部の中での FORMAT_ERROR_BACKTRACE の記録

ORA-06512 が読めるようになると、エラースタックは意味の分からない行の羅列ではなく、発生行までの道順を示す出力として使えるようになります。