【Oracle】バインド変数完全ガイド|ハードパース削減・ピーキング問題・Adaptive Cursor Sharingまで解説

【Oracle】バインド変数完全ガイド|ハードパース削減・ピーキング問題・Adaptive Cursor Sharingまで解説 Oracle

バインド変数を使わずに SQL を発行し続けると、本番データベースの共有プールが枯渇してパフォーマンスが数十倍悪化することがあります。原因は、リテラル値(定数)を直接埋め込んだ SQL は毎回「新しい SQL」として扱われ、構文解析(ハードパース)が繰り返し発生するためです。

一方でバインド変数にも落とし穴があります。バインド変数ピーキングと呼ばれる現象が原因で、偏ったデータ分布のテーブルではバインド変数を使うと逆に遅くなるケースが存在します。

この記事では、バインド変数がパフォーマンスに効く仕組みを共有プールの観点から解説し、各環境(SQL*Plus・PL/SQL・Java・Python)での使い方、CURSOR_SHARING パラメータ、バインド変数ピーキング問題とAdaptive Cursor Sharing(11g 以降)による解決策、V$SQL_BIND_CAPTURE を使った監視方法まで一通り説明します。

この記事でわかること
・バインド変数がパフォーマンスに効く仕組み(ハードパース vs ソフトパース・共有プール)
・SQL*Plus・PL/SQL・Java・Python でのバインド変数の使い方
・CURSOR_SHARING パラメータの概要と使いどころ
・バインド変数ピーキング問題の原因と対処法
・Adaptive Cursor Sharing(11g 以降)で何が改善されたか
・V$SQL / V$SQL_BIND_CAPTURE でバインド変数の状況を確認する方法
・バインド変数を使ってはいけないケース
スポンサーリンク

バインド変数が効く仕組み:ハードパースとソフトパースの違い

Oracle が SQL を受け取った際の処理フローには「ハードパース」と「ソフトパース」の 2 種類があります。

処理 説明 コスト
ハードパース SQL テキストを構文解析・意味解析し、最適な実行計画を生成してライブラリキャッシュに登録する 高(CPU・ラッチ競合・メモリ消費が大きい)
ソフトパース ライブラリキャッシュ内に同一 SQL が存在する場合、既存の実行計画を再利用する 低(ハードパースの数十分の一)

Oracle は SQL テキストが 1 文字でも異なると別の SQL として扱います。以下の 3 つの SQL は Oracle では完全に別物です。

SQL(リテラル埋め込み:毎回ハードパース発生)
-- この3つは別々のSQL ID が割り当てられ、毎回ハードパースが走る
SELECT * FROM employees WHERE department_id = 10;
SELECT * FROM employees WHERE department_id = 20;
SELECT * FROM employees WHERE department_id = 30;
SQL(バインド変数:1回ハードパースして以降はソフトパース)
-- テキストが同一なので1つのSQL IDに集約。2回目以降はソフトパース
SELECT * FROM employees WHERE department_id = :dept_id;
共有プール(ライブラリキャッシュ)の仕組み
Oracle はハードパース済みの SQL とその実行計画を共有プール(SGA 内のライブラリキャッシュ)に保存します。次回同一の SQL テキストが来た際に再利用する仕組みです。
バインド変数を使わない場合、部署数が 100 なら 100 種類の SQL がキャッシュに積まれ、古いキャッシュがどんどん追い出される(LRU)ことでキャッシュヒット率が下がり、全体のパフォーマンスが著しく低下します。
指標 リテラル埋め込み バインド変数使用
SQL ごとのライブラリキャッシュエントリ 値ごとに 1 件(大量エントリ) 1 件(再利用)
ハードパース頻度 毎回発生 初回のみ
CPU 使用率(大量 DML 時)
共有プールの消費量
SQL インジェクションリスク あり なし

SQL*Plus でのバインド変数の使い方

SQL*Plus では VARIABLE コマンドでバインド変数を宣言し、EXEC(または BEGIN/END ブロック)で値を設定してから SQL を実行します。

