【PL/SQL】プロシージャ・ファンクション完全ガイド|10軸比較・戻り値5種・DETERMINISTIC/RESULT_CACHE・AUTHID・実務10パターン

【PL/SQL】ストアドプロシージャとファンクションの違いと作り方 PL/SQL

PL/SQLで処理を再利用するための2大仕組みがストアドプロシージャ(Procedure)ストアドファンクション(Function)です。「戻り値の有無」で選ぶだけの単純な話に見えますが、実務ではDETERMINISTIC宣言、RESULT_CACHE、AUTHIDの選択、オーバーロード、PRAGMA AUTONOMOUS_TRANSACTION、パッケージ化判断、RECORDやREF CURSORを返す設計など、判断を要するポイントが多数存在します。

入門記事の多くは「プロシージャは値を返さない/ファンクションは値を返す」の一文で終わり、実際のプロジェクトで書く設計には踏み込みません。その結果、SELECT内で重いファンクションを呼び出してパフォーマンス崩壊同一ロジックが散らばって保守不能AUTHID未指定で権限事故、などのトラブルが後を絶ちません。

この記事ではプロシージャとファンクションを実務レベルで使い分けられるようになるため、10軸の比較表、作成構文、IN/OUT/IN OUTパラメータ、戻り値の多様性(スカラー/RECORD/TABLE/REF CURSOR/BOOLEAN)、呼び出し方5パターン、DETERMINISTIC/RESULT_CACHE/PARALLEL_ENABLE等の性能宣言、AUTHID DEFINER vs CURRENT_USER、オーバーロード、再帰呼び出し、パッケージ化判断、実務10パターン、アンチパターン7選まで網羅した決定版です。関連は【PL/SQL】IN・OUT・IN OUTパラメータの完全ガイド【PL/SQL】パッケージを使ったコード管理と再利用性向上【PL/SQL】基本構文完全ガイドも併読推奨。

