【Oracle】SQLヒント句の使い方完全ガイド|FULL・INDEX・USE_NL・USE_HASH・PARALLEL・実行計画の制御方法

【Oracle】SQLヒント句の使い方完全ガイド|FULL・INDEX・USE_NL・USE_HASH・PARALLEL・実行計画の制御方法 Oracle

SQLのパフォーマンスチューニングで「オプティマイザが選ぶ実行計画を変えたい」場面は少なくありません。Oracleではヒント句(Hint)をSQLに埋め込むことで、テーブルアクセス方法・JOIN方式・並列度などを明示的に指示できます。本記事ではヒント句の基本構文から、テーブルアクセス・JOIN制御・並列処理・DML高速化まで実務で使うヒントを体系的に解説します。

この記事でわかること

  • SQLヒント句の基本構文と記述ルール
  • テーブルアクセスヒント(FULL / INDEX / INDEX_FFS / NO_INDEX)
  • JOIN方式ヒント(USE_NL / USE_HASH / USE_MERGE)
  • JOIN順序ヒント(LEADING / ORDERED)
  • 並列処理ヒント(PARALLEL / NO_PARALLEL)
  • DML高速化ヒント(APPEND / APPEND_VALUES)
  • サブクエリ制御ヒント(PUSH_SUBQ / UNNEST / NO_MERGE)
  • ヒントが無視されるケースと対処法
スポンサーリンク

ヒント句の基本構文

ヒント句は /*+ ヒント名 */ の形式で、SELECT / INSERT / UPDATE / DELETE / MERGE の直後に記述します。

ヒント句の基本構文
SELECT /*+ FULL(e) */ employee_id, name, salary
FROM employees e
WHERE department_id = 10;
-- employees テーブルにフルテーブルスキャンを強制する

記述ルール

ルール 説明
記述位置 SELECT / INSERT / UPDATE / DELETE / MERGE の直後 SELECT /*+ FULL(t) */ ...
コメント形式 /*+ で始まる(+ がないと通常コメント扱い) /*+ HINT *//* HINT */ は無効)
テーブル別名 テーブルにエイリアスを付けた場合、ヒントでもエイリアスを使う FROM employees e/*+ FULL(e) */
複数ヒント 1つの /*+ */ の中にスペース区切りで複数指定できる /*+ FULL(e) PARALLEL(e, 4) */
大文字小文字 ヒント名は大文字小文字を区別しない /*+ full(e) */ も有効
ヒントの構文エラーは静かに無視される
ヒント名のスペルミスやテーブル名の間違いがあっても、Oracleはエラーを出さずにそのヒントを無視します。ヒントが効いていないと感じたら、まず構文を確認してください。

テーブルアクセスヒント

テーブルにアクセスする方法(フルスキャン or インデックススキャン)を制御するヒントです。

FULL ── フルテーブルスキャンを強制する

FULL ── フルテーブルスキャンを強制
SELECT /*+ FULL(e) */ employee_id, name
FROM employees e
WHERE department_id = 10;
-- インデックスがあっても無視し、テーブル全体をスキャンする
-- 大量の行を取得する場合にフルスキャンが有利なケースで使う

INDEX ── 特定のインデックスを使用させる

INDEX ── 特定インデックスの使用を強制
-- インデックスを1つ指定
SELECT /*+ INDEX(e idx_emp_dept) */ employee_id, name
FROM employees e
WHERE department_id = 10;

-- インデックスを複数候補として指定(オプティマイザが最適なものを選ぶ)
SELECT /*+ INDEX(e idx_emp_dept idx_emp_name) */ employee_id, name
FROM employees e
WHERE department_id = 10 AND name LIKE 'A%';

-- インデックス名を省略するとオプティマイザが任意のインデックスを選ぶ
SELECT /*+ INDEX(e) */ employee_id, name
FROM employees e
WHERE department_id = 10;

INDEX_FFS ── インデックスファストフルスキャン

INDEX_FFS ── インデックスのみをスキャンする
SELECT /*+ INDEX_FFS(e idx_emp_dept) */ department_id, COUNT(*)
FROM employees e
GROUP BY department_id;
-- テーブルにアクセスせずインデックスだけをスキャンする
-- SELECT する列がすべてインデックスに含まれている場合に有効

NO_INDEX ── 特定インデックスの使用を禁止する

