【Oracle】EXECUTE IMMEDIATE(動的SQL)完全ガイド|DDL実行・バインド変数USING・RETURNING INTO・BULK COLLECT・DBMS_SQL比較・SQLインジェクション対策まで実例で解説

【Oracle】EXECUTE IMMEDIATE(動的SQL)完全ガイド|DDL実行・バインド変数USING・RETURNING INTO・BULK COLLECT・DBMS_SQL比較・SQLインジェクション対策まで実例で解説 Oracle

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 TABLETRUNCATEDROP を実行したい
  • テーブル名・列名を変数にしたい:処理対象テーブルを引数で切り替えるプロシージャ
  • 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 の詳細とパフォーマンスチューニングは明示的カーソル完全ガイドも参照してください。