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 PROCEDURE/FUNCTION) - 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選
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軸完全比較
判断ロジック:「これを実行してほしい」→ プロシージャ、「この値が欲しい」→ ファンクション。迷うのは「処理もするけど値も返したい」ケース。①処理が主で結果が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パラメータの使い分け
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
サブプログラムが誰の権限で実行されるかを決める重要な宣言。誤って選ぶと「権限の穴」や「動かない」事故に直結します。
-- 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;
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 パッケージ化の判断基準
推奨:2本以上関連する処理があればパッケージ化。単発のバッチ処理だけは単体で、それ以外はpkg_user/pkg_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内では必須。
よくある質問
RETURNが必須で戻り値1つ、プロシージャは戻り値なし(OUTパラメータで複数返却可)。さらにファンクションはSQL文内で使えるのが最大の違いで、SELECT内でSELECT fn(x) FROM ...と呼べます。DETERMINISTICが付いていない、または入力が同じでも毎回計算されていること。解決策:①DETERMINISTIC宣言、②11g+ならRESULT_CACHEで結果キャッシュ、③可能ならSQLの式で書き直し(関数呼び出しコスト自体をなくす)。RELIES_ON句で指定したテーブルにDML(INSERT/UPDATE/DELETE)が走ると自動無効化されます。11gR2以降はRELIES_ONを省略してもOracleが依存テーブルを自動検知。明示指定の方が意図が明確なので推奨。DEFINER(呼び出し側権限不要)、②ユーザー固有のデータ操作→CURRENT_USER、③動的SQLで未検証ユーザー入力を扱う→必ずCURRENT_USERで権限拡散を防ぐ。詳細は【PL/SQL】Invoker RightsとDefiner Rightsの違いと使い分け。p_status IN VARCHAR2 DEFAULT 'active'のように宣言し、呼び出し時に省略可能。ただし後方のパラメータだけデフォルトにするか、名前付き引数(p_x => 1)で指定する必要があります。CONNECT BYや再帰CTEで書く方が高速安全。純粋な計算(factorial等)でも大きな値では反復ループに書き換える方がよい。関連記事
- 【PL/SQL】基本構文完全ガイド — 4セクション構造とブロック7種類
- 【PL/SQL】IN・OUT・IN OUTパラメータの完全ガイド — パラメータモードの深掘り
- 【PL/SQL】パッケージを使ったコード管理と再利用性向上 — パッケージ化の詳解
- 【PL/SQL】REF CURSORを使った柔軟なデータ取得方法 — REF CURSORを返すファンクション
- 【PL/SQL】パイプライン関数の仕組みと活用術 — PIPELINED関数
- 【PL/SQL】WITH FUNCTION句を使ってSQL内に関数を埋め込む方法 — インライン関数
- 【PL/SQL】Invoker RightsとDefiner Rightsの違いと使い分け — AUTHID詳解
- 【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法 — 自律トランザクション
- 【PL/SQL】動的SQLのセキュアな書き方 — EXECUTE IMMEDIATE+バインド変数
- 【PL/SQL】IF文完全ガイド — サブプログラム内のIF
- 【PL/SQL】例外処理完全ガイド — サブプログラムの例外設計
- 【PL/SQL】カーソル完全ガイド — サブプログラム内のカーソル
まとめ
- プロシージャは処理の実行(戻り値無し/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と組み合わせて活用してください。

