本記事は PostgreSQL Advent Calendar 2025 5 日目の記事です。
PL/pgSQLの性能を高めたい
2025年は年初からPL/SQLをPL/pgSQLに置き換えたり、PL/pgSQLの基本について講演したり、PL/pgSQLの文法を網羅した技術同人誌を書いたりして、思い返せばPL/pgSQLのことばかり考えた1年でした。
しかしそれでも一つだけ、PL/pgSQLで書いた関数の性能についてだけはクリアできませんでした。
実はPL/SQLと比較して、一般的にPL/pgSQLは遅いのです。。。
CREATE FUNCTIONのオプションを見てみる
PostgreSQLのCREATE FUNCTIONのマニュアルを見ていただければわかるとおり、実はCREATE FUNCTIONにもオプションがいろいろあります。しかし、その中で性能に関係すると言えるものは多くありません。強いて挙げれば次のオプションでしょうか。
| オプション | 値 | デフォルト | 説明 |
|---|---|---|---|
| PARALLEL | UNSAFE,RESTRICTED,SAFE | UNSAFE | デフォルトのUNSAFEでは並列実行されません。並列実行しても安全である場合にSAFEを指定します。 |
| COST | 数値 | 100 | プランナの実行計画策定に使用する関数の推定実行コスト。 |
| ROWS | 数値 | 1000 | プランナが想定する、この関数が返す行数の推定値。複数行返す場合にのみ指定可能。 |
並列実行できればクエリそのものの速度は上る可能性がありますが、いつでも並列可とは限りません。COSTとROWSはどちらもこの場合はこれ!と単純には決めにくく、プランナへ渡す情報となるのみなので、最適な値を指定したとしても劇的な変化が起きるとも思えません。データベースアクセスは行わず、引数に対して固定の値を返す関数であれば「IMMUTABLE」を指定することもできますが、そのような関数は多くはないでしょう。
なんかこう、どんな関数でも「これ指定しておけばOK!」みたいなものはないのでしょうか?
PL/pgSQLの文法を見てみる
マニュアルには性能の観点から、「EXCEPTIONブロックをむやみに使用してはいけません」と書いているくらいで、「こう書けば性能が良くなるよ」ということは書いていません。
どうやら外部から何かを指定してPL/pgSQLの性能を上げるということはできなさそうです。
外部からできないのであれば、仕方がありません、中身を見てみましょう。
PL/pgSQLの実装を見てみる
というわけで、PostgreSQLのソースを見てみましょう。
PostgreSQL 18のソースはここからダウンロードしました。
tar.gzを展開してみましょう。PL/pgSQLのソースは比較的わかりやすいところにあり、次のフォルダにあります。
postgresql-18.0/src/pl/plpgsql/src
フォルダの中身はこんな感じです。
ChatGPTに聞いてみる
私はC言語の素人なので、フォルダの中身を見てもさっぱりわかりません。
しかし今我々には強い味方がいます。そう、AIです。
展開したソースをもう一度zipで固め直して、ChatGPTに貼り付けて「PL/pgSQLの関数の作成から実行までの流れを教えて」と頼んでみましょう。そうするとスラスラと答えてくれます。
もちろんChatGPTが大嘘をついている可能性もあるわけですが、貼り付けたソースから根拠となる部分を提示してくれており、確認すると確かにそう書いてあるので、わりかし信じられる内容ではないかと思います。
また、リリースノートにはユーザが使用できる部分の機能の追加修正しか記載がありませんが、実はバージョンごとにPL/pgSQLの内部ソースも変わっています。おそらくバージョンを重ねるごとに改善しているのでしょう。今回確認したのはPostgreSQL 18.0のソースであり、他のバージョンでは異なるということも念頭に置いてください。
PL/pgSQLの実行
ではChatGPTに聞いてみた結果、PL/pgSQLで作成した関数がどのように実行されていくのかを確認していきます。
CREATE FUNCTION時
CREATE FUNCTION時、pl_handler.cのplpgsql_validator関数が実行されます。その中でpl_comp.cのplpgsql_compile関数が呼び出され、PL/pgSQLソースの構文解析が行われます。
PL/pgSQLの構文的に問題なければpg_procシステムカタログに関数名や引数の数とデータ型、ソースなどが保存されます。
この時点では構文解析とシステムカタログへの保存のみであり、関数を実行したりコンパイルした関数がメモリ上に展開されることもありません。また、あくまでも構文解析であり、存在しないテーブルを参照するSQLがあったり、SQLの結果をデータ型の合わない変数に格納していたとしても、構文に問題がなければCREATE FUNCTIONは正常終了します。
関数実行時
では、次はPL/pgSQLで作成した関数を実行したときはどう動くでしょうか。
まず、pl_handler.cのplpgsql_call_handler関数が実行されます。
その中でpl_comp.cのplpgsql_compile関数が呼び出され、構文解析が行われます。
CREATE時に構文解析したのにまた構文解析?と思うかもしれません。
実際、呼び出すplpgsql_compile関数は全く同じ関数です。
大きな違いは、関数の初回実行時にはplpgsql_compile関数の戻り値であるPLpgSQL_function構造体をキャッシュに保存することです。
これにより、同じ関数を実行する際は構文解析をスキップできます。
実はCREATE時には戻り値は破棄され、キャッシュに保存されません。
そしてこのPLpgSQL_function構造体が非常に重要です。
PLpgSQL_function構造体は解析したPL/pgSQLソースを抽象構文木(AST, Abstract Syntax Tree)として保持しています。
実行時にはpl_exec.cのplpgsql_exec_function関数が呼び出され、このツリーのノードをたどるようにexec_toplevel_block関数→exec_stmt_block関数→exec_stmts関数と実行していきます。
そしてキモとなるexec_stmts関数では、foreachでツリーのノードをたどり、switchでそのノードがどういうタイプの構文なのかで分岐して、それぞれの構文に合った処理関数を呼び出しています。
foreach(s, stmts)
{
PLpgSQL_stmt *stmt = (PLpgSQL_stmt *) lfirst(s);
int rc;
estate->err_stmt = stmt;
/* Let the plugin know that we are about to execute this statement */
if (*plpgsql_plugin_ptr && (*plpgsql_plugin_ptr)->stmt_beg)
((*plpgsql_plugin_ptr)->stmt_beg) (estate, stmt);
CHECK_FOR_INTERRUPTS();
switch (stmt->cmd_type)
{
case PLPGSQL_STMT_BLOCK:
rc = exec_stmt_block(estate, (PLpgSQL_stmt_block *) stmt);
break;
case PLPGSQL_STMT_ASSIGN:
rc = exec_stmt_assign(estate, (PLpgSQL_stmt_assign *) stmt);
break;
case PLPGSQL_STMT_PERFORM:
rc = exec_stmt_perform(estate, (PLpgSQL_stmt_perform *) stmt);
break;
~ 省略 ~
case PLPGSQL_STMT_COMMIT:
rc = exec_stmt_commit(estate, (PLpgSQL_stmt_commit *) stmt);
break;
case PLPGSQL_STMT_ROLLBACK:
rc = exec_stmt_rollback(estate, (PLpgSQL_stmt_rollback *) stmt);
break;
default:
/* point err_stmt to parent, since this one seems corrupt */
estate->err_stmt = save_estmt;
elog(ERROR, "unrecognized cmd_type: %d", stmt->cmd_type);
rc = -1; /* keep compiler quiet */
}
/* Let the plugin know that we have finished executing this statement */
if (*plpgsql_plugin_ptr && (*plpgsql_plugin_ptr)->stmt_end)
((*plpgsql_plugin_ptr)->stmt_end) (estate, stmt);
if (rc != PLPGSQL_RC_OK)
{
estate->err_stmt = save_estmt;
return rc;
}
} /* end of loop over statements */
いや、なんていうか、すげー力技。
これらの処理関数はいずれもpl_exec.cに存在しており、PL/pgSQLはpl_exec.cというインタプリタで動く、いわゆるインタプリタ言語と言えるでしょう。マシン語レベルにコンパイルされていたり、実行ファイルが作成されたりしているわけではありません。
ところで、他のインタプリタ知らないけど他もみんなこんな力技で実行してるんでしょうか?
余談:実行時エラーについて
CREATE FUNCTIONの構文解析時点では構文としては正しいけど実行したらエラーになるSQLが存在しても正常終了する、と前述しましたが、そのようなSQLは実際に実行されたときに初めてエラーとなります。逆に言うと、関数を実行したときに該当のSQLが実行されなければエラーにはなりません。
これは、PLpgSQL_function構造体のノードをたどるように実行すると、分岐すると必ずしも実行されないノードもあるからです。テストする際は必ずすべてのルートを通るようにしましょう。
実行後のキャッシュ
一度実行すれば構造体がキャッシュに残っているため再度コンパイルをしません。その分実行時間が短縮できます。
また、関数内でSQLを実行する場合、その実行計画などキャッシュに残りますので、ある程度短縮できます。
同じ関数を繰り返し実行する場合はこれらのキャッシュが有効になるでしょう。
しかし、これらのキャッシュはセッション単位で保持しており、セッションが継続している間しか保存されません。 一回実行してセッションを切断すると、次の接続ではもう一度コンパイルからやり直しです。ひどい。
最大限、PL/pgSQLのキャッシュを有効活用したいのであれば、セッションを使い回す工夫が必要になるでしょう。
まとめ
PL/pgSQLはオプションとかでは高速化するのは難しく、汎用性のある高速化の方法は見つかりませんでした。
今以上にPL/pgSQLを高速化するには、インタプリタを改造しなければならないでしょう。
すぐに改善は望めないので、とりあえず現状効率のいいPL/pgSQL関数を作成するには、
- 余計な処理は書かず、コンパイル後のノード数を少なくする
- なるべくキャッシュ上のプランを使用するように、セッションを使い回す工夫をする
といったところを気をつけるべきでしょう。
もしくは、シビアに性能を求められるところでPL/pgSQLを使わないことでしょうか。

