Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PL/pgSQLはなぜ「ふつうのSQL」より遅いのか

Avatar for まぐろ まぐろ
September 03, 2026

 PL/pgSQLはなぜ「ふつうのSQL」より遅いのか

同じことをするなら1本のSQLよりもPL/pgSQLほうがその仕組み上遅くなるということを、ソースコードを解析して説明しています。

Avatar for まぐろ

まぐろ

September 03, 2026

More Decks by まぐろ

Other Decks in Programming

Transcript

  1. はなぜSQLより遅いのか PL/pgSQL 自己紹介 名前: まぐろ(旧Twitter(現X): @tameguro) 所属: 都内某SI勤務のSE PostgreSQLとの関わり: PostgreSQLの設計・導入・保守な

    どをしたりしなかったりしています 今年の4月に「PL/pgSQL完全ガイド」を出しました アンカンファレンス #57 PostgreSQL 2
  2. はなぜSQLより遅いのか PL/pgSQL 今日話すこと 同じことをするなら、PL/pgSQLは通常のSQL1本より一般的に遅い。 …ということをPostgreSQL 18のソースコードを読みながら、内部構造から証 明します。 1. CREATE FUNCTION

    した時に何が起きるか 2. 初めて呼び出した時のコンパイルとキャッシュ 3. 実行エンジンはどう動いているか(ここに遅さの本質がある) 4. 式(expression)はどう評価されるか 5. 例外処理( EXCEPTION )の裏側 6. では、いつPL/pgSQLを使うべきか アンカンファレンス #57 PostgreSQL 7
  3. はなぜSQLより遅いのか PL/pgSQL この資料の作り方について 本資料は、Claude Code に PostgreSQL 18( REL_18_STABLE )のソースコー

    ドを実際にダウンロード・解析させて作成しました src/pl/plpgsql/src/ ( pl_handler.c / pl_comp.c / pl_exec.c など) src/backend/utils/cache/funccache.c 実ソースの該当関数・該当行まで遡って裏付けを取っています(Claude Codeが) スライド中のコード引用はすべて実際のソースからの抜粋(要約・簡略化 した箇所は明記) アンカンファレンス #57 PostgreSQL 8
  4. はなぜSQLより遅いのか PL/pgSQL 結論を先に 同じ結果を1本のSQLで書けるなら、 PL/pgSQLで書くより速い。 -- ふつうのSQL(1本のクエリ、セットベース) UPDATE accounts SET

    balance = balance + 100 WHERE id = 1; -- PL/pgSQLでも同じことはできるが… BEGIN UPDATE accounts SET balance = balance + 100 WHERE id = 1; END; なぜ後者の方が(一般に)遅くなるのか? この後、内部の仕組みを1つずつ見てい きます。 アンカンファレンス #57 PostgreSQL 9
  5. はなぜSQLより遅いのか PL/pgSQL とは(おさらい) PL/pgSQL 標準搭載の手続き型言語 PostgreSQL で定義 変数・制御構文( IF /

    LOOP / FOR )・例外処理を備える 中身はどう動いているのか? コンパイル言語のように機械語になるのか? それともインタプリタなのか? CREATE FUNCTION ... LANGUAGE plpgsql アンカンファレンス #57 PostgreSQL 10
  6. はなぜSQLより遅いのか PL/pgSQL 全体像 ① パース CREATE FUNCTION 時 構文解析のみ ②

    コンパイル + キャッシュ 初回呼び出し時 ASTを構築しメモリに保持 ③ 実行 呼び出しの都度 ツリーウォーク型 インタプリタ この3段階を、ソースコードを見ながら順に追っていきます。 アンカンファレンス #57 PostgreSQL 11
  7. はなぜSQLより遅いのか PL/pgSQL 構文解析だけ、とはどういうことか CREATE FUNCTION broken() RETURNS void AS $$

    BEGIN SELECT * FROM this_table_does_not_exist; END; $$ LANGUAGE plpgsql; このテーブルは存在しない それでも CREATE FUNCTION は成功する 中の SELECT 文はただのクエリ文字列として保持されるだけで、 この時点ではテーブルの存在確認もSQLのプランニングも行われない エラーになるのは、実際に関数を実行し、このクエリが実行された瞬間 アンカンファレンス #57 PostgreSQL 14
  8. はなぜSQLより遅いのか PL/pgSQL キャッシュの仕組み キャッシュキー = 関数OID + 引数の型 + トリガーコンテキスト

    同じ関数でも多態的(polymorphic)なら型ごとに別エントリ ハッシュテーブルは TopMemoryContext → セッションが続く限り保持される 別セッション(新規接続)では最初からコンパイルし直し #define FUNCS_PER_USER 128 /* initial table size */ static HTAB *cfunc_hashtable = NULL; アンカンファレンス #57 PostgreSQL 17
  9. はなぜSQLより遅いのか PL/pgSQL 回目以降はもっと速い: fn_extra 2 function = (PLpgSQL_function *) cached_function_compile(fcinfo,

    fcinfo->flinfo->fn_extra, /* ← ここ */ ...); fcinfo->flinfo->fn_extra = function; fcinfo->flinfo->fn_extra に直接コンパイル済みの構造体ポインタを保 存 同じクエリ内・同じプリペアド文での再呼び出しはハッシュ検索すら行わ ずキャッシュを再利用 キャッシュミス時 plpgsql_compile_callback が呼ばれ、フル解析を行う アンカンファレンス #57 PostgreSQL 18
  10. はなぜSQLより遅いのか PL/pgSQL コンパイル本体: plpgsql_compile_callback pl_comp.c: plpgsql_compile_callback() で関数本体をパース できあがった抽象構文木(AST)を PLpgSQL_function 構造体に格納

    関数専用のメモリコンテキスト( fn_cxt )に確保 成功したら CacheMemoryContext の下にぶら下げて長期保持 失敗時は自動的に破棄されリークしない設計 ここまでが「コンパイル」。この後は実行のたびに このASTを何度も辿ることになる。 pl_scanner.c / pl_gram.y (flex/bison) アンカンファレンス #57 PostgreSQL 19
  11. はなぜSQLより遅いのか PL/pgSQL コンパイルの結果できあがるAST PLpgSQL_function コンパイル結果として得られるASTのルート action PLpgSQL_stmt_block BEGIN ... END

    ブロック body[0] PLpgSQL_stmt_execsql 1個のSQL⽂をラップするノード sqlstmt sqlstmt(⽂字列のまま保持・この時点では未パース) UPDATE accounts SET balance = balance + 100 WHERE id = 1; 埋め込みSQLはASTノードにならず、実⾏時(フェーズ3)に初めてSPI経由でパース・プランニングされる アンカンファレンス #57 PostgreSQL 20
  12. はなぜSQLより遅いのか PL/pgSQL plpgsql_exec_function pl_exec.c: plpgsql_exec_function() 実行状態( PLpgSQL_execstate )をセットアップ 2. 呼び出し引数をローカル変数にコピー

    3. 特殊変数 FOUND を false に初期化 1. 4. exec_toplevel_block(&estate, func->action) → ここから実際の文の実行が始まる を呼ぶ アンカンファレンス #57 PostgreSQL 22
  13. はなぜSQLより遅いのか exec_stmts — PL/pgSQL 文リストを辿る pl_exec.c: exec_stmts() foreach(s, stmts) {

    PLpgSQL_stmt *stmt = (PLpgSQL_stmt *) lfirst(s); switch (stmt->cmd_type) { case PLPGSQL_STMT_ASSIGN: rc = exec_stmt_assign(estate, ...); break; case PLPGSQL_STMT_IF: rc = exec_stmt_if(estate, ...); break; ... アンカンファレンス #57 PostgreSQL 23
  14. はなぜSQLより遅いのか PL/pgSQL exec_stmts — 文リストを辿る を foreach で走査し、 switch で分岐。約20種類の文タイプ

    (ASSIGN/IF/LOOP/FORI/EXECSQL/RAISE/OPEN/FETCH…)ごとに専用のハンドラ 関数へディスパッチする。 あまりにも力技 List アンカンファレンス #57 PostgreSQL 24
  15. はなぜSQLより遅いのか PL/pgSQL これは「ツリーウォーク型インタプリタ」 コンパイルフェーズで作ったASTを、実行フェーズでそのまま辿る 機械語やバイトコードへの変換は一切行わない IF ・ LOOP ・ FOR

    なども、対応する exec_stmt_* 関数が 再帰的/反復的に exec_stmts を呼び出すことで実現している つまりPL/pgSQLは、素朴な意味でのインタプリタ言語。 このディスパッチは文を実行するたびに発生する。 ループの中なら、ループ回数だけ繰り返される。 ふつうのSQLエグゼキュータには、そもそもこの層が存在しない。 アンカンファレンス #57 PostgreSQL 25
  16. はなぜSQLより遅いのか PL/pgSQL PLpgSQL_expr の正体 x := a + 1; IF

    x > 10 THEN ... や x > 10 のような式は、生のSQLテキストのまま PLpgSQL_expr 構造体に保持されている つまりPL/pgSQL自身は式を評価する独自の計算エンジンを持たない 実際には、コアのSQLパーサ・プランナ・エグゼキュータに SPI(Server Programming Interface)経由で丸投げしている a + 1 アンカンファレンス #57 PostgreSQL 27
  17. はなぜSQLより遅いのか PL/pgSQL exec_eval_expr の流れ pl_exec.c: exec_eval_expr() if (expr->plan == NULL)

    exec_prepare_plan(estate, expr, CURSOR_OPT_PARALLEL_OK); if (exec_eval_simple_expr(estate, expr, &result, isNull, rettype, rettypmod)) return result; /* Else do it the hard way via exec_run_select */ rc = exec_run_select(estate, expr, 0, NULL); アンカンファレンス #57 PostgreSQL 29
  18. はなぜSQLより遅いのか PL/pgSQL つまり x := a + 1; は… 内部的には、こういうクエリを実行しているのと(ほぼ)同じ。

    SELECT a + 1; コアのSQLパーサ・プランナが呼ばれる PostgreSQL本体が持つ型システム・演算子解決・関数呼び出しの 仕組みをそのまま利用できる(独自実装しなくて済む) 一方で、素朴にやると式1つ評価するたびに毎回SQL実行のフルコースが走 ってしまう アンカンファレンス #57 PostgreSQL 31
  19. はなぜSQLより遅いのか PL/pgSQL simple expression 最適化 pl_exec.c: exec_eval_simple_expr() 単純なスカラー式(集約もSRFも副問い合わせも伴わないもの)は SPIのポータル/スナップショット管理を丸ごと迂回 キャッシュ済みプランから直接

    ExecEvalExpr() を呼ぶ 通常のSPI実行に比べて大幅に軽量 /* * exec_eval_simple_expr - Evaluate a simple expression * returning a Datum by directly calling ExecEvalExpr(). */ アンカンファレンス #57 PostgreSQL 32
  20. はなぜSQLより遅いのか PL/pgSQL simple expression のキャッシュと再プラン 判定結果・プランは式ごとに保持 expr->expr_simple_plan (キャッシュされたプラン) expr->expr_simple_state (実行用の

    ExprState ) スキーマ変更などでプランが無効化されたら CachedPlanIsSimplyValid() が検知し、自動的に再プラン 一度「simple」と判定された式が「非simple」に変わることはない (逆も同様)という前提で最適化されている アンカンファレンス #57 PostgreSQL 33
  21. はなぜSQLより遅いのか PL/pgSQL 最速の経路にも、消えないコストがある exec_eval_simple_expr() は毎回呼ばれるたびに、こういう処理を行う。 LocalTransactionId curlxid = MyProc->vxid.lxid; ...

    EnsurePortalSnapshotExists(); ... if (likely(CachedPlanIsSimplyValid(...))) expr->expr_simple_plan_lxid = curlxid; ... paramLI->parserSetupArg = expr; econtext->ecxt_param_list_info = paramLI; アンカンファレンス #57 PostgreSQL 34
  22. はなぜSQLより遅いのか PL/pgSQL 文もすべてSPI任せ SQL 動的 EXECUTE — これらもすべて SPI経由でコアのSQLエグゼキュータに委譲される。 PL/pgSQLは「制御構文のインタプリタ」ではあるが、

    SQLエンジンとしては何も持っていない。 SQLに関することは常にPostgreSQL本体に投げている。 SELECT INTO / INSERT / UPDATE / DELETE / アンカンファレンス #57 PostgreSQL 36
  23. はなぜSQLより遅いのか PL/pgSQL FOR ループは行を1件ずつ処理する pl_exec.c: exec_for_query() /* Fetch the initial

    tuple(s). */ SPI_cursor_fetch(portal, true, prefetch_ok ? 10 : 1); ... while (n > 0) { for (i = 0; i < n; i++) { exec_move_row(estate, var, tuptab->vals[i], tuptab->tupdesc); rc = exec_stmts(estate, stmt->body); /* ← 1行ごとにインタプリタへ */ } SPI_cursor_fetch(portal, true, prefetch_ok ? 50 : 1); /* 続きを取得 */ } アンカンファレンス #57 PostgreSQL 37
  24. はなぜSQLより遅いのか PL/pgSQL 比較: 「同じ更新」を2通りで書くと -- ふつうのSQL: エグゼキュータが内部で全行を一括処理 UPDATE accounts SET

    balance = balance * 1.01; -- PL/pgSQL: 行数ぶんだけ interpreter ⇄ SPI を往復 FOR rec IN SELECT id FROM accounts LOOP UPDATE accounts SET balance = balance * 1.01 WHERE id = rec.id; END LOOP; 後者は行ごとに: カーソルフェッチ → exec_stmts のswitchディスパッチ → exec_eval_expr / exec_stmt_execsql のパラメータリスト構築 → SPI経由のUPDATE実行、を繰り返す。 これまで見てきた各レイヤーのオーバーヘッドが、行数分だけ積み重なる。 アンカンファレンス #57 PostgreSQL 39
  25. はなぜSQLより遅いのか PL/pgSQL BEGIN...EXCEPTION...END pl_exec.c: exec_stmt_block() はどう実装されているか の中で: BeginInternalSubTransaction(NULL); PG_TRY(); {

    rc = exec_stmts(estate, block->body); ReleaseCurrentSubTransaction(); } PG_CATCH(); { ... RollbackAndReleaseCurrentSubTransaction(); 節を持つブロックは、サブトランザクションの中で実行される。 EXCEPTION アンカンファレンス #57 PostgreSQL 42
  26. はなぜSQLより遅いのか PL/pgSQL 実務上の注意点 EXCEPTION 節があるブロックは、サブトランザクション開始のコストを 毎回払う ループの中で毎回 BEGIN...EXCEPTION...END を使うと、ループ回数分サブ トランザクションが積まれる

    → パフォーマンスに影響しうる 例外処理が不要な箇所では EXCEPTION 節を書かないほうが軽い、 という理由がソースレベルで裏付けられる アンカンファレンス #57 PostgreSQL 43
  27. はなぜSQLより遅いのか PL/pgSQL まとめ: なぜPL/pgSQLは遅くなりうるのか ①インタプリタ層 exec_stmtsのswitchディスパッチが文の実行のたびに発生。ふつうのSQLエグ ゼキュータにはない層。 ②式評価のたびのコスト simple expressionでもパラメータリスト構築・有効性チェックを毎回実施。

    ③行単位の処理 FORループはカーソルで1行ずつフェッチし、その都度exec_stmtsへ戻る。セッ トベースでない。 ④EXCEPTIONのコスト サブトランザクション開始/終了が毎回のブロック実行に付随する。 アンカンファレンス #57 PostgreSQL 47
  28. はなぜSQLより遅いのか : AI 裏テーマ のおかげでOSS解析のハードルが劇的に下がっ た PL/pgSQL 読んだことのない数十万行のCコードでも、 AIと一緒になら「実際に読んで確かめる」が現実的な選択肢になる。 今回のスライドも、Claude

    CodeにPostgreSQL 18本体のソースを実際に読 ませて作成 以前なら、この規模のCコードベースを裏付けとして読むには相応の経験 と時間が必要だった 今は「なぜこの挙動になるのか」を、その場でソースの該当関数まで遡っ て検証できる みんなもPostgreSQLのソースを読んでみよう! アンカンファレンス #57 PostgreSQL 49
  29. はなぜSQLより遅いのか PL/pgSQL 参考資料 PostgreSQL 18 ソースコード( REL_18_STABLE ) src/pl/plpgsql/src/pl_handler.c src/pl/plpgsql/src/pl_comp.c

    src/pl/plpgsql/src/pl_exec.c src/backend/utils/cache/funccache.c https://github.com/postgres/postgres Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group. Portions Copyright (c) 1994, Regents of the University of California. アンカンファレンス #57 PostgreSQL 50