【PostgreSQL】スキーマを使ってテーブルを整理する

PostgreSQL

こんにちは、かつコーチです。

1つのデータベースにテーブルが50個、100個と増えてくると、userslogssettingsのような似た名前のテーブルがどのアプリ由来か分からなくなってきます。

そんなときに使うのがスキーマです。

今回は、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 USAGEALTER DEFAULT PRIVILEGESの権限設定を忘れずに
  • PostgreSQL 15以降はpublicスキーマへのCREATE権限がデフォルトで制限されている
  • マルチテナント設計では、tenant_id列・スキーマ分離・DB分離の3方式を要件で使い分ける

次に読むべき記事

テーブル整理の次は、主キーの採番方式であるシーケンスの仕組みを押さえましょう。

  • UPSERT(ON CONFLICT)とRETURNINGで往復を減らす
  • シーケンスとSERIAL・IDENTITY列の違い(近日公開)

タグ: PostgreSQL, 中級者向け, 設計

タイトルとURLをコピーしました