Go での例を含むマルチテナントデータベースパターン
マルチテナントデータベースパターンの完全ガイド
マルチテナント は、SaaS アプリケーションのための基本的なアーキテクチャパターンであり、複数の顧客(テナント)が同じアプリケーションインフラストラクチャを共有しながらも、データの分離を維持することが可能です。
適切なデータベースパターンの選択は、スケーラビリティ、セキュリティ、運用効率にとって非常に重要です。

マルチテナントパターンの概要
マルチテナントアプリケーションを設計する際には、以下の3つの主なデータベースアーキテクチャパターンから選択することが可能です:
- 共有データベース、共有スキーマ(最も一般的)
 - 共有データベース、個別スキーマ
 - テナントごとに個別のデータベース
 
それぞれのパターンには、特徴やトレードオフ、使用ケースが異なります。それぞれを詳しく見ていきましょう。
パターン1:共有データベース、共有スキーマ
これは最も一般的なマルチテナントパターンで、すべてのテナントが同じデータベースとスキーマを共有し、tenant_id列を使用してテナントデータを区別します。
アーキテクチャ
┌─────────────────────────────────────┐
│     Single Database                 │
│  ┌───────────────────────────────┐  │
│  │  Shared Schema                │  │
│  │  - users (tenant_id, ...)     │  │
│  │  - orders (tenant_id, ...)    │  │
│  │  - products (tenant_id, ...)  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘
実装例
マルチテナントパターンを実装する際には、SQLの基本知識が非常に重要です。SQLコマンドや構文に関する包括的なリファレンスが必要な場合は、SQLチートシート をご参照ください。共有スキーマパターンを設定する方法は以下の通りです:
-- tenant_idを含むusersテーブル
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    tenant_id INTEGER NOT NULL,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW(),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- パフォーマンス向上のためtenant_idにインデックスを設定
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
-- 行レベルセキュリティ(PostgreSQLの例)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON users
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
PostgreSQLに特化した機能やコマンド、RLSポリシー、スキーマ管理、パフォーマンスチューニングについては、PostgreSQLチートシート をご参照ください。
アプリケーションレベルのフィルタリング
Goアプリケーションを使用する際には、適切なORMの選択がマルチテナント実装に大きな影響を与えます。以下の例ではGORMを使用していますが、他にもいくつか優れた選択肢があります。GORM、Ent、Bun、sqlcを含むGo ORMの詳細な比較については、PostgreSQL用Go ORMの包括的なガイド をご参照ください。
// GORMを使用したGoでの例
func GetUserByEmail(db *gorm.DB, tenantID uint, email string) (*User, error) {
    var user User
    err := db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user).Error
    return &user, err
}
// テナントコンテキストを設定するミドルウェア
func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tenantID := extractTenantID(r) // サブドメイン、ヘッダー、JWTから取得
        ctx := context.WithValue(r.Context(), "tenant_id", tenantID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
共有スキーマの利点
- コストが最も低い:単一のデータベースインスタンス、インフラストラクチャが最小限
 - 運用が最も簡単:バックアップ、モニタリング、メンテナンスが1つのデータベースで可能
 - スキーマ変更が簡単:すべてのテナントに一度にマイグレーションを適用
 - テナント数が多い場合に最適:リソースの効率的な利用
 - テナント間の分析が容易:テナント間でデータを集計しやすい
 
共有スキーマの欠点
- 分離が弱い:tenant_idフィルタを忘れるとデータリークのリスク
 - ノイズの多い隣人:1つのテナントの重いワークロードが他のテナントに影響
 - カスタマイズが限られる:すべてのテナントが同じスキーマを共有
 - コンプライアンスの課題:厳しいデータ分離要件を満たすのが難しい
 - バックアップの複雑さ:個々のテナントデータを簡単に復元できない
 
共有スキーマが最適なケース
- 複数の中小テナントを持つSaaSアプリケーション
 - テナントがカスタムスキーマを必要としないアプリケーション
 - コストに敏感なスタートアップ
 - テナント数が多い(数千以上)
 
パターン2:共有データベース、個別スキーマ
各テナントは同じデータベース内に独自のスキーマを取得し、インフラストラクチャを共有しながらも、より良い分離を提供します。
個別スキーマのアーキテクチャ
┌─────────────────────────────────────┐
│     Single Database                 │
│  ┌──────────┐  ┌──────────┐         │
│  │ Schema A │  │ Schema B │  ...    │
│  │ (Tenant1)│  │ (Tenant2)│         │
│  └──────────┘  └──────────┘         │
└─────────────────────────────────────┘
個別スキーマの実装
PostgreSQLのスキーマはマルチテナントに非常に強力な機能です。PostgreSQLのスキーマ管理、接続文字列、データベース管理コマンドに関する詳細な情報が必要な場合は、PostgreSQLチートシート をご参照ください。
-- テナント用のスキーマを作成
CREATE SCHEMA tenant_123;
-- テナント操作用の検索パスを設定
SET search_path TO tenant_123, public;
-- テナントスキーマ内にテーブルを作成
CREATE TABLE tenant_123.users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);
データベース接続管理
マルチテナントアプリケーションでは、データベース接続を効率的に管理することが非常に重要です。以下の接続管理コードはGORMを使用していますが、他のORMオプションも検討する価値があります。Go ORMの比較(接続プール、パフォーマンス特性、使用ケースなど)については、Go ORM比較ガイド をご参照ください。
// スキーマ検索パス付きの接続文字列
func GetTenantDB(tenantID uint) *gorm.DB {
    db := initializeDB()
    db.Exec(fmt.Sprintf("SET search_path TO tenant_%d, public", tenantID))
    return db
}
// またはPostgreSQL接続文字列を使用
// postgresql://user:pass@host/db?search_path=tenant_123
個別スキーマの利点
- 分離がより良い:スキーマレベルの分離によりデータリークのリスクが減少
 - カスタマイズが可能:各テナントが異なるテーブル構造を持つことができる
 - コストが中程度:まだ単一のデータベースインスタンス
 - テナントごとのバックアップが容易:個々のスキーマをバックアップ可能
 - コンプライアンスに適している:共有スキーマパターンより強力
 