この記事で学べること

  • プロシージャとファンクションの10軸完全比較表
  • それぞれの作成構文(CREATE OR REPLACE PROCEDUREFUNCTION
  • IN/OUT/IN OUTパラメータの使い分け
  • 戻り値5種:スカラー/RECORD/TABLE(ネスト表)/REF CURSOR/BOOLEAN
  • 呼び出し方5パターン:PL/SQL直接/SELECT内/CALL/名前付き引数/位置引数
  • オーバーロード(同名別シグネチャ)の実装
  • 再帰プロシージャ・ファンクション
  • 性能宣言:DETERMINISTIC/RESULT_CACHE/PARALLEL_ENABLE/PIPELINED
  • AUTHID DEFINER vs CURRENT_USER(Definer Rights vs Invoker Rights)
  • PRAGMA AUTONOMOUS_TRANSACTIONで独立トランザクション化
  • ローカルサブプログラム(ブロック内定義)とWITH FUNCTION句
  • 単体 vs パッケージ化の判断基準
  • 実務10パターン+アンチパターン7選
スポンサーリンク
  1. 30秒クイックリファレンス:基本構文
  2. プロシージャ vs ファンクション:10軸完全比較
  3. ストアドプロシージャの作成
  4. ストアドファンクションの作成
  5. IN/OUT/IN OUTパラメータの使い分け
  6. ファンクションの戻り値型:5種の選択肢
    1. ①スカラー値(NUMBER/VARCHAR2/DATE等)
    2. ②RECORD(複数フィールド)
    3. ③TABLE(ネスト表:SELECT内で TABLE() 関数として使える)
    4. ④REF CURSOR(結果セット)
    5. ⑤BOOLEAN(PL/SQL限定)
  7. オーバーロード:同名別シグネチャ
  8. 性能を引き出す4つの宣言
    1. ①DETERMINISTIC
    2. ②RESULT_CACHE(11g+)
    3. ③PARALLEL_ENABLE
    4. ④PIPELINED(ストリーム返却)
  9. AUTHID:Definer Rights vs Invoker Rights
  10. PRAGMA AUTONOMOUS_TRANSACTION:独立トランザクション
  11. ローカルサブプログラム+WITH FUNCTION
    1. ローカルサブプログラム(ブロック内定義)
    2. WITH FUNCTION句(SQL内インライン定義、12c+)
  12. 単体 vs パッケージ化の判断基準
  13. 再帰呼び出し
  14. 実務パターン10選
    1. ①バリデーション+DML(プロシージャ)
    2. ②派生列計算(ファンクション+DETERMINISTIC)
    3. ③OUTパラメータで複数値
    4. ④REF CURSORで結果セット返却(API風)
    5. ⑤冪等なバッチ処理
    6. ⑥再帰ファンクション(組織階層集計)
    7. ⑦自律トランザクションでログ保存
    8. ⑧PIPELINED関数(大量データストリーム)
    9. ⑨RESULT_CACHEで参照テーブル
    10. ⑩オーバーロード(パッケージ内)
  15. アンチパターン7選
  16. よくある質問
  17. 関連記事
  18. まとめ

30秒クイックリファレンス:基本構文

プロシージャとファンクション
-- プロシージャ(値を返さない、OUTパラメータで複数値可)
CREATE OR REPLACE PROCEDURE <名前>(
  p_引数 IN|OUT|IN OUT 型,
  ...
) IS
  -- ローカル変数
BEGIN
  -- 処理
EXCEPTION
  -- 例外処理
END <名前>;
/

-- ファンクション(必ず戻り値、SELECT内でも使用可)
CREATE OR REPLACE FUNCTION <名前>(
  p_引数 IN 型,
  ...
) RETURN <戻り型> IS
  v_result <戻り型>;
BEGIN
  -- 処理
  RETURN v_result;       -- 必須
EXCEPTION
  WHEN OTHERS THEN
    RAISE;
END <名前>;
/

最初に決める3項目:値を返すか?(返す→FUNCTION/返さない→PROCEDURE)、②SQL内で使うか?(使う→DETERMINISTIC付きFUNCTION)、③パッケージに入れるか?(単体で完結しないならパッケージ化)。この3問で設計が90%決まります。

プロシージャ vs ファンクション:10軸完全比較

観点 プロシージャ ファンクション
戻り値 無し(OUTパラメータで複数可 必ず1つRETURN必須)
SQL文内使用 ✗(SELECT内で不可) ◎(SELECT fn(x) FROM ...
主な用途 処理実行(DML/バッチ/通知) 値計算/変換/派生列
DML(INSERT/UPDATE/DELETE) ◎ 自由 △ 可能だがSQL内で使う時は副作用NG
呼び出し BEGIN proc(); END;CALL proc(); v := fn();SELECT fn() ...
OUT/IN OUTパラメータ ◎ 頻繁に使う △ 可だが戻り値1つで設計するのが推奨
性能宣言 PARALLEL_ENABLE等 DETERMINISTIC/RESULT_CACHE(キャッシュ可能)
戻り値型 スカラー/RECORD/TABLE/REF CURSOR/BOOLEAN
トランザクション制御 COMMIT/ROLLBACK OK SQL内使用時はNG(AUTONOMOUS_TRANSACTIONなら可)
オーバーロード ◎(パッケージ内のみ) ◎(パッケージ内のみ)

判断ロジック:「これを実行してほしい」→ プロシージャ、「この値が欲しい」→ ファンクション。迷うのは「処理もするけど値も返したい」ケース。①処理が主で結果が1つならファンクション(戻り値=結果)、②処理が主で複数の結果ならプロシージャ(OUTで複数返却)、③純粋なデータ取得はREF CURSORを返すファンクションが多くの場面で最適。

ストアドプロシージャの作成

基本
CREATE OR REPLACE PROCEDURE update_user_status(
  p_user_id IN NUMBER,
  p_status  IN VARCHAR2 DEFAULT 'active'   -- デフォルト値指定可
) IS
  v_count NUMBER;
BEGIN
  UPDATE users
     SET status     = p_status,
         updated_at = SYSTIMESTAMP
   WHERE id = p_user_id;

  v_count := SQL%ROWCOUNT;

  IF v_count = 0 THEN
    RAISE_APPLICATION_ERROR(-20001, 'ユーザーが見つかりません: ' || p_user_id);
  END IF;

  -- 監査ログも残す
  INSERT INTO user_status_log(user_id, new_status, changed_at)
  VALUES (p_user_id, p_status, SYSTIMESTAMP);
EXCEPTION
  WHEN OTHERS THEN
    pkg_error.log(SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
    RAISE;
END update_user_status;
/
呼び出し方
-- パターン①:PL/SQLブロック内(位置引数)
BEGIN
  update_user_status(1, 'inactive');
END;
/

-- パターン②:名前付き引数(推奨:可読性UP)
BEGIN
  update_user_status(
    p_user_id => 1,
    p_status  => 'inactive'
  );
END;
/

-- パターン③:デフォルト値の活用
BEGIN
  update_user_status(p_user_id => 1);   -- p_status は 'active' になる
END;

-- パターン④:SQL*Plus / SQLcl の CALL文
CALL update_user_status(1, 'inactive');

-- パターン⑤:Javaからの呼び出し(JDBC CallableStatement)
CallableStatement cs = conn.prepareCall("{ call update_user_status(?, ?) }");

プロシージャ作成のポイント

  • CREATE OR REPLACEで再作成時の削除+作成を一発(権限情報は保持)
  • 引数名はp_プレフィックス(列名と衝突防止)
  • DEFAULTキーワードでデフォルト値指定
  • 例外処理はRAISEで呼び出し元に再送出が鉄則
  • END 名前;で対応を明示(保守性UP)

ストアドファンクションの作成

基本
CREATE OR REPLACE FUNCTION get_user_full_name(
  p_user_id IN NUMBER
) RETURN VARCHAR2
DETERMINISTIC            -- 同入力→同出力保証(SQL内で最適化される)
RESULT_CACHE             -- 結果をSGAキャッシュ(12c+)
IS
  v_first  users.first_name%TYPE;
  v_last   users.last_name%TYPE;
BEGIN
  SELECT first_name, last_name
    INTO v_first, v_last
    FROM users
   WHERE id = p_user_id;

  RETURN v_last || ' ' || v_first;
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RETURN NULL;
END get_user_full_name;
/
呼び出し方
-- パターン①:変数に代入
DECLARE
  v_name VARCHAR2(200);
BEGIN
  v_name := get_user_full_name(1);
  DBMS_OUTPUT.PUT_LINE(v_name);
END;
/

-- パターン②:SELECT内で利用(ここがファンクションの真価)
SELECT id, get_user_full_name(id) AS full_name
  FROM users
 WHERE status = 'active';

-- パターン③:WHERE条件にも
SELECT *
  FROM orders
 WHERE get_order_total(id) > 10000;

-- パターン④:名前付き引数
DECLARE
  v_name VARCHAR2(200);
BEGIN
  v_name := get_user_full_name(p_user_id => 1);
END;

ファンクションの性能3大宣言:DETERMINISTIC——同じ入力に対して同じ出力を保証(SQL内部でキャッシュ最適化)、②RESULT_CACHE(11g+)——SGAの結果キャッシュ機構に乗る(テーブル変更で自動無効化)、③PARALLEL_ENABLE——パラレルクエリ内で呼び出し可能。これらはSELECT内で呼ばれる時にパフォーマンスを数倍〜数十倍変えます。

IN/OUT/IN OUTパラメータの使い分け

モード 方向 呼び出し側での変更 主な用途
IN(デフォルト) 呼び出し側→サブプログラム 反映されない 入力値(大半)
OUT サブプログラム→呼び出し側 反映される 複数結果の返却/エラーコード
IN OUT 両方向 反映される 値の変更・加工
3種のパラメータ実例
CREATE OR REPLACE PROCEDURE calculate_order(
  p_order_id IN  NUMBER,                     -- 入力
  p_total    OUT NUMBER,                     -- 合計金額を返す
  p_tax      OUT NUMBER,                     -- 税額を返す
  p_discount IN OUT NUMBER                   -- 割引率を受け取り、最終割引額を返す
) IS
  v_subtotal NUMBER;
BEGIN
  SELECT SUM(unit_price * quantity) INTO v_subtotal
    FROM order_items
   WHERE order_id = p_order_id;

  p_discount := v_subtotal * NVL(p_discount, 0) / 100;   -- 入力率→出力金額に加工
  v_subtotal := v_subtotal - p_discount;
  p_tax   := ROUND(v_subtotal * 0.10, 0);
  p_total := v_subtotal + p_tax;
END;
/

-- 呼び出し
DECLARE
  v_total    NUMBER;
  v_tax      NUMBER;
  v_discount NUMBER := 10;   -- 10%割引
BEGIN
  calculate_order(
    p_order_id => 100,
    p_total    => v_total,
    p_tax      => v_tax,
    p_discount => v_discount
  );
  DBMS_OUTPUT.PUT_LINE('total=' || v_total || ', tax=' || v_tax || ', disc=' || v_discount);
END;

より詳細な使い分けは

IN OUTのNOCOPYヒント、例外発生時のOUT挙動、巨大コレクションの受け渡し等は【PL/SQL】IN・OUT・IN OUTパラメータの完全ガイドで詳解しています。

ファンクションの戻り値型:5種の選択肢

①スカラー値(NUMBER/VARCHAR2/DATE等)

CREATE OR REPLACE FUNCTION calc_age(p_birthday DATE)
RETURN NUMBER DETERMINISTIC IS
BEGIN
  RETURN TRUNC(MONTHS_BETWEEN(SYSDATE, p_birthday) / 12);
END;

②RECORD(複数フィールド)

-- 複数の値を構造体で返す(呼び出し元はPL/SQLのみ)
CREATE OR REPLACE PACKAGE pkg_user IS
  TYPE t_user_summary IS RECORD (
    id          NUMBER,
    name        VARCHAR2(100),
    order_count NUMBER
  );

  FUNCTION get_user_summary(p_id IN NUMBER) RETURN t_user_summary;
END;
/

CREATE OR REPLACE PACKAGE BODY pkg_user IS
  FUNCTION get_user_summary(p_id IN NUMBER) RETURN t_user_summary IS
    v_rec t_user_summary;
  BEGIN
    SELECT u.id, u.name, COUNT(o.id)
      INTO v_rec
      FROM users u
      LEFT JOIN orders o ON o.user_id = u.id
     WHERE u.id = p_id
     GROUP BY u.id, u.name;
    RETURN v_rec;
  END;
END;

③TABLE(ネスト表:SELECT内で TABLE() 関数として使える)

CREATE OR REPLACE TYPE t_numbers AS TABLE OF NUMBER;
/

CREATE OR REPLACE FUNCTION get_order_ids(p_user_id IN NUMBER)
RETURN t_numbers
PIPELINED            -- ストリーム返却(省メモリ)
IS
BEGIN
  FOR rec IN (SELECT id FROM orders WHERE user_id = p_user_id) LOOP
    PIPE ROW(rec.id);    -- 1行ずつ返す
  END LOOP;
  RETURN;
END;
/

-- SQL内で TABLE() として展開
SELECT * FROM TABLE(get_order_ids(1));

④REF CURSOR(結果セット)

CREATE OR REPLACE FUNCTION search_users(
  p_status IN VARCHAR2
) RETURN SYS_REFCURSOR IS
  v_rc SYS_REFCURSOR;
BEGIN
  OPEN v_rc FOR
    SELECT id, email FROM users
     WHERE status = p_status
     ORDER BY created_at DESC;
  RETURN v_rc;
END;
/

-- 呼び出し(Javaや.NETも受け取れる)
DECLARE
  v_rc SYS_REFCURSOR;
  v_id NUMBER;
  v_email VARCHAR2(200);
BEGIN
  v_rc := search_users('active');
  LOOP
    FETCH v_rc INTO v_id, v_email;
    EXIT WHEN v_rc%NOTFOUND;
    DBMS_OUTPUT.PUT_LINE(v_id || ': ' || v_email);
  END LOOP;
  CLOSE v_rc;
END;

⑤BOOLEAN(PL/SQL限定)

CREATE OR REPLACE FUNCTION is_admin(p_user_id IN NUMBER) RETURN BOOLEAN IS
  v_role VARCHAR2(20);
BEGIN
  SELECT role INTO v_role FROM users WHERE id = p_user_id;
  RETURN v_role = 'admin';
EXCEPTION
  WHEN NO_DATA_FOUND THEN
    RETURN FALSE;
END;
/

BEGIN
  IF is_admin(p_user_id => 1) THEN
    DBMS_OUTPUT.PUT_LINE('管理者です');
  END IF;
END;

BOOLEANの制約:PL/SQL内でのみ使用可能。SQL文内で直接BOOLEANファンクションを呼ぶことはできません(Oracle 22c以前)。SELECT内で使いたい場合はCHAR(1)NUMBER(1)を返します。

REF CURSORの詳細は【PL/SQL】REF CURSORを使った柔軟なデータ取得方法、TABLE/PIPELINEDは【PL/SQL】パイプライン関数の仕組みと活用術を参照。

オーバーロード:同名別シグネチャ

パッケージ内に限り、同じ名前で引数が異なるサブプログラムを複数定義できます。呼び出し側は引数の型・個数で適切なバージョンが自動選択されます。

オーバーロードの例
CREATE OR REPLACE PACKAGE pkg_formatter IS
  -- NUMBER版
  FUNCTION fmt(p_val IN NUMBER) RETURN VARCHAR2;

  -- DATE版
  FUNCTION fmt(p_val IN DATE) RETURN VARCHAR2;

  -- DATE + フォーマット指定版
  FUNCTION fmt(p_val IN DATE, p_format IN VARCHAR2) RETURN VARCHAR2;

  -- VARCHAR2版
  FUNCTION fmt(p_val IN VARCHAR2) RETURN VARCHAR2;
END pkg_formatter;
/

CREATE OR REPLACE PACKAGE BODY pkg_formatter IS
  FUNCTION fmt(p_val IN NUMBER) RETURN VARCHAR2 IS
  BEGIN RETURN TO_CHAR(p_val, 'FM9,999,999,999'); END;

  FUNCTION fmt(p_val IN DATE) RETURN VARCHAR2 IS
  BEGIN RETURN TO_CHAR(p_val, 'YYYY-MM-DD'); END;

  FUNCTION fmt(p_val IN DATE, p_format IN VARCHAR2) RETURN VARCHAR2 IS
  BEGIN RETURN TO_CHAR(p_val, p_format); END;

  FUNCTION fmt(p_val IN VARCHAR2) RETURN VARCHAR2 IS
  BEGIN RETURN NVL(p_val, '(null)'); END;
END;
/

-- 呼び出し側は型で自動選択
SELECT pkg_formatter.fmt(12345)       AS num_fmt,
       pkg_formatter.fmt(SYSDATE)     AS date_fmt,
       pkg_formatter.fmt(SYSDATE, 'MM/DD') AS date_fmt2
FROM dual;

オーバーロードの制限:パッケージ内でのみ可能(スタンドアロンなプロシージャ/ファンクションは不可)、②戻り値型だけの違いは不可(引数で区別できない)、③暗黙型変換で曖昧になる組み合わせは避ける(例:NUMBER版とVARCHAR2版)。

性能を引き出す4つの宣言

①DETERMINISTIC

-- 同じ入力に対して同じ出力を保証
CREATE OR REPLACE FUNCTION square(p_x IN NUMBER) RETURN NUMBER
DETERMINISTIC IS
BEGIN
  RETURN p_x * p_x;
END;

-- SQL内で同じ引数で呼ばれたら再計算されずキャッシュされる
SELECT square(x) FROM t;   -- 内部的に同じxに対しては1回だけ計算

②RESULT_CACHE(11g+)

-- SGAの結果キャッシュ領域を使う
CREATE OR REPLACE FUNCTION get_tax_rate(p_country IN VARCHAR2)
RETURN NUMBER
RESULT_CACHE
RELIES_ON(tax_rates)   -- このテーブルが変わるとキャッシュ自動無効化
IS
  v_rate NUMBER;
BEGIN
  SELECT rate INTO v_rate FROM tax_rates WHERE country = p_country;
  RETURN v_rate;
END;

③PARALLEL_ENABLE

-- パラレルクエリ内で使用可能にする
CREATE OR REPLACE FUNCTION process_row(p_val IN NUMBER) RETURN NUMBER
PARALLEL_ENABLE IS
BEGIN
  RETURN p_val * 2;
END;

-- 大量データの加工で並列度を上げられる
SELECT /*+ PARALLEL(8) */ process_row(val) FROM big_table;

④PIPELINED(ストリーム返却)

-- メモリに全件溜めず1行ずつ返す(超大量データ向き)
CREATE OR REPLACE FUNCTION generate_dates(
  p_start IN DATE,
  p_end   IN DATE
) RETURN t_dates PIPELINED IS
  v_cur DATE := p_start;
BEGIN
  WHILE v_cur <= p_end LOOP
    PIPE ROW(v_cur);
    v_cur := v_cur + 1;
  END LOOP;
  RETURN;
END;

SELECT * FROM TABLE(generate_dates(DATE '2026-04-01', DATE '2026-04-30'));

性能宣言の選び方:①SQL内で大量呼び出し+計算結果が入力だけで決まる→DETERMINISTIC、②テーブル依存の結果をキャッシュ→RESULT_CACHE、③パラレル処理で呼び出したい→PARALLEL_ENABLE、④大量行をストリーム返却→PIPELINED。これらを付けるだけで数倍〜数十倍速くなるケースあり。

AUTHID:Definer Rights vs Invoker Rights

サブプログラムが誰の権限で実行されるかを決める重要な宣言。誤って選ぶと「権限の穴」や「動かない」事故に直結します。

AUTHID 2モード
-- Definer Rights(デフォルト、省略時)
-- → プロシージャ所有者の権限で実行
CREATE OR REPLACE PROCEDURE get_secret
AUTHID DEFINER    -- 明示してもOK、省略時のデフォルト
IS
  v_secret VARCHAR2(100);
BEGIN
  SELECT value INTO v_secret FROM admin.secrets WHERE id = 1;
  -- 呼び出し側が admin.secrets への権限を持たなくても実行できる
END;

-- Invoker Rights(呼び出し側の権限)
-- → 呼び出したユーザーの権限で実行
CREATE OR REPLACE PROCEDURE show_my_data
AUTHID CURRENT_USER   -- 呼び出したユーザー自身の権限
IS
BEGIN
  -- 呼び出したユーザーが持つテーブルやデータだけアクセス
  FOR rec IN (SELECT * FROM user_tables) LOOP
    DBMS_OUTPUT.PUT_LINE(rec.table_name);
  END LOOP;
END;
AUTHID 実行権限 用途
DEFINER(デフォルト) 所有者権限 共通APIの公開(呼び出し元に権限付与不要)
CURRENT_USER 呼び出し側の権限 ユーザーごとに異なるデータを扱う/マルチテナント

DEFINERの落とし穴:所有者権限で実行されるため、呼び出し側に余計な権限を与えずに済みますが、SQLインジェクション脆弱性があると呼び出し側が本来アクセスできないデータに到達できます。動的SQLを使うサブプログラムはバインド変数+DBMS_ASSERTで厳密に検証する必要があります。詳細は【PL/SQL】動的SQLのセキュアな書き方【PL/SQL】Invoker RightsとDefiner Rightsの違いと使い分け参照。

PRAGMA AUTONOMOUS_TRANSACTION:独立トランザクション

-- エラーログのようにメイン処理がROLLBACKしても残したいデータを書く
CREATE OR REPLACE PROCEDURE log_error(
  p_code IN NUMBER,
  p_msg  IN VARCHAR2
) IS
  PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
  INSERT INTO error_log(error_time, error_code, error_msg)
  VALUES (SYSTIMESTAMP, p_code, p_msg);
  COMMIT;     -- 独立トランザクションなので呼び出し元に影響なし
END;
/

-- 呼び出し側
BEGIN
  INSERT INTO orders VALUES (...);   -- メイン処理
  log_error(0, 'checkpoint');       -- ログ保存(独立トランザクション)
  RAISE_APPLICATION_ERROR(-20001, 'テスト');   -- 例外発生
EXCEPTION
  WHEN OTHERS THEN
    log_error(SQLCODE, SQLERRM);     -- ログは残る
    ROLLBACK;                         -- メイン処理はROLLBACK(ログはそのまま)
END;

詳細は別記事で

自律トランザクションの落とし穴と活用パターンは【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法で詳解。監査ログ/Mutating Table回避/ミニメニューなどの典型用途があります。

ローカルサブプログラム+WITH FUNCTION

ローカルサブプログラム(ブロック内定義)

DECLARE
  -- ブロック内だけで使うヘルパー関数
  FUNCTION local_format(p_n IN NUMBER) RETURN VARCHAR2 IS
  BEGIN
    RETURN TO_CHAR(p_n, 'FM9,999,999,999');
  END local_format;

  -- ローカルプロシージャ
  PROCEDURE log_info(p_msg IN VARCHAR2) IS
  BEGIN
    DBMS_OUTPUT.PUT_LINE('[INFO] ' || p_msg);
  END log_info;
BEGIN
  log_info('total: ' || local_format(1234567));
END;
/

WITH FUNCTION句(SQL内インライン定義、12c+)

WITH FUNCTION fmt(p_n NUMBER) RETURN VARCHAR2 IS
BEGIN
  RETURN TO_CHAR(p_n, 'FM9,999,999,999');
END;
SELECT id, fmt(amount) AS formatted FROM orders
/

-- SQL文内だけで使える使い捨て関数
-- ストアドファンクションより呼び出しコストが低い
-- 詳細は【PL/SQL】WITH FUNCTION句ガイドを参照

使い分け

  • ブロック内だけで使う→ローカルサブプログラム
  • 単一SQL文内だけで使う→WITH FUNCTION句詳解
  • 複数の場所で使う→パッケージ
  • 単独で再利用→スタンドアロンなストアドプロシージャ/ファンクション

単体 vs パッケージ化の判断基準

基準 スタンドアロン(単体) パッケージ
関連処理数 1つだけ 複数(テーマで分類可)
オーバーロード 不可 ◎ 可能
状態保持 不可 ◎ パッケージ変数
権限管理 個別GRANT パッケージ単位でGRANT(管理ラク)
依存性管理 個別で無効化 本体変更しても仕様変わらなければ依存先は無効化しない
ロード性能 個別ロード 最初の呼び出しで全体ロード(以降キャッシュ)

推奨:2本以上関連する処理があればパッケージ化。単発のバッチ処理だけは単体で、それ以外はpkg_userpkg_orderのようにドメイン別パッケージにまとめると管理しやすい。詳細は【PL/SQL】パッケージを使ったコード管理と再利用性向上参照。

再帰呼び出し

再帰ファンクション(階層計算)
CREATE OR REPLACE FUNCTION factorial(p_n IN NUMBER) RETURN NUMBER IS
BEGIN
  IF p_n <= 1 THEN
    RETURN 1;
  END IF;
  RETURN p_n * factorial(p_n - 1);   -- 自分自身を呼ぶ
END;
/

SELECT factorial(10) FROM dual;  -- 3628800

-- 階層データの探索
CREATE OR REPLACE PROCEDURE print_tree(
  p_node_id IN NUMBER,
  p_depth   IN NUMBER DEFAULT 0
) IS
BEGIN
  FOR rec IN (SELECT id, name FROM nodes WHERE parent_id = p_node_id) LOOP
    DBMS_OUTPUT.PUT_LINE(RPAD(' ', p_depth * 2) || rec.name);
    print_tree(rec.id, p_depth + 1);   -- 再帰
  END LOOP;
END;

PL/SQLには再帰深度の実装上限があり(ORA-06502/スタックオーバーフロー)、数千階層は危険。階層クエリはSQLのCONNECT BYまたは再帰CTE(WITH ... RECURSIVEで書く方が高速かつ安全な場合が多いです。

実務パターン10選

①バリデーション+DML(プロシージャ)

CREATE OR REPLACE PROCEDURE register_user(
  p_email    IN VARCHAR2,
  p_password IN VARCHAR2,
  p_user_id  OUT NUMBER
) IS
BEGIN
  IF p_email IS NULL OR NOT REGEXP_LIKE(p_email, '^[^@]+@[^@]+\.[^@]+$') THEN
    RAISE_APPLICATION_ERROR(-20001, '無効なメールアドレス');
  END IF;
  IF LENGTH(p_password) < 8 THEN
    RAISE_APPLICATION_ERROR(-20002, 'パスワードは8文字以上');
  END IF;

  INSERT INTO users(email, password_hash)
  VALUES (p_email, hash_password(p_password))
  RETURNING id INTO p_user_id;
END;

②派生列計算(ファンクション+DETERMINISTIC)

CREATE OR REPLACE FUNCTION calc_tax(p_amount IN NUMBER) RETURN NUMBER
DETERMINISTIC IS
BEGIN
  RETURN ROUND(p_amount * 0.10);
END;

-- 派生列として使用
SELECT id, amount, calc_tax(amount) AS tax FROM orders;

③OUTパラメータで複数値

CREATE OR REPLACE PROCEDURE get_order_stats(
  p_user_id  IN  NUMBER,
  p_count    OUT NUMBER,
  p_total    OUT NUMBER,
  p_avg      OUT NUMBER
) IS
BEGIN
  SELECT COUNT(*), SUM(amount), AVG(amount)
    INTO p_count, p_total, p_avg
    FROM orders
   WHERE user_id = p_user_id;
END;

④REF CURSORで結果セット返却(API風)

CREATE OR REPLACE FUNCTION search_products(
  p_keyword IN VARCHAR2
) RETURN SYS_REFCURSOR IS
  v_rc SYS_REFCURSOR;
BEGIN
  OPEN v_rc FOR
    SELECT id, name, price FROM products
    WHERE LOWER(name) LIKE '%' || LOWER(p_keyword) || '%';
  RETURN v_rc;
END;

⑤冪等なバッチ処理

CREATE OR REPLACE PROCEDURE close_expired_orders IS
  v_count NUMBER;
BEGIN
  UPDATE orders SET status = 'closed'
   WHERE status = 'pending'
     AND created_at < SYSDATE - 30;
  v_count := SQL%ROWCOUNT;
  DBMS_OUTPUT.PUT_LINE(v_count || '件をcloseしました');
  COMMIT;
END;

⑥再帰ファンクション(組織階層集計)

CREATE OR REPLACE FUNCTION total_subordinates(p_mgr_id IN NUMBER) RETURN NUMBER IS
  v_total NUMBER := 0;
BEGIN
  FOR rec IN (SELECT id FROM employees WHERE manager_id = p_mgr_id) LOOP
    v_total := v_total + 1 + total_subordinates(rec.id);
  END LOOP;
  RETURN v_total;
END;

⑦自律トランザクションでログ保存

CREATE OR REPLACE PROCEDURE log_to_audit(p_msg VARCHAR2) IS
  PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
  INSERT INTO audit_log(ts, msg) VALUES (SYSTIMESTAMP, p_msg);
  COMMIT;
END;

⑧PIPELINED関数(大量データストリーム)

CREATE OR REPLACE FUNCTION split_csv(p_str IN VARCHAR2)
RETURN t_strings PIPELINED IS
  v_part VARCHAR2(4000);
  v_pos  PLS_INTEGER := 1;
  v_next PLS_INTEGER;
BEGIN
  LOOP
    v_next := INSTR(p_str, ',', v_pos);
    IF v_next = 0 THEN
      PIPE ROW(SUBSTR(p_str, v_pos));
      EXIT;
    END IF;
    PIPE ROW(SUBSTR(p_str, v_pos, v_next - v_pos));
    v_pos := v_next + 1;
  END LOOP;
  RETURN;
END;

SELECT * FROM TABLE(split_csv('apple,banana,cherry'));

⑨RESULT_CACHEで参照テーブル

CREATE OR REPLACE FUNCTION get_country_name(p_code IN VARCHAR2)
RETURN VARCHAR2
RESULT_CACHE RELIES_ON(countries) IS
  v_name VARCHAR2(100);
BEGIN
  SELECT name INTO v_name FROM countries WHERE code = p_code;
  RETURN v_name;
EXCEPTION
  WHEN NO_DATA_FOUND THEN RETURN NULL;
END;

⑩オーバーロード(パッケージ内)

CREATE OR REPLACE PACKAGE pkg_notify IS
  PROCEDURE send(p_user_id IN NUMBER, p_msg IN VARCHAR2);
  PROCEDURE send(p_emails IN VARCHAR2_ARRAY, p_msg IN VARCHAR2);
  PROCEDURE send(p_user IN users%ROWTYPE, p_msg IN VARCHAR2);
END;

アンチパターン7選

①SELECT内でDMLするファンクション。「この関数をSELECTしただけで値が更新される」挙動は意図と違う結果を招く。DMLするならプロシージャにする、WNPS(write no package state)を守る。

②OUTパラメータを多用したファンクション。ファンクションは戻り値1つで設計するのが鉄則。複数値を返したいならプロシージャ+OUT、またはRECORDを返す設計に。

③DETERMINISTIC宣言のウソ。内部でSELECTやSYSDATEを使うのにDETERMINISTICを付けると、Oracleが同じ入力なのに違う結果と判断し誤ったキャッシュ結果を返す。本当に純粋な関数(入力のみで結果が決まる)にだけ付ける。

④例外をすべてWHEN OTHERS THEN NULL;で潰す。エラーがサイレント失敗して呼び出し側が成功したと誤認する。必ずRAISE;で再送出するか、ログ記録+RAISE。

⑤AUTHIDを省略して事故。デフォルト(DEFINER)のまま動的SQLを使うとSQLインジェクションで権限が拡散。ユーザーデータにアクセスする動的SQLはAUTHID CURRENT_USER+バインド変数。

⑥スタンドアロン関数を乱立。関連する関数がバラバラに点在すると保守地獄。同一ドメインの関数はパッケージにまとめる。

⑦コミット/ロールバックをプロシージャ内で勝手に実行。呼び出し元の大きなトランザクション戦略を壊す。COMMITは最上位の呼び出し元が担当するのが原則。例外:ログ保存のAUTONOMOUS_TRANSACTION内では必須。

よくある質問

Qプロシージャとファンクションの決定的な違いは?
A値を返すか否かです。ファンクションはRETURNが必須で戻り値1つ、プロシージャは戻り値なし(OUTパラメータで複数返却可)。さらにファンクションはSQL文内で使えるのが最大の違いで、SELECT内でSELECT fn(x) FROM ...と呼べます。
QファンクションがSELECT内で遅い
A最も多い原因はDETERMINISTICが付いていない、または入力が同じでも毎回計算されていること。解決策:①DETERMINISTIC宣言、②11g+ならRESULT_CACHEで結果キャッシュ、③可能ならSQLの式で書き直し(関数呼び出しコスト自体をなくす)。
QOUTパラメータに初期値を渡せる?
AOUTは受け取った値を無視して呼び出し時にNULL初期化されます。初期値を渡して加工したい場合はIN OUTを使います。詳細は【PL/SQL】IN・OUT・IN OUTパラメータの完全ガイド参照。
Qオーバーロードは何個まで可能?
Aパッケージ内でいくつでも可能ですが、引数の組み合わせで曖昧さが出ると呼び出し時エラー。例:NUMBER版とINTEGER版があると暗黙変換で曖昧になる。型が明確に区別できる組み合わせに限定するのが実用的。
QRESULT_CACHEはいつ無効化される?
ARELIES_ON句で指定したテーブルにDML(INSERT/UPDATE/DELETE)が走ると自動無効化されます。11gR2以降はRELIES_ONを省略してもOracleが依存テーブルを自動検知。明示指定の方が意図が明確なので推奨。
QAUTHIDはDEFINERとCURRENT_USERどちらを使うべき?
A共通API(会員管理・注文処理等)DEFINER(呼び出し側権限不要)、②ユーザー固有のデータ操作CURRENT_USER、③動的SQLで未検証ユーザー入力を扱う→必ずCURRENT_USERで権限拡散を防ぐ。詳細は【PL/SQL】Invoker RightsとDefiner Rightsの違いと使い分け
Qファンクションで例外を吸収してNULLを返すのは良い?
A用途次第。SELECT内で使うファンクションなら、0件時にNULLを返すのが扱いやすい(SQL側でNVLできる)。一方ビジネスロジックを担うファンクションでは例外をRAISEで伝える方が呼び出し元が気づける。「レポート用の表示」か「業務整合性」かで設計判断。
Q引数のデフォルト値は指定できる?
Aできます。p_status IN VARCHAR2 DEFAULT 'active'のように宣言し、呼び出し時に省略可能。ただし後方のパラメータだけデフォルトにするか、名前付き引数(p_x => 1)で指定する必要があります。
Qパッケージにするか単体にするか
A2つ以上の関連処理があればパッケージ化が原則。パッケージ化のメリット:①オーバーロード可、②パッケージ変数で状態保持、③権限管理が楽、④依存性が疎になり再コンパイル範囲を最小化、⑤ロード性能。単体はバッチスクリプトのような独立処理のみ。
Q再帰呼び出しはどこまで許される?
AOracleのPL/SQLスタックは数千階層が限界で、深い再帰はORA-06500(スタックオーバーフロー)や性能劣化を招く。階層データはCONNECT BYや再帰CTEで書く方が高速安全。純粋な計算(factorial等)でも大きな値では反復ループに書き換える方がよい。

関連記事

まとめ

  • プロシージャは処理の実行(戻り値無し/OUTで複数返却可)、ファンクションは値の返却(戻り値必須)
  • ファンクションの最大の強みはSQL文内で呼び出せること
  • 引数モード3種:IN(デフォルト)/OUT(出力)/IN OUT(両方向)
  • 戻り値5種:スカラー/RECORD/TABLE(ネスト表)/REF CURSOR/BOOLEAN
  • 呼び出し方:PL/SQL内/SELECT内/CALL文/名前付き引数/位置引数
  • オーバーロードはパッケージ内でのみ可能(同名別シグネチャ)
  • 性能宣言:DETERMINISTIC/RESULT_CACHE/PARALLEL_ENABLE/PIPELINED
  • AUTHID DEFINER(所有者権限)vs CURRENT_USER(呼出者権限)の使い分け
  • PRAGMA AUTONOMOUS_TRANSACTIONで独立COMMIT(監査ログ等)
  • ローカルサブプログラム/WITH FUNCTION句で使い捨て関数をインライン定義
  • 関連処理が2つ以上あればパッケージ化が原則
  • アンチパターン:SELECT内DML/ファンクションOUT乱用/DETERMINISTICのウソ/例外握り潰し/COMMIT乱用

プロシージャとファンクションはPL/SQLの再利用の核心。「値を返すかどうか」以上に、DETERMINISTIC/RESULT_CACHE/AUTHID/パッケージ化の判断まで含めて設計できるようになると、保守性・性能・セキュリティが劇的に向上します。パラメータ詳細はIN・OUT・IN OUT完全ガイド、パッケージ化はパッケージ活用、権限はInvoker/Definer Rightsと組み合わせて活用してください。