【Oracle】DBMS_SQL完全ガイド|動的 SQL のカーソル操作・列数不定の SELECT・EXECUTE IMMEDIATE との使い分けまで解説

動的 SQL の多くのケースでは EXECUTE IMMEDIATE で十分対応できます。しかし「実行時まで SELECT の列数・列の型が分からない」「SQL テキストを少しずつ組み立てながらカーソルを使いまわしたい」といった高度なシナリオでは DBMS_SQL パッケージが必要になります。

DBMS_SQL はカーソルレベルで動的 SQL を制御できる低レベル API です。EXECUTE IMMEDIATE より記述量は多くなりますが、より細かい制御(列数不定の結果セット処理・バッチ DML など)が可能です。

この記事でわかること

  • DBMS_SQL の基本フロー(OPEN_CURSOR → PARSE → BIND → EXECUTE → FETCH → COLUMN_VALUE → CLOSE)
  • EXECUTE IMMEDIATE との使い分け基準
  • 列数・列型が実行時まで不明な SELECT を DESCRIBE_COLUMNS で処理する方法
  • バインド変数を使った安全な動的 DML の実行
  • DBMS_SQL.TO_REFCURSOR で結果を REF CURSOR に変換する方法
スポンサーリンク

EXECUTE IMMEDIATE と DBMS_SQL の使い分け

項目 EXECUTE IMMEDIATE DBMS_SQL
記述量 少ない(シンプル) 多い(カーソル操作が必要)
列数不定の SELECT 不可(事前に型が分かっている必要がある) 可能(DESCRIBE_COLUMNS で動的に型取得)
バインド変数 USING 句で指定(位置またはINTO) BIND_VARIABLE で名前指定
大量DML(バッチ) FORALL と組み合わせ可 EXECUTE_AND_FETCH でより柔軟な制御
カーソルの再利用 不可(都度パース) PARSE は1回で複数回 EXECUTE 可能
推奨度 通常のケースで推奨 列数不定・高度な制御が必要なときのみ

DBMS_SQL の基本フロー(DML 編)

DBMS_SQL で動的 DML(INSERT)を実行する
DECLARE
    v_cursor     INTEGER;
    v_rows_aff   INTEGER;
    v_table_name VARCHAR2(30) := 'EMPLOYEES';
    v_sql        VARCHAR2(500);
BEGIN
    -- SQL 文を組み立てる
    v_sql := 'UPDATE ' || v_table_name ||
             ' SET salary = salary * 1.1 WHERE department_id = :dept_id';

    -- ① OPEN_CURSOR でカーソルを開く
    v_cursor := DBMS_SQL.OPEN_CURSOR;

    -- ② PARSE で SQL をパースする
    DBMS_SQL.PARSE(
        c             => v_cursor,
        statement     => v_sql,
        language_flag => DBMS_SQL.NATIVE   -- NATIVE: データベースのデフォルトSQL方言
    );

    -- ③ BIND_VARIABLE でバインド変数に値をセットする
    DBMS_SQL.BIND_VARIABLE(v_cursor, ':dept_id', 50);

    -- ④ EXECUTE で SQL を実行する
    v_rows_aff := DBMS_SQL.EXECUTE(v_cursor);

    DBMS_OUTPUT.PUT_LINE(v_rows_aff || ' 行を更新しました');

    -- ⑤ CLOSE_CURSOR でカーソルを閉じる(必須)
    DBMS_SQL.CLOSE_CURSOR(v_cursor);

    COMMIT;

EXCEPTION
    WHEN OTHERS THEN
        -- 例外時もカーソルを閉じる(リソースリークを防ぐ)
        IF DBMS_SQL.IS_OPEN(v_cursor) THEN
            DBMS_SQL.CLOSE_CURSOR(v_cursor);
        END IF;
        RAISE;
END;
/

列数不定の SELECT を DESCRIBE_COLUMNS で処理する

DBMS_SQL の最大の強みは、実行時まで SELECT の列数・列型が不明な場合に対応できる点です。DESCRIBE_COLUMNS で列情報を取得してから DEFINE_COLUMN でバッファを定義します。

DESCRIBE_COLUMNS で動的 SELECT を処理する
DECLARE
    v_cursor     INTEGER;
    v_col_cnt    INTEGER;
    v_col_desc   DBMS_SQL.DESC_TAB2;   -- 列メタデータを格納する型
    v_col_value  VARCHAR2(4000);
    v_ret        INTEGER;
    v_sql        VARCHAR2(500);
