エラーの内容
ストアド関数を呼び出すと、PostgreSQLが以下のエラーをスローします:
ERROR: function send_welcome_email(unknown) does not exist
LINE 1: SELECT send_welcome_email('user@example.com');
^
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
関数が存在することはわかっているのに、なぜこうなるのか腑に落ちないでしょう。自分で書いた関数で、今まさにpgAdminで確認できるのに、アプリから呼び出した瞬間や別のロールで接続したとき、あるいは新しいpsqlセッションで実行すると、PostgreSQLはまるで知らないかのように振る舞います。
なぜこのエラーが発生するのか
関数を呼び出すとき、PostgreSQLはデータベース全体を検索するわけではありません。search_pathで定義された特定の順序でスキーマを確認します。デフォルトの順序は"$user", publicです。つまり、現在のユーザー名と同名のスキーマを最初に調べ、次にpublicを確認し、それ以外は一切検索しません。
関数をappやmyschemaのようなカスタムスキーマに配置しておきながら、そのスキーマをsearch_pathに追加し忘れると、PostgreSQLは本当に関数を見つけられません。エラーメッセージは正確であり、間違った感覚を覚えるとしても事実です。
なぜ気づかずにはまってしまうのか?よくあるケースをいくつか挙げます:
- 関数が
public以外のスキーマに作成されている(ORMやマイグレーションツールでよく起こる) - Flyway、Liquibase、またはAlembicがマイグレーション中に
SET search_pathを実行したが、アプリはそれなしで接続している - アプリのロールが、関数を作成した管理者アカウントとは異なるデフォルトの
search_pathを持っている - PostgreSQLのメジャーバージョンアップグレードによって、ロールレベルのsearch_path設定がサイレントにリセットされた
ステップ1 — 関数が実際にどこにあるか確認する
修正の前に、まず関数の所在を確認しましょう。psqlを開いて次のクエリを実行します:
SELECT n.nspname AS schema, p.proname AS function_name
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE p.proname = 'send_welcome_email';
出力例:
schema | function_name
--------+--------------------
app | send_welcome_email
(1 row)
ありました。スキーマはpublicではなくappです。この一点がすべてを説明しています。
ステップ2 — 現在のsearch_pathを確認する
SHOW search_path;
search_path
-----------------
"$user", public
(1 row)
appがリストにありません。これが変わらない限り、send_welcome_email()への修飾なし呼び出しはすべて失敗し続けます。
ステップ3 — 修正方法を選ぶ
オプションA:関数呼び出しにスキーマを明示する(最も手軽、設定不要)
すべての呼び出しにスキーマ名をプレフィックスとして付けます:
SELECT app.send_welcome_email('user@example.com');
データベースへの変更は不要です。スクリプトで一度だけ使う場合や、呼び出し箇所が2〜3か所しかない場合に適しています。ただし、あちこちで呼び出すとすぐに煩雑になります。
オプションB:現在のセッションにsearch_pathを設定する
接続中のみスキーマを追加します:
SET search_path TO app, public;
SELECT send_welcome_email('user@example.com');
セッションが終了するとリセットされます。他の接続には影響しないため、副作用なしでテストやデバッグに使えます。
オプションC:ロールに永続的なsearch_pathを設定する(アプリに推奨)
アプリが常に同じロールで接続する場合、これが最もすっきりした解決策です:
ALTER ROLE app_user SET search_path TO app, public;
app_userによる新しい接続はすべて自動的にappを参照します。既存のセッションには影響しません。アプリを再起動するか再接続して有効になることを確認してください。
オプションD:データベースレベルでsearch_pathを設定する
このデータベース上のすべてのロールが同じスキーマ順序を必要とする場合、データベースレベルで一度だけ設定します:
ALTER DATABASE mydb SET search_path TO app, public;
ロールレベルの設定がこれより優先されるため、オプションCを上書きすることはありません。利用可能な最も広い設定レバーです。共有データベースでは慎重に使用してください。
オプションE:search_pathを関数自体に組み込む(最も移植性が高い)
PostgreSQLでは、search_pathを関数定義に直接組み込むことができます。これにより、誰が呼び出しても、セッションの状態がどうであっても、関数は自身のスキーマを基準に名前を解決します:
ALTER FUNCTION app.send_welcome_email(text)
SET search_path = app, public;
または、作成時に設定します:
CREATE OR REPLACE FUNCTION app.send_welcome_email(email text)
RETURNS void
LANGUAGE plpgsql
SET search_path = app, public
AS $$
BEGIN
-- 関数の本体
END;
$$;
この方法は最も忘れにくく、最も理解しやすいです。また、悪意あるスキーマがスーパーユーザーのsearch_pathの先頭に挿入されるスキーマインジェクション攻撃を防ぐ効果もあります。一行追加する価値は十分にあります。
ステップ4 — 修正を確認する
選んだオプションを適用したら、簡単な動作確認を行いましょう:
SELECT send_welcome_email('test@example.com');
オプションCまたはDを選んだ場合は、まったく新しい接続を開いてから確認してください。これらの変更はALTER以降に開始したセッションにのみ適用されます。
ALTER ROLE後のロールの有効なsearch_pathを確認するには:
-- app_userとして再接続してから実行:
SHOW search_path;
search_path
--------------
app, public
(1 row)
この出力が確認できれば、ロールが正しく設定されており、新しい接続では追加設定なしで関数を見つけられます。
アプリケーション内での修正
エラーがNode.js、Python、Javaなどのアプリケーションコードから発生している場合、最もすっきりした対処法は、新しい接続が確立されるたびにSET search_pathを実行する接続フックを使用することです。
Node.js(node-postgres):
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
pool.on('connect', (client) => {
client.query("SET search_path TO app, public");
});
Python(psycopg2):
import psycopg2
conn = psycopg2.connect(dsn)
conn.cursor().execute("SET search_path TO app, public")
conn.commit()
SQLAlchemy(Python):
from sqlalchemy import event, text
@event.listens_for(engine, 'connect')
def set_search_path(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("SET search_path TO app, public")
cursor.close()
おまけ:すべての関数とそのスキーマを確認する
publicスキーマ以外に隠れている関数がどれだけあるかわからない場合は、次のクエリですべてのユーザー定義関数をスキーマとシグネチャとともに一覧表示できます:
SELECT n.nspname AS schema,
p.proname AS function,
pg_get_function_identity_arguments(p.oid) AS arguments
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY n.nspname, p.proname;
public以外にあるものはすべて、呼び出し元のsearch_pathにそのスキーマが含まれていなければこのエラーの候補です。マイグレーション後やメジャーバージョンアップグレード後にこのクエリを実行してみてください。2秒で完了し、これまで多くの意外な発見をもたらしてきました。
クイックリファレンス
- その場限りの修正:
schema.function_name()— 設定変更不要 - 単一セッション:
SET search_path TO schema, public - ロール単位(アプリに最適):
ALTER ROLE role SET search_path TO schema, public - データベース単位:
ALTER DATABASE db SET search_path TO schema, public - 関数単位(最も移植性が高い):
ALTER FUNCTION ... SET search_path = schema, public