個別スキーマの欠点
- スキーマ管理の複雑さ:マイグレーションはテナントごとに実行する必要がある
 - 接続のオーバーヘッド:接続ごとにsearch_pathを設定する必要がある
 - スケーラビリティの限界:スキーマ数の制限(PostgreSQLでは約10,000スキーマ)
 - テナント間のクエリが複雑:動的なスキーマ参照が必要
 - リソースの制限:まだ共有データベースリソース
 
個別スキーマが最適なケース
- 中規模のSaaS(数十から数百のテナント)
 - テナントがスキーマカスタマイズを必要とする場合
 - 共有スキーマよりより良い分離が必要なアプリケーション
 - コンプライアンス要件が中程度の場合
 
パターン3:テナントごとに個別のデータベース
各テナントは独自の完全なデータベースインスタンスを取得し、最大の分離を提供します。
個別データベースのアーキテクチャ
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│  Database 1  │  │  Database 2  │  │  Database 3  │
│  (Tenant A)  │  │  (Tenant B)  │  │  (Tenant C)  │
└──────────────┘  └──────────────┘  └──────────────┘
個別データベースの実装
-- テナント用のデータベースを作成
CREATE DATABASE tenant_enterprise_corp;
-- テナントデータベースに接続
\c tenant_enterprise_corp
-- テーブルを作成(tenant_idは不要!)
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    created_at TIMESTAMP DEFAULT NOW()
);
動的な接続管理
// 接続プールマネージャ
type TenantDBManager struct {
    pools map[uint]*gorm.DB
    mu    sync.RWMutex
}
func (m *TenantDBManager) GetDB(tenantID uint) (*gorm.DB, error) {
    m.mu.RLock()
    if db, exists := m.pools[tenantID]; exists {
        m.mu.RUnlock()
        return db, nil
    }
    m.mu.RUnlock()
    m.mu.Lock()
    defer m.mu.Unlock()
    // ライターロックを取得した後に再度確認
    if db, exists := m.pools[tenantID]; exists {
        return db, nil
    }
    // 新しい接続を作成
    db, err := gorm.Open(postgres.Open(fmt.Sprintf(
        "host=localhost user=dbuser password=dbpass dbname=tenant_%d sslmode=disable",
        tenantID,
    )), &gorm.Config{})
    
    if err != nil {
        return nil, err
    }
    m.pools[tenantID] = db
    return db, nil
}
個別データベースの利点
- 最大の分離:完全なデータ分離
 - 最高のセキュリティ:テナント間のデータアクセスのリスクなし
 - 完全なカスタマイズ:各テナントが完全に異なるスキーマを持つことができる
 - 独立したスケーリング:テナントデータベースを個別にスケーリング可能
 - コンプライアンスが容易:最も厳しいデータ分離要件を満たす
 - テナントごとのバックアップ:簡単な独立したバックアップ/リストア
 - ノイズの多い隣人がいない:テナントのワークロードが他のテナントに影響しない
 