NO_INDEX ── 特定インデックスを使わせない
SELECT /*+ NO_INDEX(e idx_emp_dept) */ employee_id, name
FROM employees e
WHERE department_id = 10;
-- オプティマイザが idx_emp_dept を選ばないようにする
-- 他のインデックスがあればそちらが選ばれ、なければフルスキャンになる
ヒント 動作 使いどころ
FULL(t) フルテーブルスキャン 大量データの一括取得、インデックスが非効率な場合
INDEX(t idx) 指定インデックスの使用 特定のインデックスが最適と確信できる場合
INDEX_FFS(t idx) インデックスファストフルスキャン SELECT列がインデックス内で完結する場合(カバリングインデックス)
INDEX_SS(t idx) インデックススキップスキャン 先頭列の条件がないが後続列の条件がある場合
NO_INDEX(t idx) 指定インデックスの使用禁止 特定のインデックスが非効率な場合

JOIN方式ヒント

複数テーブルをJOINする方式を指定するヒントです。データ量・カーディナリティ・インデックスの有無によって最適なJOIN方式は変わります。

USE_NL ── ネステッドループ結合

USE_NL ── ネステッドループ結合を指定
SELECT /*+ USE_NL(d e) */ e.employee_id, e.name, d.department_name
FROM departments d
JOIN employees e ON e.department_id = d.department_id
WHERE d.department_id = 10;
-- 駆動表(d)の各行に対し、被駆動表(e)をインデックスでルックアップする
-- 少量行×インデックスありの結合に有利

USE_HASH ── ハッシュ結合

USE_HASH ── ハッシュ結合を指定
SELECT /*+ USE_HASH(e d) */ e.employee_id, e.name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id;
-- 小さいテーブル(d)のハッシュテーブルをメモリに構築し、大きいテーブル(e)をスキャンして結合する
-- 等値結合(=)で両テーブルとも大量データの場合に有利

USE_MERGE ── ソートマージ結合

USE_MERGE ── ソートマージ結合を指定
SELECT /*+ USE_MERGE(e d) */ e.employee_id, e.name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id
ORDER BY e.department_id;
-- 両テーブルをソートしてからマージする
-- 結果をソート順で取得したい場合や、ハッシュ結合のメモリが足りない場合に使う
ヒント JOIN方式 得意な場面
USE_NL(t1 t2) ネステッドループ 少量行の結合。被駆動表にインデックスがある場合
USE_HASH(t1 t2) ハッシュ 大量行同士の等値結合。メモリに収まるハッシュテーブルが作れる場合
USE_MERGE(t1 t2) ソートマージ 結果をソート順で取得したい場合。ハッシュ用メモリが不足する場合
NO_USE_NL(t) NL結合を禁止 NL結合が選ばれてほしくない場合
NO_USE_HASH(t) ハッシュ結合を禁止 ハッシュ結合が選ばれてほしくない場合

JOIN順序ヒント(LEADING / ORDERED)

テーブルの結合順序を指定するヒントです。どのテーブルを駆動表にするかで、実行計画の効率が大きく変わります。

LEADING ── 結合順序を柔軟に指定する
SELECT /*+ LEADING(d e s) USE_NL(e) USE_HASH(s) */
    e.employee_id, e.name, d.department_name, s.salary_grade
FROM departments d
JOIN employees e ON e.department_id = d.department_id
JOIN salary_grades s ON e.salary BETWEEN s.low_sal AND s.high_sal
WHERE d.location_id = 1700;
-- d → e → s の順で結合する
-- LEADING は最もよく使われるJOIN順序ヒント
ORDERED ── FROM句に書いた順序で結合する
SELECT /*+ ORDERED USE_NL(e) USE_HASH(s) */
    e.employee_id, e.name, d.department_name
FROM departments d, employees e, salary_grades s
WHERE e.department_id = d.department_id
  AND e.salary BETWEEN s.low_sal AND s.high_sal
  AND d.location_id = 1700;
-- FROM 句に書いた順序(d → e → s)で結合する
-- LEADING のほうが柔軟なので、新規SQLでは LEADING を推奨

並列処理ヒント(PARALLEL / NO_PARALLEL)

大量データを複数のプロセスで並列に処理させるヒントです。フルスキャンやDMLの高速化に効きます。