SQL(SQL*Plus でのバインド変数宣言と使用)
-- バインド変数の宣言
VARIABLE dept_id NUMBER
VARIABLE emp_name VARCHAR2(50)

-- 値を設定
EXEC :dept_id := 10
EXEC :emp_name := 'SCOTT'

-- バインド変数を使って SELECT
SELECT employee_id, last_name, salary
FROM   employees
WHERE  department_id = :dept_id;

-- 現在の値を確認
PRINT dept_id
SQL(SQL*Plus:OUT バインド変数で PL/SQL の戻り値を受け取る)
VARIABLE result_salary NUMBER

BEGIN
    SELECT salary
    INTO   :result_salary
    FROM   employees
    WHERE  employee_id = 100;
END;
/

PRINT result_salary

PL/SQL での使い方:静的 SQL では自動的にバインド変数になる

PL/SQL ブロック内で静的 SQL(変数を直接 SQL 内で参照する形)を使うと、Oracle が自動的にバインド変数として処理します。明示的な宣言は不要です。

SQL(PL/SQL 静的 SQL:自動的にバインド変数化される)
DECLARE
    v_dept_id  employees.department_id%TYPE := 10;
    v_count    NUMBER;
BEGIN
    -- v_dept_id は Oracle が自動的にバインド変数として処理する
    SELECT COUNT(*)
    INTO   v_count
    FROM   employees
    WHERE  department_id = v_dept_id;  -- バインド変数として扱われる

    DBMS_OUTPUT.PUT_LINE('件数: ' || v_count);
END;
/
SQL(PL/SQL:NG パターン – 文字列連結でリテラル埋め込み)
DECLARE
    v_dept_id NUMBER := 10;
    v_sql     VARCHAR2(200);
BEGIN
    -- BAD: 文字列連結でリテラルを埋め込むとハードパースが毎回発生
    -- さらに SQL インジェクションリスクがある
    v_sql := 'SELECT COUNT(*) FROM employees WHERE department_id = ' || v_dept_id;
    -- EXECUTE IMMEDIATE v_sql の場合もバインド変数を使うべき
END;
/
動的 SQL(EXECUTE IMMEDIATE)でのバインド変数の使い方(USING 句)については、【PL/SQL】動的SQLのセキュアな書き方で詳しく解説しています。

Java(JDBC)・Python での使い方

Java(JDBC):PreparedStatement を使う

JDBC では Statement(リテラル埋め込み)ではなくPreparedStatement を使うことでバインド変数が有効になります。

Java(PreparedStatement でバインド変数を使用)
// NG: Statement はリテラル埋め込みになりハードパースが毎回発生
// Statement stmt = conn.createStatement();
// stmt.executeQuery("SELECT * FROM employees WHERE department_id = " + deptId);

// OK: PreparedStatement でバインド変数を使用
String sql = "SELECT employee_id, last_name, salary FROM employees WHERE department_id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, deptId);   // 1番目の ? に値を設定
ResultSet rs = pstmt.executeQuery();

// ループで繰り返し使う場合(SQL の再利用が効く)
int[] deptIds = {10, 20, 30};
for (int id : deptIds) {
    pstmt.setInt(1, id);
    rs = pstmt.executeQuery();
    // 結果処理...
}
pstmt.close();

Python(python-oracledb / cx_Oracle):プレースホルダーを使う

Python(python-oracledb でバインド変数を使用)
# NG: 文字列フォーマットでリテラルを埋め込む
# cursor.execute(f"SELECT * FROM employees WHERE department_id = {dept_id}")

# OK: 名前付きバインド変数を使用
sql = "SELECT employee_id, last_name FROM employees WHERE department_id = :dept_id"
cursor.execute(sql, dept_id=10)

# OK: 位置バインド変数を使用(:1 形式)
cursor.execute(
    "SELECT employee_id, last_name FROM employees WHERE department_id = :1 AND salary > :2",
    [10, 50000]
)

