【Oracle】UTL_FILE完全ガイド|PL/SQLでファイルを読み書きする方法・DIRECTORY設定・CSV出力・エラー対処まで実例で解説

【Oracle】UTL_FILE完全ガイド|PL/SQLでファイルを読み書きする方法・DIRECTORY設定・CSV出力・エラー対処まで実例で解説 Oracle

UTL_FILE は Oracle の組み込みパッケージで、PL/SQL プログラムからデータベースサーバー上のファイルを読み書きできます。バッチ処理での CSV 出力・ログファイルの書き出し・テキストデータの取り込みなど、実務で幅広く活用されています。

UTL_FILE が操作できるのはDBサーバー上のファイルです。クライアントPC のファイルを直接操作することはできません。ファイルのパスは DIRECTORY オブジェクトで管理します。

この記事でわかること

  • DIRECTORY オブジェクトの作成と権限付与
  • FOPEN / PUT_LINE / NEW_LINE / GET_LINE / FCLOSE の基本操作
  • CSV ファイルを出力する実務パターン
  • テキストファイルを読み込んでテーブルに INSERT するパターン
  • バイナリファイル(RAW)の読み書き
  • UTL_FILE 例外の種類と対処法
  • UTL_FILE vs 外部表(External Table)の使い分け
スポンサーリンク

事前準備:DIRECTORY オブジェクトの作成

UTL_FILE はセキュリティ上の理由から、アクセスできるパスをDIRECTORY オブジェクトとして事前に登録する必要があります。直接パス文字列を渡す旧来の方式(utl_file_dir パラメータ)はOracle 19c で完全廃止されました。必ず DIRECTORY オブジェクトを使います。

DIRECTORY の作成・確認・権限付与(DBA 権限が必要)
-- DIRECTORY オブジェクトの作成(DBA 権限が必要)
-- OS 上の実際のディレクトリが存在している必要がある
CREATE OR REPLACE DIRECTORY data_dir AS '/home/oracle/data';
CREATE OR REPLACE DIRECTORY log_dir  AS '/home/oracle/logs';

-- 作成済み DIRECTORY の確認
SELECT directory_name, directory_path
FROM dba_directories
ORDER BY directory_name;

-- ユーザーに READ / WRITE 権限を付与
GRANT READ, WRITE ON DIRECTORY data_dir TO app_user;
GRANT READ        ON DIRECTORY data_dir TO read_only_user;

-- 現在ユーザーが持つ DIRECTORY 権限を確認
SELECT grantee, grantor, table_name AS directory_name, privilege
FROM dba_tab_privs
WHERE table_name IN (SELECT directory_name FROM dba_directories)
ORDER BY grantee, directory_name;
OS 上のディレクトリが存在しない場合はエラーになる
CREATE DIRECTORY 文自体は OS ディレクトリの存在を確認しません。しかし実際にファイルを開こうとすると UTL_FILE.INVALID_PATH またはUTL_FILE.INVALID_OPERATION が発生します。Oracle プロセスオーナー(通常 oracle ユーザー)がそのディレクトリに対する読み書き権限を持っているかも確認してください。

UTL_FILE の基本操作:FOPEN / PUT_LINE / GET_LINE / FCLOSE

プロシージャ/関数 説明
UTL_FILE.FOPEN(dir, file, mode, max_linesize) ファイルを開く。mode: ‘r’=読込 ‘w’=書込(上書) ‘a’=追記 ‘rb’=バイナリ読 ‘wb’=バイナリ書
UTL_FILE.PUT(fh, str) 改行なしで文字列を書き込む
UTL_FILE.PUT_LINE(fh, str) 文字列を書き込み改行を追加する
UTL_FILE.NEW_LINE(fh, lines) 改行のみを書き込む
UTL_FILE.GET_LINE(fh, buf, len) 1行読み込む。EOF で NO_DATA_FOUND が発生
UTL_FILE.FCLOSE(fh) ファイルを閉じる
UTL_FILE.FCLOSE_ALL セッション内の全ファイルを閉じる
UTL_FILE.FFLUSH(fh) バッファをディスクに書き出す
UTL_FILE.FREMOVE(dir, file) ファイルを削除する
UTL_FILE.FRENAME(src_dir, src, dst_dir, dst, overwrite) ファイルを移動・リネームする
UTL_FILE.FCOPY(src_dir, src, dst_dir, dst) ファイルをコピーする
UTL_FILE.FGETATTR(dir, file, fexists, flen, block_size) ファイルの存在・サイズを確認する

