【完全ガイド】Oracle EXPLAIN PLAN 見方 徹底解説|DBMS_XPLAN・実行計画の読み方・チューニング
- 作成日 2026.07.28
- Oracle Database その他
Oracle SQL チューニングの最重要スキル、実行計画(EXPLAIN PLAN)の読み方:
EXPLAIN PLAN FOR
SELECT o.order_id, c.customer_name
FROM orders o JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date > SYSDATE - 30;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
結果:
------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1234 | 61700 | 45 |
| 1 | HASH JOIN | | 1234 | 61700 | 45 |
| 2 | TABLE ACCESS FULL | CUSTOMERS | 100 | 2500 | 3 |
| 3 | TABLE ACCESS BY INDEX ROWID| ORDERS | 1234 | 30850 | 42 |
|* 4 | INDEX RANGE SCAN | IDX_DATE | 1234 | | 4 |
------------------------------------------------------------------------
この計画から何を読み取るか:
- どのテーブルを先にアクセスするか
- インデックスを使っているか(FULL SCAN は要注意)
- JOIN 方法(HASH / NESTED / MERGE)
- 推定行数と実際の乖離
- どこにボトルネックがあるか
しかし、実務では:
- 記号の意味が分からない(Id, Bytes, Cost, Cardinality)
- 実行順序が読めない(木構造の解釈)
- HASH JOIN vs NESTED LOOPS の選択理由
- E-Rows vs A-Rows の乖離の見方
- Adaptive Plan の解釈
- DBMS_XPLAN.DISPLAY_CURSOR の使い方
- Real-Time SQL Monitoring の活用
- 統計情報が古い時の症状
- ヒントの効果的な使い方
GATHER_PLAN_STATISTICSヒントの活用
さらに、Oracle 12c+ の Adaptive Query Optimization により、実行時に計画が変わるケースがあり、古い解説記事では対応できないのが現状です。
現場では:
- 本番クエリの突然の遅延 → 実行計画変化を疑う
- 統計情報未更新 → CBO が誤判断
- バインドピーク → 統計収集
- 並列処理での実行計画 → Adaptive Plan
- Rails / Java からの複雑なクエリ → Auto Trace
- DBA が緊急対応 → AWR で過去計画を確認
本記事では、Oracle EXPLAIN PLAN と実行計画の完全ガイドを、リファレンスとして実用的に整理します。取得方法4種類、木構造の読み方、主要 Operation 完全リファレンス、Cost の意味、E-Rows vs A-Rows、GATHER_PLAN_STATISTICS ヒント、DBMS_XPLAN 詳細、V$SQL_PLAN / AWR、パフォーマンス改善10パターン、Rails/Java/Python 対応、実践シナリオ、FAQまで完全網羅。この1本で Oracle SQL チューニングを根本から始められるようになります。
- 1. 結論:3ステップで読み解く
- 2. 実行計画の取得方法
- 3. 実行計画の読み方
- 4. 主要 Operation リファレンス
- 5. Cost の意味
- 6. E-Rows vs A-Rows
- 7. ヒントの使い方
- 8. V$SQL_PLAN と AWR
- 9. パフォーマンス改善10パターン
- 10. Rails / Java / Python 対応
- 11. 実践シナリオ
- 12. トラブルシューティング
- 13. よくある質問(FAQ)
- 13.1. Q1. EXPLAIN PLAN と DISPLAY_CURSOR の違い
- 13.2. Q2. Cost はどう解釈?
- 13.3. Q3. E-Rows と A-Rows が大きく違う
- 13.4. Q4. FULL SCAN は必ず悪い?
- 13.5. Q5. NESTED LOOPS と HASH JOIN
- 13.6. Q6. AUTOTRACE と DISPLAY_CURSOR
- 13.7. Q7. Adaptive Plan とは
- 13.8. Q8. Rails でどう EXPLAIN?
- 13.9. Q9. 統計情報の更新頻度
- 13.10. Q10. SQL Plan Baseline とは
- 13.11. Q11. ヒントは本番で使うべき?
- 13.12. Q12. Real-Time SQL Monitoring
- 14. 参考リンク
- 15. まとめ
結論:3ステップで読み解く
時間がない方向けに、最速の理解を示します。
3ステップ
STEP 1: 実行計画を取得
STEP 2: 実行順序を追う(右から左、下から上)
STEP 3: 各 Operation とコストを評価
最速の取得コマンド
-- ① 事前に計画を確認
EXPLAIN PLAN FOR <SQL>;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- ② 実行後の実際の計画
SELECT /*+ GATHER_PLAN_STATISTICS */ <SQL>;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'ALLSTATS LAST'));
-- ③ AUTOTRACE(SQL*Plus / SQLcl)
SET AUTOTRACE ON;
<SQL>;
読み方の鉄則
木構造:
- 実行順序: 深くインデントされた行が先
- 親子関係: 親(外側)→ 子(内側)
- 兄弟: 上から下
Rows (E-Rows): オプティマイザ推定
Cost: 推定コスト(正確な指標ではない)
Bytes: 推定バイト数
Time: 推定所要時間
5大 Operation
| Operation | 意味 | 良し悪し |
|---|---|---|
| INDEX UNIQUE SCAN | 一意インデックス | ✅ 最速 |
| INDEX RANGE SCAN | 範囲インデックス | ✅ 良い |
| TABLE ACCESS BY INDEX ROWID | インデックス経由 | ✅ 良い |
| HASH JOIN | ハッシュ結合 | ✅ 大量向き |
| TABLE ACCESS FULL | フルスキャン | ⚠️ 大量なら要注意 |
危険な兆候
⚠️ FULL SCAN on 大量テーブル
⚠️ NESTED LOOPS で外側が大きい
⚠️ E-Rows と A-Rows の大きな乖離
⚠️ Cost が高い(1000+ とか)
⚠️ SORT / MERGE の巨大なメモリ使用
詳細は以下で解説します。
実行計画の取得方法
方法① EXPLAIN PLAN FOR + DBMS_XPLAN.DISPLAY
基本形(実行前の予測):
EXPLAIN PLAN FOR
SELECT * FROM orders WHERE customer_id = 100;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
特徴:
- SQL を実行せずに計画取得
- 予測値のみ(E-Rows)
- PLAN_TABLE 使用
方法② SET AUTOTRACE
SQL*Plus / SQLcl:
SET AUTOTRACE ON
SELECT * FROM orders WHERE customer_id = 100;
-- 結果 + 計画 + 統計 が表示
-- モード
SET AUTOTRACE ON -- 結果 + 計画 + 統計
SET AUTOTRACE ON EXPLAIN -- 結果 + 計画のみ
SET AUTOTRACE ON STATISTICS -- 結果 + 統計のみ
SET AUTOTRACE TRACEONLY -- 計画 + 統計(結果非表示)
SET AUTOTRACE OFF
特徴:
- 実際に実行する
- 統計情報(consistent gets, physical reads 等)も取得
- 開発時に便利
方法③ DBMS_XPLAN.DISPLAY_CURSOR(実行後)
実際の実行後の計画:
-- ヒントで統計収集を有効化
SELECT /*+ GATHER_PLAN_STATISTICS */ *
FROM orders WHERE customer_id = 100;
-- 直近の実行計画を確認
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'ALLSTATS LAST'));
特徴:
- 実際の実行結果を反映
- E-Rows と A-Rows の比較が可能
- 最も詳細
方法④ Real-Time SQL Monitoring
長時間クエリ・並列処理向け:
-- 実行中のセッションのSQL_ID取得
SELECT sql_id FROM v$session WHERE username = 'SCOTT' AND status = 'ACTIVE';
-- モニタリングレポート
SELECT DBMS_SQLTUNE.REPORT_SQL_MONITOR(sql_id => '<sql_id>') FROM DUAL;
特徴:
- 実行中に進捗確認
- 並列度、行数、時間を可視化
- Enterprise Manager でも参照可能
方法⑤ SQL Developer / SQLcl
GUI 表示:
- SQL Developer: 実行計画タブ
- SQLcl:
EXPLAINコマンド
パラメータ関連は Oracle パラメータ確認(V$PARAMETER)の記事、パフォーマンス関連は Oracle 分析関数(OVER/PARTITION BY)の記事も参照してください。
実行計画の読み方
出力例
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 50 | 3 |
| 1 | NESTED LOOPS | | 1 | 50 | 3 |
| 2 | TABLE ACCESS BY INDEX ROWID| ORDERS | 1 | 30 | 2 |
|* 3 | INDEX UNIQUE SCAN | ORDERS_PK | 1 | | 1 |
| 4 | TABLE ACCESS BY INDEX ROWID| CUSTOMERS | 1 | 20 | 1 |
|* 5 | INDEX UNIQUE SCAN | CUSTOMERS_PK | 1 | | 0 |
--------------------------------------------------------------------------
各カラムの意味
| カラム | 意味 |
|---|---|
| Id | 一意識別子(*付は述語あり) |
| Operation | 実行内容 |
| Name | オブジェクト名(テーブル、インデックス) |
| Rows (E-Rows) | 推定行数 |
| Bytes | 推定バイト数 |
| Cost | 推定コスト |
| Time | 推定時間 |
| A-Rows | 実際の行数(DISPLAY_CURSOR + STATISTICS) |
| A-Time | 実際の時間 |
実行順序の読み方
基本ルール:
1. 最も深くインデントされた(右側の)行が先に実行
2. 同じレベルなら上から下
3. 結果は親(外側)へ渡る
上記の例:
実行順序:
1. INDEX UNIQUE SCAN ORDERS_PK (Id=3)
2. TABLE ACCESS BY INDEX ROWID ORDERS (Id=2)
3. INDEX UNIQUE SCAN CUSTOMERS_PK (Id=5)
4. TABLE ACCESS BY INDEX ROWID CUSTOMERS (Id=4)
5. NESTED LOOPS (Id=1)
6. SELECT STATEMENT (Id=0)
Predicate Information
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access("O"."ORDER_ID"=100)
5 - access("C"."CUSTOMER_ID"="O"."CUSTOMER_ID")
access: インデックスアクセス条件 filter: 取得後のフィルタ
主要 Operation リファレンス
TABLE ACCESS FULL
全表スキャン:
TABLE ACCESS FULL | ORDERS
- 全ブロックを読む
- 大量テーブルでは危険
- 小さいテーブル or 大量取得なら OK
TABLE ACCESS BY INDEX ROWID
インデックス経由でテーブル参照:
TABLE ACCESS BY INDEX ROWID | ORDERS
INDEX RANGE SCAN | IDX_DATE
- インデックスで ROWID を取得
- ROWID で行を取得
- 通常は良い方法
INDEX UNIQUE SCAN
一意インデックスでピンポイント取得:
INDEX UNIQUE SCAN | PK_ORDERS
- 最速(1行のみ)
- 主キー・一意キー
INDEX RANGE SCAN
範囲インデックススキャン:
INDEX RANGE SCAN | IDX_DATE
- 範囲検索(>=, <=, BETWEEN, LIKE ‘xxx%’)
- 通常は効率的
INDEX FULL SCAN
インデックス全スキャン:
INDEX FULL SCAN | IDX_NAME
- インデックスをソート順に全走査
- ORDER BY 用(テーブルアクセスなしで結果順序化)
INDEX FAST FULL SCAN
インデックス高速全スキャン:
INDEX FAST FULL SCAN | IDX_NAME
- 順不同で並列読み
- COUNT(*) や集約向け
INDEX SKIP SCAN
複合インデックスの先頭列スキップ:
INDEX SKIP SCAN | IDX_A_B
- 複合インデックス
(A, B)で B のみで検索 - A のカーディナリティが低い場合有効
HASH JOIN
ハッシュ結合:
HASH JOIN
TABLE ACCESS FULL | SMALL_TABLE ← ビルド
TABLE ACCESS FULL | LARGE_TABLE ← プローブ
- 小さいテーブルからハッシュ表を作成
- 大きいテーブルをスキャンしてハッシュで照合
- 大量データ向き、等価結合のみ
NESTED LOOPS
ネストループ結合:
NESTED LOOPS
TABLE ACCESS ... | OUTER
INDEX ... | IDX_INNER
TABLE ACCESS BY INDEX ROWID | INNER
- 外側テーブルの各行に対して内側を検索
- 少量データ向き、駆動表が小さいときに効率的
- インデックス依存
SORT MERGE JOIN
ソートマージ結合:
SORT MERGE JOIN
SORT JOIN
TABLE ACCESS FULL | T1
SORT JOIN
TABLE ACCESS FULL | T2
- 両テーブルをソートして結合
- 非等価結合(<, >, BETWEEN)可能
- 大量データで両側ソート済みなら有効
SORT / GROUP BY / HASH GROUP BY
集約・並べ替え:
SORT ORDER BY
SORT AGGREGATE
HASH GROUP BY
- ORDER BY、GROUP BY
- SORT: メモリソート
- HASH GROUP BY: 12c+ より効率的
FILTER
フィルタ処理:
FILTER
TABLE ACCESS ...
<サブクエリ>
- WHERE 節の条件評価
- EXISTS / NOT EXISTS
Cost の意味
Cost とは
オプティマイザの推定コスト:
Cost = I/O コスト + CPU コスト + Network コスト
単位: ブロック読み取りに相当する数値。
Cost の使い方
- 相対的な比較指標
- 「Cost 1000 と Cost 10 なら 10 が速い」
- 絶対値は当てにならない(統計次第)
- 実際の実行時間との相関は完全ではない
コスト計算式(大まかに)
Full Table Scan Cost = ブロック数
Index Scan Cost = ルートからリーフまでの深さ + リーフブロック数
Cost が高い操作
- 大量テーブルの FULL SCAN
- SORT(大量データ)
- MERGE JOIN で大量ソート
Cost が低い操作
- INDEX UNIQUE SCAN(1行)
- INDEX RANGE SCAN(少量)
- NESTED LOOPS(少量)
E-Rows vs A-Rows
GATHER_PLAN_STATISTICS
実際の実行統計を収集:
SELECT /*+ GATHER_PLAN_STATISTICS */ *
FROM orders WHERE ...;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'ALLSTATS LAST'));
出力例
-----------------------------------------------------------------
| Id | Operation | Name | E-Rows | A-Rows | A-Time |
-----------------------------------------------------------------
| 1 | HASH JOIN | | 10 | 1000 | 0.5s |
| 2 | TABLE ACCESS FULL | T1 | 100 | 100 | 0.01s |
| 3 | TABLE ACCESS FULL | T2 | 1000 | 10000 | 0.4s |
-----------------------------------------------------------------
乖離の解釈
E-Rows = 10, A-Rows = 1000 の場合:
オプティマイザが 10 行と予測 → 実際は 1000 行
→ 統計情報が古い可能性
→ ヒストグラム不足
→ 誤った実行計画を選択
対処
-- 統計情報を再収集
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA', 'TABLE',
CASCADE => TRUE,
METHOD_OPT => 'FOR ALL COLUMNS SIZE AUTO');
FORMAT オプション
DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'ALLSTATS LAST')
-- オプション
'BASIC' -- 最小限
'TYPICAL' -- デフォルト
'ALL' -- 全情報
'ADVANCED' -- 詳細
'+ADAPTIVE' -- Adaptive Plan
'ALLSTATS LAST'-- 直近実行の統計
'IOSTATS' -- I/O 統計
'MEMSTATS' -- メモリ統計
'PROJECTION' -- カラム投影
'ALIAS' -- 別名
'OUTLINE' -- ヒントアウトライン
ヒントの使い方
主要ヒント
FULL / INDEX:
-- フルスキャン強制
SELECT /*+ FULL(orders) */ * FROM orders;
-- インデックス使用
SELECT /*+ INDEX(orders IDX_DATE) */ *
FROM orders WHERE order_date > SYSDATE - 30;
JOIN 方式:
-- HASH JOIN
SELECT /*+ USE_HASH(o c) */ ...
-- NESTED LOOPS
SELECT /*+ USE_NL(o c) */ ...
-- MERGE JOIN
SELECT /*+ USE_MERGE(o c) */ ...
並列度:
SELECT /*+ PARALLEL(orders 4) */ * FROM orders;
LEADING(駆動表指定):
SELECT /*+ LEADING(o) USE_NL(c) */ ...
-- o を先にアクセス、NESTED LOOPS で c を結合
FIRST_ROWS / ALL_ROWS:
SELECT /*+ FIRST_ROWS(10) */ ... -- 最初の10行を最速で
SELECT /*+ ALL_ROWS */ ... -- 全体の最速
アウトライン取得
-- 現在の実行計画のヒントを取得
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'OUTLINE'));
-- 出力例:
-- /*+
-- BEGIN_OUTLINE_DATA
-- INDEX(@"SEL$1" "ORDERS"@"SEL$1" ("ORDERS"."ORDER_DATE"))
-- ...
-- END_OUTLINE_DATA
-- */
V$SQL_PLAN と AWR
V$SQL_PLAN
現在ライブラリキャッシュに存在する計画:
SELECT sql_id, plan_hash_value
FROM v$sql
WHERE sql_text LIKE '%<キーワード>%'
FETCH FIRST 10 ROWS ONLY;
-- 計画表示
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('<sql_id>'));
AWR(Automatic Workload Repository)
過去の SQL 計画:
-- SQL テキスト
SELECT sql_id, sql_text FROM dba_hist_sqltext WHERE sql_id = '<sql_id>';
-- 計画履歴
SELECT plan_hash_value, timestamp
FROM dba_hist_sql_plan
WHERE sql_id = '<sql_id>'
GROUP BY plan_hash_value, timestamp;
-- 計画表示
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR('<sql_id>'));
計画変化の検出
-- SQL が複数の計画を持っている
SELECT sql_id, COUNT(DISTINCT plan_hash_value) AS plan_count
FROM dba_hist_sqlstat
WHERE snap_id > (SELECT MAX(snap_id) - 100 FROM dba_hist_snapshot)
GROUP BY sql_id
HAVING COUNT(DISTINCT plan_hash_value) > 1
ORDER BY plan_count DESC;
計画変化 = 性能変化の主因。
パフォーマンス改善10パターン
パターン① フルスキャンをインデックスに
-- Before: TABLE ACCESS FULL
SELECT * FROM orders WHERE order_date > SYSDATE - 30;
-- インデックス追加
CREATE INDEX idx_order_date ON orders(order_date);
-- After: INDEX RANGE SCAN
パターン② NESTED LOOPS を HASH JOIN に
大量データでの結合:
-- ヒントで強制
SELECT /*+ USE_HASH(a b) */ * FROM a, b WHERE ...;
パターン③ 統計情報再収集
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCHEMA', 'TABLE',
CASCADE => TRUE,
METHOD_OPT => 'FOR ALL COLUMNS SIZE AUTO');
パターン④ 関数を索引化
-- Before: 関数でインデックス無効化
WHERE UPPER(name) = 'JOHN'; -- FULL SCAN
-- 関数ベース索引
CREATE INDEX idx_upper_name ON emp(UPPER(name));
-- After: INDEX RANGE SCAN
パターン⑤ サブクエリを JOIN に
-- Before
SELECT * FROM orders
WHERE customer_id IN (SELECT id FROM customers WHERE region = 'ASIA');
-- After(オプティマイザが自動変換もあるが明示的に)
SELECT o.* FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE c.region = 'ASIA';
パターン⑥ 不要な列を除外
-- Before: SELECT *
-- After: SELECT necessary_columns_only
-- カバリングインデックス活用
CREATE INDEX idx_covering ON orders(customer_id, order_date, amount);
パターン⑦ パーティション プルーニング
-- パーティションテーブル
SELECT * FROM orders
WHERE order_date BETWEEN DATE '2026-06-01' AND DATE '2026-06-30';
-- 6月パーティションのみアクセス
日付範囲検索は Oracle DATE vs TIMESTAMP の違いの記事も参照してください。
パターン⑧ ソート回避
-- ORDER BY にインデックス
CREATE INDEX idx_ordered ON t(sort_col);
-- Before: SORT ORDER BY
-- After: INDEX FULL SCAN(ソート不要)
パターン⑨ 分析関数活用
-- Before: 自己 JOIN(遅い)
-- After: 分析関数(1回のスキャン)
SELECT LEAD(sales) OVER (ORDER BY month) FROM ...
分析関数の詳細は Oracle 分析関数(OVER/PARTITION BY)の記事も参照してください。
パターン⑩ Materialized View
-- 頻繁な集計を事前計算
CREATE MATERIALIZED VIEW mv_monthly_sales
BUILD IMMEDIATE
REFRESH COMPLETE ON DEMAND AS
SELECT ...;
PIVOT / Materialized View の詳細は Oracle PIVOT/UNPIVOT の記事も参照してください。
Rails / Java / Python 対応
Rails ActiveRecord
ログでの計画確認:
# 開発環境
config.active_record.verbose_query_logs = true
# EXPLAIN 実行
Order.where(status: 'pending').explain
# 結果:
# EXPLAIN for: SELECT "orders".* FROM "orders" WHERE ...
# PLAN_TABLE_OUTPUT
# ------------------------------------------------
# | Id | Operation | ... |
oracle_enhanced adapter:
ActiveRecord::Base.connection.explain(sql)
Rails 8 系の詳細は Rails 8 アップグレードガイドの記事、find/find_by/where の記事、Solid Queue 使い方の記事も参照してください。
Java (JDBC)
// EXPLAIN PLAN 経由
try (Statement stmt = conn.createStatement()) {
stmt.execute("EXPLAIN PLAN FOR " + sql);
try (ResultSet rs = stmt.executeQuery(
"SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)"
)) {
while (rs.next()) {
System.out.println(rs.getString(1));
}
}
}
Python (oracledb)
import oracledb
conn = oracledb.connect(...)
cursor = conn.cursor()
# EXPLAIN
cursor.execute(f"EXPLAIN PLAN FOR {sql}")
# 計画表示
cursor.execute("SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY)")
for row in cursor:
print(row[0])
実践シナリオ
シナリオ1:本番遅延クエリの緊急対応
-- 1. 遅い SQL 特定
SELECT sql_id, sql_text, elapsed_time / executions AS avg_time
FROM v$sql
WHERE parsing_schema_name = 'MYAPP'
ORDER BY elapsed_time DESC
FETCH FIRST 10 ROWS ONLY;
-- 2. 現在の実行計画
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('<sql_id>'));
-- 3. 過去の計画(AWR)
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR('<sql_id>'));
-- 4. 計画変化があれば SQL Plan Baseline で固定
EXEC DBMS_SPM.LOAD_PLANS_FROM_AWR(
begin_snap => X, end_snap => Y,
sql_id => '<sql_id>');
シナリオ2:新機能デプロイ前の性能検証
-- 1. 開発環境で EXPLAIN
EXPLAIN PLAN FOR <新SQL>;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- 2. 実際の実行統計取得
SELECT /*+ GATHER_PLAN_STATISTICS */ <新SQL>;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'ALLSTATS LAST'));
-- 3. Cost / A-Rows 確認
-- 4. 問題あれば修正
シナリオ3:Rails での定期監視
class SlowQueryMonitor
def self.check_query(sql)
plan = ActiveRecord::Base.connection.explain(sql)
if plan.include?("TABLE ACCESS FULL") &&
row_count_of(sql) > 10000
Rails.logger.warn "遅いクエリの可能性: #{sql}"
end
end
end
シナリオ4:Java Spring での実行計画取得
@Component
public class QueryAnalyzer {
@Autowired
private JdbcTemplate jdbc;
public List<String> explain(String sql) {
jdbc.execute("EXPLAIN PLAN FOR " + sql);
return jdbc.queryForList(
"SELECT plan_table_output FROM TABLE(DBMS_XPLAN.DISPLAY)",
String.class
);
}
}
シナリオ5:Docker Oracle でのチューニング
docker exec -it oracle-xe sqlplus scott/tiger <<EOF
SET LINESIZE 200
SET PAGESIZE 100
SET AUTOTRACE ON
SELECT * FROM emp WHERE deptno = 10;
EOF
Docker 関連は docker daemon 接続エラーの記事、Docker no space left on device の記事も参照してください。
シナリオ6:本番向けメトリクス
-- 高コスト SQL の特定
SELECT sql_id, elapsed_time, cpu_time, buffer_gets, executions
FROM v$sql
WHERE elapsed_time > 1000000 -- 1秒以上
ORDER BY elapsed_time DESC
FETCH FIRST 20 ROWS ONLY;
-- バッファヒット率
SELECT round(buffer_gets / executions, 2) AS avg_buffer_gets
FROM v$sql
WHERE sql_id = '<sql_id>';
シナリオ7:AWS RDS Oracle での実行計画
-- RDS でも DBMS_XPLAN 使用可能
-- Performance Insights で SQL 統計確認
シナリオ8:Autonomous DB での対応
-- 自動 SQL チューニング機能
-- SQL Plan Baseline 自動管理
-- Real-Time SQL Monitoring 標準
シナリオ9:Kamal デプロイ後のパフォーマンステスト
# デプロイ後の SQL 性能ベースライン取得
docker exec -it db-container sqlplus / as sysdba <<EOF
@?/rdbms/admin/awrrpt.sql
EOF
Kamal 2 デプロイの詳細は Kamal 2 デプロイの記事を参照してください。
シナリオ10:Bind Peeking 問題
-- バインド変数値でプランが変わる
-- 対策: SQL Plan Baseline
EXEC DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE(sql_id => '<sql_id>');
トラブルシューティング
EXPLAIN PLAN が動かない
PLAN_TABLE 未作成:
@?/rdbms/admin/utlxplan.sql
DISPLAY_CURSOR で権限エラー
GRANT SELECT ON V_$SQL_PLAN TO scott;
GRANT SELECT ON V_$SESSION TO scott;
GRANT SELECT ON V_$SQL_PLAN_STATISTICS_ALL TO scott;
権限系は ORA-01031: insufficient privileges の記事も参照してください。
A-Rows が表示されない
GATHER_PLAN_STATISTICS ヒント忘れ:
SELECT /*+ GATHER_PLAN_STATISTICS */ ...
または:
ALTER SESSION SET statistics_level = ALL;
計画が変わった
統計情報更新 or バインドピーク:
-- 過去の計画確認
SELECT DISTINCT plan_hash_value FROM v$sql WHERE sql_id = '<sql_id>';
FULL SCAN が消えない
- インデックスが選択制低い(例: sex 列)
- 統計情報古い
- ヒントで強制テスト
Adaptive Plan の解釈
Oracle 12c+ で実行時に計画変更:
DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'ADAPTIVE')
-- Initial Plan と Adaptive Plan 両方表示
よくある質問(FAQ)
Q1. EXPLAIN PLAN と DISPLAY_CURSOR の違い
- EXPLAIN PLAN: 実行前の予測
- DISPLAY_CURSOR: 実行後の実際
Q2. Cost はどう解釈?
相対比較指標、絶対値は当てにならない。低いほうが良い。
Q3. E-Rows と A-Rows が大きく違う
統計情報が古い、ヒストグラム不足の可能性。
Q4. FULL SCAN は必ず悪い?
NO。小さいテーブル or 大量取得なら効率的。
Q5. NESTED LOOPS と HASH JOIN
- 少量 + 索引あり: NESTED LOOPS
- 大量 + 等価結合: HASH JOIN
Q6. AUTOTRACE と DISPLAY_CURSOR
- AUTOTRACE: 実行+計画一発
- DISPLAY_CURSOR: 実行後にカーソルキャッシュから
Q7. Adaptive Plan とは
Oracle 12c+、実行時に計画を変更する機能。FORMAT => '+ADAPTIVE' で両方表示。
Q8. Rails でどう EXPLAIN?
Model.where(...).explain
Q9. 統計情報の更新頻度
デフォルトで自動収集(夜間ウィンドウ)。手動更新も可能。
Q10. SQL Plan Baseline とは
計画を固定化する機能。予期しない計画変化を防ぐ。
Q11. ヒントは本番で使うべき?
基本的には避ける。統計情報や SQL 書き換えで対応。それでも駄目なら最終手段。
Q12. Real-Time SQL Monitoring
長時間クエリを実行中に監視。REPORT_SQL_MONITOR で可視化。
参考リンク
Oracle 公式
- Oracle Database SQL Tuning Guide: Reading Execution Plans
- Oracle Database SQL Tuning Guide: Generating and Displaying Execution Plans
- DBMS_XPLAN Package
- Query Optimizer Concepts
まとめ
Oracle EXPLAIN PLAN と実行計画の要点を再整理します。
3ステップ
STEP 1: 計画を取得
STEP 2: 実行順序を追う(深いインデントが先)
STEP 3: Operation と Cost を評価
4つの取得方法
| 方法 | 特徴 |
|---|---|
| EXPLAIN PLAN + DISPLAY | 予測、実行なし |
| AUTOTRACE | 実行 + 計画 + 統計 |
| DISPLAY_CURSOR | 実行後の実際 |
| SQL Monitor | 長時間クエリの進捗 |
主要 Operation
| Operation | 意味 | 評価 |
|---|---|---|
| INDEX UNIQUE SCAN | 一意索引 | ✅ 最速 |
| INDEX RANGE SCAN | 範囲索引 | ✅ 良い |
| TABLE ACCESS BY INDEX ROWID | 索引経由 | ✅ 良い |
| HASH JOIN | ハッシュ結合 | ✅ 大量向き |
| NESTED LOOPS | ループ結合 | ✅ 少量向き |
| SORT MERGE JOIN | マージ結合 | ✅ 非等価 |
| TABLE ACCESS FULL | 全表 | ⚠️ 大量注意 |
| INDEX FULL SCAN | 索引全走査 | 状況次第 |
| INDEX SKIP SCAN | 索引スキップ | 状況次第 |
実践コマンド
-- 予測
EXPLAIN PLAN FOR <SQL>;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- 実際
SELECT /*+ GATHER_PLAN_STATISTICS */ <SQL>;
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(FORMAT => 'ALLSTATS LAST'));
-- AWR
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR('<sql_id>'));
E-Rows と A-Rows
乖離が大きい → 統計情報の問題
DBMS_STATS.GATHER_TABLE_STATS で対処
FORMAT オプション
'TYPICAL' -- デフォルト
'ALL' -- 全情報
'ALLSTATS LAST' -- 直近実行の統計
'+ADAPTIVE' -- Adaptive Plan
'OUTLINE' -- ヒントアウトライン
パフォーマンス改善10パターン
① FULL SCAN → INDEX
② NESTED LOOPS → HASH JOIN
③ 統計情報再収集
④ 関数を索引化
⑤ サブクエリ → JOIN
⑥ 不要な列を除外
⑦ パーティション プルーニング
⑧ ソート回避
⑨ 分析関数活用
⑩ Materialized View
これらの知識は、Oracle での SQL チューニング・本番障害対応・パフォーマンス設計・Rails / Java / Python 開発・監視設計・AWR 分析など、あらゆる場面で活用できます。本記事をブックマークしておけば、Oracle 実行計画を確実に読み解き改善できるようになります。
本記事は2026年6月時点の情報をもとに、Oracle Database 19c〜23ai での動作確認・公式ドキュメントに基づき作成しています。Oracle のバージョンにより挙動が異なる場合があるため、最新の情報は Oracle 公式ドキュメント(docs.oracle.com)もあわせてご確認ください。
-
前の記事
【完全ガイド】Oracle DATE vs TIMESTAMP 違い|精度・タイムゾーン・INTERVAL 徹底解説 2026.07.27
-
次の記事
【完全ガイド】ORA-00904: invalid identifier の原因と解決方法|列名タイプミス・予約語・引用符・エイリアス誤用まで徹底解説 2026.07.28
コメントを書く