Oracle でパフォーマンスを左右する要因の一つが ハードパース(Hard Parse)です。SQL を実行するたびに毎回構文解析・最適化・実行計画の生成が走ると、CPU 負荷が高まりライブラリキャッシュ(Shared Pool)が圧迫されます。
バインド変数(Bind Variables)を使うと、同じ SQL テキストを使い回してハードパースを1回に抑えられます。OLTP システムで大量の INSERT/SELECT が実行される場合、バインド変数の有無がシステム全体のスループットに直結します。
- ハードパースとソフトパースの違いとライブラリキャッシュの仕組み
- PL/SQL でのバインド変数の自動適用(暗黙バインド)と明示的な使い方
- EXECUTE IMMEDIATE の USING 句でバインド変数を渡す方法
- CURSOR_SHARING パラメータ(EXACT / FORCE)の特徴と副作用
- バインド変数ピーキングと実行計画のゆらぎ問題
- Adaptive Cursor Sharing(ACS)で最適な計画を使い分ける仕組み
ハードパースとソフトパースの違い
Oracle が SQL を実行する際の処理フローは次のとおりです。
| 処理フェーズ | ハードパース(初回) | ソフトパース(2回目以降) |
|---|---|---|
| 構文解析(Syntax Check) | 実行 | スキップ |
| 意味解析(Semantic Check) | 実行 | スキップ |
| オブジェクト名の解決・権限確認 | 実行 | スキップ |
| 最適化(オプティマイザによる実行計画生成) | 実行(高コスト) | スキップ |
| 実行計画のライブラリキャッシュへの格納 | 実行 | スキップ |
| 実行計画の取り出し | – | 実行(キャッシュヒット) |
| 実行フェーズ | 実行 | 実行 |
ライブラリキャッシュは SQL テキストが完全一致するときにヒットします。スペース1つ・大小文字の違い・リテラル値の違いがあるだけでキャッシュミスとなり、ハードパースが走ります。
-- NG: リテラルを埋め込む(実行のたびにハードパースが走る) -- 以下の3つは Oracle には別々の SQL として扱われる SELECT * FROM orders WHERE customer_id = 101; SELECT * FROM orders WHERE customer_id = 102; SELECT * FROM orders WHERE customer_id = 103; -- → 毎回ハードパース発生。1000件のリクエストで1000回のハードパース -- OK: バインド変数を使う(ライブラリキャッシュをヒットさせる) SELECT * FROM orders WHERE customer_id = :cust_id; -- → 最初の1回だけハードパース。以降はソフトパースでヒット -- V$SQL でハードパースの状況を確認する SELECT sql_text, parse_calls, executions, loads FROM V$SQL WHERE sql_text LIKE '%orders%customer_id%' AND command_type = 3 -- SELECT ORDER BY parse_calls DESC; -- loads: ハードパース回数(parse_calls - loadsがソフトパース数) -- LOADS が多い = バインド変数未使用のリテラル SQL が原因の可能性が高い
PL/SQL でのバインド変数(暗黙バインドと明示的バインド)
PL/SQL では静的 SQL を書くと Oracle が自動的にバインド変数として扱います。変数を直接 SQL 中で参照するだけで暗黙的にバインドが行われます。
-- 暗黙バインド: PL/SQL 変数は自動的にバインド変数として処理される
DECLARE
v_cust_id orders.customer_id%TYPE := 101;
v_count NUMBER;
BEGIN
-- v_cust_id はバインド変数として扱われる(Oracle が自動でバインドする)
SELECT COUNT(*) INTO v_count
FROM orders
WHERE customer_id = v_cust_id; -- ← 暗黙バインド(ハードパースは1回)
DBMS_OUTPUT.PUT_LINE(v_cust_id || '件: ' || v_count);
v_cust_id := 102;
SELECT COUNT(*) INTO v_count
FROM orders
WHERE customer_id = v_cust_id; -- ← 同じ SQL テキスト → ソフトパース
DBMS_OUTPUT.PUT_LINE(v_cust_id || '件: ' || v_count);
END;
/
-- カーソルでも同様(バインド変数として扱われる)
DECLARE
CURSOR c_orders(p_cust_id NUMBER) IS
SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id = p_cust_id; -- ← バインド変数
BEGIN
FOR rec IN c_orders(101) LOOP
DBMS_OUTPUT.PUT_LINE(rec.order_id || ': ' || rec.total_amount);
END LOOP;
FOR rec IN c_orders(102) LOOP -- 同じ SQL → ソフトパース
DBMS_OUTPUT.PUT_LINE(rec.order_id || ': ' || rec.total_amount);
END LOOP;
END;
/
EXECUTE IMMEDIATE でバインド変数を使う
動的 SQL(EXECUTE IMMEDIATE)では USING 句でバインド変数の値を渡します。SQL テキストの中では :1 や :cust_id のように名前付きプレースホルダを書きます。
-- EXECUTE IMMEDIATE + USING: 動的 SQL でもバインド変数を使う
DECLARE
v_sql VARCHAR2(500);
v_count NUMBER;
v_cust_id NUMBER := 101;
v_year NUMBER := 2024;
BEGIN
-- プレースホルダは :名前 形式で書く
v_sql := 'SELECT COUNT(*) FROM orders WHERE customer_id = :cust_id AND EXTRACT(YEAR FROM order_date) = :yr';
-- USING で値を渡す(順序は SQL テキスト内のプレースホルダの順序)
EXECUTE IMMEDIATE v_sql INTO v_count USING v_cust_id, v_year;
DBMS_OUTPUT.PUT_LINE('件数: ' || v_count);
-- IN/OUT パラメータも使える
-- IN OUT は USING に IN OUT または IN/OUT で指定する(デフォルトは IN)
END;
/
-- EXECUTE IMMEDIATE + DML でのバインド変数
DECLARE
v_sql VARCHAR2(500);
v_order_id NUMBER := 9999;
v_new_total NUMBER := 12500;
BEGIN
v_sql := 'UPDATE orders SET total_amount = :new_total WHERE order_id = :oid';
EXECUTE IMMEDIATE v_sql USING v_new_total, v_order_id;
COMMIT;
END;
/
-- BULK COLLECT との組み合わせ
DECLARE
TYPE num_tab IS TABLE OF NUMBER;
v_sql VARCHAR2(500);
v_ids num_tab;
v_dept NUMBER := 10;
BEGIN
v_sql := 'SELECT employee_id FROM employees WHERE department_id = :dept_id';
EXECUTE IMMEDIATE v_sql BULK COLLECT INTO v_ids USING v_dept;
DBMS_OUTPUT.PUT_LINE('部門 ' || v_dept || ' の社員数: ' || v_ids.COUNT);
END;
/
CURSOR_SHARING パラメータ
CURSOR_SHARING は、リテラルが埋め込まれた SQL をOracle が自動的にバインド変数に置き換えるかどうかを制御するパラメータです。
| 値 | 動作 | 推奨 |
|---|---|---|
EXACT(デフォルト) |
SQL テキストが完全一致する場合のみカーソルを共有する | 推奨(正確な動作) |
FORCE |
リテラルをシステムが自動的にバインド変数に置き換えてカーソルを強制共有する | 緊急措置のみ(副作用あり) |
FORCE に設定するとヒストグラム情報を活用した列のフィルタリング精度が落ちることがあります。特定の値に対して最適な実行計画が選ばれなくなる可能性があるため、根本的な解決策はアプリケーション側でバインド変数を使うコードに修正することです。FORCE は既存アプリケーションを短期間で応急処置する場合の手段です。
-- 現在の CURSOR_SHARING 設定を確認する
SHOW PARAMETER cursor_sharing;
-- または
SELECT name, value FROM V$PARAMETER WHERE name = 'cursor_sharing';
-- セッション単位で変更する(テスト・調査目的)
ALTER SESSION SET CURSOR_SHARING = FORCE;
-- システム全体で変更する(本番での応急措置)
ALTER SYSTEM SET CURSOR_SHARING = FORCE SCOPE = BOTH;
-- ライブラリキャッシュにリテラルが大量にある SQL を確認する
-- FORCE_MATCHING_SIGNATURE が同じ = CURSOR_SHARING=FORCE なら共有できる SQL
SELECT force_matching_signature, COUNT(*) AS cursor_count,
MAX(sql_text) AS sample_sql
FROM V$SQL
WHERE force_matching_signature != 0
GROUP BY force_matching_signature
HAVING COUNT(*) > 100 -- 100 個以上同じ SQL が溜まっているもの
ORDER BY cursor_count DESC;
-- カーソル数が多いほどバインド変数化の効果が大きい
バインド変数ピーキングと実行計画の問題
Oracle はバインド変数ピーキング(Bind Variable Peeking)という仕組みを持ちます。ハードパース(初回実行)時に、Oracle はバインド変数の実際の値を「覗き見(peek)」して最適な実行計画を生成します。この計画がライブラリキャッシュに保存され、2回目以降は同じ計画が使われます。
問題は、初回のバインド変数の値に偏りがある場合です。たとえば :status = 'PENDING'(全体の 0.1%)でハードパースが起きるとインデックス使用の計画が生成されますが、次に :status = 'COMPLETED'(全体の 90%)でその計画が再利用されるとフルスキャンの方が速い場面でインデックスが使われてしまいます。
-- バインド変数に対してどの計画が使われているかを確認する
SELECT sql_id, plan_hash_value, executions, rows_processed,
elapsed_time / 1000 AS elapsed_ms,
is_bind_sensitive, -- バインド値によって最適計画が変わりうるかどうか
is_bind_aware -- ACS が有効(適応型カーソル共有が動作中)かどうか
FROM V$SQL
WHERE sql_text LIKE '%orders%status%'
ORDER BY executions DESC;
-- SQL_ID から実行計画を確認する
SELECT * FROM TABLE(
DBMS_XPLAN.DISPLAY_CURSOR(
sql_id => '3abc1defg234x',
cursor_child_no => 0,
format => 'ALLSTATS LAST'
)
);
-- child_no が複数ある場合 → Adaptive Cursor Sharing が動いて複数の計画が保持されている
Adaptive Cursor Sharing(アダプティブカーソル共有)
Adaptive Cursor Sharing(ACS)は Oracle 11g 以降のデフォルト機能です。バインド変数の値によって実行計画を使い分ける仕組みで、バインド変数ピーキングの問題を自動的に緩和します。
ACS が動作する流れは次のとおりです。
- 初回ハードパース時にバインド値を覗いて計画を生成する(
IS_BIND_SENSITIVE = YES) - 同じ計画が異なるバインド値で使われ続けると、実行統計(行数・コスト)をモニタリングする
- バインド値によって結果の行数が大きく変わると判断されたら、新しいバインド値で再ハードパースして別の計画を生成する(
IS_BIND_AWARE = YES) - 以降は同様のバインド値の範囲に対して最適な計画が使い回される(child_no = 0, 1, 2… と複数の子カーソルが保持される)
-- ACS の動作状況を確認する
SELECT sql_id, child_number, plan_hash_value, executions,
is_bind_sensitive, -- YES: バインド値に応じて計画が変わりうる
is_bind_aware, -- YES: ACS が有効でバインド値ごとに計画を使い分け中
is_shareable -- NO: このカーソルは使われなくなった(無効化済み)
FROM V$SQL
WHERE sql_id = '3abc1defg234x'
ORDER BY child_number;
-- child_number が 0, 1, 2 ... と複数あれば ACS が複数の計画を保持している
-- 各子カーソルのバインド値の範囲を確認する
SELECT *
FROM V$SQL_CS_SELECTIVITY
WHERE sql_id = '3abc1defg234x';
-- LOW / HIGH: このカーソルが適用されるバインド値の選択率の範囲
-- SELECTIVITY: このカーソルが最適となる選択率レンジ
-- ACS が有効になっている SQL を探す
SELECT sql_id, sql_text, child_number, plan_hash_value
FROM V$SQL
WHERE is_bind_aware = 'Y'
AND executions > 1000 -- 高頻度 SQL に絞る
ORDER BY executions DESC;
バインド変数を使うべきでない場面
- DDL(CREATE / ALTER / DROP):DDL にはバインド変数を使えない。毎回一意の SQL になるが DDL の実行頻度は通常低いため問題なし
- オブジェクト名(テーブル名・列名):バインド変数はオブジェクト名に使えない。DBMS_ASSERT.SQL_OBJECT_NAME でホワイトリスト検証して動的 SQL を組み立てる
- 極端なデータ分布での一括処理:バッチ処理で毎回フルスキャンが最適な場合(例: 全レコードを処理する夜間バッチ)は NO_BIND ヒントでハードコードする方が安定することがある
- IN リストの件数が毎回変わる場合:
WHERE id IN (:1, :2, :3)は件数が固定でないとキャッシュヒットしない。件数が変わる場合は一時テーブルや BULK COLLECT を使う
まとめ
- ハードパースのコスト:SQL テキストが完全一致しないとライブラリキャッシュミスになりハードパースが走る。CPU 消費とライブラリキャッシュラッチ競合の主因
- PL/SQL の暗黙バインド:PL/SQL の静的 SQL では変数が自動的にバインド変数として扱われる。意図せずリテラルになることはない
- EXECUTE IMMEDIATE の USING 句:動的 SQL でも :bind_var + USING でバインド変数を利用できる。SQL テキストに直接値を連結しない
- CURSOR_SHARING=FORCE:アプリケーション修正なしにリテラル SQL をバインド変数化できるが、実行計画の精度が落ちる副作用がある。根本解決はアプリ側での対応
- バインド変数ピーキング:初回ハードパース時の値で実行計画が固定される。データ分布に大きな偏りがある列のフィルタリングに注意
- Adaptive Cursor Sharing:Oracle 11g 以降のデフォルト機能。バインド値の選択率に応じて複数の計画を自動で使い分けてピーキング問題を緩和する
実行計画の確認と解析は DBMS_XPLAN 完全ガイドを参照してください。アダプティブ最適化の詳細は アダプティブクエリ最適化完全ガイドも参照してください。