---
title: "PL/pgSQLとは？関数とプロシージャの違いと例外処理・カーソルを実装目線で解説"
url: "https://www.issoh.co.jp/tech/details/16998/"
published: 2026-08-26
updated: 2026-08-26
categories: ["データベース"]
publisher: "株式会社一創"
---

# PL/pgSQLとは？関数とプロシージャの違いと例外処理・カーソルを実装目線で解説

締め処理を一件ずつアプリから流したら三十分かかった。トリガの中でエラーを握りつぶしたら、データが半端な状態で残った。Oracleから移してきた手続きが、型名を直しただけでは通らない。この三つはどれもPL/pgSQLの仕様を知っていれば設計時点で避けられる種類の事故で、後から手を入れると影響範囲が読めなくなります。

この記事は、PostgreSQLに標準で入っている手続き言語PL/pgSQLを、どう書くかとどこまで任せるかの両面から扱う解説です。関数とプロシージャが分かれる境目、DOブロックと例外処理とカーソルの書き方、Oracle PL/SQLからの書き換えで詰まる箇所、そしてアプリ側とデータベース側のどちらに処理を置くかの判断基準までを、実行できるSQL付きで並べます。データベース全般の位置づけは[データベースとは？種類・DBMS・RDBとNoSQLの選び方](https://www.issoh.co.jp/tech/details/13012/)に譲り、ここは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のロールと権限設計](https://www.issoh.co.jp/tech/details/16984/)と併せて決めてください。

### 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実装](https://www.issoh.co.jp/tech/details/16978/)の判断基準と突き合わせてください。トランザクションIDの消費が積み上がったときの影響は[PostgreSQLのVACUUM運用](https://www.issoh.co.jp/tech/details/16982/)で扱っています。

### カーソルは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への移行手順](https://www.issoh.co.jp/tech/details/16988/)で扱っているため、ここでは言語仕様の差だけが対象です。

### 関数宣言とデータ型は本体をドル引用符で囲む形へ書き換える必要がある

宣言部の書式が変わります。`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の実行計画の読み方](https://www.issoh.co.jp/tech/details/16994/)の手順へそのまま接続してください。

## 受託開発でデータベース側の実装を任せる判断と内製で持つ境界線

PL/pgSQLの実装を社内で持ち続けられるのは、仕様が固まっていて変更頻度が低く、書ける人が複数いる場合です。締め処理や在庫引き当てのように業務の中核へ触れる処理を一人しか読めない状態で抱えると、その人が離れた時点で誰も直せない資産になります。

外部へ任せる判断が向くのは二つの場面です。ひとつはOracleからの移行で、書き換え対象の棚卸しと影響範囲の見積もりに経験が要る局面。もうひとつは、アプリ側とデータベース側の線引きを引き直す設計段階で、既存の手続きをどこまで残すかを決める局面です。当社の[基幹システム開発](https://www.issoh.co.jp/service/business%5Fsystem/core%5Fbusiness/)では、既存の手続き資産を読み解いたうえで残す範囲と移す範囲を分けて提示しています。

依頼するかどうかを決める材料は、手続きの本数と行数、テストの有無、そして移行予定の三つです。この三つが揃って把握できているなら内製での改修も現実的な選択で、把握できていない状態から始めるなら棚卸しだけを切り出して依頼する形が費用の面で無理がありません。

## よくある質問

### 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の選び方を実装目線で解説](https://www.issoh.co.jp/tech/details/13012/)（手続き言語を置く土台の選定）
- [OracleからPostgreSQLへの移行手順｜互換性差分とora2pg・DMSの使い分け](https://www.issoh.co.jp/tech/details/16988/)（移行ツールと工程の進め方）
- [PostgreSQLの実行計画の読み方｜EXPLAIN ANALYZEと見積もり乖離の診断](https://www.issoh.co.jp/tech/details/16994/)（関数内の問い合わせの診断）
- [PostgreSQLのUPSERT実装｜ON CONFLICTの競合ターゲット指定とMERGE文の使い分け](https://www.issoh.co.jp/tech/details/16978/)（例外に頼らない重複処理）
- [PostgreSQLのロールと権限設計｜CREATE ROLEの属性とGRANT・既定権限の決め方](https://www.issoh.co.jp/tech/details/16984/)（定義者権限の設計判断）

---

出典: [PL/pgSQLとは？関数とプロシージャの違いと例外処理・カーソルを実装目線で解説](<https://www.issoh.co.jp/tech/details/16998/>)（株式会社一創）