PARALLEL ── 並列処理を有効にする
-- 並列度 4 で SELECT を実行する
SELECT /*+ PARALLEL(e, 4) */ department_id, SUM(salary)
FROM employees e
GROUP BY department_id;

-- テーブルのデフォルト並列度を使う(テーブル定義に依存)
SELECT /*+ PARALLEL(e) */ department_id, SUM(salary)
FROM employees e
GROUP BY department_id;

-- 並列 INSERT
INSERT /*+ PARALLEL(t, 4) APPEND */ INTO target_table t
SELECT * FROM source_table;
NO_PARALLEL ── 並列処理を無効にする
-- テーブルにデフォルト並列度が設定されていても直列で実行する
SELECT /*+ NO_PARALLEL(e) */ employee_id, name
FROM employees e
WHERE department_id = 10;

DML高速化ヒント(APPEND / APPEND_VALUES)

INSERT文でダイレクトパスロードを行い、通常のINSERTよりも高速にデータを投入するヒントです。

APPEND ── ダイレクトパスINSERT
-- INSERT ... SELECT でダイレクトパスロードを使う
INSERT /*+ APPEND */ INTO target_table
SELECT * FROM source_table;
COMMIT;
-- ハイウォーターマーク(HWM)の先にデータを追加するため高速
-- 既存の空き領域は再利用されない

-- VALUES句でのダイレクトパスロード(Oracle 11g R2+)
INSERT /*+ APPEND_VALUES */ INTO target_table
VALUES (1, 'test', SYSDATE);
APPEND ヒントの注意点

  • INSERT後にCOMMITするまで、そのテーブルへのSELECTは ORA-12838 になる(同一トランザクション内でのアクセス制限)
  • テーブルのHWM(ハイウォーターマーク)の先に書くため、既存の空き領域は再利用されない
  • テーブルのREDOログ生成を抑制するには ALTER TABLE ... NOLOGGING と組み合わせる

サブクエリ・ビュー制御ヒント

NO_MERGE ── ビューやインラインビューのマージを防ぐ
SELECT /*+ NO_MERGE(v) */ v.department_id, v.total_salary
FROM (
    SELECT department_id, SUM(salary) AS total_salary
    FROM employees
    GROUP BY department_id
) v
WHERE v.total_salary > 1000000;
-- サブクエリをマージ(展開)せず、独立した処理単位として実行させる
-- オプティマイザが展開すると遅くなるケースで使う
PUSH_SUBQ / NO_PUSH_SUBQ ── サブクエリの評価タイミング
-- サブクエリを可能な限り早いタイミングで評価する
SELECT /*+ PUSH_SUBQ(@sq) */ e.employee_id, e.name
FROM employees e
WHERE e.department_id IN (
    SELECT /*+ QB_NAME(sq) */ d.department_id
    FROM departments d
    WHERE d.location_id = 1700
);

-- サブクエリの評価を後回しにする
SELECT /*+ NO_PUSH_SUBQ(@sq) */ e.employee_id, e.name
FROM employees e
WHERE e.department_id IN (
    SELECT /*+ QB_NAME(sq) */ d.department_id
    FROM departments d
    WHERE d.location_id = 1700
);
UNNEST / NO_UNNEST ── サブクエリのアンネスト制御
-- サブクエリをJOINに変換する(アンネスト)
SELECT e.employee_id, e.name
FROM employees e
WHERE e.department_id IN (
    SELECT /*+ UNNEST */ d.department_id
    FROM departments d
    WHERE d.location_id = 1700
);

-- サブクエリをJOINに変換させない
SELECT e.employee_id, e.name
FROM employees e
WHERE e.department_id IN (
    SELECT /*+ NO_UNNEST */ d.department_id
    FROM departments d
    WHERE d.location_id = 1700
);

その他の実用ヒント

