Oracle のパフォーマンスチューニングで「実行計画を確認する」という作業は日常的ですが、EXPLAIN PLAN + AUTOTRACE だけでは把握できない情報があります。実際に実行されたプランの統計・AWR に蓄積されたプラン履歴・バインド変数を使ったときの実際のカーディナリティ推定を確認するには、DBMS_XPLAN パッケージが必要です。
この記事では DBMS_XPLAN の3つの主要関数(DISPLAY・DISPLAY_CURSOR・DISPLAY_AWR)の使い方と、出力の読み方を実例で解説します。
- DBMS_XPLAN.DISPLAY・DISPLAY_CURSOR・DISPLAY_AWR の違いと使い分け
- FORMAT パラメータ(BASIC/TYPICAL/ALL/ALLSTATS LAST)の使い方
- 出力の読み方:Operation・Cost・Card・Bytes・Time の意味
- アクセス述語(Access Predicates)とフィルタ述語(Filter Predicates)の違い
- SQL_ID を使って特定クエリのプランをピンポイントで確認する方法
- バインドピーク(Bind Peeking)の問題と実際の実行統計の取得方法
DBMS_XPLAN の3つの主要関数
| 関数 | 参照先 | 用途 |
|---|---|---|
| DISPLAY | PLAN_TABLE(EXPLAIN PLAN の結果) | 事前に EXPLAIN PLAN を実行したプランを整形表示する |
| DISPLAY_CURSOR | V$SQL_PLAN / V$SQL_PLAN_STATISTICS | 現在または直近に実行された SQL の実際のプラン(実行統計付き)を確認する |
| DISPLAY_AWR | DBA_HIST_SQL_PLAN | AWR スナップショットに蓄積された過去のプランを確認する(DBA権限必要) |
EXPLAIN PLAN は「現在のセッション設定・統計情報」でのプランを予測します。バインド変数を使うクエリでは、実際に実行されたときのバインドピーク(Bind Peeking)の結果と異なる場合があります。実際に使われたプランを確認したいときは DISPLAY_CURSOR を使います。
DBMS_XPLAN.DISPLAY — EXPLAIN PLAN の結果を整形表示する
-- ① EXPLAIN PLAN でプランを PLAN_TABLE に格納 EXPLAIN PLAN FOR SELECT e.employee_id, e.last_name, d.department_name FROM employees e JOIN departments d ON e.department_id = d.department_id WHERE e.salary > 10000 ORDER BY e.last_name; -- ② DBMS_XPLAN.DISPLAY で整形表示 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY); -- デフォルト: statement_id=NULL(直近の EXPLAIN PLAN), format='TYPICAL'
-- BASIC: 最小限(Operation と Name のみ) SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format=>'BASIC')); -- TYPICAL: 標準(デフォルト)Operation, Cost, Card, Bytes, Time, Predicate 情報 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format=>'TYPICAL')); -- ALL: 最詳細(Column Projection, Alias なども含む) SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format=>'ALL')); -- OUTLINE: SQL ヒントを再現するための Outline Data を表示 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format=>'TYPICAL +OUTLINE'));
複数の EXPLAIN PLAN を区別したいときは SET STATEMENT_ID = '識別名' を使います。
-- プランに識別名を付けて保存
EXPLAIN PLAN SET STATEMENT_ID = 'PLAN_A' FOR
SELECT * FROM employees WHERE department_id = 50;
EXPLAIN PLAN SET STATEMENT_ID = 'PLAN_B' FOR
SELECT * FROM employees WHERE salary > 5000;
-- 識別名を指定して表示
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY('PLAN_TABLE', 'PLAN_A', 'TYPICAL'));
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY('PLAN_TABLE', 'PLAN_B', 'TYPICAL'));
実行計画の出力の読み方
DBMS_XPLAN の出力にはいくつかの重要な列があります。以下に主要な列の意味を示します。
| 列名 | 意味 | チューニングでの注目点 |
|---|---|---|
| Id | 操作のID(*付きは述語情報あり) | 述語情報(Predicate Information)と照合する |
| Operation | 実行した操作(TABLE ACCESS FULL など) | 意図しない FULL SCAN がないか確認 |
| Name | 操作対象のオブジェクト名 | 想定通りのインデックスが使われているか |
| Rows (Card) | オプティマイザの推定行数 | 実際の行数と大きく乖離していると統計情報が古い |
| Bytes | 推定データ量 | ソート・ハッシュ結合での一時領域使用量の目安 |
| Cost | オプティマイザの内部コスト(I/O + CPU) | コストが高い操作がボトルネック候補 |
| Time | 推定処理時間(hh:mm:ss) | 参考値(実際とは異なる場合が多い) |
アクセス述語とフィルタ述語の違い
実行計画の下部に表示される「Predicate Information」は、WHERE 条件がどの段階で適用されるかを示します。
| 種類 | 意味 | パフォーマンスへの影響 |
|---|---|---|
| access(“col”=:b1) | インデックスを使ってデータにアクセスする条件 | インデックスが有効活用されている(良い) |
| filter(“col”>100) | 読み込んだ後に行を絞り込む条件 | 余分な読み込みが発生している可能性(要確認) |
インデックスの先頭列に指定していない条件や、関数を適用した列への条件は、
access でなく filter になります。これは「インデックスは使っているが効率が悪い」状態を示すことがあります。関数ベースインデックス(FBI)の検討が有効です。
DBMS_XPLAN.DISPLAY_CURSOR — 実際の実行プランを確認する
DISPLAY_CURSOR は V$SQL_PLAN(プラン情報)と V$SQL_PLAN_STATISTICS(実行統計)から実際に使われたプランを取得します。EXPLAIN PLAN では取れない実際のカーディナリティや実行時間を確認できます。
-- ① 対象の SQL を実行(統計収集を有効にするためヒントを付ける)
SELECT /*+ GATHER_PLAN_STATISTICS */
e.employee_id, e.last_name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > 10000;
-- ② 直前に実行した SQL のプランを確認(sql_id=NULL → 直前の SQL)
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(format=>'ALLSTATS LAST'));
-- ① 対象の SQL の sql_id を取得
SELECT sql_id, sql_text, executions
FROM V$SQL
WHERE sql_text LIKE '%employees%'
AND sql_text NOT LIKE '%V$SQL%'
ORDER BY last_active_time DESC
FETCH FIRST 5 ROWS ONLY;
-- ② sql_id と child_number を指定してプランを表示
SELECT * FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => 'abc123xyz', -- V$SQL の sql_id を指定
cursor_child_no => 0, -- 通常は 0
format => 'ALLSTATS LAST'
)
);
ALLSTATS LAST の読み方
format=>'ALLSTATS LAST' を指定すると、推定値(E-Rows)と実際の値(A-Rows)が並んで表示されます。
| 列 | 意味 |
|---|---|
| E-Rows | オプティマイザの推定行数(Estimated Rows) |
| A-Rows | 実際に処理した行数(Actual Rows) |
| A-Time | 実際の処理時間(累積) |
| Buffers | 実際に読み込んだバッファブロック数(論理読み込み) |
| Reads | 物理読み込みブロック数 |
例えば「E-Rows=1, A-Rows=50000」という状態は、オプティマイザが「1行しか返らない」と誤って判断してNESTED LOOPS を選んだが、実際には 50,000 行を処理していた、というケースを示します。この場合はSQL ヒントや統計情報の更新(DBMS_STATS)で対処します。
DISPLAY_CURSOR を使うには SELECT_CATALOG_ROLE または V$SQL_PLAN への SELECT 権限が必要です。一般ユーザーが自分のセッションで実行した SQL については通常参照できます。
-- セッション全体で実行統計収集を有効にする(パフォーマンスに影響するため一時的に使用) ALTER SESSION SET statistics_level = ALL; -- 対象 SQL を実行 SELECT e.employee_id, e.last_name FROM employees e WHERE e.department_id = 50; -- プランと実行統計を確認 SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(format=>'ALLSTATS LAST')); -- 確認後は設定を戻す ALTER SESSION SET statistics_level = TYPICAL;
DBMS_XPLAN.DISPLAY_AWR — AWR からプランを取得する
DISPLAY_AWR は AWR(Automatic Workload Repository)に蓄積されたプランを参照します。「昨日は遅くなかったのに今日は遅い」という場合に、過去のプランと現在のプランを比較するのに使います。DBA 権限または SELECT ANY DICTIONARY 権限が必要です。
-- ① 対象 SQL の sql_id を DBA_HIST_SQLSTAT から確認
SELECT DISTINCT sql_id, plan_hash_value, snap_id
FROM DBA_HIST_SQLSTAT
WHERE sql_id = 'abc123xyz'
ORDER BY snap_id DESC
FETCH FIRST 10 ROWS ONLY;
-- ② AWR からプランを取得(plan_hash_value を指定して特定バージョンを参照)
SELECT * FROM TABLE(
DBMS_XPLAN.DISPLAY_AWR(
sql_id => 'abc123xyz',
plan_hash_value => 1234567890, -- NULL なら全バージョン表示
db_id => NULL, -- NULL = 現在の DB
format => 'TYPICAL'
)
);
-- AWR に蓄積されている plan_hash_value の種類を確認
-- plan_hash_value が変わっている = プランが切り替わったタイミング
SELECT
s.snap_id,
TO_CHAR(sn.begin_interval_time, 'YYYY-MM-DD HH24:MI') AS snap_time,
s.plan_hash_value,
s.executions_delta,
ROUND(s.elapsed_time_delta / NULLIF(s.executions_delta, 0) / 1000000, 2) AS avg_elapsed_sec
FROM DBA_HIST_SQLSTAT s
JOIN DBA_HIST_SNAPSHOT sn ON s.snap_id = sn.snap_id
WHERE s.sql_id = 'abc123xyz'
ORDER BY s.snap_id;
FORMAT パラメータのまとめ
| FORMAT 値 | 表示内容 | 主な用途 |
|---|---|---|
BASIC |
Operation と Name のみ | 全体構造だけ確認したい場合 |
TYPICAL(デフォルト) |
標準情報(Cost・Card・Bytes・Time・述語) | 通常のプラン確認 |
ALL |
TYPICAL + 列プロジェクション・エイリアス情報 | 詳細分析 |
ALLSTATS LAST |
TYPICAL + E-Rows/A-Rows/A-Time/Buffers(最終実行分) | 実際の実行統計との比較(DISPLAY_CURSOR 専用) |
ALLSTATS |
全実行の統計(累積) | ALLSTATS LAST より多くのデータ |
TYPICAL +OUTLINE |
TYPICAL + Outline Data(SQL プロファイル用ヒント) | プランを固定するヒントを取得したい場合 |
よく使う実践パターン
現在実行中のセッションの実行プランを確認する
-- ① 現在実行中のセッションの sql_id を確認
SELECT s.sid, s.sql_id, s.sql_child_number, s.event, s.seconds_in_wait
FROM V$SESSION s
WHERE s.status = 'ACTIVE'
AND s.wait_class != 'Idle'
ORDER BY s.seconds_in_wait DESC;
-- ② 該当セッションのプランを確認
SELECT * FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => 'abc123xyz', -- ①で取得した sql_id
cursor_child_no => 0,
format => 'TYPICAL'
)
);
プランが意図通りかを素早く確認するワンライナーパターン
-- EXPLAIN PLAN と DISPLAY を連続実行(SQL*Plus / SQLcl で使用) EXPLAIN PLAN FOR SELECT COUNT(*) FROM orders WHERE order_date >= DATE '2024-01-01'; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(format=>'TYPICAL'));
バインド変数を使ったクエリのプランを確認する
-- バインド変数を使ったクエリを実行 VARIABLE b_dept NUMBER; EXEC :b_dept := 50; SELECT /*+ GATHER_PLAN_STATISTICS */ * FROM employees WHERE department_id = :b_dept; -- 実際に使われたプランを確認(バインドピークされた値でのプラン) SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(format=>'ALLSTATS LAST +PEEKED_BINDS')); -- +PEEKED_BINDS: バインドピークした値を出力に含める(Oracle 11g 以降)
まとめ
- DISPLAY:EXPLAIN PLAN の結果を整形表示する。簡単に使えるが実際の実行とは異なる場合がある
- DISPLAY_CURSOR:実際に実行されたプランと実行統計を確認できる。
format=>'ALLSTATS LAST'で E-Rows vs A-Rows の乖離を発見できる - DISPLAY_AWR:過去のプランを確認する。プランが変わってから遅くなった場合に原因特定できる
- 述語の読み方:
accessはインデックスが効いている条件、filterは読み込み後に絞り込む条件。filter の多用は非効率の兆候 - ALLSTATS LAST:推定(E-Rows)と実際(A-Rows)の乖離が大きければ統計情報の更新やヒントで対処する
実行計画の確認は EXPLAIN PLAN・AUTOTRACE完全ガイド と合わせて使うと効果的です。遅い SQL の特定には V$SQL・AWRで遅いSQLを特定する方法 も参照してください。プランをヒントで固定したい場合は SQL ヒント句完全ガイドを参照してください。

