はじめに
SaaSアプリケーションを構築する際、ユーザーデータの分離は最も重要な課題の一つです。
この記事では、PostgreSQLでマルチテナンシーと呼ばれる戦略で実装するための3つの主要なアプローチについて紹介します。
オプション1: 共有型(Row-Level Security)
実装方法の詳細
- すべてのユーザーのデータを同一のテーブルに格納
- PostgreSQLのRow-Level Security (RLS)機能を使用してデータを論理的に分離
- 各レコードに
user_idカラムを追加して所有者を識別
具体的な実装コード
-- すべての顧客のデータが同じテーブルに格納される
CREATE TABLE projects (
id UUID DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
name TEXT,
data JSONB
);
-- RLSを有効化
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- ユーザー分離のためのポリシー作成:ユーザーは自分のデータのみ閲覧可能
CREATE POLICY user_isolation ON projects
FOR ALL
USING (user_id = current_setting('app.current_user')::UUID);
-- アプリケーション内での使用例
SET LOCAL app.current_user = '98f7a321-b65c-42e1-d890-123f';
SELECT * FROM projects; -- ユーザーのプロジェクトのみが取得される
コスト面のメリット
- 月額約$20で無制限のユーザー数をサポート可能
- ユーザー数が増えても追加コストはほぼ発生しない
- データベースリソースを効率的に共有
最適な適用シナリオ
- B2C (Business to Consumer)ビジネス
- フリーミアムモデル採用のサービス
- 数千の小規模顧客を持つサービス
- 各ユーザーのデータ量が比較的少ない場合
オプション2: スキーマ分離型
実装方法の詳細
- PostgreSQLのスキーマ機能を利用してユーザーごとに専用のスキーマを作成
- 各スキーマ内に同じ構造のテーブルセットを作成
- テーブル名は同じだが、スキーマによって論理的に分離される
具体的な実装コード
-- 各顧客に専用のスキーマを作成
CREATE SCHEMA user_youtube;
CREATE SCHEMA user_fc2;
-- 各スキーマ内に同じ構造のテーブルを作成
CREATE TABLE user_youtube.projects (...);
CREATE TABLE user_fc2.projects (...);
-- アプリケーション内での使用例
SET search_path TO user_youtube;
SELECT * FROM projects; -- スキーマによって分離されたデータにアクセス
コスト構造
- 月額$20-100(スキーマ数に依存)
- スキーマ数が増えるとパフォーマンスへの影響が出始める
- データベースオブジェクトの数に制限あり(PostgreSQLの制限による)
最適な適用シナリオ
- B2B (Business to Business)向けサービス
- 中規模の企業顧客(10-500社程度)
- 各ユーザーのデータ量が中程度~大きい場合
- スキーマレベルでのカスタマイズが必要な場合
オプション3: データベース分離型
実装方法の詳細
- ユーザーごとに完全に独立したデータベースを作成
- 最高レベルの分離を実現
- 各データベースは独自のリソース、設定、バックアップを持つ
具体的な実装コード
-- 各顧客専用のデータベースを作成
CREATE DATABASE customer_001;
CREATE DATABASE customer_002;
-- 完全な分離が実現される
-- リクエストごとに特定のデータベースに接続
コスト影響
- データベースごとに月額約$20
- ユーザー数に比例してコストが直線的に増加
- 大量のユーザーがある場合、非常に高額になる可能性あり
- 運用・管理コストも大幅に増加
最適な適用シナリオ
- エンタープライズ向けサービス
- 厳格なコンプライアンス要件(金融、医療など規制の厳しい業界)
- 少数の大規模顧客(50社未満)
- データの完全分離が最優先の場合
- 顧客ごとのカスタマイズが多い場合
実際の運用データから見る現実
規模別の実運用結果
- 5,000ユーザーをRLSで管理:問題なく動作
- 500スキーマ:管理が困難になり始める(キャッシュ問題、スキーマ切り替えのオーバーヘッド)
- 50以上のデータベース:メンテナンス、バックアップ、アップグレードが複雑化し管理が悪夢に
開発者向け現実的アドバイス
特に個人開発者やスタートアップには、オプション1(RLS)から始めることが強く推奨されています。必要に応じて後から他の方式に移行することも可能です。
RLSの完全な実装例
1. すべてのテーブルにユーザーIDを追加
ALTER TABLE users ADD COLUMN user_id UUID;
ALTER TABLE projects ADD COLUMN user_id UUID;
ALTER TABLE invoices ADD COLUMN user_id UUID;
2. ユーザー設定用のヘルパー関数を作成
CREATE OR REPLACE FUNCTION set_current_user(p_user_id UUID)
RETURNS void AS $$
BEGIN
PERFORM set_config('app.current_user', p_user_id::TEXT, true);
END;
$$ LANGUAGE plpgsql;
3. 各テーブルにRLSポリシーを適用
CREATE POLICY users_user_policy ON users
USING (user_id = current_setting('app.current_user')::UUID);
CREATE POLICY projects_user_policy ON projects
USING (user_id = current_setting('app.current_user')::UUID);
4. RLSを強制的に適用
ALTER TABLE users FORCE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;
5. アプリケーション側での実装例(Node.js)
async function executeQuery(userId, query, params) {
await db.query('SELECT set_current_user($1)', [userId]);
return db.query(query, params);
}
// すべてのクエリが自動的にフィルタリングされる
const projects = await executeQuery(
userId,
'SELECT * FROM projects' // WHERE句不要!
);
パフォーマンスへの影響
詳細なベンチマーク
-- RLSなし: 0.8ms
SELECT * FROM projects WHERE user_id = '123';
-- RLSあり: 0.9ms
SET LOCAL app.current_user = '123';
SELECT * FROM projects;
- RLSによるオーバーヘッドは約12%
- この程度のオーバーヘッドであれば、完全なデータ分離という利点を考慮すると十分許容範囲
- 適切なインデックス設計により、大規模ユーザーでもパフォーマンスを維持可能
成長に応じた段階的移行パス
ステージ1(0-100顧客)
- RLSを使用した共有テーブルアプローチ
- シンプルで管理が容易、コスト効率が最も高い
ステージ2(100-1000顧客)
- RLSを維持しつつ、読み取り専用レプリカを追加
- 読み取りクエリのスケーラビリティを向上
ステージ3(1000+顧客)
- 大規模顧客向けにスキーマ分離を検討
- 小規模顧客はRLSで継続管理
- ハイブリッドアプローチで最適化
ステージ4(エンタープライズ顧客)
- 特に重要な大口顧客には専用データベースを提供
- 追加料金を請求してコストをカバー
- 特別なコンプライアンス要件にも対応可能
よくある落とし穴とその解決策
新テーブルへのuser_id追加忘れ
-
問題: 新しいテーブルに
user_idカラムの追加を忘れると、ユーザー間でデータが漏洩する危険性 - 解決策: ベースとなるマイグレーションテンプレートを作成し、すべての新テーブルにユーザーIDを自動的に含める
クロスユーザークエリの失敗
- 問題: 管理者が複数ユーザーのデータにアクセスする際にRLSが邪魔になる
-
解決策:
SECURITY DEFINER属性を持つ関数を作成し、特定の権限を持つユーザー向けにRLSをバイパス
パフォーマンス低下
- 問題: ユーザー数増加に伴うクエリパフォーマンスの低下
-
解決策: 複合インデックスでは常に
user_idを最初に配置し、効率的なフィルタリングを確保
-- 効率的なインデックス
CREATE INDEX idx_projects_user ON projects(user_id, created_at);
まとめ
マルチユーザーPostgreSQLアーキテクチャを選択する際は、ユーザー数、データ分離要件、予算に基づいて適切な方法を選択することが重要です。多くの場合、Row-Level Securityを使用した共有テーブルアプローチから始め、ビジネスの成長に合わせて段階的に進化させていくのが賢明な戦略です。