個別データベースの欠点
- コストが最も高い:複数のデータベースインスタンスが必要でリソースが増える
 - 運用の複雑さ:多くのデータベース(バックアップ、モニタリング、マイグレーション)を管理する必要がある
 - 接続の制限:各データベースインスタンスには接続の制限がある
 - テナント間の分析:データフェデレーションまたはETLが必要
 - マイグレーションの複雑さ:すべてのデータベースにマイグレーションを実行する必要がある
 - リソースのオーバーヘッド:より多くのメモリ、CPU、ストレージが必要
 
個別データベースが最適なケース
- 高価値顧客を持つ企業向けSaaS
 - 厳格なコンプライアンス要件(HIPAA、GDPR、SOC 2)
 - テナントが大きなカスタマイズを必要とする場合
 - テナント数が少から中程度(数十から数百)
 - テナントが非常に異なるデータモデルを持つ場合
 
セキュリティの考慮
選択したパターンに関係なく、セキュリティは最も重要です:
1. 行レベルセキュリティ(RLS)
PostgreSQLのRLSは、クエリをテナントごとに自動的にフィルタリングし、データベースレベルのセキュリティレイヤーを提供します。この機能は特にマルチテナントアプリケーションにおいて非常に強力です。PostgreSQL RLS、セキュリティポリシー、その他の高度なPostgreSQL機能に関する詳細については、PostgreSQLチートシート をご参照ください。
-- RLSを有効にする
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- テナントごとの分離ポリシーを作成
CREATE POLICY tenant_isolation ON orders
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::INTEGER);
-- アプリケーションでテナントコンテキストを設定
SET app.current_tenant = '123';
2. アプリケーションレベルのフィルタリング
アプリケーションコードで常にtenant_idでフィルタリングしてください。以下の例ではGORMを使用していますが、他のORMにはそれぞれクエリ構築の独自のアプローチがあります。マルチテナントアプリケーションに適したORMの選択に関するガイドラインについては、Go ORM比較 をご参照ください。
// ❌ BAD - tenantフィルタが欠如
db.Where("email = ?", email).First(&user)
// ✅ GOOD - tenantフィルタを常に含める
db.Where("tenant_id = ? AND email = ?", tenantID, email).First(&user)
// ✅ BETTER - スコープやミドルウェアを使用
db.Scopes(TenantScope(tenantID)).Where("email = ?", email).First(&user)
3. 接続プール
テナントコンテキストをサポートする接続プーラーを使用してください:
// PgBouncerを使用したトランザクションプール
// またはアプリケーションレベルの接続ルーティングを使用
4. オーディットログ
すべてのテナントデータアクセスを記録してください:
type AuditLog struct {
    ID        uint
    TenantID  uint
    UserID    uint
    Action    string
    Table     string
    RecordID  uint
    Timestamp time.Time
    IPAddress string
}
パフォーマンス最適化
インデックス戦略
マルチテナントデータベースのパフォーマンスには適切なインデックスが非常に重要です。複合インデックスや部分インデックスを含むSQLインデックス戦略の理解は不可欠です。CREATE INDEXやクエリ最適化に関するSQLコマンドの包括的なリファレンスが必要な場合は、SQLチートシート をご参照ください。PostgreSQLに特化したインデックス機能やパフォーマンスチューニングについては、PostgreSQLチートシート をご参照ください。
-- テナントクエリ用の複合インデックス
CREATE INDEX idx_orders_tenant_created ON orders(tenant_id, created_at DESC);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
-- 一般的なテナント固有クエリ用の部分インデックス
CREATE INDEX idx_orders_active_tenant ON orders(tenant_id, created_at)
WHERE status = 'active';
クエリ最適化
// テナントクエリ用に準備されたステートメントを使用
stmt := db.Prepare("SELECT * FROM users WHERE tenant_id = $1 AND email = $2")
// テナントごとにバッチ操作
db.Where("tenant_id = ?", tenantID).Find(&users)
// 個別データベースパターン用にテナントごとの接続プールを使用
モニタリング
マルチテナントアプリケーションのモニタリングには、効果的なデータベース管理ツールが不可欠です。すべてのテナントにわたってクエリパフォーマンス、リソース使用量、データベースの健康状態を追跡する必要があります。この目的で役立つデータベース管理ツールの比較については、DBeaver vs Beekeeper比較 をご参照ください。両方のツールは、マルチテナント環境でのPostgreSQLデータベースの管理とモニタリングに優れた機能を提供しています。
テナントごとのメトリクスをモニタリングしてください:
- テナントごとのクエリパフォーマンス
 - テナントごとのリソース使用量
 - テナントごとの接続数
 - テナントごとのデータベースサイズ
 
