PostgreSQLでスキーマがつまずきの種になるのは、構文が難しいからではありません。CREATE SCHEMAは一行で済みますが、そのあと「作ったはずのテーブルが見つからない」「開発環境では通るSQLが本番で別のテーブルを読む」「アプリのロールに権限を付けたのに書き込めない」といった食い違いが出ます。原因はほぼすべて、名前解決の経路と権限の既定値がどこで決まっているかを押さえていないことに帰着します。
この記事では、PostgreSQL固有の名前空間としてのスキーマについて、search_pathが名前を解決してDDLの作成先を決める仕組み、設定できるスコープと優先順位、15で変わったpublicスキーマの権限、そしてマルチテナントをスキーマ分割で組むときの採用条件を扱います。三層スキーマ(外部・概念・内部)や他のRDBMSとの意味差といった製品非依存の整理はデータベースのスキーマとは?三層スキーマの違いと設計・管理の実務に、データベース全般の位置づけはデータベースとは?種類・DBMS・RDBとNoSQLの選び方に譲ります。動作の前提は18系(2026年8月時点)としました。
まとめ:スキーマ運用で先に押さえる4点
先に示す結論は、次の4点です。第一に、スキーマ修飾を省いた名前がどこへ解決されるかも、新しいテーブルがどこへ作られるかも、どちらもsearch_pathの先頭側から順に決まります。既定値は"$user", publicで、同名のスキーマが無ければ$userは読み飛ばされ、結果としてpublicが使われます。
第二に、search_pathは6つのスコープで指定でき、狭いほうが優先されます。ここを把握していないと「ALTER ROLEで設定したのに反映されない」という混乱が起きやすくなります。特に接続プーラーをトランザクションモードで挟んでいる場合、セッションに対するSETは次のトランザクションまで残りません。
第三に、publicスキーマの既定権限は15で変わりました。ただしこの新しい既定が効くのは新規クラスタと新しく作ったデータベースだけで、アップグレードやダンプ復元では旧来の権限がそのまま保存されます。版が15以上でも中身は14以前のまま、という環境は珍しくありません。
第四に、テナントごとにスキーマを切る設計は、テナント数がシステムカタログの行数へそのまま掛け算で効きます。移行作業もスキーマの数だけ繰り返すことになるため、採用条件は2点です。テナント数の上限が読めていること、そして移行を途中で止めても戻せる手順があることです。以下、順に根拠を見ていきます。
データベースとスキーマとテーブルの三層構造とスキーマが仕切る範囲
PostgreSQLでは、ひとつのクラスタに複数のデータベースがあり、そのデータベースの下にスキーマが並び、スキーマの中にテーブルやビュー、関数、シーケンスが入ります。テーブルの完全な名前はデータベース名とスキーマ名とテーブル名の三段ですが、SQLの中で他のデータベースを指すことはできません。実務で書くのはスキーマ名とテーブル名の二段までです。
同じ名前のテーブルを別スキーマに置ける仕組みと修飾名の書き方
スキーマが名前空間として働くため、別々のスキーマであれば同じテーブル名を並存させられます。
CREATE SCHEMA sales;
CREATE SCHEMA billing;
CREATE TABLE sales.orders (id bigserial PRIMARY KEY, amount numeric);
CREATE TABLE billing.orders (id bigserial PRIMARY KEY, closed_at timestamptz);
SELECT * FROM sales.orders; -- 明示すれば迷わない
この二つは完全に別のテーブルです。困るのは修飾を省いたSELECT * FROM ordersのほうで、どちらが返るかはその接続のsearch_path次第になります。同じSQLが環境によって別の実体を読むという事故は、ここから生まれます。
スキーマを分ける動機が名前の衝突回避と権限付与の単位である理由
スキーマを切る理由は二つに整理できます。ひとつは名前の衝突回避で、複数のアプリケーションや外部拡張を同じデータベースへ同居させるときに効きます。もうひとつは権限の単位としての働きで、GRANT USAGE ON SCHEMAを出さなければ、その中のテーブルへ個別に権限を付けても到達できません。
後者は見落とされやすい挙動です。テーブルにSELECTを付けたのに読めないという相談の多くは、スキーマ側のUSAGEが抜けています。権限は「スキーマへの到達」と「オブジェクトへの操作」の二段で判定されるためです。ロール側の属性設計とGRANTの配り方はPostgreSQLのロールと権限設計|CREATE ROLEの属性とGRANT・既定権限の決め方で扱っています。
CREATE SCHEMAで作るときに所有者と権限を同時に決める書き方
スキーマの作成にはそのデータベースに対するCREATE権限が要ります。所有者を作成者以外にしたい場合はAUTHORIZATIONを付けます。
CREATE SCHEMA sales AUTHORIZATION app_owner;
GRANT USAGE ON SCHEMA sales TO app_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA sales TO app_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA sales
GRANT SELECT ON TABLES TO app_reader;
三つめのALTER DEFAULT PRIVILEGESを忘れると、以後に追加したテーブルには権限が付きません。既存テーブルへの一括付与と、これから作るテーブルへの既定は別の指定である、と覚えておくと運用が安定します。MySQLではスキーマがデータベースと同義でこの二段構造を持たないため、移行時の設計差についてはPostgreSQLとMySQLの違いを徹底比較で扱いました。
search_pathが名前を解決する順序と新しいオブジェクトの作成先
search_pathは、スキーマ修飾を省いた名前を探す順番を並べた設定です。公式ドキュメントの記述どおり、探索は先頭から進み、最初に見つかったオブジェクトが使われます。どこにも無ければエラーになります。
既定値の$userとpublicが実際にどう展開されるかを確かめる手順
既定値は"$user", publicです。$userは接続中のロール名と同じ名前のスキーマを指し、存在しなければ黙って読み飛ばされます。展開後の実体はcurrent_schemasで確認できます。
SHOW search_path;
-- "$user", public
SELECT current_schemas(true);
-- {pg_catalog,public} -- appという名のスキーマが無い場合
SELECT current_schema();
-- public -- 新しいオブジェクトの作成先
引数を真にすると、暗黙で入るスキーマも含めた実際の探索路が返ります。設定値そのものを見るSHOWと、展開結果を見るcurrent_schemasを使い分けると、原因の切り分けが速くなります。
pg_catalogとpg_tempが明示しなくても先に検索される順序
探索路には、書いていないスキーマが二つ入ります。システムカタログのpg_catalogと、一時テーブル用のpg_tempです。前者は明示的に並べない限り探索路の先頭側で検索されるため、独自に同名の関数を作っても組み込み側が優先されます。
後者はテーブル名の解決で先頭側に入ります。一時テーブルと同名の実テーブルがある場合、修飾を省くと一時テーブル側が拾われるということです。逆にpg_catalogを探索路の後ろへ明示的に置けば、同名の自作オブジェクトを優先させることもできますが、標準の挙動を変える指定なので用途は限られます。
スキーマ修飾を省いたCREATE TABLEがどこへ作られるかの判定
新しいオブジェクトの作成先は、探索路のうち実在する先頭のスキーマです。読み取りの解決順と作成先が同じ設定で決まる点が、この仕組みの分かりにくさの中心にあります。
SET search_path TO sales, public;
CREATE TABLE invoices (id bigserial PRIMARY KEY);
-- sales.invoices として作られる
SELECT current_schema();
-- sales
「テーブルを作ったのに一覧に出てこない」という症状は、権限ではなく作成先とsearch_pathのずれであることが大半です。一覧コマンドが既定で可視オブジェクトしか返さない仕組みはPostgreSQLのテーブル一覧を取得する方法で詳しく整理しています。
search_pathを設定できる6つのスコープと優先順位の関係
search_pathは1か所で決まるものではありません。指定できる場所は6つあり、狭いスコープが広いスコープを上書きします。下ほど優先されると読んでください。
| スコープ | 指定方法 | 効く範囲 |
|---|---|---|
| クラスタ全体 | postgresql.conf | 全接続の既定 |
| データベース | ALTER DATABASE | そのDBへの接続 |
| ロール | ALTER ROLE | そのロールの接続 |
| ロールとDBの組 | ALTER ROLE IN DATABASE | 組み合わせのみ |
| セッション | SET または接続文字列 | その接続の残り |
| 関数 | 関数定義のSET句 | 関数の実行中だけ |
ロール単位とデータベース単位で永続化する書き方と反映のタイミング
アプリケーション用ロールに業務スキーマを固定したいときは、ロール側へ書きます。
ALTER ROLE app SET search_path TO sales, public;
ALTER ROLE app IN DATABASE shopdb SET search_path TO sales;
ALTER DATABASE shopdb SET search_path TO sales, public;
いずれも既存の接続には反映されません。次に張られた接続から効くという点で、アプリケーションを再起動するかコネクションプールを入れ替えるまでは旧設定が残ります。「設定したのに変わらない」の相当数はこれが理由です。
デプロイ直後だけ挙動が変わる、といった症状が出たら、まず接続の張り直しが済んでいるかを疑ってください。設定値そのものより、いつ張られた接続かのほうが効いています。
接続プーラーのトランザクションモードでSETが効かなくなる条件
PgBouncerを挟む構成では、プーリングモードによってセッション状態の扱いが変わります。公式のフィーチャーマトリクスでは、トランザクションモードにおけるSETとRESETの扱いは Never です。同じ扱いはLISTEN、PREPARE、セッションレベルのアドバイザリロックにも及びます。
つまりアプリケーション側で接続直後にSET search_pathを打つ実装は、セッションモードでは動いてもトランザクションモードでは壊れます。トランザクションごとに別のサーバ接続へ割り当てられるためです。回避策は3つあります。
-- 1) ロールへ永続化する(推奨)
ALTER ROLE app SET search_path TO sales, public;
-- 2) 接続文字列のoptionsで渡す
-- ?options=-csearch_path%3Dsales を接続URIの末尾へ付ける
-- 3) SQL側で常にスキーマ修飾する
SELECT * FROM sales.orders;
なお18系ではlibpqがsearch_pathの変更をクライアントへ通知するようになり、プーラー側が現在値を追跡しやすくなりました。ただしこれは通知の仕組みであって、トランザクションモードでセッション状態が保たれるようになったわけではありません。
関数へ固定してSECURITY DEFINERの乗っ取りを防ぐ指定方法
修飾を省いた名前の危うさが最も鋭く出るのが、定義者権限で動く関数です。探索路の先頭側にあるスキーマへ書き込める利用者がいれば、同名の関数や演算子を置いて処理を横取りできます。この経路はCVE-2018-1058として公表され、以降の公式ドキュメントは安全な利用パターンを明示するようになりました。
CREATE FUNCTION admin.reset_quota(uid bigint)
RETURNS void
LANGUAGE sql
SECURITY DEFINER
SET search_path = pg_catalog, pg_temp
AS 'UPDATE admin.quota SET used = 0 WHERE user_id = uid';
関数定義にSETを書くと、その関数の実行中だけ探索路が置き換わります。定義者権限の関数では例外なく付けてください。本文側は修飾名で書く前提になるため、記述はやや冗長になりますが、代償として横取りの経路が閉じます。
publicスキーマの権限がPostgreSQL 15で変わった範囲と実際の状態
14以前は、publicスキーマに対して全利用者がCREATE権限を持っていました。15のリリースノートは、この既定を取り除いたこと、そしてpublicスキーマの所有者をpg_database_ownerという新しいロールへ変更したことを記載しています。
新規クラスタと新規データベースにだけ新しい既定が適用される仕組み
リリースノートには但し書きが付いています。新しい既定が適用されるのは新規のクラスタと、既存クラスタ内に新しく作ったデータベースだけです。クラスタのアップグレードやダンプの復元では、publicの既存の権限設定がそのまま保存されます。
この一文の実務上の意味は小さくありません。14から15や16へアップグレードした環境は、版だけ新しくても権限は旧来のままです。「15以上だから安全」という前提でセキュリティ設計を書くと、実態とずれます。
アップグレードで旧来の権限が残る条件と自分の環境を確かめる手順
確認は権限の表示だけで済みます。
SELECT nspname, nspowner::regrole, nspacl
FROM pg_namespace
WHERE nspname = 'public';
-- nspacl の例(旧既定が残っている状態)
-- {postgres=UC/postgres,=UC/postgres}
アクセス権の一覧に、ロール名の無いエントリでUとCの両方が並んでいれば、全利用者にUSAGEとCREATEが残っている状態です。旧既定を引きずっていると判断できます。閉じる場合は次の一行を出します。
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
実行前に、publicへテーブルを作る前提のマイグレーションや拡張が動いていないかを確かめてください。開発環境で先に流し、DDLを流すロールに明示のCREATE権限を与えてから本番へ持っていく順序が無難です。
所有者がpg_database_ownerへ変わったことで許可される操作
pg_database_ownerは、そのデータベースの所有者を暗黙のメンバーとして持つ特別なロールです。14以前はpublicの所有者がブートストラップスーパーユーザーだったため、スーパーユーザーでないデータベース所有者はpublicに対して何もできませんでした。
15以降の新規データベースでは、所有者がpublicスキーマの所有者権限を持ちます。マネージドサービスのようにスーパーユーザーが借りられない環境では、この差がそのまま運用の自由度になります。所有者を誰にするかは、クラスタ構築時に決めておきたい項目です。導入そのものの手順はPostgreSQLのインストール手順にまとめています。
マルチテナントをスキーマ分割で設計するときの採用条件と限界の見極め
テナントごとにスキーマを切る設計は、PostgreSQLでは自然に書けます。接続時にsearch_pathをテナント名へ向ければ、アプリケーションのSQLをほぼ変えずにデータを分離できるためです。ただし採用可否は、テナント数の見通しで決まります。
テナント数に比例してシステムカタログの行が増える構造と概算方法
スキーマを増やしても、テーブルや索引、制約、シーケンスの定義はすべて同じデータベースの共有カタログに載ります。行数は掛け算で増えると考えてください。
1テナントあたり テーブル40 + 索引100 + 制約60 + シーケンス20 = 220
200テナント → 44,000行
1,000テナント → 220,000行
5,000テナント → 1,100,000行
カタログが膨らむと、実行計画の作成と接続直後のメタデータ参照が重くなります。加えてダンプの取得や統計情報の収集、自動バキュームの対象数も同じ比率で増えます。数百テナントまでは素直に動く一方、数千の桁へ入ると運用側の作業時間が先に限界へ達する、というのが実務での感触です。
スキーマ数だけ繰り返すマイグレーションが失敗したときの戻し方
スキーマ分割の運用負荷が最も表面化するのはスキーマ変更です。列を1本足すだけでも、テナント数と同じ回数のDDLを流すことになります。
DO $body$
DECLARE s text;
BEGIN
FOR s IN SELECT nspname FROM pg_namespace WHERE nspname LIKE 'tenant%'
LOOP
EXECUTE format('ALTER TABLE %I.orders ADD COLUMN memo text', s);
END LOOP;
END $body$;
このループを単一トランザクションで包むと、途中で失敗したとき全体が巻き戻る代わりに、長時間のロックが全テナントへ広がります。逆にテナント単位でコミットすると影響は局所化しますが、成功したスキーマと失敗したスキーマが混在した状態が残ります。
実務では後者を選び、進捗を記録するテーブルを別に持つ設計が扱いやすいです。どこまで適用済みかを行として持ち、再実行が同じ結果になるようDDLを書いておけば、途中で止めても続きから流せます。この「戻せる手順」が用意できないうちは、スキーマ分割の採用を見送ってよいと考えます。
行レベルセキュリティで共有する構成と分割を選ぶ判断の分かれ目
もうひとつの選択肢は、テーブルを共有したままテナント列で行レベルセキュリティを掛ける構成です。カタログはテナント数に依存せず、スキーマ変更も1回で済みます。代わりに、ポリシーの設定漏れがそのまま他テナントへのデータ露出につながります。
判断は次の条件で分かれます。テナント数が数百までで頭打ちの見通しがあり、テナントごとに列構成の差分を持つ必要があり、単独テナントのダンプと復元を運用要件として求められるなら、スキーマ分割が向きます。テナント数が読めない、あるいは増加が事業計画に含まれているなら、共有テーブルと行レベルセキュリティを選び、分離要件は別の手段で満たすほうが安全です。
途中で方式を変えるのは高くつきます。数百テナントを共有テーブルへ畳む作業は、実質的にデータ移行プロジェクトになるためです。初期に決め切れないなら、テナント識別列だけは全テーブルへ持たせておくと、後からどちらへも寄せられます。
スキーマ設計と権限運用を内製で回す条件と外部へ渡すときの線引き
スキーマ境界と権限の設計は、一度決めると後から動かしにくい部類の判断です。内製で回してよいのは、次の3条件がそろうときだと考えます。第一に、DDLを流すロールとアプリケーションのロールが分かれており、権限の付与が手作業でなくコード化されていること。第二に、search_pathをどのスコープで固定するかがチーム内で決まっており、接続文字列とロール設定のどちらを正とするか合意があること。第三に、テナント方式の見直しが必要になったときに、移行の停止と再開を判断できる担当者がいることです。
逆に、権限付与が個別のSQL手順書に散っている、プーラーの設定を触れる人が限られている、テナント数の見通しを持つ担当が不在という状態では、設計そのものより先に運用の器を作る必要があります。その場合はデータ分析基盤構築・MLOps構築支援のように、スキーマ設計と権限のコード化、移行手順の型づくりだけを切り出して外部へ委ねる進め方があります。手を動かす部分より、方式を決め切るところが難所になるためです。
維持費まで含めた費用の全体像から考えたい場合は、PostgreSQLのライセンスと価格:無償の範囲と実際に払う費用で5年総額の見方を示しました。ライセンス費が0円でも、テナント分割の運用工数は確実に発生します。
よくある質問
スキーマとデータベースはどう使い分ければよいですか?
同じアプリケーションの中でデータを区切るならスキーマ、まったく別のシステムならデータベースを分けます。判断の分かれ目は、ひとつの接続から両方へ問い合わせたいかどうかです。PostgreSQLでは接続中のデータベース以外を参照できないため、横断集計が必要ならスキーマ分割を選ぶことになります。
search_pathは設定せずスキーマ修飾だけで運用してもよいですか?
問題ありません。むしろ定義者権限の関数や接続プーラー配下では、修飾する運用のほうが壊れにくくなります。公式ドキュメントも探索路からpublicを外す構成を安全な利用パターンの一つとして挙げています。既存アプリケーションを修飾なしで書いている場合は、ロール単位の固定と併用するのが現実的です。
スキーマを丸ごと消すときに気を付けることはありますか?
DROP SCHEMA name CASCADEは中のテーブルもデータも一緒に落とします。他スキーマからの外部キーやビューが参照している場合、それらも連鎖して消える点に注意してください。実行前に依存関係を確認し、対象スキーマだけをダンプで退避してから流す手順が安全です。
スキーマ単位でバックアップと復元はできますか?
ダンプ取得コマンドにスキーマ指定のオプションがあり、対象を絞った取得ができます。ただし復元先のスキーマ名を変えて戻す指定は用意されていないため、別名で復元したい場合はダンプ内の名前を置換するか、いったん同名で戻してから改名する手順になります。テナントごとの切り出しを運用要件に入れるなら、ここを先に検証してください。
publicスキーマは削除してしまってよいですか?
削除自体は可能ですが、勧めません。多くの拡張が既定でpublicへ入る前提になっており、監視ツールや移行ツールが暗黙に参照している場合もあります。全利用者からCREATE権限を外し、探索路の末尾へ置くか外す運用にとどめるほうが、副作用が読めます。