Oracle PL/SQL の 動的SQL とは、実行時に SQL 文字列を組み立てて実行する仕組みです。通常の静的 SQL はコンパイル時に構文が確定しますが、動的 SQL を使うとテーブル名・列名・条件を変数で渡したり、PL/SQL から DDL(CREATE TABLE・TRUNCATE など)を実行したりできます。
Oracle の動的 SQL には主に2つの方法があります。
| 方法 | 特徴 | 使い分け |
|---|---|---|
| EXECUTE IMMEDIATE | シンプルな構文。1文の動的 SQL に最適 | ほぼすべての場面でこちらを使う(推奨) |
| DBMS_SQL | 低レベル API。カーソル操作が必要な高度な制御に使う | 列数が実行時まで不明な場合など特殊なケースのみ |
この記事でわかること
- EXECUTE IMMEDIATE の基本構文と DDL 実行パターン
- バインド変数(USING 句)で安全に値を渡す方法
- SELECT INTO との組み合わせ(INTO 句)
- RETURNING INTO で DML の結果を受け取る
- BULK COLLECT INTO で複数行を一括取得する
- OPEN FOR でカーソルとして使う(REF CURSOR)
- SQL インジェクションの仕組みと防止策
- DBMS_SQL との使い分け基準
なぜ動的 SQL が必要か:静的 SQL の限界
PL/SQL の静的 SQL はコンパイル時にテーブル名・列名が確定している必要があります。以下のような要件は静的 SQL では実現できず、動的 SQL が必要です。
- DDL の実行:PL/SQL ブロック内で
CREATE TABLE・TRUNCATE・DROPを実行したい - テーブル名・列名を変数にしたい:処理対象テーブルを引数で切り替えるプロシージャ
- WHERE 条件を動的に組み立てたい:検索条件の数が実行時まで不明な場合
- 動的な ORDER BY:ソート列を呼び出し元から指定させたい
EXECUTE IMMEDIATE の基本構文
EXECUTE IMMEDIATE の構文パターン一覧
-- ① DDL / DML(戻り値なし) EXECUTE IMMEDIATE 'SQL文字列'; -- ② バインド変数で値を渡す(USING 句) EXECUTE IMMEDIATE 'SQL文字列' USING 変数1, 変数2, ...; -- ③ SELECT の結果を受け取る(INTO 句) EXECUTE IMMEDIATE 'SELECT ...' INTO 変数 [USING 入力変数]; -- ④ DML の影響行を受け取る(RETURNING INTO) EXECUTE IMMEDIATE 'DML文' RETURNING INTO 変数 [USING 入力変数]; -- ⑤ 複数行を一括取得(BULK COLLECT INTO) EXECUTE IMMEDIATE 'SELECT ...' BULK COLLECT INTO コレクション変数; -- ⑥ REF CURSOR として開く(OPEN FOR) OPEN カーソル変数 FOR 'SELECT ...' [USING 変数];
DDL を PL/SQL から実行する
PL/SQL の静的 SQL では DDL を直接書けません(コンパイルエラー)。EXECUTE IMMEDIATE を使うと DDL を実行できます。
DDL の実行(CREATE / TRUNCATE / DROP)
BEGIN
-- テーブル作成(静的 SQL では PL/SQL ブロック内に CREATE は書けない)
EXECUTE IMMEDIATE '
CREATE TABLE work_log (
log_id NUMBER GENERATED ALWAYS AS IDENTITY,
log_date DATE DEFAULT SYSDATE,
message VARCHAR2(4000)
)';
-- TRUNCATE(DELETE と違いロールバック不可。DDL なので即コミット)
EXECUTE IMMEDIATE 'TRUNCATE TABLE work_log';
-- DROP(存在チェックなしで DROP すると ORA-00942 が発生するため注意)
EXECUTE IMMEDIATE 'DROP TABLE work_log';
EXCEPTION
WHEN OTHERS THEN
-- ORA-00942: テーブルまたはビューが存在しません
IF SQLCODE = -942 THEN
DBMS_OUTPUT.PUT_LINE('テーブルが存在しません: ' || SQLERRM);
ELSE
RAISE;
END IF;
END;
/
TRUNCATE は DDL のため自動コミットされる
EXECUTE IMMEDIATE 'TRUNCATE TABLE t' は DDL であり、実行と同時に暗黙コミットが発生します。ロールバックできないため、本番環境では必ず事前バックアップを取ってから実行してください。
テーブル名・列名を変数にする
テーブル名や列名はバインド変数(USING 句)では渡せません。文字列の連結で SQL を組み立てる必要があります。ただし連結方式は SQL インジェクションのリスクがあるため、後述する入力値の検証が必須です。
テーブル名を引数にするプロシージャ
CREATE OR REPLACE PROCEDURE truncate_table (
p_table_name IN VARCHAR2
) AS
v_sql VARCHAR2(200);
BEGIN
-- テーブル名の入力検証(英数字とアンダースコアのみ許可)
IF NOT REGEXP_LIKE(p_table_name, '^[A-Za-z0-9_]+$') THEN
RAISE_APPLICATION_ERROR(-20001,
'不正なテーブル名: ' || p_table_name);
END IF;
-- テーブル名を文字列連結して SQL を組み立てる
-- DBMS_ASSERT.SIMPLE_SQL_NAME でスキーマオブジェクト名を検証するのも有効
v_sql := 'TRUNCATE TABLE ' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_table_name);
EXECUTE IMMEDIATE v_sql;
DBMS_OUTPUT.PUT_LINE(p_table_name || ' を TRUNCATE しました');
END truncate_table;
/
-- 呼び出し例
BEGIN
truncate_table('WORK_LOG');
truncate_table('STAGING_DATA');
END;
/
バインド変数(USING 句)で安全に値を渡す
WHERE 句の値はバインド変数で渡せます。バインド変数を使うと、値がリテラルではなくパラメータとして扱われるためSQL インジェクションを防止でき、ハードパースも削減できます。
USING 句によるバインド変数の使い方
DECLARE
v_dept_id NUMBER := 50;
v_min_sal NUMBER := 300000;
v_emp_name VARCHAR2(100);
v_salary NUMBER;
v_sql VARCHAR2(500);
BEGIN
-- IN パラメータ: USING で値を渡す(:1, :2 はプレースホルダ)
v_sql := 'SELECT last_name, salary FROM employees '
|| 'WHERE department_id = :1 AND salary > :2 '
|| 'AND ROWNUM = 1 ORDER BY salary DESC';
EXECUTE IMMEDIATE v_sql
INTO v_emp_name, v_salary
USING v_dept_id, v_min_sal; -- :1=v_dept_id, :2=v_min_sal の順で渡す
DBMS_OUTPUT.PUT_LINE(v_emp_name || ': ' || v_salary);
-- OUT パラメータ(プロシージャ呼び出しで使用)
DECLARE
v_out NUMBER;
BEGIN
EXECUTE IMMEDIATE 'BEGIN :out := :in * 2; END;'
USING OUT v_out, IN 100; -- OUT/IN を明示できる
DBMS_OUTPUT.PUT_LINE('結果: ' || v_out); -- 200
END;
END;
/
バインド変数の命名について
プレースホルダは
プレースホルダは
:1, :2 のような数値、または :emp_id のような名前付きでも書けます。ただし EXECUTE IMMEDIATE では位置順で USING の引数と対応します(名前は無視)。同じプレースホルダ名が複数回現れた場合も、出現回数分だけ USING に引数を書く必要があります。
RETURNING INTO:DML の結果を受け取る
EXECUTE IMMEDIATE + RETURNING INTO
DECLARE
v_emp_id NUMBER := 1001;
v_new_sal NUMBER := 550000;
v_old_sal NUMBER;
v_sql VARCHAR2(200);
BEGIN
-- UPDATE した後の値や連番を RETURNING INTO で受け取る
v_sql := 'UPDATE employees SET salary = :new_sal '
|| 'WHERE employee_id = :emp_id '
|| 'RETURNING salary INTO :old_sal';
-- RETURNING INTO には OUT を明示
EXECUTE IMMEDIATE v_sql
USING v_new_sal, v_emp_id, OUT v_old_sal;
DBMS_OUTPUT.PUT_LINE('更新後の給与: ' || v_old_sal);
-- INSERT ... RETURNING で採番した ID を取得する例
DECLARE
v_new_id NUMBER;
BEGIN
EXECUTE IMMEDIATE
'INSERT INTO orders (order_date, customer_id) '
|| 'VALUES (SYSDATE, :cid) RETURNING order_id INTO :oid'
USING 999, OUT v_new_id;
DBMS_OUTPUT.PUT_LINE('新規注文ID: ' || v_new_id);
END;
END;
/
BULK COLLECT INTO で複数行を一括取得する
EXECUTE IMMEDIATE + BULK COLLECT INTO
DECLARE
TYPE t_names IS TABLE OF employees.last_name%TYPE;
TYPE t_sals IS TABLE OF employees.salary%TYPE;
v_names t_names;
v_sals t_sals;
v_dept NUMBER := 50;
v_sql VARCHAR2(300);
BEGIN
v_sql := 'SELECT last_name, salary FROM employees '
|| 'WHERE department_id = :1 ORDER BY salary DESC';
-- BULK COLLECT INTO で複数行を配列に取得
EXECUTE IMMEDIATE v_sql
BULK COLLECT INTO v_names, v_sals
USING v_dept;
DBMS_OUTPUT.PUT_LINE('取得件数: ' || v_names.COUNT);
FOR i IN 1 .. v_names.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(v_names(i) || ': ' || v_sals(i));
END LOOP;
END;
/
OPEN FOR:REF CURSOR として動的 SELECT を扱う
OPEN FOR でプロシージャから結果セットを返す
-- 動的 SQL を SYS_REFCURSOR で返すプロシージャ
CREATE OR REPLACE PROCEDURE get_rows_dynamic (
p_table_name IN VARCHAR2,
p_where IN VARCHAR2 DEFAULT NULL,
p_cursor OUT SYS_REFCURSOR
) AS
v_sql VARCHAR2(1000);
BEGIN
-- テーブル名を検証(DBMS_ASSERT でオブジェクト名を安全に引用符付き変換)
v_sql := 'SELECT * FROM ' || DBMS_ASSERT.SCHEMA_NAME(USER)
|| '.' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_table_name);
IF p_where IS NOT NULL THEN
-- WHERE 句を追加する場合は値の部分はバインド変数を使うべきだが、
-- 列名・演算子部分の検証は呼び出し側の責任
v_sql := v_sql || ' WHERE ' || p_where;
END IF;
OPEN p_cursor FOR v_sql; -- OPEN FOR で動的 SQL をカーソルとして開く
END get_rows_dynamic;
/
SQL インジェクションの仕組みと防止策
動的 SQL でユーザー入力を文字列連結すると、悪意ある入力によって意図しない SQL が実行される SQL インジェクション が発生します。
NG: 文字列連結で WHERE 値を渡す(インジェクション可能)
-- NG: ユーザー入力を直接連結すると危険
DECLARE
v_user_input VARCHAR2(100) := 'Smith' || ' OR 1=1 --'; -- 悪意ある入力例
v_sql VARCHAR2(500);
v_count NUMBER;
BEGIN
-- 組み立てられる SQL: SELECT COUNT(*) FROM employees WHERE last_name='Smith' OR 1=1 --'
-- → WHERE が無効化され全件カウントが返る
v_sql := 'SELECT COUNT(*) FROM employees WHERE last_name = ''' || v_user_input || '''';
EXECUTE IMMEDIATE v_sql INTO v_count;
DBMS_OUTPUT.PUT_LINE(v_count); -- 本来の件数ではなく全件数が返る
END;
/
OK: バインド変数で値を渡す(インジェクション不可)
-- OK: 値はバインド変数(USING)で渡す → SQL の構造を変えられない
DECLARE
v_user_input VARCHAR2(100) := 'Smith' || ' OR 1=1 --'; -- 同じ悪意ある入力
v_count NUMBER;
BEGIN
-- バインド変数なら入力値全体が1つの文字列値として扱われる
-- 組み立てられる SQL: SELECT COUNT(*) FROM employees WHERE last_name = :1
-- :1 の値は 'Smith' OR 1=1 -- という文字列として安全に処理される
EXECUTE IMMEDIATE
'SELECT COUNT(*) FROM employees WHERE last_name = :1'
INTO v_count
USING v_user_input;
DBMS_OUTPUT.PUT_LINE(v_count); -- 正しく 0 が返る
END;
/
| 対象 | 対策 |
|---|---|
| 値(WHERE 条件の値など) | USING 句のバインド変数で渡す(最重要) |
| テーブル名・列名 | DBMS_ASSERT.SIMPLE_SQL_NAME() で検証・引用符付きに変換 |
| スキーマ名 | DBMS_ASSERT.SCHEMA_NAME() で検証 |
| SQL 識別子全般 | DBMS_ASSERT.ENQUOTE_NAME() で二重引用符付きに変換 |
| 文字列リテラル | DBMS_ASSERT.ENQUOTE_LITERAL() でシングルクォートをエスケープ |
EXECUTE IMMEDIATE vs DBMS_SQL:使い分け基準
| 観点 | EXECUTE IMMEDIATE | DBMS_SQL |
|---|---|---|
| 記述量 | 少ない(推奨) | 多い(準備・BIND・実行・FETCH を個別に呼ぶ) |
| SELECT 列数が実行時まで不明 | 不可 | 可能(COLUMN_VALUE でループ取得) |
| カーソルを再利用したい | 不可 | 可能(OPEN_CURSOR でハンドルを保持) |
| バインド変数 | USING 句(位置ベース) | BIND_VARIABLE(名前ベース) |
| BULK COLLECT | 対応 | 非対応(FETCH を1行ずつ呼ぶ) |
| Oracle バージョン | 8i 以降 | 全バージョン対応 |
選択基準
通常は
通常は
EXECUTE IMMEDIATE で十分です。「SELECT の列数が実行時まで不明」「カーソルを複数回再利用したい」という特殊なケースのみ DBMS_SQL を使います。
まとめ
- DDL・テーブル名の可変化・動的 WHERE には EXECUTE IMMEDIATE を使う
- 値は必ず USING 句のバインド変数で渡す。文字列連結による値の渡し方は SQL インジェクションの原因
- テーブル名・列名などの識別子は
DBMS_ASSERT.SIMPLE_SQL_NAME()で検証する - SELECT の結果は
INTO(1行)またはBULK COLLECT INTO(複数行)で受け取る - DML の実行結果値は
RETURNING INTOで取得できる - SELECT 列数が実行時まで不明な高度な動的クエリは
DBMS_SQLを使う
EXECUTE IMMEDIATE でよく組み合わせる例外処理のパターンはPL/SQL 例外処理完全ガイドを、BULK COLLECT の詳細とパフォーマンスチューニングは明示的カーソル完全ガイドも参照してください。