ファイルへの書き込み:基本パターン

テキストファイルを書き込む基本パターン
DECLARE
    v_fh  UTL_FILE.FILE_TYPE;   -- ファイルハンドル
BEGIN
    -- FOPEN(ディレクトリ名, ファイル名, モード, 最大行サイズ)
    -- モード 'w' = 上書き新規作成(既存ファイルは上書き)
    -- max_linesize のデフォルトは 1024、最大 32767
    v_fh := UTL_FILE.FOPEN('LOG_DIR', 'process.log', 'w', 32767);

    UTL_FILE.PUT_LINE(v_fh, '処理開始: ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
    UTL_FILE.PUT_LINE(v_fh, '対象件数: 1000');
    UTL_FILE.PUT_LINE(v_fh, '処理完了');

    UTL_FILE.FCLOSE(v_fh);

EXCEPTION
    WHEN UTL_FILE.INVALID_PATH THEN
        DBMS_OUTPUT.PUT_LINE('ディレクトリが存在しないか権限がありません');
    WHEN UTL_FILE.INVALID_OPERATION THEN
        DBMS_OUTPUT.PUT_LINE('ファイルのオープンに失敗しました');
    WHEN OTHERS THEN
        -- 例外発生時もファイルを確実にクローズする
        IF UTL_FILE.IS_OPEN(v_fh) THEN
            UTL_FILE.FCLOSE(v_fh);
        END IF;
        RAISE;
END;
/

CSV ファイルを出力する

SELECT 結果を CSV で出力するパターンは実務で非常によく使われます。カンマを含む値は二重引用符で囲む・ヘッダー行を付けるなどの考慮が必要です。

テーブルデータを CSV ファイルに出力するプロシージャ
CREATE OR REPLACE PROCEDURE export_employees_csv (
    p_directory IN VARCHAR2 DEFAULT 'DATA_DIR',
    p_filename  IN VARCHAR2 DEFAULT 'employees.csv'
) AS
    v_fh     UTL_FILE.FILE_TYPE;
    v_line   VARCHAR2(32767);

    -- CSV の値にカンマ・ダブルクォートが含まれる場合に安全に囲む関数
    FUNCTION csv_val(p_val IN VARCHAR2) RETURN VARCHAR2 IS
    BEGIN
        IF p_val IS NULL THEN
            RETURN '';
        ELSIF INSTR(p_val, ',') > 0 OR INSTR(p_val, '"') > 0 OR INSTR(p_val, CHR(10)) > 0 THEN
            -- カンマ・ダブルクォート・改行を含む場合はダブルクォートで囲み、
            -- 内部のダブルクォートは2つに重ねてエスケープ
            RETURN '"' || REPLACE(p_val, '"', '""') || '"';
        ELSE
            RETURN p_val;
        END IF;
    END csv_val;

BEGIN
    v_fh := UTL_FILE.FOPEN(p_directory, p_filename, 'w', 32767);

    -- ヘッダー行
    UTL_FILE.PUT_LINE(v_fh, 'employee_id,last_name,first_name,salary,department_id,hire_date');

    -- データ行をカーソル FOR LOOP で出力
    FOR r IN (
        SELECT employee_id, last_name, first_name,
               salary, department_id,
               TO_CHAR(hire_date, 'YYYY-MM-DD') AS hire_date
        FROM employees
        ORDER BY employee_id
    ) LOOP
        v_line :=        r.employee_id          || ','
               || csv_val(r.last_name)          || ','
               || csv_val(r.first_name)         || ','
               || NVL(TO_CHAR(r.salary),  '')   || ','
               || NVL(TO_CHAR(r.department_id),'') || ','
               || r.hire_date;
        UTL_FILE.PUT_LINE(v_fh, v_line);
    END LOOP;

    UTL_FILE.FCLOSE(v_fh);
    DBMS_OUTPUT.PUT_LINE(p_filename || ' の出力が完了しました');

EXCEPTION
    WHEN OTHERS THEN
        IF UTL_FILE.IS_OPEN(v_fh) THEN
            UTL_FILE.FCLOSE(v_fh);
        END IF;
        RAISE;
END export_employees_csv;
/

-- 実行例
BEGIN
    export_employees_csv('DATA_DIR', 'emp_20250406.csv');
END;
/

テキストファイルを読み込む

GET_LINE は1行読み込むごとに行末の改行を除いた文字列を返します。ファイルの終端に達すると NO_DATA_FOUND 例外が発生するので、それをループ終了のシグナルとして使います。

テキストファイルを読み込んでログに出力する
DECLARE
    v_fh     UTL_FILE.FILE_TYPE;
    v_line   VARCHAR2(32767);
    v_lineno NUMBER := 0;
BEGIN
    -- モード 'r' = 読み込み専用
    v_fh := UTL_FILE.FOPEN('DATA_DIR', 'input.txt', 'r', 32767);

    LOOP
        BEGIN
            UTL_FILE.GET_LINE(v_fh, v_line);  -- 1行読み込む
            v_lineno := v_lineno + 1;
            DBMS_OUTPUT.PUT_LINE(v_lineno || ': ' || v_line);
        EXCEPTION
            WHEN NO_DATA_FOUND THEN
                EXIT;  -- EOF に達したらループを抜ける
        END;
    END LOOP;

    UTL_FILE.FCLOSE(v_fh);
    DBMS_OUTPUT.PUT_LINE('読み込み完了: ' || v_lineno || '行');

EXCEPTION
    WHEN UTL_FILE.INVALID_PATH THEN
        DBMS_OUTPUT.PUT_LINE('ディレクトリまたはファイルが見つかりません');
    WHEN OTHERS THEN
        IF UTL_FILE.IS_OPEN(v_fh) THEN
            UTL_FILE.FCLOSE(v_fh);
        END IF;
        RAISE;
END;
/
CSV ファイルを読み込んでテーブルに INSERT する
CREATE OR REPLACE PROCEDURE import_csv (
    p_directory IN VARCHAR2,
    p_filename  IN VARCHAR2
) AS
    v_fh      UTL_FILE.FILE_TYPE;
    v_line    VARCHAR2(32767);
    v_lineno  PLS_INTEGER := 0;
    v_loaded  PLS_INTEGER := 0;

    -- カンマ区切りの n 番目フィールドを取得するヘルパー
    FUNCTION get_field(p_line VARCHAR2, p_pos PLS_INTEGER) RETURN VARCHAR2 IS
        v_start PLS_INTEGER;
        v_end   PLS_INTEGER;
    BEGIN
        v_start := CASE p_pos WHEN 1 THEN 1
                   ELSE INSTR(p_line, ',', 1, p_pos - 1) + 1 END;
        v_end   := INSTR(p_line, ',', 1, p_pos);
        IF v_end = 0 THEN
            RETURN SUBSTR(p_line, v_start);      -- 最後のフィールド
        ELSE
            RETURN SUBSTR(p_line, v_start, v_end - v_start);
        END IF;
    END get_field;

BEGIN
    v_fh := UTL_FILE.FOPEN(p_directory, p_filename, 'r', 32767);

    LOOP
        BEGIN
            UTL_FILE.GET_LINE(v_fh, v_line);
            v_lineno := v_lineno + 1;

            -- 1行目(ヘッダー)はスキップ
            IF v_lineno = 1 THEN CONTINUE; END IF;
            -- 空行はスキップ
            IF TRIM(v_line) IS NULL THEN CONTINUE; END IF;

            INSERT INTO emp_staging (emp_id, last_name, salary, hire_date)
            VALUES (
                TO_NUMBER(get_field(v_line, 1)),
                get_field(v_line, 2),
                TO_NUMBER(get_field(v_line, 3)),
                TO_DATE(get_field(v_line, 4), 'YYYY-MM-DD')
            );
            v_loaded := v_loaded + 1;

        EXCEPTION
            WHEN NO_DATA_FOUND THEN EXIT;    -- EOF
            WHEN OTHERS THEN
                DBMS_OUTPUT.PUT_LINE('行' || v_lineno || 'エラー: ' || SQLERRM);
                -- エラー行をスキップして続行する場合はここで CONTINUE
        END;
    END LOOP;

    COMMIT;
    UTL_FILE.FCLOSE(v_fh);
    DBMS_OUTPUT.PUT_LINE(v_loaded || '件 INSERT しました');

EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        IF UTL_FILE.IS_OPEN(v_fh) THEN UTL_FILE.FCLOSE(v_fh); END IF;
        RAISE;
END import_csv;
/

ファイルの存在確認・削除・リネーム

FGETATTR でファイル存在確認・FREMOVE で削除
DECLARE
    v_exists     BOOLEAN;
    v_file_len   NUMBER;
    v_block_size BINARY_INTEGER;
BEGIN
    -- ファイルの存在確認とサイズ取得(FGETATTR)
    UTL_FILE.FGETATTR(
        location   => 'DATA_DIR',
        filename   => 'output.csv',
        fexists    => v_exists,
        file_length => v_file_len,
        block_size  => v_block_size
    );

    IF v_exists THEN
        DBMS_OUTPUT.PUT_LINE('ファイルサイズ: ' || v_file_len || ' バイト');

        -- 古いファイルをバックアップ用にリネームしてから新規作成
        UTL_FILE.FRENAME(
            src_location  => 'DATA_DIR',
            src_filename  => 'output.csv',
            dest_location => 'DATA_DIR',
            dest_filename => 'output_' || TO_CHAR(SYSDATE, 'YYYYMMDD') || '.bak',
            overwrite     => FALSE   -- TRUE にすると同名ファイルを上書き
        );
        DBMS_OUTPUT.PUT_LINE('バックアップ完了');
    ELSE
        DBMS_OUTPUT.PUT_LINE('ファイルが存在しません');
    END IF;

    -- ファイルの削除(存在しない場合は UTL_FILE.INVALID_OPERATION が発生)
    -- UTL_FILE.FREMOVE('DATA_DIR', 'old_file.txt');
END;
/

UTL_FILE の例外一覧と対処法

例外 発生する状況 対処
INVALID_PATH DIRECTORY が存在しない・権限がない・OSパスが存在しない DIRECTORY と OS ディレクトリの両方を確認。権限付与を確認
INVALID_MODE FOPEN のモード文字列が不正 ‘r”w”a”rb”wb”ab’のいずれかを指定
INVALID_FILEHANDLE クローズ済みのハンドルを使用 IS_OPEN で確認してから操作する
INVALID_OPERATION 読み込みモードのファイルへの書き込み等、操作が不正 モードと操作が一致しているか確認
READ_ERROR ファイル読み込み中にOSエラー ファイルの破損・権限を確認
WRITE_ERROR ファイル書き込み中にOSエラー(ディスク満杯等) ディスク空き容量・書き込み権限を確認
INTERNAL_ERROR UTL_FILE 内部エラー Oracle サポートへ連絡
NO_DATA_FOUND GET_LINE で EOF に達した ループ終了のシグナルとして使用(正常)

UTL_FILE vs 外部表(External Table)の使い分け

観点 UTL_FILE 外部表(External Table)
操作方向 読み込み・書き込みの両方 読み込みのみ(SELECT専用)
出力(書き込み) 可能(CSV出力・ログ出力) 不可
書き方 PL/SQL コードが必要 SQL の SELECT で使える
大量データ読み込み 行ごとのループ処理(遅い) SQL ベースで高速(パラレル処理も可能)
フォーマット柔軟性 行単位で自由にパース可能 固定フォーマットの CSV・固定長に適す
エラー行のスキップ PL/SQL の EXCEPTION で制御可能 REJECT LIMIT で制御
使い分けの基準

  • CSV・ログを書き出したい → UTL_FILE(外部表は書き込み不可)
  • 大量データを高速に読み込みたい → 外部表(SQL ベースで内部結合も可能)
  • 行ごとに複雑なパースや変換が必要 → UTL_FILE(PL/SQL で自由に処理)

まとめ

  • UTL_FILE はDBサーバー上のファイルを PL/SQL から操作するパッケージ
  • アクセスできるパスは DIRECTORY オブジェクトで管理(utl_file_dir は19cで廃止)
  • FOPEN → PUT_LINE/GET_LINE → FCLOSE が基本の流れ。例外発生時も FCLOSE を確実に呼ぶ
  • CSV 出力ではカンマ・ダブルクォート・改行を含む値を RFC 4180 形式でエスケープする
  • 読み込みは GET_LINE で NO_DATA_FOUND → EXIT がループ終了の定番パターン
  • ファイルの存在確認は FGETATTR、削除は FREMOVE、移動は FRENAME
  • 大量データの読み込みには外部表の方が高速。UTL_FILE は書き出しと複雑なパースに強い

UTL_FILE と組み合わせて使う例外処理の詳細はPL/SQL 例外処理完全ガイドを、CSVファイルをSQLで直接読み込む外部表については外部表(External Table)の使い方完全ガイドも参照してください。