締め処理を一件ずつアプリから流したら三十分かかった。トリガの中でエラーを握りつぶしたら、データが半端な状態で残った。Oracleから移してきた手続きが、型名を直しただけでは通らない。この三つはどれもPL/pgSQLの仕様を知っていれば設計時点で避けられる種類の事故で、後から手を入れると影響範囲が読めなくなります。
この記事は、PostgreSQLに標準で入っている手続き言語PL/pgSQLを、どう書くかとどこまで任せるかの両面から扱う解説です。関数とプロシージャが分かれる境目、DOブロックと例外処理とカーソルの書き方、Oracle PL/SQLからの書き換えで詰まる箇所、そしてアプリ側とデータベース側のどちらに処理を置くかの判断基準までを、実行できるSQL付きで並べます。データベース全般の位置づけはデータベースとは?種類・DBMS・RDBとNoSQLの選び方に譲り、ここはPostgreSQL固有の手続き実装だけを扱います。数値の前提は18系(2026年8月時点で18.6が最新マイナー)としました。
まとめ:PL/pgSQLを触る前に決める5点
結論を5点で先に置きます。第一に、PL/pgSQLは追加インストールを必要としません。信頼付き言語として既定で組み込まれているため、拡張を入れる作業なしにCREATE FUNCTIONから書き始められます。
第二に、関数とプロシージャの分かれ目は戻り値ではなくトランザクション制御です。関数は呼び出し元のトランザクションの内側で動くため途中でコミットできませんが、プロシージャは条件を満たせば処理の途中でコミットとロールバックを発行できます。
第三に、そのコミットが通るかどうかは呼び出し経路で決まります。CALLまたはDOから直接、あるいはそこからネストして呼ばれた場合だけ有効で、間にSELECTなどの問い合わせが挟まると使えません。
第四に、例外ハンドラを書いたブロックは内部で副トランザクションを作ります。この仕組みのおかげでハンドラに入った時点でブロック内の変更が自動的に巻き戻る一方、一行ごとに例外を捕まえるループは件数分の副トランザクションを生んで費用がかさみます。
第五に、Oracle PL/SQLからの持ち込みは型名の置換だけでは終わりません。本体の囲み方、変数名と列名の衝突時の挙動、逆順ループの書式、パッケージの代替、例外名とSAVEPOINTの扱いが変わるため、六か所を機械的に確認する手順を用意しておくと工数が読めます。以下、順に根拠を見ていきます。
PL/pgSQLとは何か|SQLだけでは書けない処理をサーバ側に置く言語
PL/pgSQLは、SQLに変数・条件分岐・繰り返し・例外処理といった手続き構造を足した言語です。複数の問い合わせと計算をひとまとまりにしてサーバ内部で実行できるため、クライアントとの往復が発生しません。逆に言えば、往復が減らない処理では効果も出ません。
plpgsqlは既定で組み込まれた信頼付き言語であるという前提
PL/pgSQLは信頼付き言語として扱われ、新規に作ったデータベースへ最初から入っています。CREATE EXTENSION plpgsql;を明示的に流す場面は、過去に削除された環境を復旧するときくらいです。信頼付きという扱いは、スーパーユーザ以外でも関数を作れることを意味します。
-- 言語が入っているかの確認
SELECT lanname, lanpltrusted FROM pg_language WHERE lanname = 'plpgsql';
-- 最小の関数
CREATE OR REPLACE FUNCTION add_tax(price numeric)
RETURNS numeric AS $$
BEGIN
RETURN round(price * 1.10);
END;
$$ LANGUAGE plpgsql IMMUTABLE;
末尾のIMMUTABLEは、同じ引数なら常に同じ結果を返すという宣言です。プランナはこの宣言を根拠に結果を使い回すため、テーブルを読む関数へ誤って付けると古い値が返ります。テーブルを読むならSTABLE、書くなら既定のVOLATILEのままにしてください。
サーバ側へ処理を寄せると通信の往復は減る一方で移植性は落ちる
アプリから百件の更新を投げると百往復が発生します。同じ処理を一つの関数にまとめれば往復は一回で済み、ネットワーク遅延が支配的な構成では効果が大きく出ます。ただし手続きがデータベース側に入ると、アプリのテスト資産では検証できない領域が生じる構図です。
移植性の面でも代償があります。PL/pgSQLで書いた処理はPostgreSQL以外へそのまま持っていけないため、将来別のデータベースへ移す可能性がある業務ロジックを厚く置くと、移行時の書き換え対象がそのまま増える構図です。どこまで寄せるかの線引きは本記事の後半で扱います。
関数とプロシージャの違いはトランザクション制御を持てるかどうか
戻り値の有無で説明されることが多い区別ですが、実装上の分かれ目はトランザクションにあります。関数はRETURNS voidと書けば値を返さずに済みますし、プロシージャも14以降ならOUTパラメータで値を返せます。判断の軸として使えるのは、処理の途中でコミットを打てるかどうかだけです。
関数はSELECTから呼べてプロシージャはCALLで呼び出す
関数は式の一部として書けるため、SELECTのリストやWHERE句、ビューの定義にも埋め込めます。プロシージャは11で追加された機能で、専用のCALLコマンドでしか実行できません。両者の差を表にまとめます。
| 観点 | 関数 | プロシージャ |
|---|---|---|
| 作成 | CREATE FUNCTION | CREATE PROCEDURE |
| 呼び出し | SELECT や式の中 | CALL のみ |
| 戻り値 | RETURNS で宣言 | OUT パラメータ(14以降) |
| 途中のCOMMIT | 不可 | 条件付きで可能 |
| 導入バージョン | 初期から | 11で追加 |
| 主な用途 | 値の計算・整形 | 区切りのあるバッチ |
関数が途中でコミットできないのは、呼び出し元の問い合わせがすでに一つのトランザクションとして動いているためです。バッチを千件ずつ区切って確定させたい要件は、関数では表現できません。
プロシージャがコミットできる条件と例外ハンドラ内での制限事項
公式文書はトランザクション制御が使える場所を明確に区切っています。CALLで呼ばれたプロシージャの中、DOで実行した匿名ブロックの中、そしてトップレベルからネストしたCALLやDOの中に限られます。間に問い合わせが挟まると無効になる点が実装上の落とし穴です。
-- 通る: CALL から直接
CALL nightly_close();
-- 通らない: SELECT 経由で呼ばれた先のプロシージャ
CALL proc_a(); -- proc_a の中で SELECT func_b() を呼び
-- func_b の中で CALL proc_c() すると
-- proc_c ではトランザクション制御が使えない
制限はもう三つあります。例外ハンドラを持つブロックの中ではコミットもロールバックも発行できません。PL/pgSQLはSAVEPOINTを扱えず、ハンドラ付きブロック自体が副トランザクションとして実装されているためです。読み取り専用でないカーソルで駆動されるループの中も同様に禁止されており、SECURITY DEFINERを付けたプロシージャもトランザクション制御文を実行できません。権限まわりの設計はPostgreSQLのロールと権限設計と併せて決めてください。
OUTパラメータは14以降でプロシージャでも使えるようになった
13以前のプロシージャはINOUTでしか値を返せず、入力を持たない出力だけの引数が書けませんでした。14のリリースノートには、プロシージャがOUTパラメータを持てるようになったと明記されています。古い解説記事にある「プロシージャは値を返せない」という記述は、この版を境に前提が変わりました。
CREATE OR REPLACE PROCEDURE close_month(
IN target_ym text,
OUT closed_rows bigint)
LANGUAGE plpgsql AS $$
BEGIN
UPDATE sales SET closed = true WHERE ym = target_ym;
GET DIAGNOSTICS closed_rows = ROW_COUNT;
COMMIT;
END;
$$;
CALL close_month('2026-08', NULL);
OUTパラメータを持つプロシージャをCALLするときは、出力側の位置にプレースホルダを置く点に注意してください。省略すると引数の数が合わず、該当するプロシージャが見つからないというエラーになります。
DOブロックと例外処理・カーソルで書く実際のPL/pgSQLの構文
PL/pgSQLの本体はブロック構造です。宣言部と実行部と例外部の三段で構成され、入れ子にできます。ここでは実務で書く頻度の高い三つの型を見ます。
DOブロックは名前を付けずにその場で手続きを実行する仕組みです
DOは匿名のコードブロックを一度だけ実行する命令です。関数として登録せずに済むため、移行作業やデータ補正のような一回限りの処理に向きます。引数は渡せず、値も返せません。
DO $$
DECLARE
r record;
BEGIN
FOR r IN SELECT id, qty FROM stock WHERE qty IS NULL LOOP
UPDATE stock SET qty = 0 WHERE id = r.id;
RAISE NOTICE 'fixed id=%', r.id;
END LOOP;
END;
$$;
権限は実行したユーザのものがそのまま適用されます。定義者権限で動かしたい処理をDOで書くことはできないため、その要件が出た時点でプロシージャか関数として登録する判断に切り替えてください。
例外処理のBEGIN EXCEPTIONは副トランザクションを作る
例外部を書いたブロックでエラーが起きると、そのブロック内で行ったデータ変更が自動的に巻き戻ってから例外部へ制御が移ります。OracleのようにSAVEPOINTを自分で置く必要はありません。仕組みとしては、ハンドラ付きブロックへ入る時点で副トランザクションが開始されています。
CREATE OR REPLACE FUNCTION upsert_member(p_code text, p_name text)
RETURNS text AS $$
BEGIN
INSERT INTO members(code, name) VALUES (p_code, p_name);
RETURN 'inserted';
EXCEPTION
WHEN unique_violation THEN
UPDATE members SET name = p_name WHERE code = p_code;
RETURN 'updated';
WHEN others THEN
RAISE NOTICE 'code=% state=% msg=%', p_code, SQLSTATE, SQLERRM;
RAISE;
END;
$$ LANGUAGE plpgsql;
この副トランザクションには費用が付きます。一件ごとにハンドラ付きブロックを通すループを百万件走らせれば、百万回の副トランザクションが発生してトランザクションIDと資源を消費します。重複を吸収したいだけなら例外に頼らずON CONFLICTを使う書き方のほうが安く済むため、PostgreSQLのUPSERT実装の判断基準と突き合わせてください。トランザクションIDの消費が積み上がったときの影響はPostgreSQLのVACUUM運用で扱っています。
カーソルはFORループの暗黙型と明示宣言型で使い分けを決める
行を一件ずつ処理する書き方は二通りあります。FOR rec IN SELECTと書く形は内部でカーソルが自動的に開かれ、閉じる処理も不要です。日常のループはこちらで足ります。
-- 明示宣言型: 呼び出し元へカーソルを返す
CREATE OR REPLACE FUNCTION open_orders(p_ym text)
RETURNS refcursor AS $$
DECLARE
cur refcursor := 'orders_cur';
BEGIN
OPEN cur FOR SELECT id, amount FROM orders WHERE ym = p_ym;
RETURN cur;
END;
$$ LANGUAGE plpgsql;
BEGIN;
SELECT open_orders('2026-08');
FETCH 100 FROM orders_cur;
CLOSE orders_cur;
COMMIT;
明示宣言型を選ぶのは、結果セットを呼び出し元のアプリへ少しずつ渡したい場合と、複数のカーソルを交互に進めたい場合です。refcursorで返したカーソルは同一トランザクションの中でしか有効ではないため、アプリ側で明示的にトランザクションを開いてから受け取る必要があります。
Oracle PL/SQLからの書き換えで実際に詰まる六つの記法差
PL/pgSQLはOracle PL/SQLに構造がよく似ているため、大枠は読み替えられます。工数が膨らむのは似ているのに挙動が違う箇所で、公式の移植ガイドが挙げる論点を六点に畳むと確認漏れが減ります。移行ツールと工程の進め方はOracleからPostgreSQLへの移行手順で扱っているため、ここでは言語仕様の差だけが対象です。
関数宣言とデータ型は本体をドル引用符で囲む形へ書き換える必要がある
宣言部の書式が変わります。RETURN varchar2 ISはRETURNS varchar AS $$になり、末尾はEND; $$ LANGUAGE plpgsql;で閉じます。行末に単独のスラッシュを置く終端記号も不要です。型名はvarchar2がvarcharまたはtext、numberがnumericへ対応します。
| 項目 | Oracle PL/SQL | PL/pgSQL |
|---|---|---|
| 戻り値宣言 | RETURN varchar2 | RETURNS varchar |
| 本体開始 | IS | AS とドル引用符 |
| 本体終了 | END; と終端記号 | END; の後に言語指定 |
| 可変長文字 | varchar2 | varchar または text |
| 数値 | number | numeric |
| 動的SQL | EXECUTE IMMEDIATE | EXECUTE |
変数名と列名の衝突は既定でエラーになり設定変更で挙動を変える
Oracleは問い合わせの中に同名のものがあると列名として解釈します。PostgreSQLは曖昧な参照を既定でエラーにするため、引数名とテーブルの列名を揃えて書いていたコードは移した瞬間に落ちます。plpgsql.variable_conflictをuse_columnに設定すればOracle寄りの挙動へ倒せますが、既存コードを読む人が挙動を推測しにくくなる設定です。
移行時の実務としては、設定で寄せるより引数名に接頭辞を付けて衝突自体を消すほうが後々の保守で楽になります。本記事の例で引数にp_を付けているのはこの理由によります。設定変更を選ぶ場合は関数単位でも宣言できるため、データベース全体の設定を書き換えずに済ませてください。
パッケージ変数とSAVEPOINTは同じ書き方では移せないため注意する
Oracleのパッケージに相当する機能はありません。公式ガイドはスキーマで名前空間を代替し、パッケージレベルの変数は一時テーブルで状態を持たせる方法を示しています。呼び出し名の形が変わるため、アプリ側の呼び出し文字列も併せて洗い出す対象になります。
例外まわりも三点ずれます。ブロック単位の自動巻き戻しがあるためSAVEPOINTとROLLBACK TOは書けません。例外名も異なり、dup_val_on_indexはunique_violationに読み替えます。動的SQLはEXECUTE IMMEDIATEがEXECUTEになり、文字列を組み立てる箇所はquote_literalとquote_identで囲む書き方へ直してください。逆順ループの書式が範囲の順序ごと入れ替わる点も、静かに件数が合わなくなる差分です。
アプリ側とデータベース側のどちらに業務処理を置くかを決める判断基準
PL/pgSQLで書けることと、書くべきことは別の問題です。往復が減るという利点だけで判断すると、テストできない業務ロジックがデータベースへ堆積します。判断の材料を二つに絞って整理します。
データベース側に置いて速くなるのは結果が入力より小さい処理だけ
効果が出るのは、大量の行を読んで少ない行を返す集約・突合・件数確認のような処理です。百万行を読んで一行を返す集計をアプリ側でやれば百万行が転送されますが、関数に入れれば返るのは一行だけになります。逆に、外部APIを呼ぶ処理や、結果をそのまま全件返す処理はデータベース側に置いても往復が減りません。
| 処理の性質 | 置き場所 | 理由 |
|---|---|---|
| 大量読み・少量返し | データベース側 | 転送量が減る |
| 複数文の一括実行 | データベース側 | 往復回数が減る |
| 外部API連携 | アプリ側 | 待ち時間が接続を占有 |
| 頻繁に変わる業務規則 | アプリ側 | 改修と検証が速い |
| 整合性の最終防衛 | データベース側 | 経路を問わず効く |
テストとレビューの体制が無いならデータベース側は増やさない方針にする
関数やプロシージャは、アプリのコードと同じようには扱われないことが多い資産です。バージョン管理に入っていない、単体テストが無い、レビューを通さず本番で直接置き換えられる。この三つのうち一つでも当てはまる現場では、置いた処理が誰にも読めない状態で残ります。
置くと決めたなら、定義をCREATE OR REPLACEのSQLファイルとしてリポジトリへ入れ、投入手順を移行スクリプトに載せてください。将来PostgreSQL以外へ移す計画があるかどうかも先に確認しておくべき項目です。移行予定があるのに業務ロジックを厚く置けば、そのまま書き換え工数として跳ね返ります。
PL/pgSQLの運用で踏む地雷とデバッグ・性能測定の実務対応
書いた後に効いてくるのが、中で何が起きているか見えないという性質です。関数の内側は一つの命令として扱われるため、通常の監視では中身が見えません。
RAISE NOTICEとGET DIAGNOSTICSで実行中の状態を吐き出す
デバッグの基本はRAISE NOTICEによる出力です。書式指定子は位置に応じて引数へ置き換わります。更新件数はGET DIAGNOSTICSでROW_COUNTを取り、直前の問い合わせが行を返したかどうかはFOUNDで判定できます。
本番で例外を投げる側を書くときは、RAISE EXCEPTIONにUSING ERRCODEで独自のSQLSTATEを添えると、呼び出し元のアプリが業務エラーと基盤エラーを区別できます。握りつぶしのハンドラは原因の特定を不可能にするため避けてください。
pg_stat_statementsで関数内のSQLを個別に見るための条件
pg_stat_statementsは既定の設定だと最上位の命令しか記録しません。関数の内側で流れる問い合わせを個別に見たい場合は、pg_stat_statements.trackをallに変える必要があります。この設定を知らないまま、関数が遅いとだけ見えている状態は珍しくありません。
個々の問い合わせが遅い原因を追うところからは実行計画の読み方に切り替わります。auto_explainのlog_nested_statementsを有効にすれば関数内の計画も記録できるため、PostgreSQLの実行計画の読み方の手順へそのまま接続してください。
受託開発でデータベース側の実装を任せる判断と内製で持つ境界線
PL/pgSQLの実装を社内で持ち続けられるのは、仕様が固まっていて変更頻度が低く、書ける人が複数いる場合です。締め処理や在庫引き当てのように業務の中核へ触れる処理を一人しか読めない状態で抱えると、その人が離れた時点で誰も直せない資産になります。
外部へ任せる判断が向くのは二つの場面です。ひとつはOracleからの移行で、書き換え対象の棚卸しと影響範囲の見積もりに経験が要る局面。もうひとつは、アプリ側とデータベース側の線引きを引き直す設計段階で、既存の手続きをどこまで残すかを決める局面です。当社の基幹システム開発では、既存の手続き資産を読み解いたうえで残す範囲と移す範囲を分けて提示しています。
依頼するかどうかを決める材料は、手続きの本数と行数、テストの有無、そして移行予定の三つです。この三つが揃って把握できているなら内製での改修も現実的な選択で、把握できていない状態から始めるなら棚卸しだけを切り出して依頼する形が費用の面で無理がありません。
よくある質問
PL/pgSQLを使うには拡張のインストールが必要ですか?
通常は不要です。信頼付き言語として既定で組み込まれているため、新しく作ったデータベースでもそのままCREATE FUNCTIONが通ります。過去に言語を削除した環境だけCREATE EXTENSION plpgsql;で戻してください。
関数とプロシージャはどちらを使えばよいですか?
処理の途中でコミットを打つ必要があるならプロシージャ、それ以外は関数を選んでください。値を計算して返すだけの処理や、ビューやSELECTの中から呼びたい処理は関数でしか書けません。千件ずつ確定させたいバッチのように区切りが要るものだけがプロシージャの領域です。
例外処理を入れると遅くなると聞きました。本当ですか?
ブロック単位では事実です。例外ハンドラを持つブロックは副トランザクションを作るため、一件ごとにハンドラを通すループは件数分の副トランザクションを発生させます。重複キーを吸収する用途ならON CONFLICTへ寄せ、例外は想定外の事象の記録に絞るのが実務的な使い分けになります。
Oracle PL/SQLのコードはそのまま動きますか?
動きません。宣言部の書式、型名、変数名と列名の衝突時の挙動、逆順ループの範囲順、パッケージの代替、例外名とSAVEPOINTの扱いが変わります。型名の置換だけで見積もると工数が合わないため、この六点を確認項目として先に洗い出してください。
関数の中のSQLが遅いときはどう調べますか?
まずpg_stat_statements.trackをallに変えて、関数内の問い合わせを個別に記録させてください。そのうえで遅い問い合わせを特定し、auto_explainの入れ子記録を有効にして計画を取ります。関数そのものの所要時間だけを見ていても、どの問い合わせが原因かは切り分けられません。
関連記事
- データベースとは?種類・DBMS・RDBとNoSQLの選び方を実装目線で解説(手続き言語を置く土台の選定)
- OracleからPostgreSQLへの移行手順|互換性差分とora2pg・DMSの使い分け(移行ツールと工程の進め方)
- PostgreSQLの実行計画の読み方|EXPLAIN ANALYZEと見積もり乖離の診断(関数内の問い合わせの診断)
- PostgreSQLのUPSERT実装|ON CONFLICTの競合ターゲット指定とMERGE文の使い分け(例外に頼らない重複処理)
- PostgreSQLのロールと権限設計|CREATE ROLEの属性とGRANT・既定権限の決め方(定義者権限の設計判断)