# 複数行 INSERT(executemany でバルク処理)
data = [(10, "Alice", 70000), (20, "Bob", 65000)]
cursor.executemany(
    "INSERT INTO employees (dept_id, name, salary) VALUES (:1, :2, :3)",
    data
)
conn.commit()

CURSOR_SHARING パラメータ:既存コードを変えずにバインド変数効果を得る

既存アプリケーションのコードを修正できない場合、CURSOR_SHARING パラメータを変更することで Oracle 側でリテラルをバインド変数に自動変換させることができます。

設定値 動作 使いどころ
EXACT(デフォルト) SQL テキストが完全一致した場合のみカーソルを共有 通常の運用(バインド変数を使うアプリ前提)
FORCE すべてのリテラルをバインド変数に自動変換 改修困難な既存アプリへの暫定対応
SIMILAR(廃止) 旧: リテラルが似た SQL を同一視(12c 以降廃止) 使用不可(EXACT か FORCE を使う)
SQL(CURSOR_SHARING の確認と変更)
-- 現在の設定を確認
SELECT name, value FROM v$parameter WHERE name = 'cursor_sharing';

-- セッションレベルで変更(テスト用)
ALTER SESSION SET cursor_sharing = FORCE;

-- システムレベルで変更(本番変更は慎重に)
ALTER SYSTEM SET cursor_sharing = FORCE;
CURSOR_SHARING = FORCE を本番で使う場合の注意点
① 変換後のバインド変数名はシステムが自動生成するため、デバッグ・チューニングが複雑になります。
② バインド変数ピーキング(後述)が発生しやすくなり、統計情報が偏ったテーブルで意図しない実行計画が選ばれることがあります。
③ 恒久対処はアプリケーション側でバインド変数を使うよう修正することを強く推奨します。

バインド変数ピーキング(Bind Variable Peeking)問題

バインド変数を使っても性能問題が起きるケースがあります。これが「バインド変数ピーキング」問題です。

Oracle の Optimizer は実行計画を作成するとき(ハードパース時)に、バインド変数の値を「のぞき見(peekする)」して最適な実行計画を生成します。これはデータの偏りを考慮するために導入された機能です。
しかし、最初のハードパース時の値に基づいた実行計画がキャッシュに残り続けるため、異なる選択性を持つ値で呼び出された場合に非効率な実行計画が使われることがあります。

SQL(ピーキング問題が発生するシナリオ)
-- status = 'ACTIVE' のデータは全体の99%(フルスキャンが最適)
-- status = 'CLOSED' のデータは全体の0.1%(インデックスが最適)

-- 最初に CLOSED で実行 → インデックス使用の実行計画がキャッシュに保存される
SELECT * FROM orders WHERE status = :status;  -- :status = 'CLOSED'

-- 次に ACTIVE で実行 → キャッシュされたインデックス実行計画が使われ超低速
SELECT * FROM orders WHERE status = :status;  -- :status = 'ACTIVE' ← 遅い!
バインド変数ピーキング問題が起きやすい条件
① 列の値分布が偏っている(例: status 列が “ACTIVE” 99%・”CLOSED” 1%)
② その列にヒストグラムが存在する
③ バインド変数を使っている(リテラル埋め込みなら毎回ハードパースするため問題にならない)

Adaptive Cursor Sharing(11g 以降)による改善

Oracle 11g では Adaptive Cursor Sharing(ACS)が導入され、バインド変数ピーキング問題が大幅に改善されました。

ACS は同一の SQL に対してバインド値の「選択性の範囲」ごとに複数の実行計画を保持する仕組みです。最初の実行計画で処理したとき選択性が想定と大きく異なる場合、次回は別の実行計画でリパース(再ハードパース)し、その結果を新たにキャッシュに追加します

SQL(ACS の状態を確認する)
-- 同一 SQL の複数カーソルを確認(IS_BIND_SENSITIVE, IS_BIND_AWARE)
SELECT sql_id,
       child_number,
       executions,
       is_bind_sensitive,   -- 値によって実行計画が変わりうるか
       is_bind_aware,       -- ACS が有効化されているか
       plan_hash_value
