PL/SQLが「なんとなく遅い」とき、勘や憶測で改修すると別の処理が速くなって全体は変わらない、という空振り改善になりがちです。これを「どの行に何ミリ秒費やしたか」を1行単位で可視化して根拠を持って改修できるのがDBMS_PROFILERです。行レベルのプロファイリング情報を専用テーブルに記録し、SELECT文で集計してボトルネックを特定できます。
ただしDBMS_PROFILERは「とりあえず動かしてplsql_profiler_dataを眺める」だけでは意味のある分析ができません。「どのRUNを比較するか」「どのクエリで集計するか」「どの順序で改善するか」といった分析戦略があって初めて実装の改善に直結します。本記事ではこの分析クエリ集を中心に、実測ベースで体系的にボトルネックを潰す方法を解説します。
計測ツールとしてはDBMS_HPROF(階層プロファイラ)も併存しますが、本記事はDBMS_PROFILERに絞った実戦活用として、セットアップから分析クエリ7種、ビフォーアフターのケーススタディ、オーバーヘッド管理、CI/CDへの組み込み、DBMS_HPROFとの使い分け、アンチパターン6選、FAQまで2026年版で整理します。
この記事でわかること
- DBMS_PROFILERのセットアップ手順(profload.sql実行と権限付与)
- START_PROFILER/STOP_PROFILER/FLUSH_DATAの使い方
- 3つの結果テーブル(PLSQL_PROFILER_RUNS/UNITS/DATA)の構造
- ボトルネックを特定する7種の実用分析SQLクエリ
- 計測オーバーヘッド(5〜30%)の現実と本番運用の判断
- 改修前後を客観評価するベンチ手順とビフォーアフター事例
- DBMS_PROFILER vs DBMS_HPROFの使い分け基準
- CI/CD・夜間ジョブでの自動プロファイリング
- 本番で踏むアンチパターン6選
30秒でわかるDBMS_PROFILER活用の結論
忙しい読者向けの結論先出しです。
| 結論 | 理由・効果 |
|---|---|
| ① 行レベルの遅さ特定はDBMS_PROFILER | 関数間の呼び出し関係を見るならDBMS_HPROF |
| ② RUNIDで計測実行を識別して改修前後を比較 | RUN_COMMENTに「修正前」「修正後」を入れると分析が楽 |
| ③ 分析の主役はplsql_profiler_data | 行ごとのtotal_time/total_occur/min_time/max_timeで多角分析 |
| ④ 重い順TOP10で80%の時間を支配する行を特定 | アムダールの法則:上位を倒せば全体に響く |
| ⑤ 計測オーバーヘッド5〜30%を許容できる場面で使う | 本番ピーク時は避け、ステージングか深夜時間帯で実行 |
| ⑥ FLUSH_DATAで長時間ジョブの途中観察 | 計測中でも結果が部分的に書き込まれる |
| ⑦ 結果は専用ビューを作って再利用 | 同じ分析クエリを毎回書かず、view化して即時実行可能に |
セットアップ|profload.sqlと結果テーブル
DBMS_PROFILERを使うには、計測結果を格納する3つのテーブルを事前に作成する必要があります。Oracleが提供するスクリプトprofload.sqlとproftab.sqlを実行するだけです。
パッケージのインストール(DBA作業)
DBA権限で1回だけ実行します。パッケージのEXECUTE権限が公開されるので、プロファイリングを使いたいスキーマには別途権限付与は不要なケースが多いです。
-- ① DBA権限でパッケージをインストール(一度だけ) SQL> CONNECT / AS SYSDBA SQL> @?/rdbms/admin/profload.sql -- ② 結果格納テーブルを作成(プロファイリングを行うスキーマで実行) SQL> CONNECT app_user/password SQL> @?/rdbms/admin/proftab.sql -- 作成される3つの表 -- - PLSQL_PROFILER_RUNS : 計測実行(RUN)のメタ情報 -- - PLSQL_PROFILER_UNITS : RUN内で計測されたコンパイル単位(パッケージ等) -- - PLSQL_PROFILER_DATA : 行ごとの実行回数・時間(中心テーブル) -- ③ 各テーブルの主要カラム確認 DESC PLSQL_PROFILER_RUNS; -- RUNID NUMBER (PK) -- RELATED_RUN NUMBER -- RUN_OWNER VARCHAR2(32) -- RUN_DATE DATE -- RUN_COMMENT VARCHAR2(2047) -- RUN_TOTAL_TIME NUMBER DESC PLSQL_PROFILER_UNITS; -- RUNID NUMBER (FK) -- UNIT_NUMBER NUMBER -- UNIT_TYPE VARCHAR2(32) -- UNIT_OWNER VARCHAR2(32) -- UNIT_NAME VARCHAR2(32) -- UNIT_TIMESTAMP DATE -- TOTAL_TIME NUMBER DESC PLSQL_PROFILER_DATA; -- RUNID NUMBER (FK) -- UNIT_NUMBER NUMBER -- LINE# NUMBER -- ソースコード行番号 -- TOTAL_OCCUR NUMBER -- 実行回数 -- TOTAL_TIME NUMBER -- 累計時間(ナノ秒) -- MIN_TIME NUMBER -- 1回あたり最小時間 -- MAX_TIME NUMBER -- 1回あたり最大時間
セットアップは1スキーマあたり1回だけで済みます。結果テーブルは何度RUNしても再利用されるため、過去のRUN履歴を残しておけば「先月のバッチと比較」のような分析もできます。ストレージ消費が気になる場合は古いRUNを定期的にDELETE FROM plsql_profiler_runs WHERE run_date < SYSDATE-30;等で削除すれば良いでしょう。
計測の基本フロー|開始・実行・停止・分析
計測の流れは「①START_PROFILER → ②対象処理を実行 → ③STOP_PROFILER → ④結果テーブルをSELECTで分析」の4ステップです。RUN_COMMENTにわかりやすい説明を入れておくと後の分析でRUNを区別しやすくなります。
-- ① 計測開始(RUNIDが返る)
DECLARE
v_runid NUMBER;
BEGIN
DBMS_PROFILER.START_PROFILER(
run_comment => '20260425_BEFORE_FIX',
runid => v_runid
);
DBMS_OUTPUT.PUT_LINE('RUNID = ' || v_runid);
END;
/
-- ② 計測対象を実行(普通に呼ぶだけ)
BEGIN
pkg_order_service.create_order(p_customer_id => 100, p_amount => 5000);
END;
/
-- ③ 計測停止
BEGIN
DBMS_PROFILER.STOP_PROFILER;
END;
/
-- ④ どのRUNが今回のものか確認
SELECT runid, run_date, run_comment, run_total_time/1e9 AS sec
FROM plsql_profiler_runs
ORDER BY runid DESC
FETCH FIRST 5 ROWS ONLY;
STOP_PROFILER忘れに注意。計測中はオーバーヘッドが発生し続けるため、STOP_PROFILERを呼ばずにセッションを使い続けると不要な計測が積み上がります。安全策として、対象処理をBEGIN〜EXCEPTION〜ENDで囲んで例外時もSTOP_PROFILERが呼ばれるパターンを採用してください。
分析クエリ7種|ボトルネックを多角的に特定する
計測しただけでは性能改善は進みません。結果テーブルからどう情報を引き出すかが肝です。実務で使い回せる分析クエリを7種類紹介します。
-- 最も時間を食っている行TOP10を特定する基本クエリ -- これだけで70%のボトルネック特定が完了する SELECT u.unit_name, d.line# AS line_num, d.total_occur AS occur, d.total_time/1e6 AS total_ms, ROUND(d.total_time/d.total_occur/1e3, 2) AS avg_us, ROUND(d.max_time/1e3, 2) AS max_us FROM plsql_profiler_data d JOIN plsql_profiler_units u ON d.runid = u.runid AND d.unit_number = u.unit_number WHERE d.runid = &runid AND d.total_time > 0 ORDER BY d.total_time DESC FETCH FIRST 10 ROWS ONLY;
-- 行番号とソースコードを並べて、どこが遅いか一目で分かる
WITH heavy AS (
SELECT u.unit_owner, u.unit_name, u.unit_type, d.line#,
d.total_time, d.total_occur,
RANK() OVER (ORDER BY d.total_time DESC) rk
FROM plsql_profiler_data d
JOIN plsql_profiler_units u
ON d.runid = u.runid AND d.unit_number = u.unit_number
WHERE d.runid = &runid
AND d.total_time > 0
)
SELECT h.unit_name, h.line#,
h.total_time/1e6 AS total_ms,
h.total_occur AS occur,
s.text AS source_line
FROM heavy h
JOIN all_source s
ON s.owner = h.unit_owner
AND s.name = h.unit_name
AND s.type = h.unit_type
AND s.line = h.line#
WHERE h.rk <= 20
ORDER BY h.rk;
-- 呼び出し回数が多い行は「ループ内のホットスポット」の可能性大
SELECT u.unit_name, d.line#,
d.total_occur AS occur,
d.total_time/1e6 AS total_ms,
ROUND(d.total_time/d.total_occur/1e3, 2) AS avg_us
FROM plsql_profiler_data d
JOIN plsql_profiler_units u
ON d.runid = u.runid AND d.unit_number = u.unit_number
WHERE d.runid = &runid
AND d.total_occur > 0
ORDER BY d.total_occur DESC
FETCH FIRST 10 ROWS ONLY;
-- → 数十万回・数百万回呼ばれている行は集合演算で書き換えるべき候補
-- max_time が突出して大きい行は「たまに極端に遅くなる」処理
SELECT u.unit_name, d.line#,
d.total_occur AS occur,
d.total_time/1e6 AS total_ms,
ROUND(d.max_time/1e3, 2) AS max_us,
ROUND(d.min_time/1e3, 2) AS min_us
FROM plsql_profiler_data d
JOIN plsql_profiler_units u
ON d.runid = u.runid AND d.unit_number = u.unit_number
WHERE d.runid = &runid
AND d.max_time > 1000000 -- 1ms以上のもの
ORDER BY d.max_time DESC
FETCH FIRST 10 ROWS ONLY;
-- → ロック待ち・統計情報の偏りなどイレギュラー処理を疑う
-- どのパッケージが全体時間の何%を占めているかを把握
SELECT u.unit_owner, u.unit_name, u.unit_type,
SUM(d.total_time)/1e6 AS total_ms,
ROUND(SUM(d.total_time) * 100.0 /
(SELECT SUM(total_time) FROM plsql_profiler_data WHERE runid = &runid),
2) AS pct_total
FROM plsql_profiler_data d
JOIN plsql_profiler_units u
ON d.runid = u.runid AND d.unit_number = u.unit_number
WHERE d.runid = &runid
GROUP BY u.unit_owner, u.unit_name, u.unit_type
ORDER BY total_ms DESC;
-- 修正前後で行ごとの時間がどう変化したか確認
SELECT u.unit_name, d_before.line#,
d_before.total_time/1e6 AS before_ms,
d_after.total_time/1e6 AS after_ms,
ROUND((d_before.total_time - d_after.total_time)/1e6, 2) AS saved_ms,
ROUND((d_before.total_time - d_after.total_time)
/ NULLIF(d_before.total_time, 0) * 100, 1) AS pct_saved
FROM plsql_profiler_data d_before
JOIN plsql_profiler_data d_after
ON d_before.unit_number = d_after.unit_number
AND d_before.line# = d_after.line#
JOIN plsql_profiler_units u
ON u.runid = d_before.runid
AND u.unit_number = d_before.unit_number
WHERE d_before.runid = &before_runid
AND d_after.runid = &after_runid
AND d_before.total_time > 1e6 -- 1ms以上のみ表示
ORDER BY saved_ms DESC;
-- 毎回同じクエリを書かないよう専用ビューを定義
CREATE OR REPLACE VIEW v_profiler_summary AS
SELECT r.runid,
r.run_comment,
r.run_date,
u.unit_owner,
u.unit_name,
d.line# AS line_num,
d.total_occur AS occur,
d.total_time/1e6 AS total_ms,
d.max_time/1e3 AS max_us,
d.min_time/1e3 AS min_us,
ROUND(d.total_time/NULLIF(d.total_occur,0)/1e3, 2) AS avg_us,
s.text AS source_line
FROM plsql_profiler_runs r
JOIN plsql_profiler_units u ON u.runid = r.runid
JOIN plsql_profiler_data d ON d.runid = u.runid AND d.unit_number = u.unit_number
LEFT JOIN all_source s
ON s.owner = u.unit_owner AND s.name = u.unit_name
AND s.type = u.unit_type AND s.line = d.line#;
-- 利用例:直近のRUNでTOP10を一発取得
SELECT *
FROM v_profiler_summary
WHERE runid = (SELECT MAX(runid) FROM plsql_profiler_runs)
ORDER BY total_ms DESC
FETCH FIRST 10 ROWS ONLY;
ケーススタディ|行レベル特定で40倍速化した実例
実際にDBMS_PROFILERで分析→改修した典型例を紹介します。「ループ内SELECTの数百万回実行」がボトルネックだった事例で、クエリ1の累計時間TOP10を見るだけで原因が一目瞭然になりました。
改修前|10万件処理に45秒
顧客マスタを引きながら処理する10万件ループで、TOP10分析を実行したところ次のような結果が出ました。15行目(マスタ検索SELECT)が累計時間の87%を占めています。
UNIT_NAME LINE_NUM OCCUR TOTAL_MS AVG_US PKG_ORDER 15 100,000 39,520 395 PKG_ORDER 28 100,000 2,150 21 PKG_ORDER 12 100,000 1,030 10 PKG_ORDER 42 1,000 850 850 PKG_ORDER 31 100,000 720 7 ... -- 15行目のソース: SELECT name INTO v_name FROM countries WHERE code = rec.country_code; -- → 10万回も同じ表を引いている。連想配列キャッシュで置換すべき
-- 連想配列で事前ロード
DECLARE
TYPE t_map IS TABLE OF VARCHAR2(100) INDEX BY VARCHAR2(2);
v_country t_map;
v_name VARCHAR2(100);
BEGIN
-- 一度だけマスタを取得(250件)
FOR rec IN (SELECT code, name FROM countries) LOOP
v_country(rec.code) := rec.name;
END LOOP;
-- メインループでメモリlookup(SQLゼロ)
FOR rec IN (SELECT customer_id, country_code, ... FROM customers) LOOP
v_name := v_country(rec.country_code); -- ← O(1)
-- 業務処理
END LOOP;
END;
/
改修後|45秒→1.1秒(40倍速)
改修後に再度プロファイリングし、差分比較クエリ(クエリ6)で効果を確認したところ、15行目の累計時間が39,520msから0msに(マスタSELECTが消滅)、全体実行時間が45秒→1.1秒になりました。勘ではなく数値で「どこを直せば最大効果が出るか」を判断できたことが鍵です。
このケーススタディで重要なのは「TOP10を見るだけで決まった」こと。アムダールの法則の通り、全体の80%以上の時間を占める行を特定して潰すと残りの最適化は誤差レベルになります。勘でループ全体を書き換えるより、プロファイラで「真のボトルネック」を当ててから手を入れるのが最短ルートです。
オーバーヘッドの現実|本番で使えるか
DBMS_PROFILERは計測中のオーバーヘッド5〜30%があります。行ごとに実行回数と時間を記録するためで、計測対象の処理特性によって幅があります。本番運用での使い所を整理します。
オーバーヘッドの目安と運用パターン
- SQL中心の処理:オーバーヘッド5〜10%程度。ほとんど影響なく計測可能
- 細かいPL/SQL処理(多数の行を高速実行):オーバーヘッド20〜30%。ボトルネック特定はできるが時間は信用しすぎない
- 本番ピーク時の使用:原則NG。SLA違反のリスクあり
- ステージング環境での使用:問題なし。本番相当データで実測
- 本番の深夜時間帯:影響少ない時間帯なら可。CIで自動化推奨
- FLUSH_DATAの活用:長時間ジョブで途中観察可能、途中で問題が見えたら即停止することで実害を抑えられる
DBMS_PROFILER vs DBMS_HPROF|どちらを使うか
OracleにはもうひとつのプロファイラDBMS_HPROF(階層プロファイラ)があり、両者は補完関係です。詳細はPL/SQLプロファイラ完全ガイドにありますが、本記事では使い分けの判断基準だけ整理します。
DBMS_PROFILER|行レベル分析向き
「特定のプロシージャ内のどの行が遅いか」を知りたい場合の第一選択。ソースコードの何行目に何ミリ秒費やしたか、ホットループはどこかが明確になります。1関数の内部最適化に強い。
DBMS_HPROF|呼び出しチェーン分析向き
「複数のパッケージにまたがる関数呼び出しのうちどのチェーンが遅いか」を分析したい場合。関数間の呼び出し関係に強く、「pkg_a.fooがpkg_b.barを5回呼びそれぞれ200msかかっている」のような階層構造で見える。コード全体の構造分析に向きます。
使い分けの判断
- 「特定の関数の中身を最適化したい」→ DBMS_PROFILER(本記事)
- 「全体の関数呼び出しの中でどこが遅いか俯瞰したい」→ DBMS_HPROF
- 両方使ってもよい:HPROFで「重い関数」を絞り、PROFILERで「重い行」を特定
CI/CD連携|プロファイル自動化と回帰検出
DBMS_PROFILERはCI/CDでの自動性能テストにも組み込めます。リリース前に標準シナリオを実行してプロファイルを取り、前回のリリース時の結果と比較することで性能リグレッションを自動検出できます。
-- リリース前の自動プロファイリング
-- 前提:plsql_profiler_runs に過去のRUNが残っている
SET SERVEROUTPUT ON
DECLARE
v_runid NUMBER;
v_curr_ms NUMBER;
v_prev_ms NUMBER;
v_threshold NUMBER := 1.20; -- 前回比20%増を許容
BEGIN
-- 計測開始
DBMS_PROFILER.START_PROFILER(
run_comment => 'CI_' || TO_CHAR(SYSDATE,'YYYYMMDD_HH24MI'),
runid => v_runid);
-- 標準テストシナリオを実行
pkg_perf_test.run_smoke_scenario;
DBMS_PROFILER.STOP_PROFILER;
-- 今回のRUNの合計時間
SELECT run_total_time/1e6 INTO v_curr_ms
FROM plsql_profiler_runs WHERE runid = v_runid;
-- 前回のCI RUNを取得
SELECT MIN(run_total_time)/1e6 INTO v_prev_ms
FROM plsql_profiler_runs
WHERE run_comment LIKE 'CI_%'
AND runid < v_runid
AND run_date > SYSDATE - 30;
IF v_prev_ms IS NOT NULL AND v_curr_ms > v_prev_ms * v_threshold THEN
RAISE_APPLICATION_ERROR(-20999,
'性能リグレッション検出: ' || v_curr_ms || 'ms (前回: ' || v_prev_ms || 'ms)');
END IF;
DBMS_OUTPUT.PUT_LINE('OK: ' || v_curr_ms || 'ms');
END;
/
本番で踏むアンチパターン6選
① 計測せずに改修
「ループが遅そうだから」とFORALLに書き換えたが実は別の関数が真のボトルネックだった、というのはよくある失敗。必ずプロファイラでTOP10を確認してから改修対象を決めてください。
② STOP_PROFILER忘れ
計測中はオーバーヘッドが残るため、忘れるとセッション内の他の処理にも影響します。計測対象をBEGIN〜EXCEPTION〜ENDで囲み、例外時もSTOP_PROFILERが呼ばれる形にしてください。
③ 本番ピーク時に計測
5〜30%のオーバーヘッドはSLA違反に直結する可能性があります。本番計測はステージングか深夜帯に限定し、CIで自動化してリリース前に検出する運用が現実的です。
④ 古いRUNを放置してテーブル肥大化
plsql_profiler_dataは行数が膨大になります。半年以上前のRUNを残しても活用しないので、定期的に古いRUNを削除する運用ルールを作ってください。
⑤ TOP10を見ずに全行を眺める
1万行のプロファイル結果を上から順に読むと心が折れます。まずは累計時間TOP10だけに絞って確認し、上位を解消してから次のTOP10に進む段階的アプローチが効率的です。
⑥ DBMS_PROFILERだけで関数間の遅さを見ようとする
「pkg_a.fooがpkg_b.barを呼ぶ」のような関数チェーンの遅さはDBMS_HPROFで見るべき情報。DBMS_PROFILERは行レベルなので関数階層の俯瞰には向きません。使い分けを意識してください。
よくある質問
START_PROFILERとSTOP_PROFILERの間で実行された処理だけが計測対象になります。パッケージ単位ではなく実行範囲単位で制御するため、「特定の関数だけ計測」「特定のシナリオだけ計測」が自由にできます。事前準備の処理は計測前、計測対象の本体だけSTART〜STOPで囲んでください。DBMS_PROFILER.FLUSH_DATAを呼ぶとバッファが結果テーブルに書き込まれて、計測中でも他セッションから途中結果を見られます。夜間バッチで処理が想定より遅いと気付いたとき、途中状態を分析して問題箇所を即座に特定するのに便利です。EXPDPで別環境に持ち込めば比較可能です。RUN_COMMENTに環境名・バージョンを入れる運用にしておくと集計クエリで簡単に区別できます。ただし環境のスペック差で時間値はそのまま比較できないので、「相対比率」や「ホットスポットの順位」で比較するのが現実的です。EXPLAIN PLANや10046トレース→TKPROFを使ってください。DELETE FROM plsql_profiler_data WHERE runid IN (...);で対象RUNのデータを削除し、plsql_profiler_unitsとplsql_profiler_runsからも該当行を消します。専用パッケージとしてpkg_profiler.purge_old(p_days)を作ってジョブで定期実行するのが定番です。関連記事で深掘りする
性能診断・最適化に関連する記事を集めました。
- 【Oracle】PL/SQLプロファイラ完全ガイド|DBMS_PROFILER/DBMS_HPROFでボトルネックを特定する方法(DBMS_HPROFの詳細)
- 【PL/SQL】パフォーマンス改善プレイブック|4階層モデル・チェックリスト15・RESULT_CACHE・NOCOPY・NATIVE(性能改善の全体像)
- 【PL/SQL】DBMS_TRACEで処理の実行経路を追跡する方法(実行経路追跡の補完ツール)
- 【Oracle】SQL Trace・tkprof完全ガイド|10046トレースの取得からボトルネック分析まで(SQL自体の詳細分析)
- 【PL/SQL】バルク処理完全ガイド|BULK COLLECT+FORALLで10〜100倍高速化(プロファイル後の改修武器)
- 【PL/SQL】MERGE文でUPSERTを高速・安全に実装(IF方式からの置換)
- 【PL/SQL】パイプライン関数で大量データ処理を勝たせる完全ガイド(大量変換処理の最適化)
- 【PL/SQL】コレクションを実戦活用する完全ガイド(lookup最適化の実装)
- 【PL/SQL】カーソルFORループ vs 明示的カーソル(カーソル選択の判断基準)
- 【PL/SQL】ストアドプロシージャとファンクションの違いと作り方(DETERMINISTIC・RESULT_CACHE活用)
まとめ|計測駆動でボトルネックを根拠付きで潰す
DBMS_PROFILERは「PL/SQLのどの行に何ms費やしたか」を客観的に可視化する強力なツールです。勘や憶測でチューニングする時代は終わり、計測駆動で根拠を持って改修する時代に。本記事の要点を7つに集約します。
- 計測前にprofload.sql+proftab.sqlで結果テーブルを準備
- START_PROFILER→対象実行→STOP_PROFILERの基本フローを徹底
- 累計時間TOP10だけ見れば70%のボトルネック特定が完了
- 呼び出し回数・最大時間・パッケージ別集計の多角分析で原因を絞り込む
- RUN_COMMENTに「修正前」「修正後」を入れて差分比較で改善効果を確認
- オーバーヘッド5〜30%を踏まえてステージング・深夜帯で計測
- CI/CDに組み込むことで性能リグレッションを自動検出できる
「なぜか遅い」処理が現場にあるなら、まずプロファイルしてTOP10を見るだけで正解の8割が見える状況になります。本記事の分析クエリ7種を自分のスキーマに準備しておけば、次回のチューニング作業から劇的に効率化できるはずです。