BEGIN
    -- 実行時まで列数・列名が不明な SQL(例: ユーザーが任意の SELECT を入力)
    v_sql := 'SELECT employee_id, last_name, salary FROM employees WHERE rownum <= 5';

    v_cursor := DBMS_SQL.OPEN_CURSOR;
    DBMS_SQL.PARSE(v_cursor, v_sql, DBMS_SQL.NATIVE);

    -- EXECUTE してから DESCRIBE_COLUMNS で列情報を取得する
    v_ret := DBMS_SQL.EXECUTE(v_cursor);
    DBMS_SQL.DESCRIBE_COLUMNS2(v_cursor, v_col_cnt, v_col_desc);

    DBMS_OUTPUT.PUT_LINE('列数: ' || v_col_cnt);

    -- 各列に VARCHAR2 バッファを定義する(型変換は後で行う)
    FOR i IN 1..v_col_cnt LOOP
        DBMS_OUTPUT.PUT_LINE(
            '列 ' || i || ': ' || v_col_desc(i).col_name ||
            ' (type=' || v_col_desc(i).col_type || ')');

        -- DEFINE_COLUMN で各列の受け取りバッファを設定する
        -- すべて VARCHAR2 で受け取る(汎用的な処理のため)
        DBMS_SQL.DEFINE_COLUMN(v_cursor, i, v_col_value, 4000);
    END LOOP;

    -- FETCH_ROWS で行を取得する
    WHILE DBMS_SQL.FETCH_ROWS(v_cursor) > 0 LOOP
        FOR i IN 1..v_col_cnt LOOP
            -- COLUMN_VALUE で各列の値を取り出す
            DBMS_SQL.COLUMN_VALUE(v_cursor, i, v_col_value);
            DBMS_OUTPUT.PUT(v_col_desc(i).col_name || '=' || NVL(v_col_value, 'NULL') || ' ');
        END LOOP;
        DBMS_OUTPUT.NEW_LINE;
    END LOOP;

    DBMS_SQL.CLOSE_CURSOR(v_cursor);

EXCEPTION
    WHEN OTHERS THEN
        IF DBMS_SQL.IS_OPEN(v_cursor) THEN
            DBMS_SQL.CLOSE_CURSOR(v_cursor);
        END IF;
        RAISE;
END;
/

DBMS_SQL.TO_REFCURSOR で結果を REF CURSOR に変換する

Oracle 11g 以降、DBMS_SQL.TO_REFCURSOR を使うと DBMS_SQL カーソルを REF CURSOR に変換できます。API 境界で結果セットを返す場合に便利です。

DBMS_SQL カーソルを REF CURSOR に変換して返す
-- 動的 SQL の結果を REF CURSOR で返すファンクション
CREATE OR REPLACE FUNCTION get_dynamic_result(
    p_table_name  IN VARCHAR2,
    p_where_clause IN VARCHAR2 DEFAULT NULL
) RETURN SYS_REFCURSOR AS
    v_cursor   INTEGER;
    v_ref_cur  SYS_REFCURSOR;
    v_sql      VARCHAR2(4000);
    v_ret      INTEGER;
BEGIN
    -- テーブル名をホワイトリストチェックする(SQLインジェクション対策)
    IF p_table_name NOT IN ('EMPLOYEES', 'DEPARTMENTS', 'ORDERS') THEN
        RAISE_APPLICATION_ERROR(-20001, '無効なテーブル名: ' || p_table_name);
    END IF;

    v_sql := 'SELECT * FROM ' || p_table_name;
    IF p_where_clause IS NOT NULL THEN
        v_sql := v_sql || ' WHERE ' || p_where_clause;
    END IF;

    v_cursor := DBMS_SQL.OPEN_CURSOR;
    DBMS_SQL.PARSE(v_cursor, v_sql, DBMS_SQL.NATIVE);
    v_ret := DBMS_SQL.EXECUTE(v_cursor);

    -- DBMS_SQL カーソルを SYS_REFCURSOR に変換する(Oracle 11g 以降)
    v_ref_cur := DBMS_SQL.TO_REFCURSOR(v_cursor);
    -- TO_REFCURSOR を呼んだ後は CLOSE_CURSOR を呼ばない
    -- (REF CURSOR のクローズと二重になる)

    RETURN v_ref_cur;

EXCEPTION
    WHEN OTHERS THEN
        IF DBMS_SQL.IS_OPEN(v_cursor) THEN
            DBMS_SQL.CLOSE_CURSOR(v_cursor);
        END IF;
        RAISE;
END get_dynamic_result;
/

まとめ

  • 基本フロー:OPEN_CURSOR → PARSE → BIND_VARIABLE → EXECUTE → FETCH_ROWS → COLUMN_VALUE → CLOSE_CURSOR。例外時も必ず CLOSE_CURSOR を呼ぶ
  • DESCRIBE_COLUMNS2:実行後に列数・列名・列型を取得できる。汎用的な SELECT 処理ジェネレータに不可欠
  • EXECUTE IMMEDIATE を優先する:列数・型が分かっている場合は EXECUTE IMMEDIATE の方がシンプル。DBMS_SQL は本当に必要なときのみ使う
  • TO_REFCURSOR:Oracle 11g 以降で DBMS_SQL カーソルを REF CURSOR に変換できる。変換後は CLOSE_CURSOR を呼ばないこと
  • IS_OPEN でチェック:例外ハンドラで DBMS_SQL.IS_OPEN を使ってカーソルが開いているか確認してから CLOSE することでリソースリークを防ぐ

EXECUTE IMMEDIATE を使った動的 SQL の基本は EXECUTE IMMEDIATE完全ガイドを参照してください。PL/SQL コレクション(BULK COLLECT / FORALL)との組み合わせは PL/SQL コレクション完全ガイドも参照してください。