バインド変数を使わずに 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 では完全に別物です。
-- この3つは別々のSQL ID が割り当てられ、毎回ハードパースが走る SELECT * FROM employees WHERE department_id = 10; SELECT * FROM employees WHERE department_id = 20; SELECT * FROM employees WHERE department_id = 30;
-- テキストが同一なので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 を実行します。
-- バインド変数の宣言 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
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 が自動的にバインド変数として処理します。明示的な宣言は不要です。
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;
/
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;
/
Java(JDBC)・Python での使い方
Java(JDBC):PreparedStatement を使う
JDBC では Statement(リテラル埋め込み)ではなく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):プレースホルダーを使う
# 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 を使う) |
-- 現在の設定を確認 SELECT name, value FROM v$parameter WHERE name = 'cursor_sharing'; -- セッションレベルで変更(テスト用) ALTER SESSION SET cursor_sharing = FORCE; -- システムレベルで変更(本番変更は慎重に) ALTER SYSTEM SET cursor_sharing = FORCE;
① 変換後のバインド変数名はシステムが自動生成するため、デバッグ・チューニングが複雑になります。
② バインド変数ピーキング(後述)が発生しやすくなり、統計情報が偏ったテーブルで意図しない実行計画が選ばれることがあります。
③ 恒久対処はアプリケーション側でバインド変数を使うよう修正することを強く推奨します。
バインド変数ピーキング(Bind Variable Peeking)問題
バインド変数を使っても性能問題が起きるケースがあります。これが「バインド変数ピーキング」問題です。
Oracle の Optimizer は実行計画を作成するとき(ハードパース時)に、バインド変数の値を「のぞき見(peekする)」して最適な実行計画を生成します。これはデータの偏りを考慮するために導入された機能です。
しかし、最初のハードパース時の値に基づいた実行計画がキャッシュに残り続けるため、異なる選択性を持つ値で呼び出された場合に非効率な実行計画が使われることがあります。
-- 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 の複数カーソルを確認(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 が動作するには列にヒストグラム(統計情報)が存在する必要があります。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 がどの値で呼ばれているかを把握するのに役立ちます。
-- バインド変数の使用状況と直近のバインド値を確認
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 を特定
-- 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;
バインド変数を使ってはいけないケース
バインド変数は万能ではありません。以下のケースではリテラル埋め込みの方が適切な場合があります。
| ケース | 理由 | 対処法 |
|---|---|---|
| 1 回限りのバッチ SQL・ツール実行 | ハードパースは初回のみのため、バインド変数を使っても再利用の恩恵がない | リテラル埋め込みで問題なし |
| BI ツール・アドホッククエリ | 毎回異なる条件で 1 回ずつ実行されるため、共有できる SQL がない | リテラル埋め込みで問題なし |
| 極端に偏ったデータの列で ACS が機能しない場合 | バインド変数ピーキングにより常に同じ実行計画が使われ非効率になりうる | ヒントで実行計画を固定するか、条件分岐で別々の SQL として発行する |
| DDL・スキーマ変更の自動化スクリプト | DDL にはバインド変数が使えない(テーブル名・列名の動的指定は文字列連結のみ) | 動的 SQL(EXECUTE IMMEDIATE)でテーブル名を結合するが、入力値検証は必ず行う |
よくある質問
:name に '; DROP TABLE -- が渡されても、それはあくまで文字列の値として扱われるため、SQL として実行されることはありません。文字列連結でリテラルを埋め込む場合は値の内容が SQL 構造に影響を与えるためリスクが生じます。VARIABLE 宣言は SQL*Plus のみで必要です。バインド変数の恩恵(ハードパース削減・SQL インジェクション防止)は自動的に享受できます。IN (:val1, :val2, :val3) のように個別にバインド変数を宣言するのが基本ですが、件数が可変の場合は INLIST ITERATOR を使った動的 SQL や、コレクション型と TABLE() 関数を組み合わせる方法があります。件数が 1000 件を超える場合は別途考慮が必要です(IN 句に 1000 件以上を指定する 5 つの方法参照)。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の実行計画を確認する方法を参照してください。