FROM   v$sql
WHERE  sql_text LIKE '%orders%status%'
ORDER  BY sql_id, child_number;
列名 意味
IS_BIND_SENSITIVE Y 選択性がバインド値によって変わりうる SQL(ACS の監視対象)
IS_BIND_AWARE Y ACS により複数の実行計画を保持するモードになっている
IS_BIND_AWARE N 初回または値の分布が均一で ACS が不要と判断されている
ACS を活用するためのヒント
・ACS が動作するには列にヒストグラム(統計情報)が存在する必要があります。DBMS_STATS でヒストグラムを収集しておきましょう。
OPT_PARAM('_optim_peek_user_binds' 'false') ヒントでピーキングを無効化することも可能ですが、ACS を活用する方が推奨されます。
・ACS でも対応が難しい場合は BIND_AWARE ヒント(/*+ BIND_AWARE */)を使って強制的に ACS モードを有効化できます。

V$SQL / V$SQL_BIND_CAPTURE でバインド変数の状況を確認する

バインド変数の使用状況と実際に使われた値はV$SQL および V$SQL_BIND_CAPTURE で確認できます。パフォーマンス問題の診断時に特定の SQL がどの値で呼ばれているかを把握するのに役立ちます。

SQL(V$SQL_BIND_CAPTURE:実際に使われたバインド変数の値を確認)
-- バインド変数の使用状況と直近のバインド値を確認
SELECT s.sql_id,
       s.executions,
       s.elapsed_time / NULLIF(s.executions, 0) AS avg_elapsed_us,
       b.name          AS bind_name,
       b.datatype_string,
       b.value_string  AS last_value,
       b.last_captured
FROM   v$sql s
       JOIN v$sql_bind_capture b ON s.sql_id = b.sql_id
                                 AND s.child_number = b.child_number
WHERE  s.sql_text LIKE '%employees%'
ORDER  BY s.executions DESC
FETCH  FIRST 20 ROWS ONLY;
SQL(バインド変数を使っていない SQL を特定する)
-- ライブラリキャッシュ内でハードパース多発・リテラル埋め込みの疑いがある SQL を特定
-- force_matching_signature が同じで SQL_ID が異なる → リテラル違いの同一構造 SQL
SELECT force_matching_signature,
       COUNT(*)           AS cursor_count,
       SUM(executions)    AS total_exec,
       MIN(sql_text)      AS sample_sql
FROM   v$sql
WHERE  force_matching_signature != 0
GROUP  BY force_matching_signature
HAVING COUNT(*) > 10        -- 同一構造で10種類以上のSQLがある場合が要注意
ORDER  BY cursor_count DESC
FETCH  FIRST 20 ROWS ONLY;
V$SQL の詳細な使い方(SQL_ID・ELAPSED_TIME・EXECUTIONS による遅い SQL の特定など)については【Oracle】V$SQLビューの使い方を参照してください。実行計画の確認方法(EXPLAIN PLAN・AUTOTRACE)については【Oracle】SQLの実行計画を確認する方法を参照してください。

バインド変数を使ってはいけないケース

バインド変数は万能ではありません。以下のケースではリテラル埋め込みの方が適切な場合があります。

ケース 理由 対処法
1 回限りのバッチ SQL・ツール実行 ハードパースは初回のみのため、バインド変数を使っても再利用の恩恵がない リテラル埋め込みで問題なし
BI ツール・アドホッククエリ 毎回異なる条件で 1 回ずつ実行されるため、共有できる SQL がない リテラル埋め込みで問題なし
極端に偏ったデータの列で ACS が機能しない場合 バインド変数ピーキングにより常に同じ実行計画が使われ非効率になりうる ヒントで実行計画を固定するか、条件分岐で別々の SQL として発行する
DDL・スキーマ変更の自動化スクリプト DDL にはバインド変数が使えない(テーブル名・列名の動的指定は文字列連結のみ) 動的 SQL(EXECUTE IMMEDIATE)でテーブル名を結合するが、入力値検証は必ず行う

よくある質問

