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アプリケーションに適していますが、厳格なコンプライアンス要件を持つエンタープライズ顧客には、テナントごとに個別のデータベースが必要です。
最初に要件を満たす最も単純なパターンから始め、要件が進化するにつれてより分離されたパターンへの移行を計画してください。選択したパターンに関係なく、セキュリティとデータ分離を常に最優先事項としてください。