こんにちは、かつコーチです。
1つのデータベースにテーブルが50個、100個と増えてくると、users、logs、settingsのような似た名前のテーブルがどのアプリ由来か分からなくなってきます。
そんなときに使うのがスキーマです。
今回は、PostgreSQLの名前空間としてのスキーマを使って、テーブルをグループ分けする方法を解説します。
スキーマとは?
用語の定義:スキーマ(namespace)
スキーマとは、1つのデータベースの中にテーブルや関数などをグループ分けするための「名前空間」です。
同じデータベースの中でも、スキーマが違えば同じ名前のテーブルを共存させられます。
デフォルトでは、テーブルを作成するとpublicという名前のスキーマに配置されます。
-- publicスキーマに作成される(スキーマ名を省略した場合)
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
email TEXT NOT NULL
);
-- スキーマ名を明示して作成
CREATE SCHEMA sales;
CREATE TABLE sales.orders (
order_id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
amount NUMERIC(10, 2) NOT NULL
);
sales.ordersのように「スキーマ名.テーブル名」で参照するのが基本の書き方です。
なぜスキーマが必要なのか
小規模なアプリ1つだけならpublicスキーマにテーブルを並べるだけで十分です。
ただし、以下のようなケースではスキーマ分けが効いてきます。
- 会計システムと在庫システムのように、複数の業務ドメインを1つのデータベースに同居させたい
- 開発環境で本番相当のスキーマ構造を保ちつつ、テスト用データを別領域に置きたい
- マルチテナントSaaSで、テナントごとにスキーマを分離したい
私が担当した案件では、既存の会計システムに新しい在庫管理機能を追加する際、テーブル名の衝突を避けるためにinventoryスキーマを新設しました。
public.products(会計側の商品マスタ参照用ビュー)とinventory.products(在庫管理の実データ)を分けたことで、どちらのproductsかをテーブル名だけで判断できるようになりました。
基本の書き方
手順1:スキーマを作成しテーブルを配置する
CREATE SCHEMA inventory;
CREATE TABLE inventory.products (
product_id SERIAL PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
stock_count INT NOT NULL DEFAULT 0
);
CREATE TABLE inventory.stock_movements (
movement_id SERIAL PRIMARY KEY,
product_id INT NOT NULL REFERENCES inventory.products(product_id),
quantity INT NOT NULL,
moved_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
外部キー参照も、スキーマをまたいでいなければ通常通りREFERENCESで書けます。
手順2:search_pathでスキーマ名の省略を制御する
毎回inventory.productsと書くのは冗長です。
search_path(スキーマ名を省略したときに検索する順序を決める設定)を使うと、省略記法が使えるようになります。
SET search_path TO inventory, public;
-- inventoryスキーマが優先されるため、スキーマ名を省略できる
SELECT * FROM products;
セッション単位でSET search_pathを実行するほか、ロール(ユーザー)単位でデフォルトを固定することもできます。
ALTER ROLE app_user SET search_path TO inventory, public;
アプリケーションの接続ユーザーごとにsearch_pathを設定しておくと、アプリ側のクエリを書き換えずにスキーマを切り替えられます。
つまずきやすい設定・注意点
search_pathに複数スキーマを設定した状態で同名テーブルが両方に存在すると、先に書いたスキーマが優先されます。
意図しないスキーマのテーブルを参照してしまうバグの温床になるため、本番運用では省略記法に頼りすぎず、重要なクエリではスキーマ名を明示するのが安全です。
よくあるつまずきポイント・エラー対処
Before/After:権限不足でスキーマにアクセスできない
スキーマを新設しても、既存のユーザーに権限がないとテーブルを作成・参照できません。
-- ❌ Before:スキーマの使用権限がないユーザーで実行
SET ROLE app_user;
CREATE TABLE inventory.products (product_id SERIAL PRIMARY KEY);
-- ERROR: permission denied for schema inventory
私が最初にスキーマを導入したとき、まさにこのエラーでアプリからの接続が全滅し、慌ててロールバックした経験があります。
原因は、スキーマ作成時のデフォルト権限がスキーマ所有者にしか与えられておらず、アプリ用ロールにUSAGE権限がなかったことでした。
-- ✅ After:スキーマの使用権限とテーブル作成権限を明示的に付与する
GRANT USAGE ON SCHEMA inventory TO app_user;
GRANT CREATE ON SCHEMA inventory TO app_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA inventory TO app_user;
-- 今後inventoryスキーマに作成されるテーブルにも自動で権限を付与する
ALTER DEFAULT PRIVILEGES IN SCHEMA inventory
GRANT ALL PRIVILEGES ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGESを設定しておくと、後からテーブルを追加するたびにGRANTを書く手間を省けます。
publicスキーマの権限に注意する
PostgreSQL 15以降、publicスキーマに対するCREATE権限が、デフォルトではデータベース所有者以外の一般ユーザーに付与されなくなりました。
PostgreSQL 14以前からアップグレードしたデータベースで「一般ユーザーがテーブルを作成できない」となった場合は、このデフォルト権限の変更が原因であることが多いです。
-- 必要であれば明示的に付与する
GRANT CREATE ON SCHEMA public TO app_user;
セキュリティ強化のための変更なので、安易に全ユーザーへ再付与するのではなく、本当に必要なロールだけに絞って付与しましょう。
応用・一歩先の使い方
マルチテナントでのスキーマ分離という選択肢
マルチテナントSaaSのデータ分離方法には、大きく3つの選択肢があります。
| 方式 | 概要 | 向いているケース |
|---|---|---|
| 1テーブル+tenant_id列 | 全テナントが同一テーブルを共有し、列でフィルタ | テナント数が多く、構造がほぼ同じ場合 |
| スキーマ分離 | テナントごとにスキーマを分ける | テナント数が数十〜数百程度で、分離要件が強い場合 |
| データベース分離 | テナントごとに別データベース | テナント数が少なく、厳格な分離が必須の場合 |
スキーマ分離は「1テーブル+tenant_id」よりデータ漏えいのリスクを抑えつつ、データベース分離ほど運用コストを増やさない中間の選択肢です。
ただしテナント数が数千規模になると、スキーマごとの管理コスト(マイグレーションをテナント数分実行するなど)が無視できなくなります。
テナント数の見込みと分離要件の強さで、この3方式のどれを選ぶかを検討してください。
まとめ
この記事のポイント
- スキーマはデータベース内の名前空間で、テーブルをグループ分けできる
search_pathでスキーマ名の省略ルールを制御できるが、重要なクエリでは明示が安全- スキーマ新設時は
GRANT USAGE・ALTER DEFAULT PRIVILEGESの権限設定を忘れずに - PostgreSQL 15以降は
publicスキーマへのCREATE権限がデフォルトで制限されている - マルチテナント設計では、tenant_id列・スキーマ分離・DB分離の3方式を要件で使い分ける
次に読むべき記事
テーブル整理の次は、主キーの採番方式であるシーケンスの仕組みを押さえましょう。
- UPSERT(ON CONFLICT)とRETURNINGで往復を減らす
- シーケンスとSERIAL・IDENTITY列の違い(近日公開)
タグ: PostgreSQL, 中級者向け, 設計
