[Oracle] 動的に編集したカラム数が不定なSELECT文をカーソル処理してみる。 | Archive Redo Blog

Archive Redo Blog

DBエンジニアのあれこれ備忘録

カーソル変数を使うと、動的に編集したSELECT文をカーソル処理することができます。


DECLARE
  type cur_type IS REF CURSOR;
  cur cur_type;
      .
      .
      .
BEGIN
  OPEN cur FOR 'SELECT ... FROM ...';
  LOOP
    FETCH cur INTO ...;
    EXIT WHEN cur%NOTFOUND;
          .
          .
          .
  END LOOP;
  CLOSE cur;
END;
/


条件に応じてWHERE句やORDER BY句などを変えたい場合などに便利な機能です。


しかし、カーソル変数を使っても、FETCH文のINTO句を動的に編集することはできないため、取得するカラム数が不定のSELECT文には対応できません。


そもそも、取得するカラム数が不定のSELECT文を使用すること自体極めて稀かとは思いますが、どうしてもそういうことをしたいという場合はDBMS_SQLパッケージを使えば実現可能なようです。

試しに、指定したテーブルのデータをCSVファイルに出力するというプロシージャを作成してみました。

CREATE OR REPLACE PROCEDURE OUTPUT_TABLE_AS_CSV
( 
  p_table_name   IN   VARCHAR2,
  p_directory    IN  VARCHAR2
)
IS

  TYPE table_type IS TABLE OF VARCHAR2(32767)
    INDEX BY BINARY_INTEGER;

  wk_column_name        table_type;
  wk_column_value        table_type;
  wk_column_count        NUMBER;
  wk_sql            VARCHAR2(2000);

  CURSOR column_cur IS
    SELECT COLUMN_NAME
      FROM USER_TAB_COLUMNS
      WHERE TABLE_NAME = p_table_name
      ORDER BY COLUMN_ID;

  table_cur          INTEGER;

  wk_file            UTL_FILE.FILE_TYPE;

BEGIN

  --USER_TAB_COLUMNS から指定したテーブルのカラム名を取得し、SELECT文を編集する。
  wk_column_count := 0;
  OPEN column_cur;
  LOOP
    FETCH column_cur INTO wk_column_name( wk_column_count + 1 );
    EXIT WHEN column_cur%NOTFOUND;
    wk_column_count := wk_column_count + 1;
    IF wk_sql IS NULL THEN
      wk_sql := 'SELECT ' || wk_column_name( wk_column_count );
    ELSE
      wk_sql := wk_sql || ', ' || wk_column_name( wk_column_count );
    END IF;
    wk_column_value( wk_column_count ) := null;
  END LOOP;
  CLOSE column_cur;

  --カラムが1つでも存在する場合
  IF wk_column_count > 0 THEN

    --カーソルをオープンする。
    table_cur := DBMS_SQL.OPEN_CURSOR;

    --SQL文を解析する。
    wk_sql := wk_sql || ' FROM ' || p_table_name;
    DBMS_SQL.PARSE( table_cur, wk_sql, DBMS_SQL.NATIVE );

    --カラムを定義する。
    FOR i IN 1..wk_column_count LOOP
      DBMS_SQL.DEFINE_COLUMN( table_cur, i, wk_column_value( i ), 4000 );
    END LOOP;

    --SELECT文を実行する。
    IF ( DBMS_SQL.EXECUTE( table_cur ) = 0 ) THEN

      --CSVファイルをオープンする。
      wk_file := UTL_FILE.FOPEN( p_directory, p_table_name || '.csv', 'w', 32767 );

      --カラムヘッダーを出力する。
      FOR i IN 1..wk_column_count LOOP
        --CSVファイルに出力する。
        IF i = 1 THEN
          UTL_FILE.PUT( wk_file, wk_column_name( i ) );
        ELSE
          UTL_FILE.PUT( wk_file, ',' || wk_column_name( i ) );
        END IF;
      END LOOP;
      UTL_FILE.NEW_LINE( wk_file );

      --データ行を出力する。
      WHILE DBMS_SQL.FETCH_ROWS( table_cur ) > 0 LOOP

        FOR i IN 1..wk_column_count LOOP
          --カラム値を取得する。
          DBMS_SQL.COLUMN_VALUE( table_cur, i, wk_column_value( i ) );
          --CSVファイルに出力する。
          IF i = 1 THEN
            UTL_FILE.PUT( wk_file, wk_column_value( i ) );
          ELSE
            UTL_FILE.PUT( wk_file, ',' || wk_column_value( i ) );
          END IF;
        END LOOP;
        UTL_FILE.NEW_LINE( wk_file );

      END LOOP;

      --CSVファイルをクローズする。
      UTL_FILE.FCLOSE( wk_file );

    END IF;

    --カーソルをクローズする。
    DBMS_SQL.CLOSE_CURSOR(table_cur);

  END IF;

END;
/


カーソルをオープン(OPEN_CURSOR)して、解析(PARSE)して、カラムを定義(DEFINE_COLUMN)して、実行(EXECUTE)して、フェッチ(FETCH_ROWS)して、値を取得(COLUMN_VALUE)して、クローズ(CLOSE)する...と手順は煩雑ですが、このようにカラムの値を受け取る変数にPL/SQL表などを使えばカラム数が不定でも対応することができます。

ただし、上記の例ではすべてのカラムを文字列型として処理していますが、データ型を区別して値を取得したいという場合にはもう一工夫必要です。

さすがにそこまでやってみる気はありませんが...(笑)




【関連エントリ】
[Oracle] 動的に編集したカラム数が不定なSELECT文をカーソル処理してみる。 2005/09/21
[Oracle] DBMS_SQLパッケージでORA-04030エラーが発生 2006/09/29