ヒント 動作 使いどころ
FIRST_ROWS(n) 最初のn行をできるだけ早く返す計画を選ぶ ページネーション・画面表示向けSQL
ALL_ROWS 全体スループットを最大化する計画を選ぶ(デフォルト) バッチ処理・レポート向けSQL
RESULT_CACHE 結果をサーバーキャッシュに保持する 頻繁に実行される参照系のSQL
MATERIALIZE WITH句(CTE)の結果を一時表として実体化する CTEが複数回参照される場合の最適化
INLINE WITH句(CTE)を展開してインライン化する CTEの実体化がオーバーヘッドになる場合
OPT_PARAM SQLレベルでオプティマイザパラメータを変更する 特定SQLだけ挙動を変えたい場合
QB_NAME(名前) クエリブロックに名前を付ける サブクエリ・ビューへのヒント指定時の識別用
FIRST_ROWS / ALL_ROWS ── オプティマイザモードの切り替え
-- ページネーション向け:最初の20行を素早く返す
SELECT /*+ FIRST_ROWS(20) */ employee_id, name, salary
FROM employees
WHERE department_id = 10
ORDER BY salary DESC;

-- バッチ処理向け:全体スループットを優先する
SELECT /*+ ALL_ROWS */ department_id, SUM(salary)
FROM employees
GROUP BY department_id;

ヒントが無視されるケース

ヒントを書いたのに効かない場合、以下の原因が考えられます。

原因 説明 対処法
構文エラー /*+ の後にスペースがない、ヒント名のスペルミス ヒント構文を見直す。/*+ FULL(e) */+の直後にスペース推奨)
テーブル別名の不一致 FROM employees e なのにヒントで FULL(employees) と書いている エイリアスを使っている場合はヒントでもエイリアスを使う
存在しないインデックス INDEX(e idx_xxx) だが idx_xxx が存在しない USER_INDEXES でインデックス名を確認する
矛盾するヒント /*+ FULL(e) INDEX(e idx) */ のように矛盾 片方を削除する(矛盾した場合はどちらも無視される場合がある)
ビュー越しの指定 ビューの中のテーブルに直接ヒントを指定できない QB_NAME でクエリブロックを指定するか、ビュー定義自体にヒントを入れる
セマンティック上不可 非等値結合に USE_HASH は適用できない等 JOIN方式の適用条件を確認する
ヒントが効いているか実行計画で確認する
EXPLAIN PLAN FOR
SELECT /*+ FULL(e) */ employee_id, name
FROM employees e
WHERE department_id = 10;

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
-- Operation に「TABLE ACCESS FULL」が表示されればヒントが効いている
-- 「INDEX RANGE SCAN」等が表示されればヒントが無視されている

実行計画の読み方について詳しくは「【Oracle】SQLの実行計画を確認する方法」を参照してください。

主要ヒント句まとめ

カテゴリ ヒント 用途
テーブルアクセス FULL(t) フルテーブルスキャン
INDEX(t idx) 特定インデックスの使用
INDEX_FFS(t idx) インデックスファストフルスキャン
NO_INDEX(t idx) 特定インデックスの使用禁止
JOIN方式 USE_NL(t1 t2) ネステッドループ結合
USE_HASH(t1 t2) ハッシュ結合
USE_MERGE(t1 t2) ソートマージ結合
JOIN順序 LEADING(t1 t2 ...) テーブルの結合順序を指定
ORDERED FROM句の記述順で結合
並列処理 PARALLEL(t, n) 並列度nで処理
NO_PARALLEL(t) 並列処理を無効化
DML高速化 APPEND ダイレクトパスINSERT
APPEND_VALUES VALUES句のダイレクトパスINSERT
サブクエリ制御 NO_MERGE(v) ビュー/サブクエリのマージ防止
UNNEST / NO_UNNEST サブクエリのJOIN変換制御
PUSH_SUBQ / NO_PUSH_SUBQ サブクエリの評価タイミング制御

まとめ

SQLヒント句はオプティマイザの判断を人間が上書きする強力な手段ですが、使い方を間違えると逆効果になります。以下の原則を守って活用してください。

  • まずはヒントなしで実行計画を確認し、問題のある箇所を特定してからヒントを追加する
  • テーブルにエイリアスがある場合は、ヒントでも必ずエイリアスを使う
  • LEADING + USE_NL/USE_HASH の組み合わせで結合順序と方式をセットで指定すると意図どおりの計画になりやすい
  • 大量INSERT時は APPEND + PARALLEL で大幅に高速化できるが、同一トランザクション内でのSELECT制限に注意
  • ヒントは「一時的な対処」として有効だが、根本的な改善(インデックス追加・統計情報の更新・SQL書き換え)も並行して検討する
  • 統計情報が古いためにオプティマイザが誤判断している場合は、ヒントの前に DBMS_STATS.GATHER_TABLE_STATS での統計更新を試す