【完全ガイド】Oracle EXPLAIN PLAN 見方 徹底解説|DBMS_XPLAN・実行計画の読み方・チューニング

【完全ガイド】Oracle EXPLAIN PLAN 見方 徹底解説|DBMS_XPLAN・実行計画の読み方・チューニング

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 チューニングを根本から始められるようになります。


目次

結論: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 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)もあわせてご確認ください。