バインド変数を使うとなぜ SQL インジェクションを防げるのですか?
バインド変数を使うと SQL の構造(テキスト)と値が分離され、値は SQL の一部として解釈されません。例えば :name'; DROP TABLE -- が渡されても、それはあくまで文字列の値として扱われるため、SQL として実行されることはありません。文字列連結でリテラルを埋め込む場合は値の内容が SQL 構造に影響を与えるためリスクが生じます。
PL/SQL の静的 SQL はバインド変数を宣言しなくても大丈夫ですか?
はい、PL/SQL ブロック内で変数を SQL に直接使う静的 SQL では、Oracle が自動的にバインド変数として処理します。明示的な VARIABLE 宣言は SQL*Plus のみで必要です。バインド変数の恩恵(ハードパース削減・SQL インジェクション防止)は自動的に享受できます。
バインド変数ピーキング問題は全バージョンで発生しますか?
ピーキング自体は Oracle 9i 以降で導入されています。問題になりやすいのは Oracle 10g 以前です。11g 以降では Adaptive Cursor Sharing(ACS)が導入され、選択性の変化に応じて複数の実行計画を保持するようになったため、大幅に緩和されています。ただし ACS が機能するにはヒストグラムが必要なため、統計情報の定期収集は欠かせません。
CURSOR_SHARING = FORCE に設定すれば全部解決しますか?
暫定対処としては有効ですが、根本解決にはなりません。FORCE に設定するとデバッグが複雑になり、バインド変数ピーキング問題が発生しやすくなるデメリットもあります。アプリケーションコードを修正してバインド変数を使うことが最善策です。修正が困難な場合の応急処置として使い、恒久対応と並行して計画してください。
IN 句にバインド変数を使う場合、どうすればよいですか?
標準 SQL では IN (:val1, :val2, :val3) のように個別にバインド変数を宣言するのが基本ですが、件数が可変の場合は INLIST ITERATOR を使った動的 SQL や、コレクション型と TABLE() 関数を組み合わせる方法があります。件数が 1000 件を超える場合は別途考慮が必要です(IN 句に 1000 件以上を指定する 5 つの方法参照)。
OUT パラメータのバインド変数は使えますか?
SQL*Plus では VARIABLE で宣言した変数をトリガーや PL/SQL の OUT パラメータで受け取れます。JDBC では CallableStatement を使い registerOutParameter() で OUT パラメータを登録します。Python(python-oracledb)では cursor.var() で OUT 変数を作成し、cursor.execute(sql, [in_val, out_var]) の形で渡します。

まとめ

Oracle バインド変数の要点をまとめます。

やりたいこと 方法・ポイント
ハードパースを減らしてパフォーマンスを改善する バインド変数を使い SQL テキストを統一。共有プールの再利用率を高める
SQL*Plus でバインド変数を使う VARIABLE で宣言 → EXEC で値設定 → SQL 内で :変数名 で参照
PL/SQL 静的 SQL でバインド変数を使う 変数を SQL 内で直接参照するだけで自動的にバインド変数化される
Java でバインド変数を使う Statement ではなく PreparedStatement を使う(? プレースホルダー)
Python でバインド変数を使う cursor.execute(sql, :name=value) または :1 形式のプレースホルダーを使う
既存コードを変えずにバインド変数効果を得る CURSOR_SHARING = FORCE(暫定対処。根本解決ではない)
バインド変数ピーキング問題を回避する DBMS_STATS でヒストグラムを収集し Adaptive Cursor Sharing(11g+)に任せる
バインド変数の実使用値を調べる V$SQL_BIND_CAPTURE の value_string / last_captured で確認する
リテラル埋め込み SQL を特定する V$SQL の force_matching_signature で同一構造の SQL をグルーピングして確認する

動的 SQL(EXECUTE IMMEDIATE)でのバインド変数活用については【PL/SQL】動的SQLのセキュアな書き方、V$SQL を使った SQL 監視の詳細は【Oracle】V$SQLビューの使い方、実行計画の読み方は【Oracle】SQLの実行計画を確認する方法を参照してください。