マイグレーション戦略
共有スキーマパターン
データベースマイグレーションを実装する際、選択したORMはスキーマ変更の取り扱いに影響を与えます。以下の例ではGORMのAutoMigrate機能を使用していますが、他のORMには異なるマイグレーション戦略があります。さまざまなGo ORMがマイグレーションやスキーマ管理をどのように処理するかについては、Go ORM比較 をご参照ください。
// マイグレーションはすべてのテナントに自動的に適用
func Migrate(db *gorm.DB) error {
    return db.AutoMigrate(&User{}, &Order{}, &Product{})
}
個別スキーマ/データベースパターン
// テナントごとにマイグレーションを実行
func MigrateAllTenants(tenantIDs []uint) error {
    for _, tenantID := range tenantIDs {
        db := GetTenantDB(tenantID)
        if err := db.AutoMigrate(&User{}, &Order{}); err != nil {
            return fmt.Errorf("tenant %d: %w", tenantID, err)
        }
    }
    return nil
}
決定マトリクス
| 要因 | 共有スキーマ | 個別スキーマ | 個別データベース | 
|---|---|---|---|
| 分離 | 低 | 中 | 高 | 
| コスト | 低 | 中 | 高 | 
| スケーラビリティ | 高 | 中 | 低-中 | 
| カスタマイズ | 無し | 中 | 高 | 
| 運用の複雑さ | 低 | 中 | 高 | 
| コンプライアンス | 限定的 | 良好 | 優れた | 
| 最適なテナント数 | 1000+ | 10-1000 | 1-100 | 
ハイブリッドアプローチ
異なるテナント階層にパターンを組み合わせることができます:
// 小規模テナント:共有スキーマ
if tenant.Tier == "standard" {
    return GetSharedDB(tenant.ID)
}
// エンタープライズテナント:個別データベース
if tenant.Tier == "enterprise" {
    return GetTenantDB(tenant.ID)
}
ベストプラクティス
- 常にテナントでフィルタリング:アプリケーションコードだけに頼らず、RLSを使用可能であれば使用してください。SQLの基本知識は適切なクエリ構築を保証します。クエリのベストプラクティスについては、SQLチートシート をご参照ください。
 - テナントリソース使用量をモニタリング:ノイズの多い隣人を特定し、制限してください。DBeaver vs Beekeeperガイド に記載されているデータベース管理ツールを使用してパフォーマンスメトリクスを追跡してください。
 - テナントコンテキストミドルウェアを実装:テナントの抽出と検証を中央集約してください。ORMの選択はこの実装方法に影響を与えます。Go ORM比較 をご参照ください。
 - 接続プールを使用:データベース接続を効率的に管理してください。PostgreSQLに特化した接続プール戦略については、PostgreSQLチートシート をご参照ください。
 - テナントのマイグレーションを計画:テナントをパターン間で移動できるようにする
 - ソフト削除を実装:テナントデータのハード削除ではなく、deleted_atを使用してください
 - すべてを監査:コンプライアンスのためにすべてのテナントデータアクセスをログに記録してください
 - 分離のテスト:クロステナントデータリークを防ぐために定期的なセキュリティ監査を実施してください
 
結論
適切なマルチテナントデータベースパターンの選択は、分離、コスト、スケーラビリティ、運用の複雑さの要件に大きく依存します。共有データベース、共有スキーマパターンは、ほとんどのSaaSアプリケーションに適していますが、厳格なコンプライアンス要件を持つエンタープライズ顧客には、テナントごとに個別のデータベースが必要です。
最初に要件を満たす最も単純なパターンから始め、要件が進化するにつれてより分離されたパターンへの移行を計画してください。選択したパターンに関係なく、セキュリティとデータ分離を常に最優先事項としてください。